自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從 12.9K 的前端開源項目我學到了啥?

開發(fā)
阿寶哥帶領大家開啟 BetterScroll 2.0 源碼的學習之旅。

[[343776]]

 接下來本文的重心將圍繞 插件化 的架構設計展開,不過在分析 BetterScroll 2.0 插件化架構之前,我們先來簡單了解一下 BetterScroll。

一、BetterScroll 簡介
BetterScroll 是一款重點解決移動端(已支持 PC)各種滾動場景需求的插件。它的核心是借鑒的 iscroll 的實現(xiàn),它的 API 設計基本兼容 iscroll,在 iscroll 的基礎上又擴展了一些 feature 以及做了一些性能優(yōu)化。

BetterScroll 1.0 共發(fā)布了 30 多個版本,npm 月下載量 5 萬,累計 star 數(shù) 12600+。那么為什么升級 2.0 呢?

  1. 做 v2 版本的初衷源于社區(qū)的一個需求: 
  2.  
  3. BetterScroll 能不能支持按需加載? 
  4. 來源于:BetterScroll 2.0 發(fā)布:精益求精,與你同行 

為了支持插件的按需加載,BetterScroll 2.0 采用了 插件化 的架構設計。CoreScroll 作為最小的滾動單元,暴露了豐富的事件以及鉤子,其余的功能都由不同的插件來擴展,這樣會讓 BetterScroll 使用起來更加的靈活,也能適應不同的場景。

下面是 BetterScroll 2.0 整體的架構圖:

該項目采用的是 monorepos 的組織方式,使用 lerna 進行多包管理,每個組件都是一個獨立的 npm 包:

與西瓜播放器一樣,BetterScroll 2.0 也是采用 插件化 的設計思想,CoreScroll 作為最小的滾動單元,其余的功能都是通過插件來擴展。比如長列表中常見的上拉加載和下拉刷新功能,在 BetterScroll 2.0 中這些功能分別通過 pull-up 和 pull-down 這兩個插件來實現(xiàn)。

插件化的好處之一就是可以支持按需加載,此外把獨立功能都拆分成獨立的插件,會讓核心系統(tǒng)更加穩(wěn)定,擁有一定的健壯性。

好的,簡單介紹了一下 BetterScroll,接下來我們步入正題來分析一下這個項目中一些值得我們學習的地方。

二、開發(fā)體驗方面
2.1 更好的智能提示
BetterScroll 2.0 采用 TypeScript 進行開發(fā),為了讓開發(fā)者在使用 BetterScroll 時能夠擁有較好的智能提示,BetterScroll 團隊充分利用了 TypeScript 接口自動合并的功能,讓開發(fā)者在使用某個插件時,能夠有對應的 Options 提示以及 bs(BetterScroll 實例)能夠有對應的方法提示。

2.1.1 智能插件 Options 提示

2.1.2 智能 BetterScroll 實例方法提示

接下來,為了后面能更好地理解 BetterScroll 的設計思想,我們先來簡單介紹一下插件化架構。

三、插件化架構簡介
3.1 插件化架構的概念
插件化架構(Plug-in Architecture),是一種面向功能進行拆分的可擴展性架構,通常用于實現(xiàn)基于產品的應用。插件化架構模式允許你將其他應用程序功能作為插件添加到核心應用程序,從而提供可擴展性以及功能分離和隔離。

插件化架構模式包括兩種類型的架構組件:核心系統(tǒng)(Core System)和插件模塊(Plug-in modules)。應用邏輯被分割為獨立的插件模塊和核心系統(tǒng),提供了可擴展性、靈活性、功能隔離和自定義處理邏輯的特性。

圖中 Core System 的功能相對穩(wěn)定,不會因為業(yè)務功能擴展而不斷修改,而插件模塊是可以根據實際業(yè)務功能的需要不斷地調整或擴展。插件化架構的本質就是將可能需要不斷變化的部分封裝在插件中,從而達到快速靈活擴展的目的,而又不影響整體系統(tǒng)的穩(wěn)定。

插件化架構的核心系統(tǒng)通常提供系統(tǒng)運行所需的最小功能集。插件模塊是獨立的模塊,包含特定的處理、額外的功能和自定義代碼,來向核心系統(tǒng)增強或擴展額外的業(yè)務能力。通常插件模塊之間也是獨立的,也有一些插件是依賴于若干其它插件的。重要的是,盡量減少插件之間的通信以避免依賴的問題。

3.2 插件化架構的優(yōu)點
靈活性高:整體靈活性是對環(huán)境變化快速響應的能力。由于插件之間的低耦合,改變通常是隔離的,可以快速實現(xiàn)。
可測試性:插件可以獨立測試,也很容易被模擬,不需修改核心系統(tǒng)就可以演示或構建新特性的原型。
性能高:雖然插件化架構本身不會使應用高性能,但通常使用插件化架構構建的應用性能都還不錯,因為可以自定義或者裁剪掉不需要的功能。
介紹完插件化架構相關的基礎知識,接下來我們來分析一下 BetterScroll 2.0 是如何設計插件化架構的。

四、BetterScroll 插件化架構實現(xiàn)
對于插件化的核心系統(tǒng)設計來說,它涉及三個關鍵點:插件管理、插件連接和插件通信。下面我們將圍繞這三個關鍵點來逐步分析 BetterScroll 2.0 是如何實現(xiàn)插件化架構。

4.1 插件管理
為了統(tǒng)一管理內置的插件,也方便開發(fā)者根據業(yè)務需求開發(fā)符合規(guī)范的自定義插件。BetterScroll 2.0 約定了統(tǒng)一的插件開發(fā)規(guī)范。BetterScroll 2.0 的插件需要是一個類,并且具有以下特性:

1.靜態(tài)的 pluginName 屬性;

2.實現(xiàn) PluginAPI 接口(當且僅當需要把插件方法代理至 bs);

3.constructor 的第一個參數(shù)就是 BetterScroll 實例 bs,你可以通過 bs 的 事件 或者 鉤子 來注入自己的邏輯。

這里為了直觀地理解以上的開發(fā)規(guī)范,我們將以內置的 PullUp 插件為例,來看一下它是如何實現(xiàn)上述規(guī)范的。PullUp 插件為 BetterScroll 擴展上拉加載的能力。

顧名思義,靜態(tài)的 pluginName 屬性表示插件的名稱,而 PluginAPI 接口表示插件實例對外提供的 API 接口,通過 PluginAPI 接口可知它支持 4 個方法:

finishPullUp(): void:結束上拉加載行為;
openPullUp(config?: PullUpLoadOptions): void:動態(tài)開啟上拉功能;
closePullUp(): void:關閉上拉加載功能;
autoPullUpLoad(): void:自動執(zhí)行上拉加載。
插件通過構造函數(shù)注入 BetterScroll 實例 bs,之后我們就可以通過 bs 的事件或者鉤子來注入自己的邏輯。那么為什么要注入 bs 實例?如何利用 bs 實例?這里我們先記住這些問題,后面我們再來分析它們。

4.2 插件連接
核心系統(tǒng)需要知道當前有哪些插件可用,如何加載這些插件,什么時候加載插件。常見的實現(xiàn)方法是插件注冊表機制。核心系統(tǒng)提供插件注冊表(可以是配置文件,也可以是代碼,還可以是數(shù)據庫),插件注冊表含有每個插件模塊的信息,包括它的名字、位置、加載時機(啟動就加載,或是按需加載)等。

這里我們以前面提到的 PullUp 插件為例,來看一下如何注冊和使用該插件。首先你需要使用以下命令安裝 PullUp 插件:

  1. $ npm install @better-scroll/pull-up --save 

成功安裝完 pullup 插件之后,你需要通過 BScroll.use 方法來注冊插件:

  1. import BScroll from '@better-scroll/core' 
  2. import Pullup from '@better-scroll/pull-up' 
  3.  
  4. BScroll.use(Pullup) 

然后,實例化 BetterScroll 時需要傳入 PullUp 插件的配置項。

  1. new BScroll('.bs-wrapper', { 
  2.   pullUpLoad: true 
  3. }) 

現(xiàn)在我們已經知道通過 BScroll.use 方法可以注冊插件,那么該方法內部做了哪些處理?要回答這個問題,我們來看一下對應的源碼:

  1. // better-scroll/packages/core/src/BScroll.ts 
  2. export const BScroll = (createBScroll as unknown) as BScrollFactory 
  3. createBScroll.use = BScrollConstructor.use 

在 BScroll.ts 文件中, BScroll.use 方法指向的是 BScrollConstructor.use 靜態(tài)方法,該方法的實現(xiàn)如下:

  1. export class BScrollConstructor<O = {}> extends EventEmitter { 
  2.   static plugins: PluginItem[] = [] 
  3.   static pluginsMap: PluginsMap = {} 
  4.  
  5.   static use(ctor: PluginCtor) { 
  6.     const name = ctor.pluginName 
  7.     const installed = BScrollConstructor.plugins.some
  8.       (plugin) => ctor === plugin.ctor 
  9.     ) 
  10.     // 省略部分代碼 
  11.     if (installed) return BScrollConstructor 
  12.     BScrollConstructor.pluginsMap[name] = true 
  13.     BScrollConstructor.plugins.push({ 
  14.       name
  15.       applyOrder: ctor.applyOrder, 
  16.       ctor, 
  17.     }) 
  18.     return BScrollConstructor 
  19.   } 

通過觀察以上代碼,可知 use 方法接收一個參數(shù),該參數(shù)的類型是 PluginCtor,用于描述插件構造函數(shù)的特點。PluginCtor 類型的具體聲明如下所示:

  1. interface PluginCtor { 
  2.   pluginName: string 
  3.   applyOrder?: ApplyOrder 
  4.   new (scroll: BScroll): any 

當我們調用 BScroll.use(Pullup) 方法時,會先獲取當前插件的名稱,然后判斷當前插件是否已經安裝過了。如果已經安裝則直接返回 BScrollConstructor 對象,否則會對插件進行注冊。即把當前插件的信息分別保存到 pluginsMap({}) 和 plugins([]) 對象中:

另外調用 use 靜態(tài)方法后,會返回 BScrollConstructor 對象,這是為了支持鏈式調用:

  1. BScroll.use(MouseWheel) 
  2.   .use(ObserveDom) 
  3.   .use(PullDownRefresh) 
  4.   .use(PullUpLoad) 

現(xiàn)在我們已經知道 BScroll.use 方法內部是如何注冊插件的,注冊插件只是第一步,要使用已注冊的插件,我們還需要在實例化 BetterScroll 時傳入插件的配置項,從而進行插件的初始化。對于 PullUp 插件,我們通過以下方式進行插件的初始化。

  1. new BScroll('.bs-wrapper', { 
  2.   pullUpLoad: true 
  3. }) 

所以想了解插件是如何連接到核心系統(tǒng)并進行插件初始化,我們就需要來分析一下 BScroll 構造函數(shù):

  1. // packages/core/src/BScroll.ts 
  2. export const BScroll = (createBScroll as unknown) as BScrollFactory 
  3.  
  4. export function createBScroll<O = {}>( 
  5.   el: ElementParam, 
  6.   options?: Options & O 
  7. ): BScrollConstructor & UnionToIntersection<ExtractAPI<O>> { 
  8.   const bs = new BScrollConstructor(el, options) 
  9.   return (bs as unknown) as BScrollConstructor & 
  10.     UnionToIntersection<ExtractAPI<O>> 

在 createBScroll 工廠方法內部會通過 new 關鍵字調用 BScrollConstructor 構造函數(shù)來創(chuàng)建 BetterScroll 實例。因此接下來的重點就是分析 BScrollConstructor 構造函數(shù):

  1. // packages/core/src/BScroll.ts 
  2. export class BScrollConstructor<O = {}> extends EventEmitter { 
  3.   constructor(el: ElementParam, options?: Options & O) { 
  4.     const wrapper = getElement(el) 
  5.     // 省略部分代碼 
  6.     this.plugins = {} 
  7.     this.hooks = new EventEmitter([...]) 
  8.     this.init(wrapper) 
  9.   } 
  10.    
  11.   private init(wrapper: MountedBScrollHTMLElement) { 
  12.     this.wrapper = wrapper 
  13.     // 省略部分代碼 
  14.     this.applyPlugins() 
  15.   } 

通過閱讀 BScrollConstructor 的源碼,我們發(fā)現(xiàn)在 BScrollConstructor 構造函數(shù)內部會調用 init 方法進行初始化,而在 init 方法內部會進一步調用 applyPlugins 方法來應用已注冊的插件:

  1. // packages/core/src/BScroll.ts 
  2. export class BScrollConstructor<O = {}> extends EventEmitter {   
  3.   private applyPlugins() { 
  4.     const options = this.options 
  5.     BScrollConstructor.plugins 
  6.       .sort((a, b) => { 
  7.         const applyOrderMap = { 
  8.           [ApplyOrder.Pre]: -1, 
  9.           [ApplyOrder.Post]: 1, 
  10.         } 
  11.         const aOrder = a.applyOrder ? applyOrderMap[a.applyOrder] : 0 
  12.         const bOrder = b.applyOrder ? applyOrderMap[b.applyOrder] : 0 
  13.         return aOrder - bOrder 
  14.       }) 
  15.       .forEach((item: PluginItem) => { 
  16.         const ctor = item.ctor 
  17.     // 當啟用指定插件的時候且插件構造函數(shù)的類型是函數(shù)的話,再創(chuàng)建對應的插件 
  18.         if (options[item.name] && typeof ctor === 'function') { 
  19.           this.plugins[item.name] = new ctor(this) 
  20.         } 
  21.       }) 
  22.   } 

在 applyPlugins 方法內部會根據插件設置的順序進行排序,然后會使用 bs 實例作為參數(shù)調用插件的構造函數(shù)來創(chuàng)建插件,并把插件的實例保存到 bs 實例內部的 plugins({}) 屬性中。

到這里我們已經介紹了插件管理和插件連接,下面我們來介紹最后一個關鍵點 —— 插件通信。

4.3 插件通信
插件通信是指插件間的通信。雖然設計的時候插件間是完全解耦的,但實際業(yè)務運行過程中,必然會出現(xiàn)某個業(yè)務流程需要多個插件協(xié)作,這就要求兩個插件間進行通信;由于插件之間沒有直接聯(lián)系,通信必須通過核心系統(tǒng),因此核心系統(tǒng)需要提供插件通信機制。

這種情況和計算機類似,計算機的 CPU、硬盤、內存、網卡是獨立設計的配置,但計算機運行過程中,CPU 和內存、內存和硬盤肯定是有通信的,計算機通過主板上的總線提供了這些組件之間的通信功能。

同樣,對于插件化架構的系統(tǒng)來說,通常核心系統(tǒng)會以事件總線的形式提供插件通信機制。提到事件總線,可能有一些小伙伴會有一些陌生。但如果說是使用了 發(fā)布訂閱模式 的話,應該就很容易理解了。這里阿寶哥不打算在展開介紹發(fā)布訂閱模式,只用一張圖來回顧一下該模式。

對于 BetterScroll 來說,它的核心是 BScrollConstructor 類,該類繼承了 EventEmitter 事件派發(fā)器:

  1. // packages/core/src/BScroll.ts 
  2. export class BScrollConstructor<O = {}> extends EventEmitter {   
  3.   constructor(el: ElementParam, options?: Options & O) { 
  4.     this.hooks = new EventEmitter([ 
  5.       'refresh'
  6.       'enable'
  7.       'disable'
  8.       'destroy'
  9.       'beforeInitialScrollTo'
  10.       'contentChanged'
  11.     ]) 
  12.     this.init(wrapper) 
  13.   } 

EventEmitter 類是由 BetterScroll 內部提供的,它的實例將會對外提供事件總線的功能,而該類對應的 UML 類圖如下所示:

講到這里我們就可以來回答前面留下的第一個問題:“那么為什么要注入 bs 實例?”。因為 bs(BScrollConstructor)實例的本質也是一個事件派發(fā)器,在創(chuàng)建插件時,注入 bs 實例是為了讓插件間能通過統(tǒng)一的事件派發(fā)器進行通信。

第一個問題我們已經知道答案了,接下來我們來看第二個問題:”如何利用 bs 實例?“。要回答這個問題,我們將繼續(xù)以 PullUp 插件為例,來看一下該插件內部是如何利用 bs 實例進行消息通信的。

  1. export default class PullUp implements PluginAPI { 
  2.   static pluginName = 'pullUpLoad' 
  3.   constructor(public scroll: BScroll) { 
  4.     this.init() 
  5.   } 

在 PullUp 構造函數(shù)中,bs 實例會被保存到 PullUp 實例內部的 scroll 屬性中,之后在 PullUp 插件內部就可以通過注入的 bs 實例來進行事件通信。比如派發(fā)插件的內部事件,在 PullUp 插件中,當距離滾動到底部小于 threshold 值時,觸發(fā)一次 pullingUp 事件:

  1. private checkPullUp(pos: { x: number; y: number }) { 
  2.   const { threshold } = this.options 
  3.   if (...) { 
  4.       this.pulling = true 
  5.       // 省略部分代碼 
  6.       this.scroll.trigger(PULL_UP_HOOKS_NAME) // 'pullingUp' 
  7.   } 

知道如何利用 bs 實例派發(fā)事件之后,我們再來看一下在插件內部如何利用它來監(jiān)聽插件所感興趣的事件

  1. // packages/pull-up/src/index.ts 
  2. export default class PullUp implements PluginAPI { 
  3.   static pluginName = 'pullUpLoad' 
  4.   constructor(public scroll: BScroll) { 
  5.     this.init() 
  6.   } 
  7.  
  8.   private init() { 
  9.     this.handleBScroll() 
  10.     this.handleOptions(this.scroll.options.pullUpLoad) 
  11.     this.handleHooks() 
  12.     this.watch() 
  13.   } 

在 PullUp 構造函數(shù)中會調用 init 方法進行插件初始化,而在 init 方法內部會分別調用不同的方法執(zhí)行不同的初始化操作,這里跟事件相關的是 handleHooks 方法,該方法的實現(xiàn)如下:

  1. private handleHooks() { 
  2.   this.hooksFn = [] 
  3.   // 省略部分代碼 
  4.   this.registerHooks( 
  5.     this.scroll.hooks, 
  6.     this.scroll.hooks.eventTypes.contentChanged, 
  7.     () => { 
  8.       this.finishPullUp() 
  9.     } 
  10.   ) 

很明顯在 handleHooks 方法內部,會進一步調用 registerHooks 方法來注冊鉤子:

  1. private registerHooks(hooks: EventEmitter, name: string, handler: Function) { 
  2.   hooks.on(name, handler, this) 
  3.   this.hooksFn.push([hooks, name, handler]) 

通過觀察 registerHooks 方法的簽名可知,它支持 3 個參數(shù),第 1 個參數(shù)是 EventEmitter 對象,而另外 2 個參數(shù)分別表示事件名和事件處理器。在 registerHooks 方法內部,它就是簡單地通過 hooks 對象來監(jiān)聽指定的事件。

那么 this.scroll.hooks 對象是什么時候創(chuàng)建的呢?在 BScrollConstructor 構造函數(shù)中我們找到了答案。

  1. // packages/core/src/BScroll.ts 
  2. export class BScrollConstructor<O = {}> extends EventEmitter { 
  3.   constructor(el: ElementParam, options?: Options & O) { 
  4.     // 省略部分代碼 
  5.     this.hooks = new EventEmitter([ 
  6.       'refresh'
  7.       'enable'
  8.       'disable'
  9.       'destroy'
  10.       'beforeInitialScrollTo'
  11.       'contentChanged'
  12.     ])  
  13.   } 

很明顯 this.hooks 也是一個 EventEmitter 對象,所以可以通過它來進行事件處理。好的,插件通信的內容就先介紹到這里,下面我們用一張圖來總結一下該部分的內容:

介紹完 BetterScroll 插件化架構的實現(xiàn),最后我們來簡單聊一下 BetterScroll 項目工程化方面的內容。

五、工程化方面
在工程化方面,BetterScroll 使用了業(yè)內一些常見的解決方案:

lerna:Lerna 是一個管理工具,用于管理包含多個軟件包(package)的 JavaScript 項目。
prettier:Prettier 中文的意思是漂亮的、美麗的,是一個流行的代碼格式化的工具。
tslint:TSLint 是可擴展的靜態(tài)分析工具,用于檢查 TypeScript 代碼的可讀性,可維護性和功能性錯誤。
commitizen & cz-conventional-changelog:用于幫助我們生成符合規(guī)范的 commit message。
husky:husky 能夠防止不規(guī)范代碼被 commit、push、merge 等等。
jest:Jest 是由 Facebook 維護的 JavaScript 測試框架。
coveralls:用于獲取 Coveralls.io 的覆蓋率報告,并在 README 文件中添加一個不錯的覆蓋率按鈕。
vuepress:Vue 驅動的靜態(tài)網站生成器,它用于生成 BetterScroll 2.0 的文檔。
因為本文的重點不在工程化,所以上面阿寶哥只是簡單羅列了 BetterScroll 在工程化方面使用的開源庫。如果你對 BetterScroll 項目也感興趣的話,可以看看項目中的 package.json 文件,并重點看一下項目中 npm scripts 的配置。

 

 

責任編輯:姜華 來源: 全棧修仙之路
相關推薦

2021-10-25 05:43:40

前端技術編程

2020-02-22 15:01:51

后端前端開發(fā)

2021-03-09 09:55:02

Vuejs前端代碼

2020-07-07 08:52:16

機器學習機器學習工具人工智能

2022-03-27 09:06:04

React類型定義前端

2020-12-31 10:47:03

開發(fā)Vuejs技術

2016-01-18 10:06:05

編程

2021-04-15 08:15:27

Vue.js源碼方法

2020-02-22 14:49:30

畢業(yè)入職半年感受

2020-11-04 07:13:57

數(shù)據工程代碼編程

2024-04-12 08:54:13

從庫數(shù)據庫應用

2021-01-02 09:48:13

函數(shù)運算js

2021-07-28 07:01:09

薅羊毛架構Vue+SSR

2020-10-30 12:40:04

Reac性能優(yōu)化

2019-08-27 10:49:30

跳槽那些事兒技術Linux

2013-06-27 10:31:39

2019-08-16 17:14:28

跳槽那些事兒技術Linux

2015-06-29 13:47:19

創(chuàng)業(yè)創(chuàng)業(yè)智慧

2023-06-06 08:14:18

核心Docker應用程序

2011-10-18 11:43:25

UNIXC語言丹尼斯·里奇
點贊
收藏

51CTO技術棧公眾號