前端工程化-构建
构建的核心是资源管理。简单说,构建就是把前端工程师开发的源代码进行编译、压缩、打包等一系列操作,最终产出可以直接上线或者可供后端工程师的资源。
构建可以划分为纯前端构建和前后端协作构建。
这两个不是专业术语,如果你有更合适的称谓,欢迎指正。
所谓纯前端构建,就是说不涉及后端模板的构建,经过构建之后的前端代码可以直接上线。这种情形下大多是数据驱动UI的web应用,模板只负责提供空白的容器和基础的静态资源,UI的文档结构交由前端JavaScript实现。这个过程可以使用一些框架,比如近期较流行的React、Vue等;也使用较轻量级的JavaScript模板工具,比如的underscore template、jsmart、Mustache等;甚至可以直接拼接字符串。
前后端协作构建与纯前端构建唯一的不同是加入了对后端模板的依赖,这也是目前绝大多数web应用的工作模式。这种模式下,构建工具要额外处理模板中对静态资源的引用地址。我们在浅析前端工程化一文中提到的便是前后端协作的构建模式,也是本文将要讨论的方向。
下面我们细化资源管理的每个关键点,共同探讨一下前端工程中构建环节的工作内容和面临的问题。
1. 基本功能
如果是纯前端构建(不涉及后端模板),在资源管理方面,编译工具需要完成的事情包括:
- 代码审查。包括eslint、sass审查;
- 预编译。包括es6/7语法转译、sass预编译css、spirit图片生成;
- 资源嵌入。小于某个尺寸(比如10kb)的静态资源替换为base64,以减少一次http请求;
- 依赖分析。扫描模块依赖关系并产出,整个流程大致可以规划为:
是否合并这一步由用户选择,比如项目中使用requirejs作为前端模块化方案,考虑到缓存、异步等问题可能选择不合并。这种情况下往往需要在本次构建产出的资源依赖表的基础上进行二次构建,后续会详细说明。
- uglify和compress;
- hash指纹 。利用md5算法对比静态资源更改,给文件名加上hash指纹,并产出资源定位表;
- release。将构建后的文件产出到指定目录,这个目录通常是本地的,经本地测试通过后可以push到线上服务器。同时,release阶段会产出资源定位表,以便模板构建和二次构建中使用;
- 模板构建。根据release阶段产出的资源定位表,将模板中静态资源的引用地址更新为构建后的绝对路径。
2.整体流程
一套完整的构建流程如下图所示:
具体构建流程中的各个行为并不是严格按照上图的前后顺序进行,可以自行安排。
上图中提到的各个构建行为中,代码审查、预编译、uglify&compress、hash指纹实现较容易,各构建模式中没有差异,本文便不再赘述。而依赖打包管理和模板构建是需要额外配置并且方案不唯一,下面详细探讨这两个行为的具体内容。
3. 依赖管理
依赖管理之所以方案不唯一是因为每个项目可能会采用不同的客户端模块加载方案(AMD/CMD/CommonJS)。构建平台本身不应该面向某一种方案,而应该是可配置的。
比如某个项目客户端的模块化方案采用AMD,使用requirejs作为模块加载器,那么在构建平台产出上述资源依赖表之后,还需要对requirejs进行配置,这个阶段我们可以称为二次构建。
二次构建要用到依赖分析之后的资源依赖表和release之后产出资源定位表。假设资源依赖表格式如下:
{
'a.js': {
deps:['b.js','c.js']
}
'c.js': {
deps: ['d.js']
}
'e.js': {
deps: ['f.js']
}
}
资源定位表的格式如下:
{
'src/js/a.js': 'release\js\a.sdf43n.js',
'src/js/b.js': 'release\js\b.sdf43n.js',
'src/js/c.js': 'release\js\c.sdf43n.js',
'src/js/d.js': 'release\js\d.sdf43n.js',
'src/js/e.js': 'release\js\e.sdf43n.js',
'src/js/f.js': 'release\js\f.sdf43n.js',
}
在二次构建阶段,requirejs根据资源依赖表和资源定位表需要作出如下配置:
requirejs.config({
baseUrl: '/',
path: {
'a.js': 'release\js\a.sdf43n.js',
'b.js': 'release\js\b.sdf43n.js',
//...
},
shim: {
'c.js': {
deps: ['d.js']
}
//...
}
});
虽然同步依赖的文件原则上应该打包成一个文件,但是构建平台不应该制定一些条律,所以在requirejs配置完成之后,构建平台应该还需要提供是否压缩打包的配置项。
此外,如果开发阶段使用es6语法,客户端使用AMD方案的话,在二次构建之前还需要将es6 module模块编译为AMD规范,这一步在预编译阶段完成。
4. 模板构建
模板构建的核心问题是如何同步更新静态资源的引用地址。除此之外,模板中往往还包含一些由Controller输出的动态数据,在构建过程中需要谨慎处理各模板引擎的语法。
目前对模板的处理分为两种模式:由前端负责构建和由后端负责构建。
这两种模式不仅仅是分工的不同,同时涉及开发方案的不同。
4.1 模板由前端构建
如果模板由前端构建工具进行编译,交到后端开发者手里的模板中对静态资源的引用地址是已经更新后的url,后端开发者不需要对模板进行额外操作便可以直接进行下一步流程(测试、部署)。
但是前端构建工具必须谨慎处理模板引擎的语法,以免造成“误伤”。后端模板引擎多种多样,前端构建工具很难做到百分百覆盖。所以通常情况下需要对模板中静态资源的url添加额外标识位,以处理文本的方式识别标识位并进行替换。比如:
<script src='[__static-start__]src/js/index.js[/__static-end__]'></script>
上述代码中将index.js
的url用类ubb格式的开闭标签包裹起来。假设经编译后index.js
的资源定位表如下:
{
'src/js/index.js': 'static.daojia.com/js/index.sfdf232.js'
}
前端构建工具首先获取模板文件的文本内容,正则出[__static-start__]src/js/index.js[/__static-end__]
,然后替换为static.daojia.com/js/index.sfdf232.js
。
以上的操作往往非常耗时,并且不能保证百分百正确率。
4.2 模板由后端构建
模板有后端构建的意思是,后端开发人员对模板引擎进行扩展,书写一个模板引擎语法的资源寻址function。比如php使用smarty模板引擎实现一个cdn
方法:
<?php
/*
* Smarty plugin
* -------------------------------------------------------------
* File: function.cdn.php
* Type: function
* Name: cdn
* Purpose: transform internal cdn path to online format
* -------------------------------------------------------------
*/
function smarty_function_cdn($params, Smarty_Internal_Template $smarty){
//...
}
还可以针对不同类型的静态资源扩展对应的cdn寻址方法:
<?php
/*
* Smarty plugin
* -------------------------------------------------------------
* File: function.js.php
* Type: function
* Name: js
* Purpose: print script tag by url
* -------------------------------------------------------------
*/
require_once('function.cdn.php');
function smarty_function_js($params, $smarty){
//...
}
然后前端开发人员在编写smarty模板时便可以使用如下语法引入静态资源:
{js url="/src/js/index.js"}
前端构建工具只需要处理静态资源即可,后端模板在部署上线后会将index.js
的url更新为线上地址。
模板交由后端构建的优点是可以对每种模板引擎有针对性的处理,而且是很工程化的构建方式;缺点是需要额外书写寻址function,但是这个缺点相对于优点来说微不足道。
4.3 小结
综上所述,模板前端构建和后端构建的对比如下:
根据表格的对比数据可以看出模板后端构建相比前端构建有很大优势。但是作为构建平台,应该同时支持两种模式。所以在开发构建平台的时候,开发者应该提供前端构建的功能接口,由用户选择是否采用。
5. 总结
本文简单讲述构建平台在不同开发模式下对应的构建方案。以上内容是笔者的一些经验和思考,肯定会有不足和错误之处,欢迎大家反馈,共同探讨。