本文译自:https://css-tricks.com/comparing-the-new-generation-of-build-tools/
在过去的一年里,出现了一批新的开发者工具,它们正在紧跟过去几年主导前端开发的工具,包括 webpack、Babel、Rollup、Parcel、create-react-app
。
这些新的工具并不是为了完成完全相同的功能而设计的,每个工具都有不同的目标和功能。尽管存在差异,但这些工具有一个共同的目标:改善开发者体验
。
概览
esbuild
Snowpack
Vite
wmr
Feature comparison
Wrapping up
具体来说,我想对每一个进行评估,概述它们的作用,为什么我们需要它们,以及它们的使用案例。比较并不总是公平的,我们在这篇文章中看到的东西也不是直接的竞争对手。事实上,Snowpack
和 Vite
在某些任务中都使用了 esbuild
。我们的目标更多的是为了更好地了解运行任务的开发者工具的格局,让我们的工作更轻松。通过这种方式,我们就能看到有哪些选择,以及它们是如何配合的,这样我们就能在需要的时候做出最好的选择。
当然,我分析的所有的这些都会受到我使用 React
和 Preact
的经验的影响。我对这些框架库比较熟悉,但我也会关注它们对其他前端框架的支持。
为啥这些工具现在都出现了?
在某种程度上,我认为这些工具的到来是对 JavaScript
工具疲劳的一种反应。
Snowpack、Vite
和 wmr
都用到了浏览器中的原生 JavaScript
模块。早在 2018
年,Firefox 60
发布时默认启用了 ECMAScript 2015
的 Module
。此后,各大浏览器引擎都支持原生 JavaScript
模块。Node.js
也在2019年11月推出了原生 JavaScript
模块。在2021年的今天,我们还在寻找原生 JavaScript
模块能够带来哪些新的可能性。
这些工具和现有的工具有什么不同?
无论我们在开发服务器上使用 webpack、Rollup
还是 Parcel
,工具都会从我们的源代码和 node_modules
文件夹中把我们的整个代码库打包在一起,通过构建过程运行这些代码,比如 Babel、TypeScript
或 PostCSS
,然后将打包的代码推送到我们的浏览器上。这一切都需要花费大量的工作,并且会使开发服务器在更大的代码库中慢慢爬行,甚至在所有的工作都用于缓存和优化之后也是如此。
Snowpack、Vite
和 wmr
开发服务器则不采用这种模式。相反,它们会等到浏览器找到一个 import
语句,并为模块发出 HTTP
请求。只有在这个请求发出后,该工具才会对请求的模块和模块导入树中的任何叶节点应用转换,然后将这些转换提供给浏览器。这大大加快了速度,因为在推送到开发服务器的过程中减少了工作。
你会注意到描述中缺少了 esbuild
。它首先是一个 bundler
程序。它并不像其他工具那样绕开 bundler
。相反,esbuild
通过避免昂贵的转换、利用并行化和使用Go语言来快速处理代码。
实验
我从 React
文档中选取了一个示例应用,并使用文中所提到的每个工具重新构建了它。我选择的项目是 Yogita Verma
的 Snap Shot
。这里有一个原始 repo
的链接,还有一个我的repo
链接,里面有四个版本的 Snap Shot
,每个版本都使用不同的构建工具。我们稍后会比较每个构建步骤的输出。重新构建这个应用程序,让我可以测试开发人员将一些相当标准的 React
依赖项添加到工具(包括 React Router
和 axios
)中的体验。
- 原始 repo:https://github.com/Yog9/SnapShot
- Demo:https://github.com/Elliotclyde/build-tool-test
可以比较的功能
在我们深入了解每个工具的具体内容之前,它们都支持以下功能(在不同程度上)。
- 对原生
JavaScript
模块的一流支持 TypeScript
编译(但不进行类型检查)JSX
- 用于扩展性的插件
API
- 内置开发服务器
CSS bundling
和对CSS-in-JS
的支持。
所有这些工具都可以将 TypeScript
编译成 JavaScript
,但即使有类型错误也会这样做。为了进行正确的类型检查,你需要安装T ypeScript
,并在你的 JavaScript
根文件上运行 tsc --noEmit
,或者使用编辑器插件来观察类型错误。
好了,下面我们来具体看看每个工具。
esbuild
esbuild
是由 Evan Wallace
(Figma的CTO)创建的。它的主要特点是,它提供的构建步骤比基于 Node
的打包器快 10×-100×
(根据他们自己的基准)。它没有提供许多你可能会在 create-react-app
这样的工具中找到的开发者便利。但是有越来越多的 esbuild starter
启动器出现来填补这些空白,包括 create-react-app-esbuild,estrella
和 Snowpack
,它们的构建步骤使用 esbuild
。
esbuild
是非常新的。它还没有达到 1.0
版本,还没有完全准备好用于生产使用 — 但它已经不远了。它为你提供了直观的 JavaScript
和带有智能默认值的命令行 API
。
用例
esbuild
完全改变了 bundler
的游戏规则。它将在大型代码库中发挥最大的作用,因为 esbuild
和 node
打包器之间的速度差异会成倍增加。当 esbuild
达到 1.0
的时候,它在大型生产站点中会非常有用,将为团队节省大量等待构建完成的时间。不幸的是,大型生产站点必须要等到 esbuild
变得稳定。在此期间,它只是很好地增加了一些速度,让你在项目中的 bundling
变得更快。
esbuild
快如闪电的速度对于你正在做的任何工作来说都是一种奖励。减少等待构建运行的时间,对开发者的体验总是有好处的! 考虑到这一点,如果你是在做快速应用的原型,你可能会想要从比 esbuild
更高级的东西开始--否则,在获得我们期望的 JavaScript
中的便利之前,你需要花一些时间引入依赖项并配置你的环境生态系统。另外,如果你想尽可能地减小 bundle
包的大小,你可能会想使用 Rollup
和 terser
,它们会产生略小的 bundle
大小。
设置
我决定以一种幼稚的方式在 esbuild
中启动一个 React
项目:npm安装 esbuild、React
和 ReactDOM
。我创建了一个 src/code秘密花园.jsx
文件和一个 dist/index.html
文件。然后,我使用下面的命令将app编译成一个 dist/bundle.js
文件。
./node_modules/.bin/esbuild src/code秘密花园.jsx --bundle --platform=browser --outfile=dist/bundle.js
当我在浏览器中打开 index.html
时,我遇到了 "白屏 "和 "Uncaught ReferenceError: process is not defined " 的控制台错误。文档和CLI都准确地解释了你需要做什么来避免这种情况,但对于初学者来说,这可能有点 "捉襟见肘",因为在 bundling React
时,它需要一个额外的参数。
--define:process.env.NODE_ENV="production"
或者,如果你在 npm
脚本中包含了 esbuild
,就像这样写来转义引号。
--define:process.env.NODE_ENV=\"production\"
任何绑定到浏览器的需要 node
环境变量的库都需要这个 define
参数。Vue 2.0
也需要这些参数。你在使用 Preact
时不会有同样的问题,因为它不需要任何环境变量,而且默认情况下已经为浏览器准备好了。
在运行了带有定义参数的命令后,我的 "Hello world ConardLi " React 应用完美地运行了。JSX
可以使用 .jsx
文件开箱即用。也就是说,React
需要手动导入,然后将JSX转换为 React.createElement
。然而,有一些方法可以在 JSX
中添加自动导入,或为 Preact
配置JSX。
用法
esbuild
为开发服务器提供了一个 -serve
的选项。它绕过了文件系统,直接从内存中为模块提供服务,确保浏览器不会提取旧版本的模块。然而,它不包括实时/热重载,所以你会发现自己在保存后要刷新浏览器,这不是一个良好的体验。
我决定使用新发布的 watch
功能.这告诉 esbuild
在每次保存源文件时重新编译代码。但是我们仍然需要一个服务器来查看我们保存的变化。我们可以拉入一个开发服务器包,比如 Luke Jackson
的 servor
。
npm install servor --save-dev
然后我们就可以使用 esbuild
的 Javascript API
作为服务器启动,同时运行 esbuild
的 watch
模式。让我们在项目的根目录创建一个名为 watch.js
的文件。
// watch.js
const esbuild = require("esbuild");
const servor = require("servor");
esbuild.build({
// pass any options to esbuild here...
entryPoints: ["src/app.jsx"],
outdir: "dist",
define: { "process.env.NODE_ENV": '"production"' },
watch: true,
});
async function serve(){
console.log("running server from: http://localhost:8080/");
await servor({
// pass any options to servor here...
browser:true,
root: "dist",
port: 8080,
});
}
serve();
现在在命令行中运行 node watch.js
。这为我们提供了一个很好的开发服务器,但是同样,它也不能给我们提供热更新或者快速刷新(也就是说,你的客户端状态不会被保存)。但这已经足够满足我的测试需求了。
即使我们每次保存文件时都要对整个应用程序进行重新编译,但在 esbuild
变慢之前,我们需要有一个相当庞大的应用程序。在我设置了这个工具之后,我从更改中得到了即时的反馈。我的电脑使用的是2012年的英特尔i7,所以它肯定不是一台顶级的机器。
如果你需要一个带有实时重载和一些 React
默认值的预配置 esbuild
版本,你可以克隆这个 repo
。
https://github.com/Elliotclyde/esbuild-react-starter
支持的文件
如果这是你的风格,esbuild
可以在 JavaScript
中导入 CSS
。它将会把CSS编译成一个输出文件,名字和你的主输出 JavaScript
文件一样。它还可以默认打包 CSS @import
语句。目前还没有对CSS模块的支持,但有计划。
用于 esbuild
的插件社区正在不断壮大。例如,Vue单文件组件和 Svelte
组件都有可用的插件。
esbuild
可以使用 JSON
文件,并且可以将它们 bundle
到 JavaScript
模块中,无需任何配置。
它还可以用 JavaScript
导入图片,可以选择将图片转换为数据URL或复制到输出文件夹中。这种行为在默认情况下并没有启用,但你可以在你的 esbuild
配置对象中添加以下内容来启用这两个选项。
loader: { '.png': 'dataurl' } // Converts to data url in JS bundle
loader: { '.png': 'file' } // Copies to output folder
代码拆分似乎是一项正在进行中的工作,但大多数情况下是以ESM输出格式进行的,而且看起来确实是项目的优先级。另外值得一提的是,tree-shaking
是 esbuild
默认内置的,无法关闭。
生产构建
在 esbuild 命令中使用 "minify "和 "bundle " 选项不会创建一个像 Rollup/Terser
流水线一样小的 bundle
。这是因为 esbuild
牺牲了一些bundle
大小的优化来尽可能少的通过你的代码。然而,根据你的项目,这种差异可能是微不足道的,但对于bundle
速度的提高来说是值得的。在我的 Snap Shot
应用程序的克隆中,esbuild
创建了一个177KB的包,这比使用 rollup
和 terser
的 Vite
产生的165KB多不了多少。
总结
esbuild是一个非常强大的工具。但如果你习惯于零配置的设置,那可能会很困难。如果你需要更多,那么你可能想看看下一个工具,基于esbuild的Snowpack。
Snowpack
Snowpack
是由 Skypack
和 Pika
的创造者开发的一款构建工具。它提供了一个很棒的开发服务器,并且是以 "非打包式开发 "的理念创建的。
引用文档中的一句话 "你应该能够使用一个打包程序,因为你想要,而不是因为你需要。"
默认情况下, Snowpack
的构建步骤并没有将文件打包到一个单一的包中,而是提供了在浏览器中运行的非打包esmodules
。实际上 esbuild
是作为一个依赖关系包含在其中的,但我们的想法是使用 JavaScript
模块,只有在需要时才与 esbuild
打包。
Snowpack
有一些非常精巧的文档,包括一个与J avaScript
框架一起使用的指南列表,以及一堆模板。有些指南还在不断完善中,但其他的指南,比如针对 React
的指南,就很不错,很清晰。看起来 Snowpack
也把 Svelte
当做一等公民来对待。实际上,我第一次听说 Snowpack
是在2020年Svelte峰会上 Rich Harris
的 "未来主义Web开发 "演讲中。也就是说,即将推出的 Svelte
元框架 SvelteKit
本来应该由 Snowpack
提供支持,但后来改用了 Vite
(我们接下来会对其进行评测)。
使用案例
如果你想在非打包部署上加倍努力,Snowpack
是个不错的选择。你可能会用少量的模块来编写源代码,这就意味着你不会用非捆绑构建来创建一个大的请求瀑布。如果你不需要额外的复杂性和技术债务,那么 Snowpack
是一个很好的选择。一个很好的用例是,如果你正在增量地将前端框架采用到服务器渲染或静态的应用程序中。你可以从node生态系统中获得尽可能少的工具,但你仍然会得到声明式前端框架的好处。
其次,我认为 Snowpack
是 esbuild
的一个很好的封装器。如果你想尝试 esbuild
,但同时又想拥有一个开发服务器和预先编写的前端框架模板,那么选择 Snowpack
是不会错的。在 Snowpack
配置的构建步骤中启用 esbuild
,你就可以了。
就目前的情况来看,我认为 Snowpack
不会是像 create-react-app
这样的零配置工具的最佳替代品,因为如果你有一个大的应用,需要一个超级花哨的优化生产就绪的构建步骤,你就需要自己导入插件并配置它们。
设置
让我们通过命令行来启动Snowpack的项目。
代码语言:javascript复制mkdir snowpackproject
cd snowpackproject
npm init #fill with defaults
npm install snowpack
现在,让我们在package.json中添加以下内容。
代码语言:javascript复制// package.json
"scripts": {
"start": "snowpack dev",
"build": "snowpack build"
},
接下来,我们将创建一个配置文件。
代码语言:javascript复制// Mac or Linux
touch snowpack.config.js
// Windows
new-item snowpack.config.js
我认为 Snowpack
最神奇的地方在于在配置文件中设置一个看似无害的键值对。例如,把这个粘贴到配置文件中。
// snowpack.config.js
module.exports = {
packageOptions: {
"source": "remote",
}
};
source: remote
启用了一种叫做流式导入的东西。通过流式导入使 Snowpack
能够绕过npm安装,将裸导入(例如,从import React from‘ React’
)转换为 Skypack
的CDN导入。
继续前进,让我们创建一个 index.html
文件。
<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">>
<title>Snowpack streaming imports</title>
</head>
<body>
<div id="root"></div>
<!-- Note the type="module". This is important for JavaScript module imports. -->
<script type="module" src="app.js"></script>
</body>
</html>
最后,我们将添加一个 code秘密花园.jsx
文件。
// code秘密花园.jsx
import React from 'react'
import ReactDOM from 'react-dom'
const App = ()=>{
return <h1>Welcome to Snowpack streaming imports!</h1>
}
ReactDOM.render(<App />,document.getElementById('root')); 0
注意,我们在任何阶段都没有安装 React
或 ReactDOM
的npm。但如果我们像这样启动 Snowpack
开发者服务器。
./node_modules/.bin/snowpack dev
我们的应用程序还能用。
Snowpack
不是从 nodemodules
文件夹中提取,而是从 Skypack
中提取npm包,Skypack
是一个托管 npm
注册表的CDN,它是预先优化的,可以在浏览器中工作。然后,Snowpack
将它放在一个 ./snowpack/pkg
URL中。
用法
这离基于 Node/npm
的工作流还有很大的差距。我们实际上看到的是一个新的基于 CDN/JavaScript
模块的工作流。
然而,如果我们的应用按原样运行生产构建,Snowpack
会抛出一个错误。这是因为它需要知道在构建时要使用哪个版本的 React
和 ReactDOM
。你可以通过一个 snowpack.deps.json
来解决这个问题,它可以通过运行下面的程序自动创建。
./node_modules/.bin/snowpack add react
./node_modules/.bin/snowpack add react-dom
这不会从 npm
下载包,但它会记录用于 Snowpack
构建所使用包的版本。
一个需要注意的是,我们会错过开发者的错误信息,因为 Skypack
会发布生产版本的包。
即使我们没有使用流式导入,Snowpack
开发服务器也会将 node_modules
中的每个依赖关系打包成一个 JavaScript
文件,将这些文件转换为本地 JavaScript
模块,然后将其提供给浏览器。这意味着浏览器可以缓存这些脚本,只有在它们发生变化时才会重新请求它们。开发服务器会在保存时自动刷新,但不会保留客户端的状态。所有来自 node
的依赖关系似乎都能正常工作,不管它们是使用传统的模块格式还是 node API
(比如我们在 esbuild
中遇到的臭名昭著的 process.env
)。
在 React
中保存客户端状态需要 react-refresh
,它需要一些自己的 Babel
包作为依赖。这些包不是默认包含的,但可以使用更最大化的React模板。该模板拉入了 react-refresh、Prettier、Chai
和 React Testing Library
,总体的 Node
依赖包重达 80MB
。
npx create-snowpack-app my-react-project --template @snowpack/app-template-react
支持的文件
支持JSX,但同样,默认情况下只支持 .jsx
文件。Snowpack
会自动检测是使用 React
还是 Preact
,并据此决定使用哪种渲染函数来进行JSX转换。但是,如果我们想进一步定制JSX,就需要通过他们的插件引入 Babel
。还有一个 Snowpack
插件可以用于 Vue
单文件组件,当然也可以用于 Svelte
组件。此外,Snowpack
还可以编译 TypeScript
,但对于类型检查,我们需要 TypeScript
插件。
CSS可以导入到 JavaScript
中,并在运行时被扔到文档 <head>
中。只要CSS模块的扩展名为 .module.css
,也支持开箱即用的 scoping
。
导入的JSON文件将被强制转换为一个 JavaScript
模块中,并以对象作为默认导出。Snowpack
支持图片,并将其复制到生产文件夹中。为了配合它的非打包理念,Snowpack
不将图像作为数据URL纳入捆绑中。
生产构建
默认的 snowpack
构建命令基本上是将源文件结构复制到一个输出文件夹中。对于编译成 JavaScript
的文件(例如TypeScript, JSX, JSON, .vue, .svelte
),它将每个单独的文件转换成一个独立的浏览器友好的 JavaScript
模块。
这很好用,但对于生产来说并不是很好,因为如果源码被分割成很多文件,可能会引起大量的请求。在 Snap Shot
应用中,我最终得到了 184KB
的源文件,然后又从 Skypack
中请求了 105KB
的依赖关系,这就造成了一个非常大的请求。
然而,Snowpack
将 esbuild
作为一个依赖项,我们可以通过在Snowpack配置中添加一个 "optimization "对象,使 esbuild
能够打包、最小化和编译我们的代码。
// snowpack.config.js
module.exports = {
optimize: {
bundle: true,
minify: true,
target: 'es2018',
},
这样就可以使用 esbuild
提供的优化功能来运行代码,所以只要加入这些选项,我们就可以得到和之前使用 esbuild
一样的构建。
总结
Snowpack
通过功能齐全的开发服务器、详细的文档和易于安装的模板提供轻量级的开发人员体验。你可以决定是否要打包你的应用程序以及如何打包。如果你想要一个既能提供开发服务器又能提供更有意见的构建步骤的工具,你可能会想看看我们列表中的下一个工具 Vite
。
查看更多文章分类,戳这里