下面,我们讲一下如何将工程化能力应用在构建阶段,主要介绍几种常见的构建工具以及 polyfill、Babel。最后针对目前最流行的构建工具 webpack 的配置优化。
# 为什么需要构建
既然要了解在构建过程中的工程化手段,那么首先我们要知道什么是构建,为什么在前端项目中需要构建。
在编程领域,我们普遍认为编译是将源代码经过一系列处理转换为机器语言(汇编)的过程。构建指的是将我们在开发环境写的代码,转换成可交付的生产环境的代码的过程,其中包括了编译。
记得大学时候《网页开发》这门课的时候,老师给我们讲,网页开发的一大优势就是开发便捷,HTML\JavaScript\CSS 不需要编译就可以直接在浏览器中运行,我们甚至可以使用记事本来进行前端开发,所以,最原始的前端是不需要编译过程的。
在 JQuery 之前的时代,浏览器厂商还没有多,不需要处理各种各样的兼容性问题。前端所做的工作只是写一些简单的 HTML\JS\CSS 代码,也不需要编译,可以直接打开 .html文件在浏览器中运行。
到了 JQuery 时代,虽然市面上已经有各种各样的浏览器,但是 JQuery 已经帮我们解决了绝大多数的兼容性问题,我们只需要引入 JQuery 的源码就可以忽略浏览器兼容性带来的问题,也可以直接将开发的代码在浏览器环境下运行。
那么为什么现代前端开发,我们就需要构建过程呢?
首先是在现代前端开发中,我们需要使用最新的 ES6\7\8 语法、我们需要使用 TS 来进行类型管理,我们需要使用 Less/Sass 辅助我们开发 CSS。这些新技术在提升开发效率的时候,会有一个共同的问题:无法直接在浏览器中解析(或者部分浏览器不支持)。所以,我们就需要在开发的时候加入编译的环节,将我们开发的代码编译为浏览器可识别、可运行的代码。
其次是在模块化发展起来之后,我们将模块\组件的粒度拆分的更细,方便开发的同时也引入了新的问题,每一个文件都会被引入到 HTML 文件中,用户在使用浏览器访问页面的时候就会增加请求数量。想象一下,一个中型项目的开发,拆分出来的组件少则几十个,多则上百个。如果不进行文件合并,那么在性能上,会严重增加用户的等待时长。 所以我们为了减少请求次数,还需要将所有小文件“打包”为一个或多个大文件。
最后在我们项目发布上线的时候,我们需要保证项目整体的可交付性和可维护性。可交付性主要需要进行单元测试来保证代码质量、需要使用 ESLint 等代码规范约束工具对代码进行检查;可维护性主要需要对代码压缩、混淆、合并来减少代码体积和请求数量来保证性能问题。
所以构建就是将我们平时开发的源代码转换为可交付的代码,主要的过程有以下几点:
- 编译:将源代码转化为浏览器可执行代码,例如
.jsx、.vue文件转换、.sass文件转换为 css 文件。 - 代码校验:使用例如 ESLint 等工具自动检测代码格式是否符合团队要求。
- 文件优化:删除未引用代码(tree-shaking)、压缩文件体积、合并文件、制作“雪碧图”、代码混淆等。
- 自动化测试:自动化执行测试用例,进行单元测试或快照对比。
工程化在构建的过程要做的事情就是将这一系列的流程用代码控制,自动执行这一系列负责且耗时的重复劳动。在发展的过程中,也涌现出了一系列的前端构建工具,比如 Gulp、Grunt、Webpack、Rollup 等,下面我们就分析下几种常见的构建工具及其优缺点。
# npm script
在前面《探索 npm 安装机制》中,我们简单的介绍了 npm 相关的内容。npm 是 nodeJS 内置的包管理工具,而 npm script 是 npm 内置的自定义脚本执行命令。其允许你在 package.json 中使用 script 字段定义脚本。比如,我们想在开发时运行 dev.js 文件,我们就可以这样设置:
{
"scripts": {
"dev: "node dev.js",
},
}
然后在终端中运行 npm run dev,就会对应执行 dev.js 中的内容。
在 package.json 的 script 对象中,每个属性对应一个 shell 脚本,每当执行 npm run xxx的时候,都会新创建一个新的 Shell 执行对应的命令。
npm scripts 的优点是方便快捷,只要是 Shell 可以执行的命令,都可以写在 npm scripts 中。并且不需要安装其他依赖。但是缺点也很明显,功能单一,无法进行一些复杂的功能。
# Grunt

Grunt 和 npm script 类似,都可以认为是 JavaScript 的任务运行器。和 npm scripts 不同的是,Grunt 有丰富的插件体系,可以使用其内置的或者社区中的封装了常见任务的插件。Grunt 还可以管理任务之间的依赖关系,自动化执行任务。
Grunt 需要在配置文件 gruntfile.js 中配置每个任务具体的执行代码和依赖关系。例如,我们创建一个 jshint任务,对 Gruntfiles.js 和 src源码以及测试用例 test 文件夹下所有的 .js 文件进行监听,如果有变动的话就执行 jshint这个任务(task)。需要的插件依赖有:grunt-contrib-jshint、grunt-contrib-watch,在启动 gunt 的时候就默认注册 jshint任务。
我们可以做如下配置:
module.exports = function(grunt) {
// 插件的配置信息
grunt.initConfig({
jshint: {
files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
options: {
globals: {
jQuery: true
}
}
},
watch: {
files: ['<%= jshint.files %>'],
tasks: ['jshint']
}
});
// 使用到的插件
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-watch');
// 启动 Grunt 时需要注册哪些任务
grunt.registerTask('default', ['jshint']);
};
然后在根目录下执行 grunt dev就会启动 grunt 并执行注册的 task。
Grunt 的优点和 npm scripts 一样,方便快捷,并且社区也有丰富的插件资源,Grunt 已经到了发展
