ASS.js 模块化笔记
ASS.js 是我两年前开坑的项目,直到最近整个项目代码都还是写在一个文件里的,我一个人开发维护没什么问题,但是别人如果要看源码的话可能会比较吃力(虽然没人来看)。而且我也在考虑加上一些测试,但因为整个文件对外只输出一个 ASS
变量,没法获取内部变量,不知道如何给一个 IIFE 做测试。于是我开始考虑把整个项目模块化以及添加构建过程。
目标
ASS.js 作为一个前端库,对用户应当只提供一个 ass.js 文件,对外通过 UMD 模式只暴露一个 ASS
构造函数,然后用户 new 一个实例出来就可以调用 API 了。虽然是有一些全局的 CSS,但还是可以通过 JavaScript 去添加,避免用户再引入一个 ass.css 文件。在 ASS.js 中因为生成 CSS Animation 的效率问题,我有考虑使用 Worker 来提高性能,Chrome 和 Firefox 可以使用 createObjectURL
模拟文件链接,IE 要使用 Worker 就不得不另增一个 ass-worker.js 文件了。不过我之后是考虑使用 JavaScript 动画代替 CSS 动画,所以目前还是不用 Worker 了。
ASS.js 这样涉及到图形渲染的前端库似乎是比较难做测试的。我目前还完全没有写测试的经验,只是有个大概的概念。解析部分应该就是比较典型的单元测试,根据 ASS 规范给定输入能有期望的输出就行;渲染部分我就不知道该怎么测试了,是去模拟用户操作吗?是去判断生成的 DOM 的结构和属性吗?是渲染后截图与 xy-VSFilter 或 libass 的截图对比吗?因为 ASS 规范中奇葩的字号设定,ASS.js 中的字号只能去模拟 VSFilter,没法完全相同,这样截图就基本行不通了。初步的目标是把解析部分都写好测试,渲染部分暂时人工测试,等以后慢慢摸索最佳方案。
最终自动化构建的流程应该是,运行某条命令后,跑完测试,计算覆盖率,生成 ass.js 和 ass.min.js 文件。
方案选择
测试框架准备用 Mocha,除了之前大致看过它的用法,相比于 Jasmine,个人对 Mocha 单词本身更有好感,毕竟作为一个萌二,能从抹茶上感受到一些日系要素。什么?你说抹茶起源于中国?但抹茶现在兴盛于日本而且说不定这个词还是逆输入的。什么?你说 Mocha 的意思是摩卡,Matcha 才是抹茶?我……第一次知道 Mocha 不是抹茶我的内心是崩溃的,然后搜到一个帖子,最早是叫「末茶」,传到日本后变成「抹茶」,英文 Matcha 便是「まっちゃ」的罗马音。不过不管是 Mocha 还是 Matcha,都可以 Gulp 掉。于是构建工具就决定用 Gulp 了。
相比于 AMD,我对 CommonJS 更加熟悉,最开始我是决定用 CommonJS。虽说 AMD 更适合浏览器环境,但 ASS.js 最终是打包成一个文件的,不管选哪个都只是内部代码的组织方式,与运行环境无关。而且测试框架选择了 Mocha,总得在 Node.js 下运行吧?CommonJS 应该会更方便。
关于打包器,有 Browserify 和 Webpack 可以选择,但因为我都没有实际用过,也不知道哪个更合适。还有一个比较火的是 Rollup,只把模块中要用到的部分打包进来,最终的代码体积会相对比较小。但是 ASS.js 中不会引入第三方依赖,所有代码都是自己写的,必然都会用到,那么它文件体积的优势也不明显了吧。而且 Rollup 专注于 ES6 模块,似乎还不能直接打包 CommonJS 模块,需要另外插件支持。不过 Rollup 打包出来的代码会比较干净,Browserify 和 Webpack 都会在打包出来的文件中加一大坨辅助函数,尤其是 Webpack 的 CSS 加载插件,这一大坨代码只是为了设置 body {background: yellow;}
,根本没法忍。那么要不要把 ASS.js 用 ES6 重写呢?目前如果代码要在浏览器运行,就不得不把 ES6 转成 ES5,我没有深入了解过 Babel,虽然知道顾虑是多余的,我还是不太放心转换出来的代码。
我也去参考了一些其他的前端项目,jQuery 是通过 AMD 方式模块化的;three.js 是直接把 THREE
对象作为全局变量,函数全写在对象里面,最后直接把文件们合在一起;two.js 把各个模块用匿名函数包一下只传了 Two
和几个依赖库的变量进去,最后也直接把文件合在一起了,事实上和 three.js 一样,也就是给 Two
添加方法和属性的方式;D3.js 是用作者自己写的 smash 打包,基本原理大概也就是根据 import 的内容直接合并文件吧,而且已经不再维护并建议使用 Rollup。
实现
总之我开始尝试把 1300 行的单个文件拆分成多个 CommonJS 模块。然后就发现非常痛苦,代码的耦合性太高了,经常某个函数要用到一个内部的全局变量,或者 this
要绑定到 ASS
这个构造函数。已经拆不下去了,可能要重写不少代码。之前有看过函数式编程相关的文章,感觉完全做到是不太可能的,可以尽量往那边靠吧。于是我重新思考到底要不要用 CommonJS 模式?不用的话 Mocha 就没法直接 require 到,怎么测试?然后发现了 rewire 可以获取模块的内部变量,这样测试就没问题了。
最终方案是一个文件一个函数,构建时直接合并到一起,最后外面包一个 UMD。这样生成的代码结构会和未拆分前的结构完全一致,有种莫名的安心感。本来我模块化主要就是为了可以添加测试,并不是为了模块化而模块化。原先全局的 CSS 我是直接写成一行作为字符串赋值给 JavaScript 变量然后添加的,现在有了构建过程可以独立写到一个文件,然后在构建时压缩一下替换 JavaScript 文件中的变量。当然对于每一个函数还是要继续改进,要尽可能地减少耦合,不然测试起来还是会很麻烦。
拆分的时候遇到了程序员最难的问题:命名。该怎么决定目录结构和文件名啊,于是又去看了下其他的项目。src 放源文件,tests 放测试文件,dist 放生成的目标文件,这些基本可以确定。src 里面该是怎样的结构?想了好久,感觉 ASS.js 可以分为解析和渲染两部分,便是 parser 和 renderer 两个目录,平级的放一个 index.js 入口文件。不过解析很容易确定,某个函数到底算不算渲染就比较纠结了。总之姑且是这么分了一下,等更加函数式之后再改吧。