Importmap 还是 jsbundling?我全都要
从 Rails 7 开始,Importmap 成为处理 JavaScript 加载的默认机制。它可以充分利用 HTTP/2 的并行下载和缓存机制,避免打一个大包每次改动都需要下载所有代码。
对于 js 依赖,Importmap 提供了一个 pin
功能,例如运行:
./bin/importmap pin local-time
Importmap 就会从 CDN 下载 local-time
的 js 文件放到 vendor/javascript
目录,自动添加 config/importmap.rb
配置,随后就可以在 js 文件里面导入:
import LocalTime from "local-time"
LocalTime.start()
但某些 js 库预设开发者会使用打包工具,没有将源码打包成一个完整的包,而是拆分了很多文件,这时候用 importmap pin
就会遇到问题。例如 Lit,如果执行:
bin/importmap pin lit
会看到输出:
Pinning "lit" to vendor/javascript/lit.js via download from https://ga.jspm.io/npm:[email protected]/index.js
Pinning "@lit/reactive-element" to vendor/javascript/@lit/reactive-element.js via download from https://ga.jspm.io/npm:@lit/[email protected]/reactive-element.js
Pinning "lit-element/lit-element.js" to vendor/javascript/lit-element/lit-element.js.js via download from https://ga.jspm.io/npm:[email protected]/lit-element.js
Pinning "lit-html" to vendor/javascript/lit-html.js via download from https://ga.jspm.io/npm:[email protected]/lit-html.js
Pinning "lit-html/is-server.js" to vendor/javascript/lit-html/is-server.js.js via download from https://ga.jspm.io/npm:[email protected]/is-server.js
可以看到 Lit 引用了很多子包。糟糕的是,即使下载了这么多包,导入还是不完整的,如果在 js 代码中 `import { LitElement } from "lit",会在浏览器中报错:
GET http://localhost:3000/assets/css-tag.js net::ERR_ABORTED 404 (Not Found)
这是因为 @lit/reactive-element
这个包中有很多可选模块没有下载下来。但如果下载所有可选模块,那么 importmap 配置会膨胀得很厉害。有一个 PR 正在处理(#235),不好说能不能解决,因为问题在于库作者没有考虑不打包导入的需求。
那么不妨改变一下思路,先用 jsbunding 将依赖打包,然后再用 importmap 导入。以下展示如何实现。
实现
假设已经使用 Rails 创建了项目,并且默认使用了 importmap:
rails new myapp
代码在 Rails 8.0.0.beta1 测试,但应该可用于 Rails 7+。
接下来安装 jsbundling:
./bin/bundle add jsbundling-rails
./bin/rails javascript:install:esbuild
这时你会看到 js 编译错误,因为 jsbundling 和 importmap 的默认配置有冲突,接下来会修复冲突。
删除 app/views/layouts/application.html.erb
内这行内容:
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
修改 package.json
,将内容改为:
"scripts": {
"build": "esbuild app/assets/javascripts/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets"
}
注意路径改为 app/assets/javascripts/*.*
,这是以后放置需要 esbuild 编译的 js 文件的目录。
在 config/applicatoin.rb
里面添加内容:
config.assets.excluded_paths << Rails.root.join("app/assets/javascripts")
创建文件夹 app/javascript/src/
,添加文件 app/assets/javascripts/lit.js
,内容为:
export * from 'lit';
通过 yarn 安装 lit 包:
yarn add lit
在 config/importmap.rb
内添加配置:
pin "lit", to: "lit.js"
现在启动开发进程 ./bin/dev
,你会看到 esbuild 将 lit 编译到 app/assets/builds/lit.js
。打开浏览器查看页面源码,importmap 的内容增加了:
"imports": {
...
"lit": "/assets/lit-9c62c803.js",
...
}
整个工作流程是:esbuild 将 app/assets/javascripts
的源码编译到 app/assets/build
,app/assets/build
的内容会被 assets pipeline 处理,在 config/importmap.rb
中添加导入名和文件名的映射,模块就可以被应用的 js 代码导入。
现在,你可以在 js 中 import { LitElement } from "lit"
。
总结
本文使用 esbuild 和 importmap 结合的方式,解决 impotmap 无法处理复杂依赖的问题。虽然这破坏了 nobuild 的期望,还是能利用到细粒度缓存的优点。在 importmap 普遍被 js 包兼容前,不妨用这个方法处理复杂依赖。