做者:嵇智vue
距離 BetterScroll v1 版本發佈,至今已經 3 年多,因爲它在移動端良好的滾動體驗與性能以及多種滾動場景的支持,深受社區的青睞。用戶也能夠基於 BetterScroll 抽象出各類複雜的業務滾動組件,期間依託於 BetterScroll,咱們還開源了基於 Vue2.0 的移動端組件庫 cube-ui。node
目前 BetterScroll 的 star 數已經超過 1.1 萬,GitHub 有大約 3.2 萬倉庫使用了它。滴滴內部的業務,好比國內司乘兩端、國外司乘兩端等核心業務都大量使用 BetterScroll,它經受住了各類業務場景的考驗。webpack
隨着大量的業務場景使用以及社區的反饋與建議,v1 版本也暴露了一些問題,主要分爲以下四個方面:git
先來看下最終的總體 BetterScroll v2 版本的架構圖:github
從總體架構圖能夠看出,目前總體 BetterScroll v2 版本除了實現核心滾動外,還額外提供不少插件:web
v2 版本的誕生就是爲了解決 v1 暴露出來的問題,這裏咱們將從上面的四個問題分別來揭祕重構過程當中的思考與實踐。chrome
v1 的架構設計借鑑於 Vue 2.0 的代碼組織方式,可是因爲不一樣的 Feature(picker、slide、scrollbar 等
) 都是與核心滾動寫在一塊兒,致使沒法按需引入。typescript
備註:此處的按需引入指的是用戶可能只須要實現簡單的列表滾動效果,卻被迫加載冗餘代碼,好比全部 Feature 的代碼,形成包體積過大的問題。npm
爲了解決這個問題,咱們就必須找到一種合理的方式將各個 Feature 代碼單獨拆分,獨立引用,答案就是插件化方案。那麼 v2 版本的一個核心關鍵點就是如何設計插件化的機制,咱們當時是從下面三個步驟來思考的:瀏覽器
因爲拆分紅細粒度的功能類,考慮到老用戶監聽事件或者獲取屬性都是操縱 CoreScroll,咱們內部有統一的事件冒泡層以及屬性代理層,將內部類的事件或者屬性都代理到 CoreScroll 上。
借鑑 webpack tapable 延伸出來的 hooks
的概念(並不須要 tapable 那麼強大),職能類之間經過 hooks
(即 EventEmitter 經典的訂閱發佈者模式加強版) 來處理流程中鉤子邏輯;
借鑑 Vue 2.x 插件註冊機制(代碼以下),減小老用戶的心智負擔。
import BScroll from '@better-scroll/core'
import Slide from '@better-scroll/slide'
// 只需註冊插件便可,無額外心智負擔
BScroll.use(Slide)
let bs = new BScroll('.wrapper', {
slide: { /* 插件配置項 */ }
})
複製代碼
所以 v2 的總體雛形就已經好了,考慮到後期會有不少插件實現不一樣的業務場景需求,v2 版本採用了 Lerna 來管理多個包,使用 @better-scroll
做爲包的命名前綴,這樣對於用戶來講有更好的辨識度。TypeScript 的靜態類型,加上整個的社區十分紅熟豐富的生態,BetterScroll 自己 Feature 已經不少,且將來還會繼續增長,綜合看很是適合用 TypeScript 進行開發。
TIPS:
Lerna 發包失敗始終是開發者(包括做者)繞不過去的話題,目前也有不少 issue 與博客在討論這個問題,供參考:lerna 發佈失敗後的解決方案、lerna issue 1894、publish 失敗問題
v1 版本新增 Feature 的時候,有些邏輯代碼是與核心滾動代碼糅合在一塊兒,形成後期擴展可維護性都會慢慢下降,隨之而來的困擾也有包體積無限制的增長。那麼若是將 Feature 與 核心滾動 CoreScroll 部分進行完全分離,將 Feature 作成插件的模式,既能解決包體積的問題,擴展也變得相對容易,迭代的穩定性也變好了。
在 v2 版本中,一個插件的通常實現以下:
class InfinityScroll {
static pluginName = 'infinity'
constructor(public bscroll: BScroll) {
// ...your own logic
}
}
// 假設已經註冊了 InfinityScroll
new BScroll('.wrapper', {
infinity: { /* 插件配置項 */ }
// infinity 要與插件的 pluginName 對應上
})
複製代碼
插件必須擁有一個靜態屬性 pluginName,這個屬性對應的值必須與初始化 BetterScroll 傳入的配置對象的 key 對應,不然內部查找不到對應的插件。這個方案充分考慮了開發者使用時候的成本,同時也儘可能下降和 v1 版本的差別。
在實現了核心的插件機制後,對於各類 Feature 則是經過一個個插件的形式來豐富 BetterScroll 的總體生態。
在 v1 版本中,測試覆蓋率不到 40%,可能也是由於 BetterScroll 在以前是一個巨大的類,編寫單元測試也逐漸地困難了起來,這樣在後期迭代升級的時候會埋下隱患,這也就是所說的穩定性保證差。
那麼在 v2 版本,爲了保證總體功能的穩定性,控制發版質量,咱們不但添加了單元測試,還額外引入了功能測試作進一步保障。
單元測試
以前參與的 cube-ui 的單測是採用 karma + mocha
的方案,不過須要安裝各類插件,還須要作很多配置。已經 0202 年了,最終調研對比發如今現有的 BetterScroll 場景中使用 Jest 做爲測試框架是合適的,它自己集成了 Mock
、Test Runner
、Snapshot
等強大的功能,基本上算是開箱即用,很好的知足須要。
在編寫單元測試過程當中,用的最可能是強大的 manual-mocks 能力。
舉個簡單的場景來深刻淺出地闡述咱們對單元測試的見解以及如何藉助 Jest manual-mocks 解決問題。
假如咱們的源碼文件結構以下:
- src
- Core.ts
- Helper.ts
複製代碼
Core
與 Helper
的代碼以下:
// Core.ts 代碼以下
export default class Core {
constructor (helper: Helper) {
this.helper = helper
}
getHammer (type: string) {
if (this.helper.isHammer(type)) {
return ('Got hammer')
} else {
return ('No hammer is available')
}
}
}
// Helper.ts 代碼以下
export default class Helper {
isHammer (type: string) {
return type === 'hammer'
}
}
複製代碼
準備工做就緒,如今要開始測試 Core#getHammer
函數,這時咱們核心開發成員之間發出了兩種不一樣的聲音。
方案一:導入 Helper
原始代碼(即 src/Helper.ts
),讓其走全流程;
方案二:單元測試應該以函數或者類做爲最小的粒度,作法傾向於傳統的測試行業的概念,認爲 Helper
應該被 mock 掉(使用 src/__mocks__/Helper.ts
),換句話來講, Helper
做爲另一個測試單元,它必須保證本身的功能徹底正確,但對於 Core.ts
的單測,不該該引入原始的 Helper
。
最後的最後,咱們選擇了更爲嚴謹的方案二。
藉助於 Jest manual-mocks 的能力,編寫測試就變得更愉快與明確了。
更改文件結構
src
+ __mocks__
+ Helper.ts
+ __tests__
+ Core.spec.ts
Core.ts
Helper.ts
複製代碼
加了目錄 __mocks__
以及 __mocks__/Helper.ts
文件,而且加了測試目錄 __tests__
與 Core.spec.ts
。
完善 manual-mocks
// __mocks__/Helper.ts
const Helper = jest.fn().mockImplementation(() => {
return {
isHammer: jest.fn().mockImplementation((type) => {
return type === 'MockedHammer'
})
}
})
export default Helper
複製代碼
編寫 Core.spec.ts
import Helper from '../Helper.ts'
import Core from '../Core.ts'
// 使用 '__mocks__/Helper.ts'
// 引入的 Helper 就是 mock 處理過的~
jest.mock('../Helper.ts')
describe('Core tests', () => {
it('should work well with "MockedHammer"', () => {
const core = new Core(new Helper() // Mock 事後的 Helper)
expect(core.getHammer('MockedHammer')).toBe('Got hammer') // 經過
})
})
複製代碼
從上述能夠看出,咱們利用 Jest 更改了 Helper.ts
的導出,用的是 __mocks__
目錄下的,再也不是原始的 Helper.ts
,這樣各個模塊自身須要保障自身邏輯正確性,同時對於異常分支的邏輯測試會變得更容易。
頗有趣, 對吧?
功能測試
因爲 BetterScroll 是一個與瀏覽器強相關的滾動庫,單元測試是用來保證單個模塊的輸入輸出正確性,因此還須要其餘的手段來保證核心滾動、插件等的行爲表現符合預期,所以咱們就採用了 jest-puppeteer,它的理念就是 Run your tests using Jest & Puppeteer,這裏有必要介紹一下 Puppeteer。
Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol
用個人工地英語翻譯一下就是:
Puppeteer 是一個經過 DevTools 協議控制 Chrome 行爲而且提供更優雅的 API 的 Node 類庫。
DevTools 這個協議很重要,接下來仍會說起到。
你打開它的官網會發現,它的功能有不少,包括生成 PDF、表單、UI 測試、谷歌插件測試等等,網上也有不少文章介紹如何使用它來作爬蟲。
下面截取核心滾動的功能測試片斷代碼:
describe('CoreScroll/vertical', () => {
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/core/default')
})
it('should render corrent DOM', async () => {
const wrapper = await page.$('.scroll-wrapper')
const content = await page.$('.scroll-content')
expect(wrapper).toBeTruthy()
await expect(content).toBeTruthy()
})
it('should trigger eventListener when click wrapper DOM', async () => {
let mockHandler = jest.fn()
page.once('dialog', async dialog => {
mockHandler()
await dialog.dismiss()
})
// wait for router transition ends
await page.waitFor(1000)
await page.touchscreen.tap(100, 100)
await expect(mockHandler).toHaveBeenCalled()
})
})
複製代碼
從上邊的示例代碼能夠看到,Puppeteer 的 API 都是很是語義化的,並且內部的 API 都是返回 Promise。
在逐漸豐富功能測試的時候,仍是很愉快的,可是難題仍是不期而遇。
BetterScroll 功能測試強相關聯 Touch、Mouse、MouseWheel 等事件,然而此時的 Puppeteer(v1.17.0) 並無提供所有的接口。
既然 Puppeteer 是一個經過協議控制 Chrome 的類庫,那爲啥不把它內部的實現先粗略的瞭解一下呢?
秉着這個想法,在研究了 Puppeteer 的核心實現,最終整理髮現,只要理清一條主線,其他的是照葫蘆畫瓢、參考 DevTools Protocol 文檔便可。
下面是簡略的流程圖。
第一步:利用 node 的 child_process 模塊啓動 Chromium
;
第二步:監聽命令行的輸出,獲取 browserWSEndpoint
,它是一個 URL 地址,傳給 WebSocket,這樣 Puppeteer 與 Chromium 的雙向推送關係就創建了;
第三步:實例化 Connection,創建 Session 會話以及 實例化 Browser 類,那麼用戶操做的都是這個 browser 實例,好比打開一個頁面標籤(browser.newPage()
)。在實例化 Connection 的內部,其實有不少細節,DevTools Protocol 就是現成的 API 文檔,換句話來講,只要咱們按着這個 API 文檔經過 WebSocket 給 Chromium 去發消息,就能驅使它做出響應的行爲。
接下來結合文檔以及源碼,咱們發現只要發送 Input.synthesizePinchGesture
以及 Input.synthesizeScrollGesture
消息(文檔在這),就能驅使 Chromium 做出 scroll、 zoom、mouseWheel 等事件交互效果,那麼對於 BetterScroll 的各類插件以及核心滾動的功能測試就手到擒來啦!
所以,咱們對 Puppeteer 作了部分擴展,extendTouch、extendMouseWheel 以知足功能測試須要。
那麼功能測試的寫的任務就算能夠所有完成啦。
功能測試算是告一段落了,可是新問題又出現了:跑功能測試,是依賴 examples 下的代碼來啓動服務,而後在用 Puppeteer 去訪問示例代碼的服務,最後跑全部的測試用例。也就意味着跑功能測試就須要先把服務準備好,再跑功能測試,這裏咱們須要一種更爲工程化的手段來解決這個問題!
這個問題的關鍵是怎麼確保 examples 代碼的服務啓動再跑功能測試,那麼是否是能夠從 webpack
下手,尤爲是 webpackDevServer
。經過研究它的源碼實現,發現內部引用的 webpack-dev-middleware,其中有一個 API,叫作 waitUntilValid
,接收一個 callback
。這個 API 能保證服務已經啓動而且 bundle 是可訪問的。
那麼解決方案就以下,在 vue.config.js
注入 webpack 的 配置:
module.exports.configureWebpack = {
devServer: {
before (app, server) {
server.middleware.waitUntilValid(() => {
// 服務已經 ready,啓動 e2e 測試
execa('npm', ['run', 'test:e2e'], { stdio: 'inherit' })
})
}
}
}
複製代碼
至此,這就是測試部分的探索以及實踐,作完這部分,對咱們自身而言,有個最大的體會:工程師的價值在於探索與解決問題。
v1 版本的文檔以及示例代碼頗受吐槽,尤爲是示例部分給了新入坑的小夥伴們很大的心智負擔,好比文檔內部沒有實際代碼片斷、示例耦合各類無關的 Vue 邏輯。在 v2,這些問題將會獲得改善。
首先因爲咱們的技術棧是 Vue,其周邊 VuePress 則是一個很好用的文檔框架,它將 Vue、webpack、Markdown 的能力發揮到極致,也能很好的定製主題、實現國際化,而且它插件化的架構設計給 VuePress 帶來了很大的靈活性以及擴展能力,因此咱們就選型了 VuePress 來完成相關 API 文檔化。儘管 VuePress 開箱即用,基本知足咱們編寫文檔的大部分要求,但仍然須要額外的一些擴展。
這裏想要實現上面圖片的功能,要有二維碼,組件的代碼片斷,要把 examples 目錄下的組件真正渲染在 markdown 裏面。第一和第三點都特別好實現,VuePress 提供這能力,可是第二點,在 markdown 同步展現 examples 組件對應的代碼,這是個棘手的問題。
那麼,深刻研究 VuePress 的實現是必要的,VuePress 內部是使用 markdown-it 來編譯 md
擴展名的文件。要解決這個問題,看來須要深刻研究下 markdown-it 的底層實現,也順道產出了 markdown-it 源碼以及插件的解讀系列;發現基於 VuePress 的插件機制能夠知足咱們定製化的需求,所以寫了 extract-code 插件,並約定 markdown 文件只要以下的代碼,那麼就會被 extract-code
處理。
// 抽取 default.vue 文件的 template 標籤內容
<<< @/examples/vue/components/infinity/default.vue?template
// 抽取 default.vue 文件的 script 標籤內容
<<< @/examples/vue/components/infinity/default.vue?script
複製代碼
如此一來,咱們每次更改 examples 下面的示例代碼,文檔也會同步更新到對應的部分。
注意: 因爲 VuePress 爲了加快 markdown 文件的編譯速度,內部使用 cache-loader 作緩存,意思是若是 markdown 內容沒有發生變化,直接取緩存的內容,雖然示例代碼變化,可是對於 markdown 文件來講,內容實際上是未改變的。
TIPS: 若是你不喜歡代碼塊的主題,能夠研究下大名鼎鼎的 prism,由於 VuePress 的內部就是用這個插件去作高亮的。
回顧咱們在作 BetterScroll 2.0 版本的大致歷程,一路雖有坎坷,但更多的是收穫、總結和沉澱。
固然,這一切都是團隊內同窗的共同努力,核心同窗:嵇智、馮偉堯、崔靜,社區同窗 YuLe 的屢次貢獻,也還有不少同窗提了很好的建議,謝謝你們的辛勞、貢獻,這是一個彼此學習、共同成長的過程。也要額外感謝 BetterScroll 原做者黃軼大佬的信任。
BetterScroll 2.0 目前通過了 20 多個 alpha 版本,已經發布了 beta 版本,可是倒是已經穩定了的版本,內部和社區已經有了大量的下載使用,將來咱們會持續作一些事情:
同時,也會在 BetterScroll 2.0 的基礎上產出新版本的組件庫,在本來已經優化、提效的基礎之上進行二次提效,助力業務。
但願能有愈來愈多的人使用,同時也有更多的你參與進來,一塊兒共建,讓 BetterScroll 的整個生態變得 Better。