我们,全栈式Rails工程师,已经走过了漫长的道路。我们一开始是全栈工程师,我们的后端和前端都在一个框架内。我们有资产管道(用sprockets)来帮助我们维护这个生态系统。 当时间到了,我们引入webpacker来填补sprockets的不足。
在这篇文章中,我们将看看如何从webpacker迁移到esbuild,从而在我们的旅程中迈出下一步。
以Rails 5的方式实现JS的旅程
前台的发展速度很快,浏览器技术已经很难跟上。因此,额外的工具,如npm/yarn、webpack和babel出现在现场。这些工具为捆绑和编译javascript的资产管道提供了一个现代的选择。
在这一点上,有些人跳出了全栈的框架,选择了Rails API + SPA前端。 渐进式的Rails社区将webpacker带入了这个世界,这让我们能够继续忠于这个宏伟的单体。
Rails 5.2引入了Webpacker作为替代的javascript编译器。它在Rails 6中取代了sprockets,成为默认的javascript编译器。因此,Rails的方法是用webpack编译javascript,而把其他的东西通过sprockets留在资产管道中。
Webpacker使webpack很容易为我们的Rails应用进行配置,但它也带来了自己的一系列问题。快进几年,2017年以来发生了很多变化。 由于浏览器技术的改进和Webpack(er)引入的摩擦,替代方案变得更有吸引力。
Rails 7现在提供了一种新的默认方式来在我们的应用程序中包含javascript,但忠实于其理论,它允许在默认方式不合适的情况下使用替代方案。Rails 7中的javascript的默认方式是使用导入地图。
如果我们需要转译或编译我们的javascript,导入地图就不是正确的答案。这意味着.jsx、.ts和.vue文件不能被导入地图所容纳(目前)。 那么,我们是否无限期地被困在Webpacker的列车上呢?幸运的是,绝对不是。在本文的其余部分,我们将讨论jsbundling-rails与esbuild如何成为我们下一个 "下车 "点。
jsbundling-rails与esbuild
Jsbundling-rails是一个宝石,它提供了必要的配置,使我们能够使用我们选择的javascript捆绑器。Jsbundling-rails只是简单地添加了一些rake任务,创建入口点并设置最终的构建路径。
理论上,我们不需要坚持使用任何特定的javascript捆绑器。只要我们保持预期的入口点并将捆绑后的输出交付给app/assets/builds
,Jsbundling就能发挥作用。
Esbuild是一个高性能的javascript捆绑器。esbuild的核心版本并不具备webpacker的所有功能。本页解释了为什么有一些功能esbuild永远不会支持。
对于一个洒满了javascript的Rails应用程序来说,esbuild核心版足以完成工作。
使用esbuild迁移到jsbundling-rails
安装jsbundling-rails
首先,我们将gem添加到我们的Gemfile中。
+ gem 'jsbundling-rails'
然后我们捆绑安装,并在终端运行以下程序。
./bin/rails javascript:install:esbuild
安装脚本提供了默认的esbuild配置,其中包括。
- 对.gitignore文件的更新
- 一个用于运行多个进程的procfile
foreman
- 一个app/assets/builds目录
- 对manifest.js的更新
- 一个app/javascript/application.js入口点文件
- 一个javascript包含标签
- 一个bin/dev脚本
- 更新package.json的依赖性
如果安装脚本没有添加构建脚本,我们需要将其添加到 package.json 中。所以我们在package.json文件中添加以下内容。
"scripts": { "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds" }
这个默认的构建脚本将使我们可以通过运行yarn build
来编译我们的js资产。
从任何开发脚本中删除webpack
我们需要从所有的开发脚本中删除webpack引用,这些脚本包括Procfile脚本和我们bin目录下的脚本。我们之前运行的安装脚本将yarn build –watch
添加到我们的开发procfile中,这是编译我们的javascript所需的唯一构建脚本。
删除webpacker标签
我们在上一步运行的脚本在我们的application.html.erb文件中添加了一个javascript include标签。这个标签将包括新的javascript入口javascript/application.js
,以便将你的构建脚本包含在你的应用程序中。
然而,我们需要在整个项目中搜索所有javascript_pack_tag
的实例,并删除这些实例,因为它们只被webpacker所需要。
移动入口点
我们有一个新的入口点:app/javascript/application.js
。我们需要将webpacker入口点的内容从app/javascript/packs/application.js
拷贝到app/javascript/application.js
。
将require
语句转换成相对路径的导入语句是很重要的。
// this
require("controllers")
require("@rails/ujs").start();
// becomes
import "./controllers"
import Rails from “@rails/ujs”
Rails.starts()
一旦所有内容都被移到app/javascript
目录中,我们就可以删除app/javascript/packs
。在我们更新导入到新的application.js文件中的相对路径时,请密切注意。
删除webpacker
删除以下文件,但要记住首先在你的esbuild构建脚本中添加任何所需的配置。 看看添加esbuild配置文件,以了解创建更复杂的esbuild配置的情况。
- ./bin/webpack
- ./bin/webpack-dev-server
- ./config/initializers/webpacker.rb
- ./config/webpacker.yml
- ./config/webpack/development.js
- ./config/webpack/environment.js
- ./config/webpack/production.js
- ./config/webpack/test.js
在我们的Gemfile中删除webpacker gem,之后再捆绑安装。
- gem 'webpacker'
最后我们还可以从我们的package.json中删除webpacker包。
yarn remove @rails/webpacker webpack-dev-server
一些小问题
可选择添加一个esbuild.config.js
esbuild的API可以通过命令行、Javascript或者Go来访问。我们添加到package.json中的脚本显示了如何从命令行访问esbuild。在某些情况下,使用Javascript或Go可能更方便。我们将简要地演示如何使用Javascript。
我们之前在package.json文件中定义的构建脚本在一行中包含了所有构建选项。对于更复杂的项目来说,使用外部构建脚本可能更合适。
我们可以转换我们前面看到的构建脚本。我们如何称呼这个构建脚本其实并不重要,但把它叫做esbuild.config.js可能更有意义。这似乎是其他javascript配置文件所使用的命名惯例。
// esbuild.config.js
require('esbuild').build({
entryPoints: ['app/javascript/application.js'],
bundle: true,
sourcemap: true,
watch: process.argv.includes("--watch"),
outdir: 'app/assets/builds',
}).catch(() => process.exit(1))
请注意,我们没有使用app/javascript/*.*
这个入口点。这是因为glob扩展是由我们的shell而不是由esbuild完成的。我们需要包括一个库,比如glob,在把路径传给esbuild之前,先扩展glob模式。
现在我们可以更新我们的package.json构建脚本到”build”: “node esbuild.config.js”
。一切都应该像以前一样工作。
如果你有一个更复杂的webpack配置希望迁移到esbuild上,你可能想查看一下esbuild的API。请记住,有些webpack的功能只能通过esbuild的某个插件来完成。
使用jQuery
在Webpacker中,你可能已经通过这样的方式使jQuery在全球范围内可用。
environment.plugins.prepend(
'Provide',
new webpack.ProvidePlugin({
$: 'jquery/src/jquery',
jQuery: 'jquery/src/jquery'
})
)
在esbuild中,我们需要采取不同的方法来实现同样的结果。我们不能简单地像这样导入jquery。
// app/javascript/application.js
import “jquery”
window.jQuery = jquery
window.$ = jquery
问题是,导入语句会被吊起来。所以任何需要jquery的导入语句都会被吊起,在窗口对象分配之前执行。这意味着依赖jquery的导入脚本将在jquery可用之前执行。
相反,我们创建一个新的文件,让我们把它叫做jquery.js
。
// app/javascript/jquery.js
import jquery from “jquery”
window.jQuery = jquery
window.$ = jquery
然后我们把这个文件导入我们的主入口,application.js
,像这样:
// app/javascript/application.js
import “./jquery”
现在,在jquery导入之后的所有导入都可以访问jquery窗口对象。
使用全局对象
通过Webpack,我们可以将一个全局变量分配给全局对象。Webpack会自动将其转换为窗口对象。
如果我们需要与webpack中的全局对象保持向后兼容,那么我们需要在构建命令中添加--define:global=window
。然而,如果不需要向后兼容,那么我们可以非常容易地搜索并以window
替换所有global
的实例。
实时重载
正如我们上面提到的,esbuild不支持热模块重载(HMR),它甚至不在路线图上。在被Webpack的HMR宠坏了一段时间后,这可能是一个我们不希望没有的开发者的奢侈品。
我们可能无法做到HMR,但我们可以添加配置来实现实时重载。 实时重载本质上是在被监视的文件被更新时触发页面重载。
为了获得最佳的开发者体验,我们需要从npm添加Chokidar。Chokidar是一个文件观察库,允许我们观察任何一组我们希望的文件的变化。结合esbuild的闪电速度,我们可以在最小的延迟下观察并触发重建。
首先,我们可能想通过在终端运行以下命令来添加Chokidar。
yarn add -D chokidar
然后,我们将为我们的esbuild配置添加一个文件。正如我们前面指出的,这个文件的名字并不重要。如果有意义的话,我们可以叫它esbuild.config.js。
#!/usr/bin/env node
const chokidar = require("chokidar");
const esbuild = require("esbuild");
const http = require("http");
const path = require("path");
const clients = [];
const watch = process.argv.includes("--watch");
const watchedDirectories = [
"./app/javascript/**/*.js",
"./app/views/**/*.html.erb",
"./app/assets/stylesheets/*.css",
];
const bannerJs = watch
? ' (() => new EventSource("http://localhost:8082").onmessage = () => location.reload())();'
: "";
const config = {
entryPoints: ["application.js"],
bundle: true,
sourcemap: true,
incremental: watch,
outdir: path.join(process.cwd(), "app/assets/builds"),
absWorkingDir: path.join(process.cwd(), "app/javascript"),
banner: { js: bannerJs },
};
if (watch) {
http
.createServer((req, res) => {
return clients.push(
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Access-Control-Allow-Origin": "*",
Connection: "keep-alive",
})
);
})
.listen(8082);
(async () => {
const result = await esbuild.build(config);
chokidar.watch(watchedDirectories).on("all", (event, path) => {
if (path.includes("javascript")) {
console.log(`rebuilding ${path}`);
result.rebuild();
}
clients.forEach((res) => res.write("data: update\n\n"));
clients.length = 0;
});
})();
} else {
esbuild.build(config).catch(() => process.exit(1));
}
我们不想太深入地研究esbuild配置api,相反我们将对这个配置文件做一个快速的概述。
首先,我们导入所需的库,并设置一些变量。
配置中包含了我们传递给esbuild的构建选项。这个重载技术的核心是包含在banner选项中。在这里,我们指定我们想在构建工件中预置一段Javascript。当--watch
标志被传入时,额外的Javascript会在收到来自localhost:8082的消息时重新加载页面。
接下来我们有一个条件部分。我们进行检查,如果我们在看,那么我们就使用节点的http模块创建一个本地网络服务器。这个服务器可以向浏览器发送请求以触发页面刷新。
接下来我们有一个异步的自启动函数,定义了chokidar的行为。它观察指定的观察目录中的变化。当javascript文件被更新时,它就会重新构建。最后,它向浏览器发送一个消息。
现在我们需要在package.json中更新我们的构建脚本,使它看起来像这样:
"scripts": {
"build": "node esbuild-dev.config.js"
}
最后,我们要确保我们的Procfile.dev传入watch选项。这个文件的js部分必须包括--watch
选项,像这样:
js: yarn build –-watch
我们现在可以用./bin/dev
来启动开发服务器。为了确保它的工作,我们可以对任何一个javascript、css或erb文件进行修改。我们可以通过在watchDirectories
变量中添加相应的目录来观察更多的文件。
迁移到esbuild之前的考虑因素
我们不需要离开webpacker。事实上,有一个叫做shakapacker的维护性宝石。在内部,它仍然使用webpacker
,所以我们可以轻松地试用shakapacker,看看它是否合适。
jsbundling-rails与assets管道(Sprockets或Propshaft)一起工作。我们需要确保在我们的Gemfile中存在Sprockets或Propshaft。
请注意,esbuild正处于所谓的 "后期测试 "阶段,它还没有达到1.0.0版本。这并不意味着在维护稳定性和向后兼容性方面投入了大量的精力。这并不是什么大问题,但对于esbuild不被认为是可以生产的应用来说,请记住这一点。
总结
用jsbundling和esbuild取代webpacker有几个好处。其中,最常见的原因是esbuild的构建速度非常快。对于一个相对较小的应用程序,下图显示了webpacker与esbuild相比是多么的慢。
另一个我们可能想迁移到esbuild的原因是,它可能更容易使用和维护。Webpacker以其灵活性而闻名。Esbuild在设计上不如Webpack灵活。这正符合Rails开发者的想法,在这里,约定俗成而不是配置一直是我们使用的工具的一个重要特征。