本篇文章是在阮一峰大佬的推荐中发现的,个人认为还是不错的。在中文社区的文章中很难看到这样一篇有关前端构建系统的论述。
首先是为什么要做构建?原因是浏览器版本不同,原生的 js 源码遵循的标准也不同,那么直接喂给浏览器就不一定能运行,所以需要对源码进行转换,比如将 ES6 转为 ES5、JSX 的转译等。
当项目变大时,源码体积自然就变大了,这时就需要分析文件之间的依赖关系,然后依据这些关系将文件合并在一起并成一个文件,然后统一给到浏览器,不然频繁请求的瀑布流就会对网络资源造成浪费。
为了进一步缩小文件大小,空格、注释那些无关紧要的东西就可以全都移除,这就是压缩的作用。
常见的打包工具有:Webpack、Parcel、Rollup、esbuild 和 Turbopack。Webpack 一直是不错的选择,利用 SWC、esbuild 等编译加载器更是可以快速实现源码转化;库的打包可以选择 Rollup;Turbopack 一直有被推荐在大型的 Monorepos 技术架构中,回头我打算尝试一下。
在打包时需要做的考量,除了源码编译能力,还要考虑如何分割代码、静态资源如何压缩,由于 ES6 模块具有静态的导出和导入,这个特点就可以用它来树摇使得冗余代码进一步瘦身。
为了能使得构建工具强得可怕,典型的如 Next.js、Vite 都是可以一键生成项目结构的,开发和构建能力很全面。 Next.js 能开发 SSR、Vite 可以打包小型组件库。
如果线上代码报错了怎么办?映射文件就很重要。热加载就不必说了,不过需要知道的是:Vite 依靠浏览器的 ESM 支持不会在开发服务器下打包应用,这种热重载速度飞快,如果当前页面所依赖的文件成千上万那也将是一个灾难,可能出白屏。
最后是 Monorepos 技术,当多个项目依赖同一种工具库、或者是像大型的组件库都会采用这种架构使得项目的管理难度降低,并且既能管理大型应用,又能对单个模块库进行单独地打包构建,实践上手难度会比较大。
总之,技术本身没有好坏之分,只是各有千秋,需要我们根据项目当下的情况来判断技术选择。
以下是原文:
June 8, 2024
2024 年 6 月 8 日
Developers write JavaScript; browsers run JavaScript. Fundamentally, no build step is necessary in frontend development. So why do we have a build step in modern frontend?
开发者编写 JavaScript;浏览器运行 JavaScript。从根本上说,前端开发不需要构建步骤。那么,为什么在现代前端开发中我们需要一个构建步骤呢?
As frontend codebases grow larger, and as developer ergonomics become more important, shipping JavaScript source code directly to the client leads to two primary problems:
随着前端代码库的不断扩大以及开发者体验变得越来越重要,直接将 JavaScript 源代码发送到客户端会导致两个主要问题:
Unsupported Language Features: Because JavaScript runs in the browser, and because there are many browsers out there of a variety of versions, each language feature you use reduces the number of clients that can execute your JavaScript. Furthermore, language extensions like JSX are not valid JavaScript and will not run in any browser.
不支持的语言特性:由于 JavaScript 在浏览器中运行,并且存在许多不同版本的浏览器,每个你使用的语言特性都会减少能够执行你 JavaScript 的客户端数量。此外,像 JSX 这样的语言扩展并不是有效的 JavaScript,无法在任何浏览器中运行。
Performance: The browser must request each JavaScript file individually. In a large codebase, this can result in thousands of HTTP requests to render a single page. In the past, before HTTP/2, this would also result in thousands of TLS handshakes.
性能问题:浏览器必须单独请求每个 JavaScript 文件。在大型代码库中,这可能会导致数千个 HTTP 请求来渲染一个页面。在过去,在 HTTP/2 出现之前,这还会导致数千次 TLS 握手。
In addition, several sequential network round trips may be needed before all the JavaScript is loaded. For example, if
index.js
importspage.js
andpage.js
importsbutton.js
, three sequential network round trips are necessary to fully load the JavaScript. This is called the waterfall problem.此外,在加载所有 JavaScript 之前,可能需要多次连续的网络往返。例如,如果
index.js
导入page.js
,而page.js
又导入button.js
,那么需要三次连续的网络往返才能完全加载这些 JavaScript。这被称为瀑布流问题。Source files can also be unnecessarily large due to long variable names and whitespace indentation characters, increasing bandwidth usage and network loading time.
源文件也可能由于变量名过长和空白缩进字符而变得不必要地大,增加了带宽使用量和网络加载时间。
Frontend build systems process source code and emit one or more JavaScript files optimized for sending to the browser. The resulting distributable is typically illegible to humans.
前端构建系统处理源代码,并生成一个或多个优化过的 JavaScript 文件,方便发送到浏览器。生成的可分发文件通常对人类不可读。
1. Build Steps 构建步骤
Frontend build systems typically consist of three steps: transpilation, bundling, and minification.
前端构建系统通常包括三个步骤:转译、打包和压缩。
Some applications may not require all three steps. For example, smaller codebases may not require bundling or minification, and development servers may skip bundling and/or minification for performance. Additional custom steps may also be added.
某些应用程序可能不需要所有三个步骤。例如,较小的代码库可能不需要打包或压缩,开发服务器可能会为了性能跳过打包和/或压缩。也可以添加额外的自定义步骤。
Some tools implement multiple build steps. Notably, bundlers often implement all three steps, and a bundler alone may be sufficient to build straightforward applications. Complex applications may require specialized tools for each build step that provide larger feature sets.
一些工具实现了多个构建步骤。特别是,打包工具通常实现了所有三个步骤,而单独使用一个打包工具可能就足以构建简单的应用程序。复杂的应用程序可能需要为每个构建步骤提供更大功能集的专用工具。
1.1. Transpilation 转译
Transpilation solves the problem of unsupported language features by converting JavaScript written in a modern version of the JavaScript standard to an older version of the JavaScript standard. These days, ES6/ES2015 is a common target.
转译通过将用现代 JavaScript 标准编写的 JavaScript 代码转换为较旧版本的 JavaScript 标准来解决不支持的语言特性问题。现在,ES6/ES2015 是一个常见的目标。
Frameworks and tools may also introduce transpilation steps. For example, the JSX syntax must be transpiled to JavaScript. If a library offers a Babel plugin, that usually means that it requires a transpilation step. Additionally, languages such as TypeScript, CoffeeScript, and Elm must be transpiled to JavaScript.
框架和工具也可能引入转译步骤。例如,JSX 语法必须转译为 JavaScript。如果一个库提供了 Babel 插件,通常意味着它需要一个转译步骤。此外,像 TypeScript、CoffeeScript 和 Elm 这样的语言也必须转译为 JavaScript。
CommonJS modules (CJS) must also be transpiled to a browser-compatible module system. After browsers added widespread support for ES6 Modules (ESM) in 2018, transpilation to ESM has generally been recommended. ESM is furthermore easier to optimize and tree-shake since its imports and exports are statically defined.
CommonJS 模块(CJS)也必须转译为浏览器兼容的模块系统。在 2018 年浏览器普遍支持 ES6 模块(ESM)之后,转译为 ESM 通常是推荐的做法。由于 ESM 的导入和导出是静态定义的,它更容易进行优化和树摇(tree-shake)。
The transpilers in common use today are Babel, SWC, and TypeScript Compiler.
当前常用的转译器包括 Babel、SWC 和 TypeScript 编译器。
Babel (2014) is the standard transpiler: a slow single-threaded transpiler written in JavaScript. Many frameworks and libraries that require transpilation do so via a Babel plugin, requiring Babel to be part of the build process. However, Babel is hard to debug and can often be confusing.
Babel(2014)是标准的转译器:一个慢速的单线程转译器,使用 JavaScript 编写。许多需要转译的框架和库通过 Babel 插件进行转译,因此 Babel 需要成为构建过程的一部分。然而,Babel 的调试难度较大,且常常令人困惑。
SWC (2020) is a fast multi-threaded transpiler written in Rust. It claims to be 20x faster than Babel; hence, it is used by the newer frameworks and build tools. It supports transpiling TypeScript and JSX. If your application does not require Babel, SWC is a superior choice.
SWC(2020)是一个快速的多线程转译器,使用 Rust 编写。它声称比 Babel 快 20 倍,因此被新兴框架和构建工具广泛使用。它支持 TypeScript 和 JSX 的转译。如果你的应用程序不需要 Babel,SWC 是一个更优的选择。
TypeScript Compiler (tsc) also supports transpiling TypeScript and JSX. It is the reference implementation of TypeScript and the only fully featured TypeScript type checker. However, it is very slow. While a TypeScript application must typecheck with the TypeScript Compiler, for its build step, an alternative transpiler will be much more performant.
TypeScript 编译器(tsc)也支持 TypeScript 和 JSX 的转译。它是 TypeScript 的参考实现,并且是唯一一个全面的 TypeScript 类型检查器。然而,它非常慢。虽然 TypeScript 应用程序必须使用 TypeScript 编译器进行类型检查,但在构建步骤中,使用替代的转译器会更高效。
It is also possible to skip the transpilation step if your code is pure JavaScript and uses ES6 Modules.
如果你的代码是纯 JavaScript 并且使用 ES6 模块,也可以跳过转译步骤。
An alternative solution for a subset of unsupported language features is a polyfill. Polyfills are executed at runtime and implement any missing language features before executing the main application logic. However, this adds runtime cost, and some language features cannot be polyfilled. See core-js.
另一种解决不支持的语言特性的方案是使用 polyfill。Polyfills 在运行时执行,实现在主应用逻辑执行之前所缺失的语言特性。然而,这会增加运行时开销,并且一些语言特性无法通过 polyfill 实现。可以参考 core-js
。
All bundlers are also inherently transpilers, as they parse multiple JavaScript source files and emit a new bundled JavaScript file. When doing so, they can pick which language features to use in their emitted JavaScript file. Some bundlers are additionally capable of parsing TypeScript and JSX source files. If your application has straightforward transpilation needs, you may not need a separate transpiler.
所有打包工具本质上也是转译器,因为它们解析多个 JavaScript 源文件并生成一个新的打包后的 JavaScript 文件。在此过程中,它们可以选择在生成的 JavaScript 文件中使用哪些语言特性。一些打包工具还能够解析 TypeScript 和 JSX 源文件。如果你的应用程序的转译需求比较简单,可能不需要单独的转译器。
1.2. Bundling 打包
Bundling solves the need to make many network requests and the waterfall problem. Bundlers concatenate multiple JavaScript source files into a single JavaScript output file, called a bundle, without changing application behavior. The bundle can be efficiently loaded by the browser in a single round-trip network request.
打包解决了需要进行多个网络请求和瀑布流问题。打包工具将多个 JavaScript 源文件连接成一个单一的 JavaScript 输出文件,称为捆绑文件(bundle),而不改变应用程序的行为。浏览器可以通过一次网络往返请求高效地加载这个捆绑文件。
The bundlers in common use today are Webpack, Parcel, Rollup, esbuild, and Turbopack.
当前常用的打包工具包括 Webpack、Parcel、Rollup、esbuild 和 Turbopack。
Webpack (2014) gained significant popularity around 2016, later becoming the standard bundler. Unlike the then-incumbent Browserify, which was commonly used with the Gulp task runner, Webpack pioneered “loaders” that transformed source files upon import, allowing Webpack to orchestrate the entire build pipeline.
Webpack(2014)在 2016 年左右获得了显著的普及,后来成为了标准的打包工具。与当时常用的 Browserify(通常与 Gulp 任务运行器一起使用)不同,Webpack 首创了“加载器”(loaders),这些加载器在导入时转换源文件,使得 Webpack 能够协调整个构建流程。
Loaders allowed developers to transparently import static assets inside JavaScript files, combining all source files and static assets into a single dependency graph. With Gulp, each type of static asset had to be built as a separate task. Webpack also supported code splitting out of the box, simplifying its setup and configuration.
加载器允许开发者在 JavaScript 文件中透明地导入静态资产,将所有源文件和静态资产组合成一个单一的依赖图。使用 Gulp 时,每种类型的静态资产必须作为单独的任务进行构建。Webpack 还开箱即用地支持代码分割,简化了其设置和配置。
Webpack is slow and single-threaded, written in JavaScript. It is highly configurable, but its many configuration options can be confusing.
Webpack 速度较慢,使用 JavaScript 编写,并且是单线程的。它的配置选项非常多,虽然高度可配置,但这些选项可能会令人困惑。
Rollup (2016) capitalized on the widespread browser support of ES6 Modules and the optimizations it enabled, namely tree shaking. It produced far smaller bundle sizes than Webpack, leading Webpack to later adopt similar optimizations. Rollup is a single-threaded bundler written in JavaScript, only slightly more performant than Webpack.
Rollup(2016)利用了浏览器对 ES6 模块的广泛支持及其带来的优化,特别是树摇(tree shaking)。它生成的捆绑文件比 Webpack 小得多,促使 Webpack 后来也采用了类似的优化。Rollup 是一个使用 JavaScript 编写的单线程打包工具,性能仅比 Webpack 略高。
Parcel (2018) is a low-configuration bundler designed to “just work” out of the box, providing sensible default configurations for all steps of the build process and developer tooling needs. It is multithreaded and much faster than Webpack and Rollup. Parcel 2 uses SWC under the hood.
Parcel(2018)是一个低配置的打包工具,旨在“开箱即用”,为构建过程的所有步骤和开发者工具需求提供合理的默认配置。它是多线程的,比 Webpack 和 Rollup 快得多。Parcel 2 在底层使用了 SWC。
Esbuild (2020) is a bundler architected for parallelism and optimal performance, written in Go. It is dozens of times more performant than Webpack, Rollup, and Parcel. Esbuild implements a basic transpiler as well as a minifier. However, it is less featureful than the other bundlers, providing a limited plugin API that cannot directly modify the AST. Instead of modifying source files with an esbuild plugin, the files can be transformed before being passed to esbuild.
Esbuild(2020)是一个为并行处理和最佳性能而设计的打包工具,使用 Go 编写。它的性能比 Webpack、Rollup 和 Parcel 高出几十倍。Esbuild 实现了一个基本的转译器和一个压缩器。然而,它的功能不如其他打包工具丰富,提供了一个有限的插件 API,不能直接修改 AST。相反,文件可以在传递给 esbuild 之前进行转换,而不是通过 esbuild 插件直接修改源文件。
Turbopack (2022) is a fast Rust bundler that supports incremental rebuilds. The project is built by Vercel and led by the creator of Webpack. It is currently in beta and may be opted into in Next.js.
Turbopack(2022)是一个快速的 Rust 打包工具,支持增量重建。该项目由 Vercel 开发,由 Webpack 的创始人主导。目前处于测试阶段,可以在 Next.js 中选择使用。
It is reasonable to skip the bundling step if you have very few modules or have very low network latency (e.g. on localhost). Several development servers also choose not to bundle modules for the development server.
如果你有很少的模块或网络延迟非常低(例如在本地环境中),跳过打包步骤是合理的。一些开发服务器在开发模式下也选择不对模块进行打包。
1.2.1. Code Splitting 代码分割
By default, a client-side React application is transformed into a single bundle. For large applications with many pages and features, the bundle can be very large, negating the original performance benefits of bundling.
默认情况下,客户端 React 应用程序被转化为一个单一的捆绑文件。对于具有许多页面和功能的大型应用程序,捆绑文件可能非常大,从而抵消了捆绑的原始性能优势。
Dividing the bundle into several smaller bundles, or code splitting, solves this problem. A common approach is to split each page into a separate bundle. With HTTP/2, shared dependencies may also be factored out into their own bundles to avoid duplication at little cost. Additionally, large modules may split into a separate bundle and lazy-loaded on-demand.
将捆绑文件拆分为多个较小的捆绑文件,或称为代码分割(code splitting),可以解决这个问题。一种常见的方法是将每个页面拆分成一个单独的捆绑文件。随着 HTTP/2 的普及,共享的依赖项也可以拆分到它们自己的捆绑文件中,以避免重复并且成本很低。此外,较大的模块可以拆分为单独的捆绑文件,并在需要时进行懒加载。
After code splitting, the filesize of each bundle is greatly reduced, but additional network round trips are now necessary, potentially re-introducing the waterfall problem. Code splitting is a tradeoff.
在代码分割之后,每个捆绑文件的文件大小大大减少,但现在需要额外的网络往返请求,这可能会重新引入瀑布流问题。代码分割是一种权衡。
The filesystem router, popularized by Next.js, optimizes the code splitting tradeoff. Next.js creates separate bundles per page, only including the code imported by that page in its bundles. Loading a page preloads all bundles used by that page in parallel. This optimizes bundle size without re-introducing the waterfall problem. The filesystem router achieves this by creating one entry point per page (pages/**/*.jsx
), as opposed to the single entry point of traditional client-side React apps (index.jsx
).
由 Next.js 推广的文件系统路由器优化了代码分割的权衡。Next.js 为每个页面创建单独的捆绑文件,仅在这些捆绑文件中包含该页面导入的代码。加载一个页面时,会并行预加载该页面使用的所有捆绑文件。这种方法优化了捆绑文件的大小,同时避免了重新引入瀑布流问题。文件系统路由器通过为每个页面创建一个入口点(如 pages/**/*.jsx
),而不是传统客户端 React 应用程序的单一入口点(如 index.jsx
),来实现这一点。
1.2.2. Tree Shaking 树摇
A bundle is composed of multiple modules, each of which contains one or more exports. Often, a given bundle will only make use of a subset of exports from the modules it imports. The bundler can remove the unused exports of its modules in a process called tree shaking. This optimizes the bundle size, improving loading and parsing times.
一个捆绑文件由多个模块组成,每个模块包含一个或多个导出。通常,一个给定的捆绑文件只会使用它导入的模块中的一部分导出。打包工具可以通过一个叫做树摇(tree shaking)的过程来移除模块中未使用的导出。这优化了捆绑文件的大小,从而提高加载和解析速度。
Tree shaking depends on static analysis of the source files, and is thus impeded when static analysis is made more challenging. Two primary factors influence the efficiency of tree shaking:
树摇依赖于对源文件的静态分析,因此在静态分析变得更具挑战性时,树摇的效果会受到影响。影响树摇效率的两个主要因素是:
Module System: ES6 Modules have static exports and imports, while CommonJS modules have dynamic exports and imports. Bundlers are thus able to be more aggressive and efficient when tree shaking ES6 Modules.
模块系统:ES6 模块具有静态的导出和导入,而 CommonJS 模块具有动态的导出和导入。因此,打包工具在进行 ES6 模块的树摇时,能够更加积极和高效。
Side Effects: The
sideEffects
property ofpackage.json
declares whether a module has side effects on import. When side effects are present, unused modules and unused exports may not be tree shaken due to the limitations of static analysis.副作用:
package.json
的sideEffects
属性声明了一个模块在导入时是否具有副作用。当存在副作用时,由于静态分析的限制,未使用的模块和未使用的导出可能无法被树摇掉。
1.2.3. Static Assets 静态资源
Static assets, such as CSS, images, and fonts, are typically added to the distributable in the bundling step. They may also be optimized for filesize in the minification step.
静态资产,如 CSS、图片和字体,通常在打包步骤中添加到可分发文件中。在压缩步骤中,它们也可能会进行文件大小优化。
Prior to Webpack, static assets were built separately from the source code in the build pipeline as an independent build task. To load the static assets, the application had to reference them by their final path in the distributable. Thus, it was common to carefully organize assets around a URL convention (e.g. /assets/css/banner.jpg
and /assets/fonts/Inter.woff2
).
在 Webpack 之前,静态资产通常在构建流程中作为独立的构建任务与源代码分开构建。为了加载这些静态资产,应用程序必须通过它们在可分发文件中的最终路径来引用它们。因此,通常会围绕 URL 约定(例如 /assets/css/banner.jpg
和 /assets/fonts/Inter.woff2
)仔细组织资产。
Webpack “loaders” allowed the importing of static assets from JavaScript, unifying both code and static assets into a single dependency graph. During bundling, Webpack replaces the static asset import with its final path inside the distributable. This feature enabled static assets to be organized with their associated components in the source code and created new possibilities for static analysis, such as detecting non-existent assets.
Webpack 的“加载器”(loaders)允许从 JavaScript 中导入静态资产,将代码和静态资产统一到一个单一的依赖图中。在打包过程中,Webpack 会将静态资产的导入替换为其在可分发文件中的最终路径。这一特性使得静态资产可以与源代码中的相关组件一起组织,并为静态分析创造了新的可能性,例如检测不存在的资产。
It is important to recognize that the importing of static assets (non-JavaScript-or-transpiles-to-JavaScript files) is not part of the JavaScript language. It requires a bundler configured with support for that asset type. Fortunately, the bundlers that followed Webpack also adopted the “loaders” pattern, making this feature commonplace.
需要认识到,静态资产的导入(即非 JavaScript 文件或转译为 JavaScript 的文件)并不是 JavaScript 语言的一部分。它需要一个配置了对该资产类型支持的打包工具。幸运的是,继 Webpack 之后的打包工具也采纳了“加载器”模式,使得这一功能变得非常普遍。
1.3. Minification 压缩
Minification resolves the problem of unnecessarily large files. Minifiers reduce the size of a file without affecting its behavior. For JavaScript code and CSS assets, minifiers can shorten variables, eliminate whitespace and comments, eliminate dead code, and optimize language feature use. For other static assets, minifiers can perform file size optimization. Minifiers are typically run on a bundle at the end of the build process.
压缩(Minification)解决了文件不必要的庞大问题。压缩器在不影响文件行为的情况下,减少文件的大小。对于 JavaScript 代码和 CSS 资产,压缩器可以缩短变量名、消除空白和注释、删除无用代码,并优化语言特性使用。对于其他静态资产,压缩器也可以进行文件大小优化。压缩器通常在构建过程的最后阶段对捆绑文件进行处理。
Several JavaScript minifiers in common use today are Terser, esbuild, and SWC. Terser was forked from the unmaintained uglify-es. It is written in JavaScript and is somewhat slow. Esbuild and SWC, mentioned previously, implement minifiers in addition to their other capabilities and are faster than Terser.
目前常用的 JavaScript 压缩器包括 Terser、esbuild 和 SWC。Terser(官网)是从不再维护的 uglify-es 分叉而来的。它使用 JavaScript 编写,速度相对较慢。Esbuild 和 SWC,如前所述,除了其他功能外,还实现了压缩器,速度比 Terser 快。
Several CSS minifiers in common use today are cssnano, csso, and Lightning CSS. Cssnano and csso are pure CSS minifiers written in JavaScript and thus somewhat slow. Lightning CSS is written in Rust and claims to be 100x faster than cssnano. Lightning CSS additionally supports CSS transformation and bundling.
目前常用的 CSS 压缩器包括 cssnano、csso 和 Lightning CSS。Cssnano(官网)和 csso(GitHub)是用 JavaScript 编写的纯 CSS 压缩器,因此速度较慢。Lightning CSS(官网)使用 Rust 编写,并声称比 cssnano 快 100 倍。Lightning CSS 还支持 CSS 转换和打包。
2. Developer Tooling 开发者工具
The basic frontend build pipeline described above is sufficient to create an optimized production distributable. There exist several classes of tools that augment the basic build pipeline and improve upon developer experience.
上述基本的前端构建流程足以创建一个优化过的生产可分发文件。除此之外,还有几类工具可以增强基本的构建流程,并提升开发者体验。
2.1. Meta-Frameworks 元框架
The frontend space is notorious for the challenge of picking the “right” packages to use. For example, of the five bundlers listed above, which should you pick?
前端领域以选择“正确”包的挑战而著称。例如,在上述提到的五种打包工具中,你应该选择哪一个?
Meta-frameworks provide a curated set of already selected packages, including build tools, that synergize and enable specialized application paradigms. For example, Next.js specializes in Server-Side Rendering (SSR) and Remix specializes in progressive enhancement.
元框架(Meta-frameworks)提供了一套精选的工具包,包括构建工具,这些工具包协同工作并支持特定的应用程序范式。例如,Next.js 专注于服务器端渲染(SSR),而 Remix 专注于渐进增强(progressive enhancement)。
Meta-frameworks typically provide a preconfigured build system, removing the need for you to stitch one together. Their build systems have configurations for both production and development servers.
元框架通常提供预配置的构建系统,免去了你需要自己拼凑的麻烦。它们的构建系统为生产和开发服务器都进行了配置。
Like meta-frameworks, build tools like Vite provide preconfigured build systems for both production and development. Unlike meta-frameworks, they do not force a specialized application paradigm. They are suitable for generic frontend applications.
类似于元框架,构建工具如 Vite 也提供了针对生产和开发的预配置构建系统。不同的是,它们不强制要求特定的应用程序范式,因此适用于通用的前端应用程序。
2.2. Sourcemaps 源码映射
The distributable emitted by the build pipeline is illegible to most humans. This makes it difficult to debug any errors that occur, as their tracebacks point to illegible code.
构建流程生成的可分发文件对于大多数人来说是不可读的。这使得调试发生的错误变得困难,因为错误的堆栈跟踪指向不可读的代码。
Sourcemaps resolve this problem by mapping code in the distributable back to its original location in the source code. The browser and triage tools (e.g. Sentry) use the sourcemaps to restore and display the original source code. In production, sourcemaps are often hidden from the browser and only uploaded to triage tools to avoid publicizing the source code.
源码映射(Sourcemaps) 解决了这个问题,通过将可分发文件中的代码映射回其在源代码中的原始位置。浏览器和调试工具(如 Sentry)使用源码映射来恢复和显示原始源代码。在生产环境中,源码映射通常会隐藏在浏览器中,仅上传到调试工具,以避免公开源代码。
Each step of the build pipeline can emit a sourcemap. If multiple build tools are used to construct the pipeline, the sourcemaps will form a chain (e.g. source.js
-> transpiler.map
-> bundler.map
-> minifier.map
). To identify the source code corresponding to the minified code, the chain of source maps must be traversed.
构建流程的每一步都可以生成一个源码映射。如果使用多个构建工具来构建流程,这些源码映射将形成一个链条(例如 source.js
-> transpiler.map
-> bundler.map
-> minifier.map
)。要识别与压缩代码对应的源代码,必须遍历源码映射链条。
However, most tools are not capable of interpreting a chain of sourcemaps; they expect at most one sourcemap per file in the distributable. The chain of sourcemaps must be flattened into a single sourcemap. Preconfigured build systems will solve this problem (see Vite’s combineSourcemaps
function).
然而,大多数工具无法解释源码映射链条;它们最多只能处理每个文件一个源码映射。源码映射链条必须被扁平化为一个单一的源码映射。预配置的构建系统可以解决这个问题(参见 Vite 的 combineSourcemaps
函数)。
2.3. Hot Reload
Development servers often provide a Hot Reload feature, which automatically rebuilds a new bundle on source code changes and reloads the browser. While greatly superior to rebuilding and reloading manually, it is still somewhat slow, and all client-side state is lost on reload.
开发服务器通常提供热重载(Hot Reload)功能,能够在源代码更改时自动重建新的打包文件并重新加载浏览器。虽然比手动重建和重新加载要好得多,但它仍然有些慢,并且每次重新加载都会丢失所有客户端状态。
Hot Module Replacement improves upon Hot Reload by replacing changed bundles in the running application, an in-place update. This preserves the client-side state of unchanged modules and reduces the latency between code change and updated application.
热模块替换(Hot Module Replacement,HMR)在热重载的基础上进行了改进,通过在运行中的应用程序中替换已更改的打包文件进行就地更新。这保留了未更改模块的客户端状态,并减少了代码更改与应用程序更新之间的延迟。
However, each code change triggers the rebuild of all the bundles that import it. This has a linear time complexity relative to bundle size. Hence, in large applications, Hot Module Replacement can become slow due to the growing rebundling cost.
然而,每次代码更改都会触发所有导入该代码的打包文件的重建。这在打包文件较大的情况下具有线性时间复杂度,因此,在大型应用程序中,热模块替换可能由于不断增加的重打包成本而变得缓慢。
The no-bundle paradigm, currently championed by Vite, counters this by not bundling the development server. Instead, Vite serves ESM modules, each corresponding to a source file, directly to the browser. In this paradigm, each code change triggers a single module replacement in the frontend. This results in a near-constant refresh time complexity relative to application size. However, if you have many modules, the initial page load may take longer.
无打包范式(no-bundle paradigm),当前由 Vite 推崇,通过不在开发服务器中打包来应对这一问题。相反,Vite 直接将每个对应于源文件的 ES6 模块(ESM 模块)提供给浏览器。在这种范式下,每次代码更改会在前端触发单个模块替换。这使得相对于应用程序大小,刷新时间复杂度接近常数。然而,如果模块很多,初始页面加载可能会更慢。
2.4. Monorepos
In organizations with multiple teams or multiple applications, the frontend may be split into multiple JavaScript packages, but retained in a single repository. In such architectures, each package has its own build step, and together they form a dependency graph of packages. The applications reside at the roots of the dependency graphs.
在拥有多个团队或多个应用程序的组织中,前端代码可能会被拆分成多个 JavaScript 包,但仍保留在单个代码库中。在这种架构中,每个包都有自己的构建步骤,这些包一起形成了一个依赖图。应用程序位于依赖图的根部。
Monorepo tools orchestrate the building of the dependency graph. They often provide features such as incremental rebuilds, parallelism, and remote caching. With these features, large codebases can enjoy the build times of small codebases.
Monorepo 工具负责协调依赖图的构建。它们通常提供增量重建、并行处理和远程缓存等功能。通过这些功能,大型代码库可以享受小型代码库的构建时间。
The broader industry-standard monorepo tools, like Bazel, support a broad set of languages, complicated build graphs, and hermetic execution. However, JavaScript for frontend is one of the hardest ecosystems to completely integrate with these tools, and there is currently little prior art.
行业标准的广泛 monorepo 工具,如 Bazel,支持广泛的语言、复杂的构建图和封闭的执行。然而,JavaScript 前端是这些工具完全整合中最困难的生态系统之一,目前几乎没有先例。
Fortunately, there exist several monorepo tools designed specifically for frontend. Unfortunately, they lack the flexibility and robustness of Bazel et al., most notably hermetic execution.
幸运的是,也存在一些专门为前端设计的 monorepo 工具。遗憾的是,它们缺乏 Bazel 等工具的灵活性和可靠性,特别是在封闭执行方面。
The frontend-specific monorepo tools in common use today are Nx and Turborepo. Nx is more mature and featureful, while Turborepo is part of the Vercel ecosystem. In the past, Lerna was the standard tool for linking multiple JavaScript packages together and publishing them to NPM. In 2022, the Nx team took over Lerna, and Lerna now uses Nx under the hood to power builds.
当前常用的前端特定 monorepo 工具包括 Nx 和 Turborepo。Nx 更加成熟和功能丰富,而 Turborepo 是 Vercel 生态系统的一部分。在过去,Lerna 是将多个 JavaScript 包链接在一起并发布到 NPM 的标准工具。2022 年,Nx 团队接管了 Lerna,现在 Lerna 在内部使用 Nx 来驱动构建。
3. Trends 趋势
Newer build tools are written in compiled languages and emphasize performance. Frontend builds were terribly slow in 2019, but modern tools have greatly sped it up. However, modern tools have smaller feature sets and are sometimes incompatible with libraries, so legacy codebases often cannot easily switch to them.
较新的构建工具通常是用编译语言编写的,注重性能。2019年的前端构建速度非常慢,但现代工具大大提高了速度。然而,现代工具的功能集较小,有时与一些库不兼容,因此遗留代码库通常无法轻松切换到这些新工具上。
Server-Side Rendering (SSR) has become more popular after the rise of Next.js. SSR does not introduce any fundamental differences to frontend build systems. SSR applications must also serve JavaScript to the browser, and they thus execute the same build steps.
服务端渲染(SSR)在 Next.js 兴起后变得更加流行。SSR 不会对前端构建系统引入任何根本性的区别。SSR 应用程序也必须将 JavaScript 提供给浏览器,因此它们执行相同的构建步骤。