從 12.9K 的前端開源項(xiàng)目我學(xué)到了啥?
近期我們團(tuán)隊(duì)的小伙伴小池同學(xué)分享了 “BetterScroll 2.0 發(fā)布:精益求精,與你同行” 這篇文章到團(tuán)隊(duì)內(nèi)部群,看到了 插件化 的架構(gòu)設(shè)計(jì),阿寶哥突然來了興趣,因?yàn)橹鞍毟缭趫F(tuán)隊(duì)內(nèi)部也做過相關(guān)的分享。既然已經(jīng)來了興趣,那就決定開啟 BetterScroll 2.0 源碼的學(xué)習(xí)之旅。
接下來本文的重心將圍繞 插件化 的架構(gòu)設(shè)計(jì)展開,不過在分析 BetterScroll 2.0 插件化架構(gòu)之前,我們先來簡(jiǎn)單了解一下 BetterScroll。
一、BetterScroll 簡(jiǎn)介
BetterScroll 是一款重點(diǎn)解決移動(dòng)端(已支持 PC)各種滾動(dòng)場(chǎng)景需求的插件。它的核心是借鑒的 iscroll 的實(shí)現(xiàn),它的 API 設(shè)計(jì)基本兼容 iscroll,在 iscroll 的基礎(chǔ)上又?jǐn)U展了一些 feature 以及做了一些性能優(yōu)化。
BetterScroll 1.0 共發(fā)布了 30 多個(gè)版本,npm 月下載量 5 萬,累計(jì) star 數(shù) 12600+。那么為什么升級(jí) 2.0 呢?
做 v2 版本的初衷源于社區(qū)的一個(gè)需求:
BetterScroll 能不能支持按需加載? 來源于:BetterScroll 2.0 發(fā)布:精益求精,與你同行
為了支持插件的按需加載,BetterScroll 2.0 采用了 插件化 的架構(gòu)設(shè)計(jì)。CoreScroll 作為最小的滾動(dòng)單元,暴露了豐富的事件以及鉤子,其余的功能都由不同的插件來擴(kuò)展,這樣會(huì)讓 BetterScroll 使用起來更加的靈活,也能適應(yīng)不同的場(chǎng)景。
下面是 BetterScroll 2.0 整體的架構(gòu)圖:

(圖片來源:https://juejin.im/post/6868086607027650573)
該項(xiàng)目采用的是 monorepos 的組織方式,使用 lerna 進(jìn)行多包管理,每個(gè)組件都是一個(gè)獨(dú)立的 npm 包:

與西瓜播放器一樣,BetterScroll 2.0 也是采用 插件化 的設(shè)計(jì)思想,CoreScroll 作為最小的滾動(dòng)單元,其余的功能都是通過插件來擴(kuò)展。比如長(zhǎng)列表中常見的上拉加載和下拉刷新功能,在 BetterScroll 2.0 ?中這些功能分別通過 pull-up 和 pull-down 這兩個(gè)插件來實(shí)現(xiàn)。
插件化的好處之一就是可以支持按需加載,此外把獨(dú)立功能都拆分成獨(dú)立的插件,會(huì)讓核心系統(tǒng)更加穩(wěn)定,擁有一定的健壯性。
好的,簡(jiǎn)單介紹了一下 BetterScroll,接下來我們步入正題來分析一下這個(gè)項(xiàng)目中一些值得我們學(xué)習(xí)的地方。
二、開發(fā)體驗(yàn)方面
2.1 更好的智能提示
BetterScroll 2.0 采用 TypeScript 進(jìn)行開發(fā),為了讓開發(fā)者在使用 BetterScroll 時(shí)能夠擁有較好的智能提示,BetterScroll 團(tuán)隊(duì)充分利用了 TypeScript 接口自動(dòng)合并的功能,讓開發(fā)者在使用某個(gè)插件時(shí),能夠有對(duì)應(yīng)的 Options 提示以及 ?bs(BetterScroll 實(shí)例)能夠有對(duì)應(yīng)的方法提示。
2.1.1 智能插件 Options 提示

2.1.2 智能 BetterScroll 實(shí)例方法提示

接下來,為了后面能更好地理解 BetterScroll 的設(shè)計(jì)思想,我們先來簡(jiǎn)單介紹一下插件化架構(gòu)。
三、插件化架構(gòu)簡(jiǎn)介
3.1 插件化架構(gòu)的概念
插件化架構(gòu)(Plug-in Architecture),是一種面向功能進(jìn)行拆分的可擴(kuò)展性架構(gòu),通常用于實(shí)現(xiàn)基于產(chǎn)品的應(yīng)用。插件化架構(gòu)模式允許你將其他應(yīng)用程序功能作為插件添加到核心應(yīng)用程序,從而提供可擴(kuò)展性以及功能分離和隔離。
插件化架構(gòu)模式包括兩種類型的架構(gòu)組件:核心系統(tǒng)(Core System)和插件模塊(Plug-in modules)。應(yīng)用邏輯被分割為獨(dú)立的插件模塊和核心系統(tǒng),提供了可擴(kuò)展性、靈活性、功能隔離和自定義處理邏輯的特性。

圖中 Core System 的功能相對(duì)穩(wěn)定,不會(huì)因?yàn)闃I(yè)務(wù)功能擴(kuò)展而不斷修改,而插件模塊是可以根據(jù)實(shí)際業(yè)務(wù)功能的需要不斷地調(diào)整或擴(kuò)展。插件化架構(gòu)的本質(zhì)就是將可能需要不斷變化的部分封裝在插件中,從而達(dá)到快速靈活擴(kuò)展的目的,而又不影響整體系統(tǒng)的穩(wěn)定。
插件化架構(gòu)的核心系統(tǒng)通常提供系統(tǒng)運(yùn)行所需的最小功能集。插件模塊是獨(dú)立的模塊,包含特定的處理、額外的功能和自定義代碼,來向核心系統(tǒng)增強(qiáng)或擴(kuò)展額外的業(yè)務(wù)能力。通常插件模塊之間也是獨(dú)立的,也有一些插件是依賴于若干其它插件的。重要的是,盡量減少插件之間的通信以避免依賴的問題。
3.2 插件化架構(gòu)的優(yōu)點(diǎn)
靈活性高:整體靈活性是對(duì)環(huán)境變化快速響應(yīng)的能力。由于插件之間的低耦合,改變通常是隔離的,可以快速實(shí)現(xiàn)。 可測(cè)試性:插件可以獨(dú)立測(cè)試,也很容易被模擬,不需修改核心系統(tǒng)就可以演示或構(gòu)建新特性的原型。 性能高:雖然插件化架構(gòu)本身不會(huì)使應(yīng)用高性能,但通常使用插件化架構(gòu)構(gòu)建的應(yīng)用性能都還不錯(cuò),因?yàn)榭梢宰远x或者裁剪掉不需要的功能。
介紹完插件化架構(gòu)相關(guān)的基礎(chǔ)知識(shí),接下來我們來分析一下 BetterScroll 2.0 是如何設(shè)計(jì)插件化架構(gòu)的。
四、BetterScroll 插件化架構(gòu)實(shí)現(xiàn)
對(duì)于插件化的核心系統(tǒng)設(shè)計(jì)來說,它涉及三個(gè)關(guān)鍵點(diǎn):插件管理、插件連接和插件通信。下面我們將圍繞這三個(gè)關(guān)鍵點(diǎn)來逐步分析 BetterScroll 2.0 是如何實(shí)現(xiàn)插件化架構(gòu)。
4.1 插件管理
為了統(tǒng)一管理內(nèi)置的插件,也方便開發(fā)者根據(jù)業(yè)務(wù)需求開發(fā)符合規(guī)范的自定義插件。BetterScroll 2.0 約定了統(tǒng)一的插件開發(fā)規(guī)范。BetterScroll 2.0 的插件需要是一個(gè)類,并且具有以下特性:
1.靜態(tài)的 pluginName 屬性;
2.實(shí)現(xiàn) PluginAPI 接口(當(dāng)且僅當(dāng)需要把插件方法代理至 bs);
3.constructor 的第一個(gè)參數(shù)就是 BetterScroll 實(shí)例 bs,你可以通過 bs 的 事件 或者 鉤子 來注入自己的邏輯。
這里為了直觀地理解以上的開發(fā)規(guī)范,我們將以內(nèi)置的 PullUp 插件為例,來看一下它是如何實(shí)現(xiàn)上述規(guī)范的。PullUp 插件為 BetterScroll 擴(kuò)展上拉加載的能力。

顧名思義,靜態(tài)的 pluginName 屬性表示插件的名稱,而 PluginAPI 接口表示插件實(shí)例對(duì)外提供的 API 接口,通過 PluginAPI 接口可知它支持 4 個(gè)方法:
finishPullUp(): void:結(jié)束上拉加載行為; openPullUp(config?: PullUpLoadOptions): void:動(dòng)態(tài)開啟上拉功能; closePullUp(): void:關(guān)閉上拉加載功能; autoPullUpLoad(): void:自動(dòng)執(zhí)行上拉加載。
插件通過構(gòu)造函數(shù)注入 BetterScroll 實(shí)例 bs,之后我們就可以通過 bs 的事件或者鉤子來注入自己的邏輯。那么為什么要注入 bs 實(shí)例?如何利用 bs 實(shí)例?這里我們先記住這些問題,后面我們?cè)賮矸治鏊鼈儭?/p>
4.2 插件連接
核心系統(tǒng)需要知道當(dāng)前有哪些插件可用,如何加載這些插件,什么時(shí)候加載插件。常見的實(shí)現(xiàn)方法是插件注冊(cè)表機(jī)制。核心系統(tǒng)提供插件注冊(cè)表(可以是配置文件,也可以是代碼,還可以是數(shù)據(jù)庫(kù)),插件注冊(cè)表含有每個(gè)插件模塊的信息,包括它的名字、位置、加載時(shí)機(jī)(啟動(dòng)就加載,或是按需加載)等。
這里我們以前面提到的 PullUp 插件為例,來看一下如何注冊(cè)和使用該插件。首先你需要使用以下命令安裝 PullUp 插件:
$?npm?install?@better-scroll/pull-up?--save
成功安裝完 pullup 插件之后,你需要通過 BScroll.use 方法來注冊(cè)插件:
import?BScroll?from?'@better-scroll/core'
import?Pullup?from?'@better-scroll/pull-up'
BScroll.use(Pullup)
然后,實(shí)例化 BetterScroll 時(shí)需要傳入 PullUp 插件的配置項(xiàng)。
new?BScroll('.bs-wrapper',?{
??pullUpLoad:?true
})
現(xiàn)在我們已經(jīng)知道通過 BScroll.use 方法可以注冊(cè)插件,那么該方法內(nèi)部做了哪些處理?要回答這個(gè)問題,我們來看一下對(duì)應(yīng)的源碼:
//?better-scroll/packages/core/src/BScroll.ts
export?const?BScroll?=?(createBScroll?as?unknown)?as?BScrollFactory
createBScroll.use?=?BScrollConstructor.use
在 BScroll.ts 文件中, BScroll.use 方法指向的是 BScrollConstructor.use 靜態(tài)方法,該方法的實(shí)現(xiàn)如下:
export?class?BScrollConstructor?extends?EventEmitter?{
??static?plugins:?PluginItem[]?=?[]
??static?pluginsMap:?PluginsMap?=?{}
??static?use(ctor:?PluginCtor)?{
????const?name?=?ctor.pluginName
????const?installed?=?BScrollConstructor.plugins.some(
??????(plugin)?=>?ctor?===?plugin.ctor
????)
????//?省略部分代碼
????if?(installed)?return?BScrollConstructor
????BScrollConstructor.pluginsMap[name]?=?true
????BScrollConstructor.plugins.push({
??????name,
??????applyOrder:?ctor.applyOrder,
??????ctor,
????})
????return?BScrollConstructor
??}
}
通過觀察以上代碼,可知 use 方法接收一個(gè)參數(shù),該參數(shù)的類型是 PluginCtor,用于描述插件構(gòu)造函數(shù)的特點(diǎn)。PluginCtor 類型的具體聲明如下所示:
interface?PluginCtor?{
??pluginName:?string
??applyOrder?:?ApplyOrder
??new?(scroll:?BScroll):?any
}
當(dāng)我們調(diào)用 BScroll.use(Pullup) 方法時(shí),會(huì)先獲取當(dāng)前插件的名稱,然后判斷當(dāng)前插件是否已經(jīng)安裝過了。如果已經(jīng)安裝則直接返回 BScrollConstructor 對(duì)象,否則會(huì)對(duì)插件進(jìn)行注冊(cè)。即把當(dāng)前插件的信息分別保存到 pluginsMap({}) 和 plugins([]) 對(duì)象中:

另外調(diào)用 use 靜態(tài)方法后,會(huì)返回 BScrollConstructor 對(duì)象,這是為了支持鏈?zhǔn)秸{(diào)用:
BScroll.use(MouseWheel)
??.use(ObserveDom)
??.use(PullDownRefresh)
??.use(PullUpLoad)
現(xiàn)在我們已經(jīng)知道 BScroll.use 方法內(nèi)部是如何注冊(cè)插件的,注冊(cè)插件只是第一步,要使用已注冊(cè)的插件,我們還需要在實(shí)例化 BetterScroll 時(shí)傳入插件的配置項(xiàng),從而進(jìn)行插件的初始化。對(duì)于 PullUp 插件,我們通過以下方式進(jìn)行插件的初始化。
new?BScroll('.bs-wrapper',?{
??pullUpLoad:?true
})
所以想了解插件是如何連接到核心系統(tǒng)并進(jìn)行插件初始化,我們就需要來分析一下 BScroll 構(gòu)造函數(shù):
//?packages/core/src/BScroll.ts
export?const?BScroll?=?(createBScroll?as?unknown)?as?BScrollFactory
export?function?createBScroll<O?=?{}>(
??el:?ElementParam,
??options?:?Options?&?O
):?BScrollConstructor?&?UnionToIntersection>?{
??const?bs?=?new?BScrollConstructor(el,?options)
??return?(bs?as?unknown)?as?BScrollConstructor?&
????UnionToIntersection>
}
在 createBScroll 工廠方法內(nèi)部會(huì)通過 new 關(guān)鍵字調(diào)用 BScrollConstructor 構(gòu)造函數(shù)來創(chuàng)建 BetterScroll 實(shí)例。因此接下來的重點(diǎn)就是分析 BScrollConstructor 構(gòu)造函數(shù):
//?packages/core/src/BScroll.ts
export?class?BScrollConstructor?extends?EventEmitter?{
??constructor(el:?ElementParam,?options?:?Options?&?O)?{
????const?wrapper?=?getElement(el)
????//?省略部分代碼
????this.plugins?=?{}
????this.hooks?=?new?EventEmitter([...])
????this.init(wrapper)
??}
??
??private?init(wrapper:?MountedBScrollHTMLElement)?{
????this.wrapper?=?wrapper
????//?省略部分代碼
????this.applyPlugins()
??}
}
通過閱讀 BScrollConstructor 的源碼,我們發(fā)現(xiàn)在 BScrollConstructor 構(gòu)造函數(shù)內(nèi)部會(huì)調(diào)用 init 方法進(jìn)行初始化,而在 init 方法內(nèi)部會(huì)進(jìn)一步調(diào)用 applyPlugins 方法來應(yīng)用已注冊(cè)的插件:
//?packages/core/src/BScroll.ts
export?class?BScrollConstructor?extends?EventEmitter?{??
??private?applyPlugins()?{
????const?options?=?this.options
????BScrollConstructor.plugins
??????.sort((a,?b)?=>?{
????????const?applyOrderMap?=?{
??????????[ApplyOrder.Pre]:?-1,
??????????[ApplyOrder.Post]:?1,
????????}
????????const?aOrder?=?a.applyOrder???applyOrderMap[a.applyOrder]?:?0
????????const?bOrder?=?b.applyOrder???applyOrderMap[b.applyOrder]?:?0
????????return?aOrder?-?bOrder
??????})
??????.forEach((item:?PluginItem)?=>?{
????????const?ctor?=?item.ctor
????//?當(dāng)啟用指定插件的時(shí)候且插件構(gòu)造函數(shù)的類型是函數(shù)的話,再創(chuàng)建對(duì)應(yīng)的插件
????????if?(options[item.name]?&&?typeof?ctor?===?'function')?{
??????????this.plugins[item.name]?=?new?ctor(this)
????????}
??????})
??}
}
在 applyPlugins 方法內(nèi)部會(huì)根據(jù)插件設(shè)置的順序進(jìn)行排序,然后會(huì)使用 bs 實(shí)例作為參數(shù)調(diào)用插件的構(gòu)造函數(shù)來創(chuàng)建插件,并把插件的實(shí)例保存到 bs 實(shí)例內(nèi)部的 plugins({}) 屬性中。

到這里我們已經(jīng)介紹了插件管理和插件連接,下面我們來介紹最后一個(gè)關(guān)鍵點(diǎn) —— 插件通信。
4.3 插件通信
插件通信是指插件間的通信。雖然設(shè)計(jì)的時(shí)候插件間是完全解耦的,但實(shí)際業(yè)務(wù)運(yùn)行過程中,必然會(huì)出現(xiàn)某個(gè)業(yè)務(wù)流程需要多個(gè)插件協(xié)作,這就要求兩個(gè)插件間進(jìn)行通信;由于插件之間沒有直接聯(lián)系,通信必須通過核心系統(tǒng),因此核心系統(tǒng)需要提供插件通信機(jī)制。
這種情況和計(jì)算機(jī)類似,計(jì)算機(jī)的 CPU、硬盤、內(nèi)存、網(wǎng)卡是獨(dú)立設(shè)計(jì)的配置,但計(jì)算機(jī)運(yùn)行過程中,CPU 和內(nèi)存、內(nèi)存和硬盤肯定是有通信的,計(jì)算機(jī)通過主板上的總線提供了這些組件之間的通信功能。

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

對(duì)于 BetterScroll 來說,它的核心是 BScrollConstructor 類,該類繼承了 EventEmitter 事件派發(fā)器:
//?packages/core/src/BScroll.ts
export?class?BScrollConstructor?extends?EventEmitter?{??
??constructor(el:?ElementParam,?options?:?Options?&?O)?{
????this.hooks?=?new?EventEmitter([
??????'refresh',
??????'enable',
??????'disable',
??????'destroy',
??????'beforeInitialScrollTo',
??????'contentChanged',
????])
????this.init(wrapper)
??}
}
EventEmitter 類是由 BetterScroll 內(nèi)部提供的,它的實(shí)例將會(huì)對(duì)外提供事件總線的功能,而該類對(duì)應(yīng)的 UML 類圖如下所示:

講到這里我們就可以來回答前面留下的第一個(gè)問題:“那么為什么要注入 bs 實(shí)例?”。因?yàn)?bs(BScrollConstructor)實(shí)例的本質(zhì)也是一個(gè)事件派發(fā)器,在創(chuàng)建插件時(shí),注入 bs 實(shí)例是為了讓插件間能通過統(tǒng)一的事件派發(fā)器進(jìn)行通信。
第一個(gè)問題我們已經(jīng)知道答案了,接下來我們來看第二個(gè)問題:”如何利用 bs 實(shí)例?“。要回答這個(gè)問題,我們將繼續(xù)以 PullUp 插件為例,來看一下該插件內(nèi)部是如何利用 bs 實(shí)例進(jìn)行消息通信的。
export?default?class?PullUp?implements?PluginAPI?{
??static?pluginName?=?'pullUpLoad'
??constructor(public?scroll:?BScroll)?{
????this.init()
??}
}
在 PullUp 構(gòu)造函數(shù)中,bs 實(shí)例會(huì)被保存到 PullUp 實(shí)例內(nèi)部的 scroll 屬性中,之后在 PullUp 插件內(nèi)部就可以通過注入的 bs 實(shí)例來進(jìn)行事件通信。比如派發(fā)插件的內(nèi)部事件,在 PullUp 插件中,當(dāng)距離滾動(dòng)到底部小于 threshold 值時(shí),觸發(fā)一次 pullingUp 事件:
private?checkPullUp(pos:?{?x:?number;?y:?number?})?{
??const?{?threshold?}?=?this.options
??if?(...)?{
??????this.pulling?=?true
??????//?省略部分代碼
??????this.scroll.trigger(PULL_UP_HOOKS_NAME)?//?'pullingUp'
??}
}
知道如何利用 bs 實(shí)例派發(fā)事件之后,我們?cè)賮砜匆幌略诓寮?nèi)部如何利用它來監(jiān)聽插件所感興趣的事件。
//?packages/pull-up/src/index.ts
export?default?class?PullUp?implements?PluginAPI?{
??static?pluginName?=?'pullUpLoad'
??constructor(public?scroll:?BScroll)?{
????this.init()
??}
??private?init()?{
????this.handleBScroll()
????this.handleOptions(this.scroll.options.pullUpLoad)
????this.handleHooks()
????this.watch()
??}
}
在 PullUp 構(gòu)造函數(shù)中會(huì)調(diào)用 init 方法進(jìn)行插件初始化,而在 init 方法內(nèi)部會(huì)分別調(diào)用不同的方法執(zhí)行不同的初始化操作,這里跟事件相關(guān)的是 handleHooks 方法,該方法的實(shí)現(xiàn)如下:
private?handleHooks()?{
??this.hooksFn?=?[]
??//?省略部分代碼
??this.registerHooks(
????this.scroll.hooks,
????this.scroll.hooks.eventTypes.contentChanged,
????()?=>?{
??????this.finishPullUp()
????}
??)
}
很明顯在 handleHooks 方法內(nèi)部,會(huì)進(jìn)一步調(diào)用 registerHooks 方法來注冊(cè)鉤子:
private?registerHooks(hooks:?EventEmitter,?name:?string,?handler:?Function)?{
??hooks.on(name,?handler,?this)
??this.hooksFn.push([hooks,?name,?handler])
}
通過觀察 registerHooks 方法的簽名可知,它支持 3 個(gè)參數(shù),第 1 個(gè)參數(shù)是 EventEmitter 對(duì)象,而另外 2 個(gè)參數(shù)分別表示事件名和事件處理器。在 registerHooks 方法內(nèi)部,它就是簡(jiǎn)單地通過 hooks 對(duì)象來監(jiān)聽指定的事件。
那么 this.scroll.hooks 對(duì)象是什么時(shí)候創(chuàng)建的呢?在 BScrollConstructor 構(gòu)造函數(shù)中我們找到了答案。
//?packages/core/src/BScroll.ts
export?class?BScrollConstructor?extends?EventEmitter?{
??constructor(el:?ElementParam,?options?:?Options?&?O)?{
????//?省略部分代碼
????this.hooks?=?new?EventEmitter([
??????'refresh',
??????'enable',
??????'disable',
??????'destroy',
??????'beforeInitialScrollTo',
??????'contentChanged',
????])?
??}
}
很明顯?this.hooks?也是一個(gè)?EventEmitter?對(duì)象,所以可以通過它來進(jìn)行事件處理。好的,插件通信的內(nèi)容就先介紹到這里,下面我們用一張圖來總結(jié)一下該部分的內(nèi)容:

介紹完 BetterScroll 插件化架構(gòu)的實(shí)現(xiàn),最后我們來簡(jiǎn)單聊一下 BetterScroll 項(xiàng)目工程化方面的內(nèi)容。
五、工程化方面
在工程化方面,BetterScroll 使用了業(yè)內(nèi)一些常見的解決方案:
lerna:Lerna 是一個(gè)管理工具,用于管理包含多個(gè)軟件包(package)的 JavaScript 項(xiàng)目。 prettier:Prettier 中文的意思是漂亮的、美麗的,是一個(gè)流行的代碼格式化的工具。 tslint:TSLint 是可擴(kuò)展的靜態(tài)分析工具,用于檢查 TypeScript 代碼的可讀性,可維護(hù)性和功能性錯(cuò)誤。 commitizen & cz-conventional-changelog:用于幫助我們生成符合規(guī)范的 commit message。 husky:husky 能夠防止不規(guī)范代碼被 commit、push、merge 等等。 jest:Jest 是由 Facebook 維護(hù)的 JavaScript 測(cè)試框架。 coveralls:用于獲取 Coveralls.io 的覆蓋率報(bào)告,并在 README 文件中添加一個(gè)不錯(cuò)的覆蓋率按鈕。 vuepress:Vue 驅(qū)動(dòng)的靜態(tài)網(wǎng)站生成器,它用于生成 BetterScroll 2.0 的文檔。
因?yàn)楸疚牡闹攸c(diǎn)不在工程化,所以上面阿寶哥只是簡(jiǎn)單羅列了 BetterScroll 在工程化方面使用的開源庫(kù)。如果你對(duì) BetterScroll 項(xiàng)目也感興趣的話,可以看看項(xiàng)目中的 package.json 文件,并重點(diǎn)看一下項(xiàng)目中 npm scripts 的配置。
當(dāng)然 BetterScroll 項(xiàng)目還有很多值得學(xué)習(xí)的地方,剩下的就等大家去發(fā)掘吧,歡迎感興趣的小伙伴跟阿寶哥一起交流與討論。
六、參考資源
BetterScroll 2.0 文檔
