做者:嵇智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。