对于早期的前端 SPA 项目,Backbone.js + Require.js 是一种常见的技术组合,分别提供了基础的 MVC 框架和模块化能力。
对于这样的既有项目,在之前的文章中也进行过分析,常常面临依赖不清、封装混乱,以及缺乏测试等问题;对之进行维护和新需求开发时,结合其本身特点,在 TDD 的方式下进行渐进的改善,而非推倒重来,无疑是个可行的办法。
本文将尝试用一个重构实例来抛砖引玉,讲解如何对其应用较新的 jest 测试框架,并用 ES6 class 等新手段升级 Backbone.View 视图组件和改善页面结构,希望能对类似项目的改善起到开启思路的作用。
关于测试、重构的各种概念,不再重复介绍;请先参阅以下文章:
Backbone.js / Require.js 技术栈回顾
Require.js 模块化
首先说 Require.js,在没有 webpack 的日子里,这是最常见的模块化管理工具。
其本身可以提供 AMD 规范的 JS 模块,并提供了通过插件加载文本模板等能力。
在实际的项目中,我们采用了 ES6 语法和 ESM 模块规范来编写源文件,并借助 babel 将其转译为 UMD 模块;最后通过 Require.js 提供的优化工具 来打包,并由 Require.js 本身在浏览器里实现模块的加载。
当然,采用 ES6语法 和 babel 并非一定必要,AMD 也是可以实现测试的。
Backbone.js
不同于提供整套方案的 Angular 的是, Backbone.js 提供了一个非常基础和自由的 MVC 框架结构,不仅可以用多种方式组织项目,也可以自由替换其中的某一部分。
其主要功能模块包括:
Events:提供一系列事件的绑定和触发等功能
Model: 对数据或状态的转化、校验、计算派生值、提供访问控制等,也负责数据的远程同步等,并有事件触发机制;作用类似于 MobX
Collection: Model 的集合
Router: 提供了 SPA 的前端路由功能,支持 hashChange 和 pushState 两种方式
Sync: 一些远程请求的方法
View: 可以拼装模板数据、绑定事件等的视图组件
在我们的实际项目中,视图层同时支持了 Backbone.View 和早期的 react@13,这也正体现了其灵活之处。
通常的 Backbone 项目也可以忽略文中涉及 react 的部分。
升级测试框架
和之前文章中的例子相同,本次依然采用 Jest 作为测试框架。
原有用例
早期的项目中其实是有一些单元测试代码的,主要是用 Jasmine 对部分 model/collection 进行了测试。由于 Jest 内置了 Jasmine2,所以这部分的语法问题不大,基本可以无痛迁移。
早先测试的主要问题在于:
一是没有整合到工作流中,采用单独的网页作为载体,久而久之就会遗忘这个步骤,用例可能失效,新加入的团队成员也不会注意到这项工作的存在
二是当时对 model/collection 的单元测试并不严谨,依赖了提供 mock 数据的 php 服务器环境
三是由于视图层没有很好的组件化,从而缺乏对视图组件的测试
jest for Backbone 的实践
jest 是比较新的测试框架,默认零配置,但也提供了灵活的适配方法,可以适应各种项目,包括 Backbone.js 的情况。
这位 @captbaritone 小哥提供了一个很好的讲解视频 (需要科学上网 https://www.youtube.com/watch?v=BwzjVNTxnUY&t=15s),并且配上了实例代码(https://github.com/captbaritone/tdd-jest-backbone)。
配置必要的依赖和映射
配置两种 npm script,分别用于开发时实时运行测试和 build 时运行测试
目标项目中,其实是用 babel 5 做的 ES6 转译;但是由于之前的源代码已经全部采用了 ES6 语法开发(部分初始 AMD 代码也做过自动转化),所以我们完全可以在测试时采用较新的 babel 6
加入对老版本 react 的支持
根据目标项目的情况采用了 enzyme-adapter-react-13 做适配
用 cross-env 设置环境变量 test,从而配置出适用于 jest 的 .babelrc 文件,且不影响生产环境
根据项目中的具体情况,按原来的规则做好组件名称的映射
将单元测试加入到 build 任务
如果只写好了测试,而单独存在,只能用 执行的话,那就重蹈了原来的覆辙;这里借助 插件将其加入已有的 工作流:
这样在之后的 build 任务中,一旦有单元测试未通过,整个流程将停止执行。
测试 model 和 collection
一个 model 大概长这个样子:
在测试中注入全局 url 前缀
可以发现 model 中依赖了以个全局变量中的属性
首先编写一个假的全局对象:
测试套件中,在 model 之前引入这个模块就可以了:
用 sinon 拦截异步请求
搞定了异步请求的地址,自然要拦截真正的请求;
Backbone 中的请求,包括 Backbone.sync / model.fetch() 等, 本质上还是调用的 jQuery 中的 方法(默认情况下),也就是传统的 xhr 方式,使用 sinon 就可以很好的胜任这种暗度陈仓的工作:
校验操作的测试
调用 Backbone.Model 实例的 isValid() 方法,会得到数据是否有效的布尔值结果,同时触发内部的 validate() 方法,并更新其 validationError 的值;利用这些特性,我们可以做如下测试:
collection 的测试和 model 相比并无特别,不再赘述
view 之必然的 testable 组件化
开篇提到过,项目中以前的过时测试用例中,是缺少 view 视图层部分的。
这一方面是囿于当时测试意识的不足,更主要的原因是没能很好解决组件化的问题。
要对 view 进行测试,就得将其拆分重构为功能明确、便于复用的各种小型组件。
Backbone.View 的 ES6 class 进化
首先进行的,是类似于 React.createClass 向 class extends Component 的进化,Backbone.View 也是可以华丽转身的。
传统的 view 写法是这样的:
采用 ES6 class 的写法,则可能是:
组件的提取
目标项目的很多页面,没有合理的封装出子组件,而仅仅是把需要复用部分的 html 提取成模板,在本页面“拼装”多个子模板,或和其他页面复用。这部分归因于 Backbone 的“过分自由”,官网或者当时的通用实践中并未给出很好的组件化方案,只是停留在用依赖的 underscore 实现 _.template() 的阶段。
另一个难点在于,Backbone.View 的 constructor / initialize “构造函数”中,并不能接受自定义的 props 参数。
解决的办法是进行一定的外层封装:
也可以“继承”一个 View:
在页面中使用时,先传参获取到真正的 Backbone.View 组件:
再手动调用其 render() 方法并加入页面视图的 DOM 中:
这样就在很大程度上实现了 Backbone.View 组件的封装和嵌套。
测试 Backbone.View 组件
比之于测试 react 还需要 enzyme 等的支持,测试 Backbone.View 其实要简单许多,只需要获取到其 $el 属性,调用 jQuery 的惯有方法即可:
对方法调用的测试
自然还是用 sinon 来做:
处理用 require.js 的 text 插件引入的模板
Backbone.js + Require.js 在测试中的一个小问题是:页面或组件中一般会用 text.js 组件引入模板,其 ES6 形式为:
因为测试环境没有 require.js 或者 webpack 的加持,我们只能想办法将其劫持,并将正确的结果注入对应的测试模块中;
要实现这一目的,就要用到 方法,其缺点是用了这个就不能用 ES6 的 import 语法了,配置和使用简要说明如下:
总结
jest 灵活的配置能力,使其能方便的应用于各种类型既有项目的 TDD 开发和重构
之前的其他测试框架下的用例,可以快速迁移到 jest 中
Backbone.View 视图组件在经过 ES6 升级和合理封装后,可以明显改善页面的整洁度,并顺利应用于单元测试
可以用 sinon.createFakeServer() 拦截 Backbone.Model 中的异步请求
原来用 Require.js 下的 text.js 组件引入的模板,也可以用 jest.doMock() 很好的支持
将单元测试任务加入原有的 build 工作流,可以保证相关代码之后的持续有效
(end)
-------------------------------------
领取专属 10元无门槛券
私享最新 技术干货