1. 初版构建工具
1.1. Grunt
Grunt 是前端第一个正式的构建工具,它基于 Node.js 开发。Grunt 同样是基于插件实现功能拓展增强,但对于像Webpack上很多能力,如HMR、Scope Hoisting等都是不支持的,可以作为学习Webpack前的了解。
Grunt 更像是一种自动化的配置工具集,就如官方所说,Grunt 是 The JavaScript Task Runner,每个 Grunt 任务通常必须创建中间文件将结果传递给其他任务。
所能实现的功能包括:检查每个 JS 文件语法、合并两个 JS 文件、将合并后的 JS 文件压缩、将 SCSS 文件编译、将Less文件转换为CSS文件等等。
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
concat: {
options: {
separator: ';',
},
dist: {
src: ['src/**/*.js'],
dest: 'dist/<%= pkg.name %>.js'
}
},
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
},
dist: {
files: {
'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
}
}
},
quit: {
files: ['test/**/*.html']
},
jshint: {
files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
options: {
// options here to override JSHint defaults
globals: {
jQuery: true,
console: true,
module: true,
document: true
}
}
},
watch: {
files: ['<%= jshint.files %>'],
tasks: ['jshint', 'quit']
}
});
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-quit');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.registerTask('test', ['jshint', 'quit']);
grunt.registerTask('default', ['jshint', 'quit', 'concat', 'uglify']);
};
1.2. Gulp
Gulp跟Grunt类似,都是基于task驱动执行的,可以完成javascript/coffee/sass/less/html/image/css 等文件的测试、检查、合并、压缩、格式化、浏览器自动刷新、部署文件生成。Gulp的优点在于Gulp更倾向于写代码的方式,但相关的插件资源不如Grunt。
同样的,现代的构建工具基本不使用Gulp了,有兴趣看官网自行学习,这里不详细介绍。
const { series, parallel } = require('gulp');
function clean(cb) {
// body omitted
cb();
}
function css(cb) {
// body omitted
cb();
}
function javascript(cb) {
// body omitted
cb();
}
exports.build = series(clean, parallel(css, javascript));
2. 基于Webpack改进的构建工具
2.1. Rollup
Rollup 和 Webpack 都通过解析 JavaScript 的依赖树,将代码打包成适合浏览器或 Node.js 执行的指定版本 JavaScript。然而,Rollup 更轻量化,生成的代码不会像 Webpack 那样包含大量的内部结构,而是尽可能保持代码的原始状态。配置方面,Rollup 相对简单,但由于缺乏 devServer 和 HMR热模块替换功能,因此通常用于 JavaScript 库的开发,而非应用开发。
Rollup 主要负责将代码转码为目标 JavaScript,并且可以根据需要生成支持 UMD、CommonJS 和 ES 模块格式的代码。Vue、React、Angular 等框架的源码打包工具中都能看到 Rollup 的应用痕迹。
2.1.1. 使用方式
1. 浏览器环境使用的应用程序
(1). 无需考虑浏览器兼容问题
-
开发者编写ES模块代码 => Rollup 通过入口文件递归识别 ESM 模块 => 最终生成一个或多个 bundle.js => 浏览器可以直接通过 <script type="module"> 引入并运行。
(2). 需考虑浏览器兼容问题
-
这种情况下会复杂一些,需要额外使用 polyfill 库,或者与 Webpack 结合使用,以确保兼容性。
2. 打包成 npm 包
(1). ESM 模块打包
-
开发者编写 ESM 代码 => Rollup 通过入口文件递归识别 ESM 模块 => 可以配置输出多种模块格式(ESM、CJS、UMD、AMD) => 最终打包成一个或多个 bundle.js。
-
如果需要生成 CJS(CommonJS)格式的模块,开发者可以使用 @rollup/plugin-commonjs 插件。
(2). Rollup 更适合打包 JS 库
-
由于 Rollup 生成的库支持 ESM 的 tree shaking(摇树优化),这有助于将库的体积最小化。因此,许多像 React、Vue 等库的源码都是用 Rollup 打包的。
3. Webpack 与 Rollup 打包的差异
-
历史背景:Webpack 诞生于 ESM 标准出台之前,当时浏览器只能通过 script 标签加载模块,且没有作用域。为了实现模块的作用域隔离,Webpack 使用了 IIFE(立即执行函数表达式),这也是为什么 Webpack 打包后的代码结构看起来比较复杂,模块之间需要通过函数来隔离作用域。
-
CJS 兼容性:Webpack 需要在浏览器中模拟 CommonJS(CJS)的 require 和 module.exports 方法,因此注入了大量代码来实现这一功能,这也是 Webpack 打包后代码混乱的原因之一。
-
浏览器不支持 CJS 的原因:CJS 是同步的,在 Node.js 环境下运行速度快,因为无需等待网络响应。 浏览器是客户端,需要等待服务器响应,因此不能同步执行,否则会影响用户体验。 Webpack 为了兼容早期发布到 npm 的 CJS 包,保留了 IIFE 结构和代码注入,导致打包后的代码结构复杂且混乱。
-
Rollup 的优势:Rollup 诞生于 ESM 标准出台之后,专为 ESM 设计,没有历史包袱,因此能够生成精简且没有额外注入的代码。
2.1.2. 使用总结
rollup在构建JavaScript方面比Webpack有更大的优势:
-
构建速度明显快于Webpack。
-
生成的代码量很小。
-
配置方式其实非常简单。
2.2. Parcel
与 Webpack 相比,Parcel 采用 assets 方式组织文件,可以直接构建任何类型的文件,而 Webpack 则必须以 JS 为入口组织其他文件,这在使用体验上是一种提升。
Parcel 的速度优势来自于多核处理和文件系统缓存技术,这使得二次构建更快。其缓存机制使用 C++ 编写,效率更高,类似于 Webpack 的 dll 插件。此外,虽然 Parcel 本身是零配置的,但针对 HTML、JS 和 CSS 文件的处理仍需配置 .posthtmlrc、.babelrc 和 .postcssrc 文件。因此,Parcel 更适合小型、简单的项目,对于定制化需求较高的项目,建议使用 Webpack,因为其社区资源更为丰富。
2.2.1. 主要特点
Parcel 的主要特点包括:
-
零配置:无需复杂配置,开箱即用。
-
更快的构建速度:利用多核处理和文件系统缓存,提升构建效率。
-
自动安装依赖:简化开发流程,提升便捷性。
2.2.2. 基础使用
Parcel 以零配置著称,所以在使用方面非常方便,几乎不用配置就可以打包简单场景的代码。
2.2.3. 使用总结
Parcel 是一种顺应时代需求的构建工具,提供了 code splitting、HMR、sourcemap、publicPath、tree shaking、scope hoist、共享模块、UMD 等基本功能,并且官方也在持续维护,对于简单项目,Parcel 是一个值得尝试的工具。
3. 突破JavaScript语言的构建工具
3.1. Esbuild
3.1.1. 工具简介
1. 使用 Go 语言
JavaScript 是一种解释型语言,每次执行时,代码都需要由解释器逐行翻译成机器语言并执行。而 Go 是编译型语言,在编译阶段就将源码转译为机器码,启动时直接执行这些机器码即可。这意味着,用 Go 语言编写的程序不需要经历 JavaScript 那样的动态编译过程,执行效率更高。
2. 多线程支持
Go 天生支持多线程,在打包过程中能更高效地解析和生成代码。相比之下,JavaScript 本质上是单线程语言,即使引入了 WebWorker 规范,也只能在特定环境中实现多线程操作。此外,Go 的多个线程可以共享相同的内存空间,而 JavaScript 的线程则各自拥有独立的内存堆。这种设计使得 Go 能够在多个处理单元间直接共享数据,进一步提升运行性能。
3. 全量定制
在传统的构建工具如 Webpack 和 Rollup 中,许多功能依赖插件实现,如 Babel 用于 ES 版本转译,ESLint 用于代码检查,TSC 用于 TypeScript 转译等。Esbuild 则选择了完全重写所有编译流程中的工具,包括 JS、TS、JSX、JSON 等文件的加载、解析、链接和代码生成逻辑。这种方法虽然开发成本高,且可能带来封闭的风险,但通过从头定制,Esbuild 能够始终优先考虑性能优化。例如:
-
重写 TypeScript 转译工具,放弃类型检查,仅做代码转换。
-
将传统构建工具中多个高内聚低耦合的处理单元合并,减少数据流转,降低性能损耗。
-
保持一致的数据结构和高效缓存策略。
这种深度定制使得编译链条保持一致,并优化了各个环节的性能。尽管在功能、可读性和可维护性上有所牺牲,但 Esbuild 几乎达到了极致的编译性能。
4. 结构一致性
在 Webpack 中,使用 babel-loader 处理 JavaScript 代码时,源码可能需要多次转换:
-
Webpack 读取源码时是字符串形式。
-
Babel 将源码解析为 AST(抽象语法树)。
-
Babel 将高版本 AST 转换为低版本 AST。
-
Babel 再将低版本 AST 生成低版本源码(字符串形式)。
-
Webpack 解析低版本源码。
-
Webpack 将多个模块打包成最终产物。
这个过程中,源码在字符串和 AST 之间反复转换,效率较低。而 Esbuild 通过重写大多数编译工具,在编译的各个阶段共享相似的 AST 结构,减少了字符串与 AST 之间的转换,提升了内存使用效率。
不过,由于 Esbuild 对某些功能(如 Vue、Angular 等)的支持仍在逐步实现中,目前还不适合直接用于生产环境。但从编译性能来看,Esbuild 极具竞争力,这也是 Vite 和 Snowpack 选择 Esbuild 作为编译工具的原因之一。
3.1.2. 工具特性
Esbuild 特性主要包括:
-
极快的构建速度,无需缓存。
-
支持 ES6 和 CommonJS 模块。
-
支持对 ES6 模块进行 tree shaking。
-
API 可用于 JavaScript 和 Go。
-
兼容 TypeScript 和 JSX 语法。
-
支持 Source maps 和代码压缩。
-
支持插件系统。
Esbuild 的官方文档详细介绍了 API 的使用方法,涉及不同文件类型的 loader 配置和插件的使用。Esbuild 的插件配置相对简单,仅有四个钩子:onResolve、onLoad、onStart、onEnd,使用起来比 Webpack 更加简洁。
3.1.3. 使用示例
1. 安装Esbuild
首先,使用 npm 下载并本地安装 Esbuild。Esbuild 提供了预构建的本地可执行文件,可以通过以下命令安装:
npm install --save-exact --save-dev esbuild
这个命令会将 esbuild 安装到本地的node_modules 文件夹中。你可以通过运行以下命令来验证安装是否成功:
./node_modules/.bin/esbuild --version
推荐使用 npm 安装 Esbuild 的本地可执行文件,但如果你不想使用 npm 进行安装,还有其他安装方式可供选择。
2. 创建示例
以下是一个使用 Esbuild 的简单示例,展示了它的基本功能。
首先,安装 React 和 React-dom 包:
npm install react react-dom
接着,创建一个名为 app.jsx 的文件,并添加以下代码:
import * as React from 'react'
import * as Server from 'react-dom/server'
let Greet = () => <h1>Hello, world!</h1>
console.log(Server.renderToString(<Greet />))
然后,使用 Esbuild 打包这个文件:
./node_modules/.bin/esbuild app.jsx --bundle --outfile=out.js
此时,Esbuild 会生成一个名为 out.js 的文件,其中包含了你的代码和 React 库的打包内容。该文件是完全自包含的,不再依赖 node_modules 目录。如果你使用 node out.js 运行该代码,你应该会看到如下输出:
<h1 data-reactroot="">Hello, world!</h1>
3. 构建脚本
你可能会频繁地运行构建命令,因此自动化这个过程非常有必要。可以在 package.json 文件中添加一个构建脚本,如下所示:
{
"scripts": {
"build": "esbuild app.jsx --bundle --outfile=out.js"
}
}
这个脚本直接使用 Esbuild 命令,而不需要指定相对路径。因为在 scripts 部分中的命令会自动包含在路径中,前提是已经安装了 Esbuild。
你可以通过以下命令运行构建脚本:
npm run build
然而,当你需要为 Esbuild 传递多个选项时,使用命令行界面可能会变得繁琐。对于更复杂的使用场景,建议使用 Esbuild 的 JavaScript API 编写构建脚本。例如,以下代码展示了如何使用 Esbuild 的 JavaScript API:
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['app.jsx'],
bundle: true,
outfile: 'out.js',
})
注意,这段代码需要保存在 .mjs 扩展名的文件中,因为它使用了 import 关键字
build 函数在子进程中运行 Esbuild 可执行文件,并返回一个 Promise,当构建完成时该 Promise 会被解析。Esbuild 还提供了同步 API buildSync,但异步 API 更适合构建脚本,因为插件仅在异步 API 中可用。更多关于构建 API 的配置选项可以参考 API 文档。
4. 为浏览器进行打包
Esbuild 默认输出适用于浏览器的代码,因此无需额外配置即可开始。如果是开发环境,建议启用源码映射(sourcemap),而在生产环境中,则建议启用代码压缩(minify)。你还可以配置目标浏览器环境,以便将较新的 JavaScript 语法转换为较旧的语法。命令可能如下所示:
esbuild app.jsx --bundle --minify --sourcemap --target=chrome58,firefox57,safari11,edge16
有些 npm 包可能并未设计用于浏览器环境。对于这种情况,你可以使用 Esbuild 的配置选项来解决问题。例如,对于未定义的全局变量,可以使用 define 特性或在更复杂的情况下使用 inject 特性。
3.2. SWC
3.2.1. SWC介绍
SWC是一个由 rust 实现的快速Web编译器,主要通过多线程技术提升了JavaScript的编译性能。
在前端构建工具中,Webpack和Babel的性能瓶颈多源于JavaScript的单线程处理。为了解决这一问题,市场上出现了诸如用Go语言实现的Esbuild和用Rust语言实现的SWC等工具。其中,SWC的目标是替代Babel,其设计初衷就是对标Babel。因此,SWC实现了大多数Babel的功能。
SWC的配置方式与Babel相似。在Webpack中,SWC也提供了类似于babel-loader的swc-loader。我们可以在项目根目录下创建 .swcrc 文件,来指定常见的编译任务,如浏览器兼容性、模块化、代码压缩以及打包等,具体配置可以参考官方文档。
此外,SWC还支持插件机制,基本上能够覆盖Babel的功能。然而,目前SWC仍在不断完善中,某些功能尚不完善,如 @babel/types类似的功能、对TypeScript的支持等。因此,建议在生产环境中暂时保持观望,待功能更为成熟后再考虑全面使用。
3.2.2. SWC使用
SWC 与 babel 一样,将命令行工具、编译核心模块分化为两个包。
-
@swc/cli 类似于 @babel/cli。
-
@swc/core 类似于 @babel/core。
npm i -D @swc/cli @swc/core
通过如下命令,可以将一个 ES6 的 JS 文件转化为 ES5。
npx swc source.js -o dist.js
转化后的结果如下:
const start = () => {
console.log('app started')
}
// 转为
var start = function() {
console.log("app started");
};
1. 配置的格式
与Babel类似,在根目录下使用.swcrc。
SWC 与 babel 一样,支持类似于 .babelrc 的配置文件:.swcrc,配置的格式为 JSON。
{
"$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,
// Requires v1.2.50 or upper and requires target to be es2016 or upper.
"keepClassNames": false
},
"minify": false
}
Babel 的插件系统被 SWC 整合成了 jsc.parser 内的配置,基本上大部分插件都能照顾到。而且,SWC 还继承了压缩的能力,通过 minify 属性开启,jsc.minify 用于配置压缩相关的规则。
2. API的使用
导入 @swc/core 模块,可以在 node.js 中调用 api 直接进行代码的编译
import { readFileSync } from 'fs'
import { transform } from '@swc/core'
const run = async () => {
const code = readFileSync('./source.js', 'utf-8')
const result = await transform(code, {
filename: "source.js",
})
// 输出编译后代码
console.log(result.code)
}
run()
3. 打包项目
打包的能力在V1中称为 spack,但在 V2 中将重命名为 swcpack。spack.config.js 将被 swcpack.config.js 取代,但目前还没发布V2,所以目前还是使用spack。
// spack.config.js
const { config } = require('@swc/core/spack')
module.exports = config({
// mode:production | debug | none,目前还没有使用
entry: { // 入口文件,可以指定文件或者文件夹映射
'web': __dirname + '/src/index.ts',
},
output: { // 出口文件
path: __dirname + '/lib',
name: 'index.js' // name可选
},
module: {},
});
引入的文件为:
// index.ts
import { B } from "./common";
console.log(B)
// common.ts
export const A = 'foo';
export const B = 'bar';
4. 基于ES Module的bundleless构建工具
Browserify、Webpack、Rollup、Parcel这些工具的思想都是递归循环依赖,然后组装成依赖树,优化完依赖树后生成代码。
但是这样做的缺点就是慢,需要遍历完所有依赖,即使 Parcel 利用了多核,Webpack 也支持多线程,在打包大型项目的时候依然慢可能会用上几分钟,存在性能瓶颈。
所以基于浏览器原生 ESM 的运行时打包工具出现:
可以看到,我们只需要打包当前所需要的资源,对于而不用打包整个项目,开发时的体验相比于 bundle 类的工具只能用极速来形容。bundleless 类运行时打包工具的启动速度是毫秒级的,因为不需要打包任何内容,只需要起两个 server,一个用于页面加载,另一个用于 HMR 的 WebSocket,当浏览器发出原生的 ES module 请求,server 收到请求只需编译当前文件后返回给浏览器不需要管依赖。
4.1. Bundleless诞生的背景
Bundleless的诞生取决于客户端模块化特性支持以及网络层新特性。
4.1.1. HTTP 2
因为HTTP1.x不支持多路复用,HTTP1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求的限制。因此我们需要做的是将同域的一些静态资源比如JS等,做一个资源合并,将多次请求不同的JS文件,合并成单次请求一个合并后的大JS文件。其实这也就是Webpack的bundle由来。
而HTTP 2实现了TCP链接的多路复用,因此同域名下不再有请求并发数的限制,我们可以同时请求同域名的多个资源,这个并发数可以很大,比如并发10,50,100个请求同时去请求同一个服务下的多个资源。因为HTTP 2实现了多路复用,因此一定程度上,将多个静态文件打包到一起,从而减少请求次数,就不是必须的。
主流浏览器对HTTP 2的支持情况如下:
除了IE以外,大部分浏览器对HTTP2的支持程度都很好,所以如果不用考虑兼容IE低版本,同时也不需要兼容低版本浏览器,不需要考虑不支持HTTP 2的场景,所以在此情况下,让我们在使用Bundleless上成为了可能。
4.1.2. 浏览器ESM
先来简单看下ESM的代码例子:
// a.js
export let a = 1
// main.js
import a from 'a.js'
console.log(a)
上述的ESM就是我们经常在项目中使用的ESM,在支持ES 6的浏览器中是可以直接使用的。
<html lang="en">
<body>
<div id="container">my name is {name}</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js'
new Vue({
el: '#container',
data: {
name: 'Bob'
}
})
</script>
</body>
</html>
上述的代码中我们直接可以运行,我们根据script的 type="module" 可以判断浏览器支不支持ESM,如果不支持,该script里面的内容就不会运行。
首先我们来看主流浏览器对于ESM的支持情况:
以下是标签的支持情况:
从上图可以看出来,主流的Edge, Chrome, Safari, and Firefox (+60)等浏览器都已经开始支持ESM。
4.1.3. 总结
浏览器对于HTTP 2和ESM的支持,使得我们可以减少模块的合并,以及减少对于js模块化的处理。
-
如果浏览器支持HTTP 2,那么一定程度上,我们不需要合并静态资源。
-
如果浏览器支持ESM,那么我们就不需要通过构建工具去维护复杂的模块依赖和加载关系。
4.2. Snowpack
4.2.1. Snowpack介绍
Snowpack 是一个用于提升 Web 开发效率的轻量级新型构建工具。
在开发过程中,Snowpack 为你的项目提供了免打包式的服务。每个文件只需构建一次就被永远缓存起来。当文件发生变化时,Snowpack 重新构建发生变化的文件然后在浏览器中直接更新,而没有在重新打包上浪费时间,因为它是通过模块热替换来实现的。
Snowpack 为你带来了两全其美的效果:快速、免打包式的开发,以及打包式生产构建中的优化性能。
针对npm依赖,NPM 包主要是使用模块语法(Common.js,或 CJS)发布的,如果没有一些构建处理,就不能在浏览器上运行。虽然用浏览器原生的 ESM import和export语句编写的代码会直接在浏览器中运行,但在导入 npm 包后你都会退回到打包式开发时代。
Snowpack 采取了一种不同的方法:Snowpack 没有因为这个打包整个应用程序,而是单独处理 npm 依赖。以下是它的工作原理。
node_modules/react/* -> http://localhost:3000/web_modules/react.js
node_modules/react-dom/* -> http://localhost:3000/web_modules/react-dom.js
-
Snowpack 扫描网站/应用程序引入的所有 npm 包。
-
Snowpack 从 node_modules 目录中读取这些已安装的依赖包。
-
Snowpack 将所有 npm 依赖分别打包到单个 JavaScript 文件中,例如:react 和 react-dom 分别转换为 react.js 和 react-dom.js。
-
每个转换来的文件在经过 ESM 的 import 语句导入后,都可以直接在浏览器中运行。
-
因为 npm 依赖很少改变,Snowpack 很少需要重建它们。
在 Snowpack 执行完对 npm 依赖的处理后,任何包都可以被导入并直接在浏览器中运行,不需要额外的打包或工具。这种在浏览器中原生导入 npm 包的能力而无需打包器是所有免打包式开发工具和 Snowpack 建立的基础。
4.2.2. Snowpack原理
-
Snowpack 会扫描工作目录下所有源码,得出所有的依赖列表。具体来说,对于 css、less、sass、scss 文件,会扫描所有的 @import 语句,对于 html、svelte、vue 文件,会扫描所有 <script> 标签内的 import 语句;对于 js、jsx、mjs、ts、tsx 文件,会解析所有的 import 语句,得到依赖的 npm 模块名称。
-
接下来,根据依赖模块名称,尝试去找 npm 模块入口文件。采用的策略和 node 依赖查找机制类似,找到 package.json 文件后,先找 export map 指定的入口文件,再依次找 browser:module、module、main:esnext、browser、main 等字段,保证优先使用 ESM 入口。
-
使用 rollup 打包,将依赖的模块打包成一个个 ESM,从而可以直接在浏览器运行,输出到缓存目录,默认是 ./node_modules/.cache/snowpack/dev下。这一步打包操作也可以同时避免 ESM 依赖地狱的问题。
-
最后把依赖的模块与打包后文件绝对路径的映射记录下来,后面构建文件阶段根据这个映射对业务代码中原有的 import 语句进行改写。
首先,如果是对依赖模块的请求,会直接返回之前构建好的 ESM。
接下来,对于业务代码的处理,则会先经过文件的编译过程。这一过程是由不同的插件来实现,分为 load 和 transform 两个阶段。load 阶段是将业务代码编译成为浏览器可以直接运行的代码,比如 TypeScript、JSX 到 JS,Sass 到 CSS。transform 是对编译后代码做进一步的处理,典型的比如 PostCss。
4.2.3. 使用Esbuild
mjs、jsx、ts、tsx 这几种格式的脚本文件浏览器是无法直接执行的,为此,Snowpack 内置了一个 Esbuild 插件,如果用户没有显式指定用于处理 mjs、jsx、ts、tsx 的插件,那么这个内置的插件就会生效,用 Esbuild 进行这几类文件的编译,从而默认支持 TypeScript、JSX:
// add internal JS handler plugin if none specified
const needsDefaultPlugin = new Set(['.mjs', '.jsx', '.ts', '.tsx']);
plugins
.filter(((resolve) => !!resolve)
.reduce((arr, a) => arr.concat(a.resolve.input), [] as string[])
.forEach(((ext) => needsDefaultPlugin.delete(ext));
if (needsDefaultPlugin.size > 0) {
plugins.unshift(execPluginFactory(esbuildPlugin, {input: [...needsDefaultPlugin]}));
}
Esbuild 插件的实现,其实就是在 load 阶段使用 Esbuild 进行编译:
export function esbuildPlugin(config: SnowpackConfig, {input}: {input: string[]}): SnowpackPlugin {
return {
name: '@snowpack/plugin-esbuild',
resolve: {
input,
output: ['.js'],
},
async load({filePath}) {
esbuildService = esbuildService || (await startService());
const contents = await fs.readFile(filePath, 'utf-8');
const isPreact = checkIsPreact(filePath, contents);
const {js, jsSourceMap, warnings} = await esbuildService.transform(contents, {
loader: getLoader(filePath),
jsxFactory: isPreact ? 'h' : undefined,
jsxFragment: isPreact ? 'Fragment' : undefined,
sourcefile: filePath,
sourcemap: config.buildOptions.sourceMaps,
});
return {
'.js': {
code: js || '',
map: jsSourceMap,
},
};
},
cleanup() {
esbuildService && esbuildService.stop();
},
};
}
4.3. Rspack
Rspack 是一个快速的 JavaScript 和 TypeScript 构建工具,它的设计灵感来源于 Webpack,但使用了 Rust 编写,因而在性能上有很大的提升。Rspack 支持 React 应用的打包,在构建速度和体积优化上都有显著优势。下面介绍 Rspack 在 React 应用中的基础使用及其工作原理。
4.3.1. 基础使用
1. 安装 Rspack
首先需要在项目中安装 rspack 及其相关插件:
npm install --save-dev @rspack/core @rspack/cli @rspack/plugin-react
2. 配置 Rspack
在项目根目录下创建 rspack.config.js 文件,配置 Rspack 打包规则:
const path = require('path');
const ReactPlugin = require('@rspack/plugin-react').default;
module.exports = {
entry: './src/index.jsx', // 入口文件
output: {
path: path.resolve(__dirname, 'dist'), // 输出目录
filename: 'bundle.js', // 输出文件名
},
module: {
rules: [
{
test: /\.jsx?$/, // 处理 .js 和 .jsx 文件
exclude: /node_modules/,
use: 'babel-loader',
},
{
test: /\.css$/, // 处理 CSS 文件
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new ReactPlugin(), // 使用 React 插件
],
resolve: {
extensions: ['.js', '.jsx'], // 解析文件扩展名
},
mode: 'development', // 开发模式
};
3. 添加脚本命令
在 package.json 中添加构建命令:
{
"scripts": {
"build": "rspack"
}
}
4. 运行构建
使用命令进行构建:
npm run build
运行后,Rspack 会在 dist 目录下生成打包后的文件。
4.3.2. 原理分析
Rspack 的核心思想与 Webpack 类似,通过模块化的方式将应用的各个部分进行打包,但 Rspack 通过以下几个关键点提升了性能:
-
Rust 的高性能优势:Rspack 使用 Rust 语言实现,Rust 的编译器优化和内存安全特性,使得构建过程更加高效,并减少了内存泄漏的可能性。
-
多线程并行处理:Rspack 天生支持多线程并行处理,这在大型项目中表现尤为明显,能够显著减少构建时间。
-
依赖分析与缓存优化:Rspack 通过高效的依赖分析和缓存机制,减少了重复构建的时间,使得增量构建更为迅速。
-
与 Webpack 高度兼容:Rspack 的配置和插件机制与 Webpack 类似,大多数 Webpack 插件可以直接使用,这降低了迁移成本。
4.4. Vite
5. 补充资料
-
模块化规范:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
-
不同模块化规范详解:https://zh.javascript.info/modules-intro
-
早期的构建工具 grunt、gulp:https://gruntjs.com、https://gulpjs.com
-
rollup、parcel:https://rollupjs.org、https://parceljs.org/
-
Rollup awesome:https://github.com/rollup/awesome
-
esbuild:https://esbuild.github.io/
-
esbuild 上层封装:https://tsup.egoist.dev
-
rust 基础语法:https://doc.rust-lang.org/book/
-
rspack:https://rspack.dev/zh/
-
了解 rolldown:https://github.com/rolldown/rolldown
-
rust js ast parser:https://docs.rs/oxc_parser/latest/oxc_parser/#
-
Trae + Lynx:https://www.trae.ai/home