前端打包构建工具

本文按照前端打包构建工具发布的顺序,根据原理分析各个打包构建工具的特点,涉及前端打包构建工具以及计算机网络常见面试题。

为什么要前端构建工具?

模块化开发、兼容性、压缩减小代码体积、分片......

前端构建工具发展

  1. 2009年,CommonJS,浏览器以外的 JS API 规范发布。同年,NodeJS 发布,并将 CommonJS 作为其模块化规范。

  2. 2011年,RequireJS 发布,也就是以后的 AMD 规范。

  3. 2013年,Grunt、Gulp发布。

  4. 2014年,兼容浏览器端和服务器端的模块化规范 UMD 发布。

  5. 2014年,babel 发布。同年,webpack 发布。

  6. 2015年,ES6 规范发布,JS 正式有了官方的模块化规范。

  7. 2015年,基于 ES6 的 rollup 发布,实现了 Tree Shaking 的能力。

  8. 2017年,Parcel 发布。

  9. 2019年,snowpack发布,能将 node_modules 转为 ESM。

  10. 2020年,go 语言开发的 esbuild 发布。

  11. 2021年,Vite 发布。

  12. 2021年,rust 语言开发的 SWC 发布。

初代构建工具

初代构建工具包括 Grunt 和 Gulp,现在已经很少使用了,这两种构建工具具有一个相同的特点:都是基于任务的构建工具。

Grunt

Grunt 的配置文件Gruntfile.js示例如下:

module.exports = function(grunt) {
  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'
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-uglify');

  grunt.registerTask('default', ['uglify']);
};

可以看到 grunt 的配置被放在对象中,通过grunt.initConfig方法传递,也就是说它是配置驱动的构建工具。

他的缺点在于,任务数量很多时,配置将会相当复杂。并且,它的任务包括读取磁盘内容,处理文件,写入磁盘这三个流程,如果要对多个文件进行多个任务,那么会有多次磁盘读写的操作(I/O 不友好)。

Gulp

Gulp 也是基于任务驱动的构建工具,目前阿里的 ahooks 使用了 gulp 来打包 cjs。

Gulp 最大的特点在于它是编程式的构建工具。

Gulp 的配置文件示例如下:

const { src, dest, task } = require('gulp');
const babel = require('gulp-babel');

gtask('js', function() {
  return src('src/*.js')
    .pipe(babel())
    .pipe(dest('output/'));
})

其中,srcdist方法用于处理文件,可以看到 gulp 通过pipe管道,或者说是一种流水线的方式来处理文件,并且它也是基于任务的。

相比Grunt,gulp 通过管道机制,解决了反复读写的问题。

webpack

webpack是现代前端构建工具的基石,它有以下几个核心概念:

  • Entry

  • Output

  • Module

  • Resolve

  • Chunk

  • Loader

  • Plugin

webpack以串行的方式运行,它的本质就是要构建如官网中的依赖图,流程如下:

  1. 初始化:合并 shell 语句和配置文件中的参数。

  2. 开始编译:根据参数初始化 Compiler 对象,加载对应的插件,执行 run 方法。

  3. 找到入口:根据配置找到 entry 入口文件。

  4. 开始编译:从入口文件出发,调用模块对应的 Loader 翻译成AST,当遇到导入语句时,就要将它加入到依赖的模块列表,并且进行递归,直到弄清所有模块的依赖关系。

  5. 输出资源:将转换后的模块组装成chunk,并且转换成独立的文件加入到输出列表。

  6. 写入文件:根据 output 把文件写入到文件系统。

webpack的问题是:

  • 在ESM以前,CommonJS中只能通过script标签引入,是没有作用域这一概念的,所以webpack打包后的产物都是以IIFE(立即执行函数)的方式来实现作用域。

浏览器为什么不支持CommonJS?

因为 CommonJS 是同步的,如果有异步操作就需要去等待,这样会阻塞我们的运行过程,影响用户的体验,而node中是服务器端,可以用同步的规范。

基于webpack改进的构建工具

Rollup

rollup是基于webpack改进的构建工具,相比webpack,rollup速度更快,打包后代码体积更小,因为它使用 ES6 模块化规范,能够支持 Tree-shaking,所以更受一些第三方类库的青睐,我们的 Vue 和 React 框架就使用了 rollup 进行构建。

以Vue为例,他通过genConfig函数生成了 rollup 可以识别的配置:

function genConfig (name) {
  const opts = builds[name] // 根据传入的name获取builds中对应的配置对象
  const config = {
    input: opts.entry, // 入口
    external: opts.external, // 需要排除的外部依赖
    plugins: [
      flow(), // 用flow做类型保护
      alias(Object.assign({}, aliases, opts.alias)) // 收集所有的参数
    ].concat(opts.plugins || []),
    // 出口
    output: {
      file: opts.dest, // 目标文件
      format: opts.format, // 格式化类型
      banner: opts.banner, // 文案
      name: opts.moduleName || 'Vue'
    },
    onwarn: (msg, warn) => {
      if (!/Circular/.test(msg)) {
        warn(msg)
      }
    }
  }

  Object.defineProperty(config, '_name', {
    enumerable: false,
    value: name
  })

  return config // 这样就生成了方便rollup去解析的config对象
}

可以看出rollup与webpack类似,都是配置为核心的构建工具,但是他的配置相对简单,没有 loader 去对文件进行处理,并且没有 devServer 和 HMR,所以 rollup 一般不用于业务开发。

Parcel

要说配置简单,就不得不说Parcel,因为它的配置完全是内置的,也就是黑盒的。

他提供了基本的能力,包括code splitting、HMR、sourcemap、publicPath、tree shaking、scope hoist、share module、UMD等,对于没有定制需要的项目可以使用。

突破JS语言特性的构建工具

不得不说前端越来越卷了,为了卷死同行,构建工具开始使用其他语言的特性来提升性能。

SWC

SWC 是基于 rust 语言开发的,用来对标 babel 的编译器(比 babel 快20倍),因为 JS 构建工具的性能瓶颈在于 JS 语言本身的特性(单线程)。它包含了 Compiler 和 Bundler 的能力,只不过目前将它用作 Compiler。

SWC的用法和 babel 基本一致,webpack当中也有对应的swc-loader,我们通过创建.swcrc来进行配置。

{
  "$schema": "https://json.schemastore.org/swcrc",
  "jsc": {
    "parser": {
      "syntax": "ecmascript", // 还支持TS
      "jsx": false,
      "dynamicImport": false,
      "privateMethod": false,
      "functionBind": false,
      "exportDefaultFrom": false,
      "exportNamespaceFrom": false,
      "decorators": false,
      "decoratorsBeforeExport": false,
      "topLevelAwait": false,
      "importMeta": false
    },
    "transform": null,
    "target": "es5",
    "loose": false,
    "externalHelpers": false,
    "keepClassNames": false
  },
  "minify": false
}

缺点是 TS 支持做的还不够好,目前使用上还存在一些问题。

esbuild

用实力说话:

特性:

  • 速度快,无需缓存

  • 支持ES6和CommonJS模块

  • 支持Tree-Shaking

为什么快?

Go语言

JS 是一门解释型语言,即每次执行需要将源码编译成机器码,然后执行;Go 语言是一门编译型语言,在编译阶段已经将源码转为机器码,后续只要执行机器码即可。

多线程

Go 语言具有多线程的能力,并且多个线程共享内存空间,虽然 JS 中可以引入 WebWorker,但是每个线程都有自己独立的内存堆,需要通过 postMessage 来共享线程间的数据。

全量定制

esbuild 完全重写了整个编译流程,开发成本较高,但收益巨大。

  • 重写了 ts 转译工具,并且只关注于代码转换,放弃类型检查

  • 混合多个编译过程(词法分析、语法分析、代码转换、代码生成)

统一的数据结构

在编译时共用相似的 AST 结构,提升内存使用效率。

  • esbuild 目前对于前端框架的支持较弱,并且没有像 ES5 降级、HMR等能力,所以目前普遍使用基于 esbuild 的下一代前端构建工具。

基于esbuild的bundleless工具

bundleless原理

HTTP2.0

HTTP2.0相比1.0有哪些区别?

HTTP2.0和HTTP1.x的区别在于:

  • 二进制分帧

  • 多路复用

  • 头部压缩

  • 服务器推送

因为HTTP2的特性,我们就不再需要将文件进行合并,从而减少请求次数了。

ESModule

浏览器可以直接使用<script type="module">来使用 ESM。

Bundleless 具有以下优势:

  • 冷启动时间大大缩短:因为只需要关注当前请求需要的模块,而不是整个 bundle。

  • HMR 速度不受项目整体体积影响:启动两个server,一个负责运行项目,一个负责HMR,两个server进行 WebSocket 连接,当浏览器发起 ESM 请求,只需要将当前文件进行编译,返回给浏览器。

Snowpack

Snowpack 实现了免打包和快速 HMR,优化了打包时的性能。

Vite

Vite是最新一代前端构建工具,旨在解决冷启动慢、HMR延迟等问题。

开发环境

开发环境无需打包,使用 esbuild 对 node_modules 进行与构建,将结果存到node_modules/.vite,如果 package.json, lockfile, vite.config.js中的字段发生变化,就会重新触发预构建。

依赖浏览器的缓存技术,例如设置max-age对模块进行缓存。

生产环境

生产环境中即使有 HTTP2 的支持,但为了生产环境中的性能需要,还是需要进行 Tree-shaking、懒加载以及分 chunk。所以 vite 目前选择了 rollup 来进行打包。

请求拦截

因为除了原生 JS 资源外,还可能有 jsx、tsx、vue 等多种文件,所以针对不同资源的请求要进行拦截,Vite 的做法是启动 Koa 服务器,将对应的资源转成 JS 模块来进行加载。

  • 在请求中,对于node_modules模块的请求的路径会被替换成/@modules/,当浏览器收到后,会对/@modules/再次发起请求,并且再次被 Vite 拦截,由 Vite 访问真正的模块后返回给浏览器。

  • 对于.vue文件,Vite会分为三个请求(template, script, style),浏览器会先收到包含 script 逻辑的 App.vue 的响应,然后解析到 template 和 style 的路径后,会再次发起 HTTP 请求来请求对应的资源,此时 Vite 对其拦截并再次处理后返回相应的内容。

热更新

Vite 的热更新就是在客户端与服务端建立了一个 websocket 连接,当代码被修改时,服务端发送消息通知客户端去请求修改模块的代码,完成热更新。

  • 服务端:服务端做的就是监听代码文件的改变,在合适的时机向客户端发送 websocket 信息通知客户端去请求新的模块代码。

  • 客户端:Vite 中客户端的 websocket 相关代码在处理 html 中时被写入代码中。可以看到在处理 html 时,vite/client 的相关代码已经被插入。

Turbopack

Turbopack 还没有正式发布,它是基于 Rust 语言编写的构建工具,创建 Turbopack 就是为了提高 Next.js 的速度,希望它能够取代 Webpack,成为下一代 Web 打包工具。https://turbo.build/pack

  • 34
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值