流程
关于流程,是从项目启动到发布的过程。在前端通常我们都做些什么?
- 切图,即从设计稿中获取需要的素材,并不是所有前端开发都被要求切图,也不是所有前端开发都会切图,但请享受学习新知识的过程吧。
- 创建模版(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 更习惯一些,不过还是很赞,支持一个