type
Post
status
Published
date
Jul 2, 2021
slug
summary
tags
前端
构建工具
category
技术分享
icon
password
Property
Aug 9, 2023 01:09 AM

背景

谈起前端构建工具,大家第一个想到的应该就是webpack了,作为目前应用最广泛的前端构建工具,以其发达的生态,基于模块化的开发备受前端青睐,但是你在webpack使用过程中,有没有这样一个疑问:前端构建的生态是怎么形成的,除了webpack以外还有什么构建工具?本文就一起探索前端构建工具的演化。

模块化的演进

提到前端构建工具的特点,应该第一个想到的就是模块化这个概念了。
说到模块化就不得不讲起commonJs的诞生。

没有模块化之前的javascript

让我们回忆下最开始的javascript引入方式
<script src="./a.js"></script> <script src="./b.js"></script> <script> console.log('hello world') </script>
通过script设置src或者在script中书写js代码即可运行js,但是这样会带来很多问题:
  • 全局变量混乱,每个文件的全局变量都有可能被覆盖
  • 每个单独的js模块没有独立的标识
  • 每个模块都需要可能依赖上一个模块的变量,模块关系混乱
这时候有一些框架对于解决这些问题给出了自己的答案,其中最典型的是jQuery,jQuery使用了IFFE(立即执行函数)和闭包的特性,将内部的方法封装到一个变量中,并在函数的结尾将此变量挂载在window对象上,比较好的实现了js模块。
实现代码类似于下方
(function(global){ // var jQuery = ........ global.jQuery = global.$ = jQuery; })(window)
但是并没有解决变量依赖的问题,这时commonJS应运而生,赋予了js模块化的能力。

commonJS的诞生

commonJS创立之初的目标是为javascript在网页浏览器建立模块约定。commonJS有以下几个主要规则:
  • 模块通过exports来向外暴露API,exports只能是一个object对象,暴露的API位改对象的属性
  • require(dependency)函数通过模块标识引入其他依赖模块,执行的结果即为别的模块暴露出来的API。
  • 如果被函数引入的模块依赖其他模块,则依次加载其他依赖模块
example:
// math.js exports.add = function() { var sum = 0, i = 0, args = arguments, l = args.length; while (i < l) { sum += args[i++]; } return sum; }; // increment.js var add = require('math').add; exports.increment = function(val) { return add(val, 1); }; // program.js var inc = require('increment').increment; var a = 1; inc(a); // 2
彼时nodeJS刚诞生不久,还缺乏包管理工具,于是诞生了Node Package Manage(即npm),此时正值commonJS风头正盛,于是npm就使用了commonJS作为模块化方案。
但是因为commonJS导入一个模块时是同步的,在浏览器环境加载模块会产生更大的网络I/O,加上浏览器环境是天然异步的,所以commonJS并不能直接推广到浏览器上,势必需要新的模块化规范。a

AMD(Asynchronous Module Definition)

AMD规范是基于浏览器的异步模块规范,AMD规范采用依赖前置的规范,先把需要用到的依赖想提前写在dependencies数组中,等所有依赖下载完成之后再调用回调函数获取模块。
require([module], callback);
但是因为有CommonJS的原因,大家对于使用回调函数的方式颇有微词,最后实现了一个Simplified CommonJS wrapping(简称CJS)版本
对于AMD规范新增了两点feature:
  • 如果dependencies数组中有require、exports、或module,则与commonJS的实现保持一致
  • 如果dependencies省略不写,则默认为[‘require’, ‘exports’, ‘module’],回调中也会传入三者。

CMD(Common Module Definition)

因为AMD的提前加载问题,被很多开发者担心会有性能问题而吐槽。
之后在借鉴了CommonJS、AMD等模块化方案之后,玉伯写出了SeaJS,在此基础上,玉伯提出了Common Module Definition(简称CMD)这一标准规范。
CMD规范的主要内容与AMD大致相同,不过保留了CommonJS中最重要的延迟加载、就近声明特性。

UMD(Universal Module Definition)

UMD本质上并不是一个真正的模块化方案,而是将CommonJS与AMD结合起来
UMD做出了如下规定:
  • 优先判断是否存在exports方法,如果存在,则采用CommonJS方式加载模块
  • 其次判断是否存在define方法,如果存在,则采用AMD方式加载模块
  • 最后判断global对象上是否定义了所需依赖,如果存在,则直接使用;反之则抛出一场。
这样一来,模块开发者就可以使自己的模块同时支持CommonJS和AMD的导出方式,而模块使用者也无需关注自己依赖的模块使用的是哪种方案。

ES Module

来到2016年,经过了两年的讨论,ECMAScript6.0终于通过决议,成为了国际标准。
在此标准中,首次引入了import和export两个关键字,并且提供了被称为ES Module的模块化方案。
2017年9月上旬,Chrome61.0版本发布,首次在浏览器端支持了ES Module。

构建工具的发展

bundle 类的构建工具

Grunt

随着nodeJS的逐渐流行,基于NodeJS的自动化构建工具Grunt诞生。
Grunt可以帮助我们自动化处理重复的任务,例如压缩、编译、单元测试、lint等。
Grunt的配置文件如图所示。
module.exports = function(grunt) { // Project configuration. grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), uglify: { options: { banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n' }, build: { src: 'src/<%= pkg.name %>.js', dest: 'build/<%= pkg.name %>.min.js' } } }); // 加载包含 "uglify" 任务的插件。 grunt.loadNpmTasks('grunt-contrib-uglify'); // 默认被执行的任务列表。 grunt.registerTask('default', ['uglify']); };
grunt的出现标志着前端从刀耕火种的时代过渡到现代化。

Gulp

Gulp基于nodeJS流的配置使得相对grunt更加方便直观
通过最少的 API,掌握 Gulp 毫不费力,构建工作尽在掌握:如同一系列流管道。
const { src, dest, parallel } = require('gulp'); const pug = require('gulp-pug'); const less = require('gulp-less'); const minifyCSS = require('gulp-csso'); const concat = require('gulp-concat'); function html() { return src('client/templates/*.pug') .pipe(pug()) .pipe(dest('build/html')) } function css() { return src('client/templates/*.less') .pipe(less()) .pipe(minifyCSS()) .pipe(dest('build/css')) } function js() { return src('client/javascript/*.js', { sourcemaps: true }) .pipe(concat('app.min.js')) .pipe(dest('build/js', { sourcemaps: true })) } exports.js = js; exports.css = css; exports.html = html; exports.default = parallel(html, css, js);

Webpack

webpack的概念随着es2015的发布,以及webpack2的发布:支持es Module,babel,typescript,jsx,Angular组件和vue组件,webpack搭配三大前端框架成为最佳选择,至此webpack成为前端工程化的核心。
webpack是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph) ,然后将你项目中所需的每一个模块组合成一个或多个 bundles ,它们均为静态资源,用于展示你的内容。
webpack构建流程如下:
  • 初始化阶段
    • 初始化webpack.config.js中的配置参数
    • 创建compiler参数
    • 遍历用户定于的plugin集合,执行插件的apply方法
    • 加载webpack内置插件
  • 构建阶段
    • 从entry文件开始,调用loader将模块转化为js内容
    • 调用parser将js内容转化为AST
    • 从AST找到文件引用的依赖
    • 递归处理依赖并且分析出依赖图
  • 生成阶段
    • 根据上一步分析出的依赖图,按照entry/动态引入两种规则分别将模块打包到不同的chunk中
    • 将生成的chunks写入文件系统

Rollup

在es6发布之后,rollup提出了treeShanking的概念,根据esModule的静态语法特性,删除未被实际使用的代码(webpack已实现)
相对于webpack来说,rollup更加专注于纯javascript方面,大多被用作打包library库。

基于浏览器es Module的构建工具

上面讲的构建工具的思想都是分析js模块,组装依赖树之后生成代码。
但是对于大一点的项目来说,启动和打包项目可能需要很多分钟,所以出现了一系列基于浏览器端esModule的构建工具,优化了项目启动流程。

snowpack

snowpack在开发环境中使用原生esmodule模块代替以往的打包方式。
snowpack启动的速度是毫秒级的,因为不需要打包任何内容,只需要启动两个服务,一个用于页面加载,另一个用于HMR的websocket通知,当浏览器发出原生的es module请求时候,服务之需要编译请求的文件返回给浏览器就可以了。
对于生产环境打包,snowpack可以通过集成第三方打包工具(比如webpack)进行打包。

vite

vite也是基于浏览器es Module的构建工具
与snowpack有如下区别(’借鉴’:
  • snowpack配合的打包工具是不捆绑的,vite vite绑定了rollup作为打包工具
  • vite使用esbuild进行依赖pre-building,使得服务器冷启动和依赖关系无效时的重新捆绑有了显著的性能改进
  • vite支持monorepo
  • vite对于vue的支持更好
  • 社区相对比较发达(粉丝多(逃

其他构建工具

es-build

es-build是一个Go语言编写的打包工具,打包的速度是其他基于nodejs的打包工具的10-100倍,并且API可以同时运用到Go与javascript
notion image
但是因为目前es-build仍不算很稳定,所以暂不推荐使用
Typescript装饰器rxjs入门