Rails中如何从webpacker迁移到esbuild

283 阅读9分钟

我们,全栈式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-railsesbuild如何成为我们下一个 "下车 "点。

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文件的更新
  • 一个用于运行多个进程的procfileforeman
  • 一个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相比是多么的慢。

image.png

另一个我们可能想迁移到esbuild的原因是,它可能更容易使用和维护。Webpacker以其灵活性而闻名。Esbuild在设计上不如Webpack灵活。这正符合Rails开发者的想法,在这里,约定俗成而不是配置一直是我们使用的工具的一个重要特征。