Skip to content

Latest commit

 

History

History
178 lines (129 loc) · 12.5 KB

9.常见构建工具及其对比.md

File metadata and controls

178 lines (129 loc) · 12.5 KB

在前面的章节中,我们介绍了在开发阶段何如使用工程化的手段进行降本提效,主要有脚手架、npm、还有模块化和组件化的相关内容,以及我们需要遵循的团队协作规范。

下面,我们讲一下如何将工程化能力应用在构建阶段,主要介绍几种常见的构建工具以及 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.jssrc源码以及测试用例 test 文件夹下所有的 .js 文件进行监听,如果有变动的话就执行 jshint这个任务(task)。需要的插件依赖有:grunt-contrib-jshintgrunt-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 已经到了发展的成熟阶段,一般的构建任务需求都可以找到成熟的解决方案。其灵活性也可以让你自由配置想要的任务。Grunt 的缺点就是配置略为繁琐。

Gulp

Gulp 也是一款老牌的自动化构建工具,和 npm scripts、Gunt 这样的任务运行器不同的是,Gulp 引入了 stream(流)的概念,底层使用 node 强大的 stream(流)能力,提高构建速度。

基于其 stream 的能力,gulp 也有一套自己的生态插件处理 stream,比如我们想要将 less 源文件编译为 css 文件可以如下设置:

  • 先引入依赖模块
  • 在通过 gulp.task 创建 less 任务
  • 然后使用 gulp.src 读取 less 文件,将读取到的文件交给 less 模块插件处理,处理完的结果使用 gulp.dest 输出为 css 文件。
  • 然后我们再使用 gulp.watch 设置监听任务,当 less 文件变化的时候,就执行 less 任务。

gulpfile.js 配置文件如下:

// 引入依赖模块
const gulp = require('gulp');
const eslint = require('gulp-eslint');
const less = require('gulp-less');

// 创建 less 任务,将 less 源文件经过过 less 依赖的处理,流式处理并输出 css 文件
gulp.task('less', function() {
  gulp.src('./less/*.less')
    .pipe(less())
    .pipe(gulp.dest('./css'));
});

// 监听文件,当 less 文件变化时执行 less 任务
gulp.task('watch', function() {
  gulp.watch('./less/*.less', ['less']);
  gulp.watch('./js/*.js', ['scripts']);
})

Gulp 和 Grunt 类似,也经常拿来比较,两者的相同之处是都引入的 task 任务的概念,Gulp 吸取了 Grunt的优点的同时,有着更简便的写法,通过流(Stream)的概念来简化多任务之间的配置和输出,让任务更加简洁和容易上手。但是 Gulp 和 Gunt 相同的缺点都是集成度不高,需要很多的配置文件,不能开箱即用。

webpack

终于来到了 webpack 的时代!上面的 Grunt、Gulp 你可能没有听说过,但是 webpack 你一定不会没有听说过。

本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具,使用 webpack 处理应用程序时,需要一个入口或多个入口文件,根据入口文件可以递归寻找出其依赖文件,构建出一个依赖图,然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

在 webpack 中,一切的资源:JS\CSS\IMG 等,都是一个个的模块,可以通过 Loader 进行文件转换、Plugin 插件系统进行文件处理,最后对模块进行组合和打包,输出浏览器可以使用的资源。

比如,我们使用 raw-loader 让静态资源内联,然后使用 HtmlWebpackPlugin 插件,为应用程序生成一个 HTML 文件,并自动将生成的所有 bundle 注入到此文件中。那么我们可以做如下配置:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-first-webpack.bundle.js',
  },
  module: {
    rules: [{ test: /.txt$/, use: 'raw-loader' }],
  },
  plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};

webapck 的优点有很多,最值得一提的应该是其强大的功能,只有你想不到,没有 webpack 做不到 ´ヾ(=・ω・=)o。并且有着丰富的社区资源,社区庞大且活跃。webpack 还可以做到开箱即用,有着良好的开发体验。说到不足之处的话,因为其功能强大,所以配置信息也较为庞大,上手成本虽然简单,但是想要精确需要深入研究,webpack 配置工程师名不虚传!

Rollup

Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码。Rollup 和 webpack 类似,但是但 Rollup 更为小巧,可以充分利用 ESM 各项特性的高效打包器。

其配置信息十分简介和便于理解,下面是一个常见的配置信息:

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js', // rollup支持的多种输出格式(有amd,cjs, es, iife 和 umd)
    format: 'iife',
  },
}

Rollup 是在 webpack 流行后出现的,其功能和 Webpack 类似。因为其特性,天然支持 Tree Shaking(去除没有被使用的代码)(但是后来 webpack 也推出了类似的功能)。相比与 webpack,Rollup 的社区和生态没有其完善,开发体验会较差一些。但是其打包出来的产物也没有 webpack 那么多“冗余”的代码,更为轻量。

所以,如果你是开发一个 JS 库,那么 Rollup 会是一个比较好的选择,因为其打包速度更快,打包出来的体积也更小巧。

总结

本篇文章,我们先介绍了为什么前端项目需要构建,并且简述了在构建阶段需要做哪些事情。然后,按照时间顺序,介绍了几个常见构建工具:npm scripts、Grunt、Gulp、Webpack、Rollup 的基本用法和优缺点。构建工具的演进也从侧面反映了前端工程化发展的道路,从刀耕火种到引入自动化控制流程,如今的一个脚本命令或者操作就可以实现代码编译、代码校验、文件优化、自动化测试等一系列的构建流程。

目前为止,不得不说 webpack 因为其强大的功能和完善的社区和生态链从众多的构建工具中脱颖而出,成为构建工具的首选。在接下来的章节中,我们会再介绍 下在 webpack 打包中如何优化构建速度和构建产物。

并且在稍后的文章中,我还会介绍 no-bundle 无包构建方案,敬请期待。