流程
关于流程,是从项目启动到发布的过程。在前端通常我们都做些什么?
- 切图,即从设计稿中获取需要的素材,并不是所有前端开发都被要求切图,也不是所有前端开发都会切图,但请享受学习新知识的过程吧。
- 创建模版(html、jade、haml)、脚本(javascript、coffeescript)、样式(css、less、sass、stylus)文件,搭建基础的项目骨架。
- 文件(jade、coffeescript、less、sass...)编译
- 执行测试用例
- 代码检测
- 移除调试代码
- 静态资源合并与优化
- 静态资源通过 hash 计算指纹化
- 部署测试环境
- 灰度发布现网
工具化
每个流程中的过程单元,我们抽象为一个 Task,即任务。把可重复规则的过程进行工具化,如把 JavaScript 代码压缩过程工具化,而 UglifyJS 是具体执行任务的工具,CSS 代码压缩器 CleanCSS 是具体执行任务的工具。
工具文化几乎是大平台互联网公司共有的特质,我们无法确定是工具文化驱动了 Google、Facebook 这类互联网公司的快速发展,还是快速发展的需要使其在内推广工具文化,但可以明确的是工具文化必不可少。在 Facebook 第二位中国籍工程师王淮的书中也提到提到:
当时招聘他进 Facebook 的总监黄易山,是对内部工具的最有力倡导者:
|
1 2 |
他极度建议,公司要把最好的人才放到工具开发那一块,因为工具做好了,可以达到事半功倍的效果,所有人的效率都可以得到提高,而不仅仅是工程师。 |
在腾讯,工具文化虽没有被明确指出,但大平台公司对工具化的坚持是一致的:凡是被不断重复的过程,将其工具化,绑定到自动化流程之中。技术产品也需要 Don’t make me think 的方式来推广最佳实践。总而言之:依靠工具,而不是经验。
自动化流程
任务工具化是自动化流程的基础,我想你已经听说过任务运行器 Grunt。Grunt 帮助开发者把任务单元建立连接,如代码编译 Task 执行完后执行检测 Task,检测 Task 执行完后执行压缩 Task。虽然 Grunt 是基于 Node.js 平台,但其定位是个通用任务管理器,通用往往意味着更高的学习与实施成本。专注于 Web 开发领域腾讯有 Mod.js 来实施前端自动化,通过 Mod.js 有效的简化 Web 开发自动化流程实施成本。
实施 Mod.js
Mod.js 并不是简单的任务运行器,其内置集成了 Web 前端开发常用的工具集,覆盖了 80% 的前端使用场景,而另外的 20% 则可通过 Mod.js 的插件机制来扩展。
相遇
Mod.js:https://github.com/modjs/mod 可通过 NPM 来安装最新的版本, 在你来到 Node.js 的编程世界时已同时附带了 NPM,当前 Mod.js 最新版本 0.4.x 要求 Node.js 要求>= 0.8.0:
|
1 |
<span class="nv">$ </span>npm install modjs -g |
-g 参数表示把 Mod.js 安装到全局,如此 mod 命令将会在 system path 内,方便在任何一个目录启动 Mod.js 任务。
相识
Mod.js 通过 Modfile.js 文件驱动任务执行,可以手动创建一个 Modfile.js 文件,也可以通过模版初始化一个 Modfile.js 文件:
|
1 |
<span class="nv">$ </span>mod init modfile |
Modfile.js 是一个 Plain Node Module, 通过 Runner 对象来描述任务的具体执行过程:
|
1 2 |
<span class="c1">// 暴露Runner对象</span> <span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{}</span> |
如是异步配置,则可通过回调模式传递 Runner 对象:
|
1 2 3 4 5 6 7 |
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">options</span><span class="p">,</span> <span class="nx">done</span><span class="p">){</span> <span class="nx">setTimeout</span><span class="p">(</span> <span class="kd">function</span><span class="p">(){</span> <span class="c1">// 回调Runner对象</span> <span class="kd">var</span> <span class="nx">runner</span> <span class="o">=</span> <span class="p">{};</span> <span class="nx">done</span><span class="p">(</span><span class="nx">runner</span><span class="p">);</span> <span class="p">},</span> <span class="mi">1000</span><span class="p">)</span> <span class="p">}</span> |
借此一瞥通常 Runner 对象的全貌:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">version</span><span class="o">:</span> <span class="s2">">=0.4.3"</span><span class="p">,</span> <span class="nx">plugins</span><span class="o">:</span> <span class="p">{</span> <span class="nx">pngcompressor</span> <span class="o">:</span> <span class="s2">"mod-png-compressor"</span><span class="p">,</span> <span class="nx">compress</span> <span class="o">:</span> <span class="s2">"grunt-contrib-compress"</span> <span class="p">},</span> <span class="nx">tasks</span><span class="o">:</span> <span class="p">{</span> <span class="nx">asset</span><span class="o">:</span> <span class="s2">"asset"</span><span class="p">,</span> <span class="nx">online</span><span class="o">:</span> <span class="s2">"online_dist"</span><span class="p">,</span> <span class="nx">offline</span><span class="o">:</span> <span class="s2">"offline_dist"</span><span class="p">,</span> <span class="nx">offlinePackage</span><span class="o">:</span> <span class="s2">"{{offline}}/package.zip"</span><span class="p">,</span> <span class="nx">rm</span><span class="o">:</span> <span class="p">{</span> <span class="nx">online</span><span class="o">:</span> <span class="p">{</span> <span class="nx">dest</span><span class="o">:</span> <span class="s2">"{{online}}"</span> <span class="p">},</span> <span class="nx">offline</span><span class="o">:</span> <span class="p">{</span> <span class="nx">dest</span><span class="o">:</span> <span class="s2">"{{offline}}"</span> <span class="p">}</span> <span class="p">},</span> <span class="nx">replace</span><span class="o">:</span> <span class="p">{</span> <span class="nx">src</span><span class="o">:</span> <span class="s1">'./js/**/*.js'</span><span class="p">,</span> <span class="nx">search</span><span class="o">:</span> <span class="s2">"@VERSION"</span><span class="p">,</span> <span class="nx">replace</span><span class="o">:</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./package.json'</span><span class="p">).</span><span class="nx">version</span> <span class="p">},</span> <span class="nx">build</span><span class="o">:</span> <span class="p">{</span> <span class="nx">options</span><span class="o">:</span> <span class="p">{</span> <span class="nx">src</span><span class="o">:</span> <span class="p">[</span><span class="s2">"*.html"</span><span class="p">]</span> <span class="p">},</span> <span class="nx">online</span><span class="o">:</span> <span class="p">{</span> <span class="nx">dest</span><span class="o">:</span> <span class="s2">"{{online}}"</span><span class="p">,</span> <span class="nx">rev</span><span class="o">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">offline</span><span class="o">:</span> <span class="p">{</span> <span class="nx">dest</span><span class="o">:</span> <span class="s2">"{{offline}}"</span><span class="p">,</span> <span class="nx">rev</span><span class="o">:</span> <span class="kc">false</span> <span class="p">}</span> <span class="p">},</span> <span class="nx">cp</span><span class="o">:</span> <span class="p">{</span> <span class="nx">options</span><span class="o">:</span> <span class="p">{</span> <span class="nx">src</span><span class="o">:</span> <span class="p">[</span><span class="s2">"./img/**"</span><span class="p">]</span> <span class="p">},</span> <span class="nx">online</span><span class="o">:</span> <span class="p">{</span> <span class="nx">dest</span><span class="o">:</span> <span class="s2">"{{online}}/img/"</span><span class="p">,</span> <span class="nx">rev</span><span class="o">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">offline</span><span class="o">:</span> <span class="p">{</span> <span class="nx">dest</span><span class="o">:</span> <span class="s2">"{{offline}}/img/"</span><span class="p">,</span> <span class="nx">rev</span><span class="o">:</span> <span class="kc">false</span> <span class="p">}</span> <span class="p">},</span> <span class="nx">pngcompressor</span><span class="o">:</span> <span class="p">{</span> <span class="nx">src</span><span class="o">:</span> <span class="s2">"./img/**/*.png"</span> <span class="p">},</span> <span class="nx">compress</span><span class="o">:</span> <span class="p">{</span> <span class="nx">dist</span><span class="o">:</span> <span class="p">{</span> <span class="nx">options</span><span class="o">:</span> <span class="p">{</span> <span class="nx">archive</span><span class="o">:</span> <span class="s1">'{{offlinePackage}}'</span> <span class="p">},</span> <span class="c1">// includes files in path</span> <span class="nx">files</span><span class="o">:</span> <span class="p">[</span> <span class="p">{</span> <span class="nx">expand</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">cwd</span><span class="o">:</span> <span class="s1">'{{online}}/'</span><span class="p">,</span> <span class="nx">src</span><span class="o">:</span> <span class="p">[</span><span class="s1">'*.html'</span><span class="p">],</span> <span class="nx">dest</span><span class="o">:</span> <span class="s1">'qq.com/web'</span> <span class="p">},</span> <span class="p">{</span> <span class="nx">expand</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">cwd</span><span class="o">:</span> <span class="s1">'{{online}}/img'</span><span class="p">,</span> <span class="nx">src</span><span class="o">:</span> <span class="p">[</span><span class="s1">'**'</span><span class="p">],</span> <span class="nx">dest</span><span class="o">:</span> <span class="s1">'cdn.qq.com/img'</span> <span class="p">}</span> <span class="p">]</span> <span class="p">}</span> <span class="p">}</span> <span class="p">},</span> <span class="nx">targets</span><span class="o">:</span> <span class="p">{</span> <span class="k">default</span><span class="o">:</span> <span class="p">[</span><span class="s2">"rm"</span><span class="p">,</span> <span class="s2">"pngcompressor"</span><span class="p">,</span> <span class="s2">"replace"</span><span class="p">,</span> <span class="s2">"build"</span><span class="p">,</span> <span class="s2">"cp"</span><span class="p">],</span> <span class="nx">offline</span><span class="o">:</span> <span class="p">[</span><span class="s2">"default"</span><span class="p">,</span> <span class="s2">"compress:dist"</span><span class="p">]</span> <span class="p">}</span> <span class="p">}</span> |
version描述依赖的 Mod.js 版本plugins描述依赖的插件,支持 Mod.js 插件与 Grunt 插件tasks描述不同类别任务的执行targets描述不同组合的目标,目标是需执行任务的集合
Mod.js 的配置项追究极简易懂,即使不懂 JavaScript 语法也能看懂配置与修改配置。
相知
在执行 mod 命令时,Mod.js 会在当前目录下查找是否存在 Modfile.js 文件。当找到 Modfile.js 文件时,Mod.js 将读取 Modfile.js 里的配置信息,如识别到有配置 Mod.js 插件,会自动安装没有安装过的插件,插件不仅可以是发布到 NPM 的包,也可以是存在本地的自定义任务。
Mod.js 加载插件的方式是通过 Node 的 require 机制,然后执行暴露的 exports.run,这与 Mod.js 内置任务的完全一样的机制。
在命令行下,通常执行 mod 时是需指定 Modfile.js 中某一特定目标,但当存在命名为 default 的目标或配置中只有一个独立目标时,此时目标的指定是可选的,Mod.js 会自动识别唯一的存在或 default 的目标:
|
1 2 3 |
targets: <span class="o">{</span> dist: <span class="o">[</span><span class="s2">"rm"</span>, <span class="s2">"cp"</span><span class="o">]</span> <span class="o">}</span> |
|
1 2 |
<span class="c"># 等价于 mod dist</span> <span class="nv">$ </span>mod |
配置有 default 目标的场景:
|
1 2 3 4 |
targets: <span class="o">{</span> default: <span class="o">[</span><span class="s2">"rm"</span>, <span class="s2">"cp"</span><span class="o">]</span>, other: <span class="o">[</span><span class="s2">"compress"</span><span class="o">]</span> <span class="o">}</span> |
|
1 2 |
<span class="c"># 等价于 mod default</span> <span class="nv">$ </span>mod |
深入任务
任务是具体执行的类别,从配置示例开始阐述:
|
1 2 3 4 5 |
<span class="nx">tasks</span><span class="o">:</span> <span class="p">{</span> <span class="nx">min</span><span class="o">:</span> <span class="p">{</span> <span class="nx">src</span><span class="o">:</span> <span class="s2">"./js/*.js"</span> <span class="p">}</span> <span class="p">}</span> |
以上配置了一个文件压缩的 min 类别任务,src 描述需要压缩的文件:js 目录的所有 js 文件。src 支持 unix glob 语法来描述输入文件集,其匹配规则如下:
匹配符:
- "*" 匹配 0 个或多个字符
- "?" 匹配单个字符
- "!" 匹配除此之外的字符
- "[]" 匹配指定范围内的字符,如:[0-9] 匹配数字 0-9 [a-z] 配置字母 a-z
- "{x,y}" 匹配指定组中某项,如 a{d,c,b}e 匹配 ade ace abe
示例:
|
1 2 3 4 5 6 7 8 9 10 |
c/ab.min.js <span class="o">=</span>> c/ab.min.js *.js <span class="o">=</span>> a.js b.js c.js c/a*.js <span class="o">=</span>> c/a.js c/ab.js c/ab.min.js c/<span class="o">[</span>a-z<span class="o">]</span>.js <span class="o">=</span>> c/a.js c/b.js c/c.js c/<span class="o">[</span>!abe<span class="o">]</span>.js <span class="o">=</span>> c/c.js c/d.js c/a?.js <span class="o">=</span>> c/ab.js c/ac.js c/ab???.js <span class="o">=</span>> c/abdef.js c/abccc.js c/<span class="o">[</span>bdz<span class="o">]</span>.js <span class="o">=</span>> c/b.js c/d.js c/z.js <span class="o">{</span>a,b,c<span class="o">}</span>.js <span class="o">=</span>> a.js b.js c.js a<span class="o">{</span>b,c<span class="o">{</span>d,e<span class="o">}}</span>x<span class="o">{</span>y,z<span class="o">}</span>.js <span class="o">=</span>> abxy.js abxz.js acdxy.js acdxz.js acexy.js acexz.js |
更多任务配置规则深入:https://github.com/modjs/mod/blob/master/doc/tutorial/configuring-tasks.md
如任务没有配置 dest,默认在输入文件同级目录下输出.min 后缀的文件:
|
1 2 3 |
uglifyjs Minifying ./js/unminify.js -> js/unminify.min.js uglifyjs Original size: 1,393. Minified size: 449. Savings: 944 (210.24%) |
内置的 min 任务支持三种文件类别的压缩,JavaScript、CSS 与 HTML,是对 uglifyjs、cleancss 与 htmlminfier 任务的代理。min 通过识别文件后缀进行具体任务的分发。所以 min 任务的 src 选项需指定具体的后缀。通常每个不同类别的任务都支持 src 与 dest,且 Mod.js 会结合实际项目中常见的场景,dest 往往都是可选的,如上 min 任务默认的 dest 是在当前目录下输出待.min 后缀的文件,同时后缀名是支持通常 suffix 选项配置的。
每个内置任务支持的所有参数选项可通过 Mod.js 的在线文档查看:https://github.com/modjs/mod/tree/master/doc
同时有丰富的演示项目来辅助不同任务的配置:
- 合并 JS 文件
- 合并 CSS 文件,自动合并 import 文件
- AMD 模块文件编译
- CMD 模块文件编译
- 多页面项目中 AMD 模块编译
- JS 文件条件编译
- CSS 文件条件编译
- HTML 文件条件编译
- JS 文件压缩
- CSS 文件压缩
- HTML 文件压缩
- 代码移除,如 alert、console
- 文件 EOL 移除
- 文件 Tab 移除
- 图片 DataURI
- 创建目录
- 复制文件或目录
- 规则替换,如版本号累加
不可或缺的插件机制
Mod.js 支持 2 种生态的插件:Mod.js 与 Grunt。插件的配置同样是在 Runner 对象下:
|
1 2 3 4 5 6 7 8 |
<span class="nx">plugins</span><span class="o">:</span> <span class="p">{</span> <span class="c1">// Mod.js NPM 插件</span> <span class="nx">sprite</span><span class="o">:</span> <span class="s2">"mod-stylus"</span><span class="p">,</span> <span class="c1">// Mod.js 本地插件</span> <span class="nx">mytask</span><span class="o">:</span> <span class="s2">"./tasks/mytask"</span> <span class="c1">// Grunt NPM 插件</span> <span class="nx">compress</span><span class="o">:</span> <span class="s2">"grunt-contrib-compress"</span> <span class="p">}</span> |
同样附上演示项目来辅助不同插件的配置:
如插件未安装在项目目录下或与 Mod.js 同级的全局目录下,Mod.js 会自动通过 NPM 安装配置的插件。什么情况需要手动把插件安装在全局下?在实际项目开发中我们往往会对同一项目拉不同的分支进行开发,他们依赖的插件版本是相同的,此时如果在不同分支都安装一个冗余的插件版本项目是多余的,所以当你确定这是个插件是共享的,可以手动通过 npm install -g mod-stylus 来安装到全局。同时项目目录中插件版本权重永远是高于全局的,如需避免加载全局的版本,只需手动在项目安装即可。
限于篇幅,更多插件相关说明可访问以下主题页面:
零配置快速项目构建
虽说是零配置构建项目,不如称之为基于 DOM 的项目构建,这个主题的内容与我之前在 Qing 项目中讨论的主题的一致的,在此只附上示例:
另外免配置文件对 Sea.js 2.1+项目的支持正在开发中,会下 Mod.js 的下一迭代中支持。
服务化
了解完如何实施 Mod.js 进行自动化时,仅是停留在工具的层面,如何将其进一步的提升?了解一个事实,服务优于工具。如何将其封装成服务,用户无需安装 Mod.js,无需执行命令,只需做一次事情:提交代码,中间的过程无需关注,最终把持续构建的结果反馈给用户。这是下一步需要去完善的,建立接入机制,让工具以服务的形式完全融入流程中。


一个程序员前端 2016 年 5 月 31 日
这个应该是看团队规模,还有业务需求来的吧
康韦乐 2016 年 1 月 8 日
有空一起交流一下
weibo5555323 2016 年 1 月 7 日
这么多工具 都是重复造轮子。但学习成本都不低。 做个带图像界面的,点几下鼠标就配置完。那才是真的牛
前端自动化 | Web前端 腾讯AlloyTeam Blog | 愿景: 成为业界卓越的Web团队! | ShareTextOnline 2014 年 11 月 5 日
[…] 通过前端自动化 | Web 前端 腾讯 AlloyTeam Blog | 愿景: 成为业界卓越的 Web 团队!. […]
Sigma 2014 年 7 月 13 日
感谢贵团队一如既往的无私分享!
bruce 2014 年 6 月 24 日
能不能改善下,文件条件编译好像只能传入后缀名为.html 的文件。 传入.jade 的文件没有效果
tonylua 2014 年 5 月 16 日
看起来比 grunt 晦涩
32 2014 年 4 月 3 日
尝试下 gulp,thanks
welpher.yu 2014 年 11 月 20 日
我也是用 gulp,感觉很好理解
Mayon 2014 年 3 月 2 日
对于资深的前端来说,Mod.js 其实就是跑在 Nodejs 里面的一个工具,方便就拿来用。但是,很多前端入门不久或者是从设计等岗位转过来的,即使给他们一份完善的标配,他们也搞不定。
如果能整出一个在线构建工具,也许更利于在团队里面推行,这也是我最近在思考的问题。话说,你们内部是怎么推行 Mod.js 的呢?好奇下。嘿嘿
元彦 2014 年 3 月 4 日
是的,无论是 mod 还是 grunt,对于设计师都有一定门槛,更别提 gulp 了,设计师喜欢 gui,工程师喜欢 cli(至少我身边的圈子)
ck 2014 年 3 月 2 日
和百度的 fis 对比有什么不同吗
元彦 2014 年 3 月 4 日
首先早些时候,fis 比 mod 成熟些,将近 1 年的迭代,现在 mod 已趋于稳定。对于工具更多的是细节上的差异,因为目标是一致的
tcdona 2014 年 3 月 1 日
这么用心的 mod.js 是否考虑抛弃类 grunt 晦涩的配置方式呢。
元彦 2014 年 3 月 2 日
这种配置风格属于类 xml 式,受 javaer 熟知的 ant 影响。
scgy5555 2014 年 3 月 3 日
对于 gulp 怎么看 忽略掉本质它只改了一下语法就火起来了
元彦 2014 年 3 月 4 日
不能说只是语法不同,的确是两种不同的思维模式
thREam 2014 年 3 月 29 日
看了一下觉得 gulp 更习惯一些,不过还是很赞,支持一个