最近,我把我的AnyCable演示应用程序升级到Ruby 3和Rails 7,并使用其新的资产管理工具。结果,assets:precompile
,变得快如闪电,但我们在此过程中失去了一个重要的生产力功能:实时重载。在2022年切换回Webpacker并不是一个好主意。幸运的是,Vite Ruby已经出现在我的视线中很长时间了,所以我决定试一试。
自从引入资产管道(Sprocket)后,Rails已经有了解决资产问题的答案。这对整个世界的Web开发框架来说是重要的一步,不仅仅是对Ruby和Rails。
然后,前端革命开始了,而我们,Rails社区,需要迎头赶上。于是,Webpacker诞生了。尽管它很好地实现了自己的目的,但人们总觉得Webpacker是Rails生态系统中的一个 "异类"。
Rails 7在资产捆绑器的历史上翻开了新的一页。Webpacker已经退役;取而代之的是,我们有一些处理前端的官方方法:导入地图、jsbundling-rails、cssbundling-rails、tailwindcss-rails。所有这些都是建立在现代工具之上的,与Rails配合得很好,而且很容易操作。好吧,也许除了这种多样性会给开发者带来的混乱。
问题是,它们提供了类似于Sprokets的体验,也就是面向构建。但对许多开发者来说,即时反馈很重要,他们已经习惯了。因此,问题是:什么是webpack-dev-server
的现代替代品?我的答案是Vite。
在这篇文章中,我想分享我的Vite Ruby设置(使用AnyCable演示),我将涵盖以下主题:
- 在Rails上开始使用Vite
- 实时重载和HRM
- 是否要对Vite进行dockerize?
在Rails上开始使用Vite
从 "<whatever>bundling-rails
"迁移到Vite几乎和vite_rails文档中所说的一样简单:安装gem并运行安装rake任务(bundle exec vite install
)。
我把javascript_include_tag
和stylesheet_link_tag
助手分别替换为vite_javascript_tag
和vite_stylesheet_tag
,并把vite.json
中的sourceCodeDir
值更新为frontend
(因为我的设置偏离了 Rails 的app/javascript
方法)。
我还创建了frontend/entrypoints/application.css
文件,以指向我的styles/index.css
(之前被esbuild用来编译app/assets/builds/application.css
)。
在这些小改动之后,我期望我的应用程序能够在没有任何额外改动的情况下工作(有Vite Ruby自动构建功能的支持)。但相反,我在我的服务器日志中看到了这个:
Building with Vite ⚡️
vite v2.9.13 building for development...
transforming...
✓ 13 modules transformed.
Could not resolve './**/*_controller.js' from frontend/controllers/index.js
error during build:
Error: Could not resolve './**/*_controller.js' from frontend/controllers/index.js
at error (/app/node_modules/rollup/dist/shared/rollup.js:198:30)
at ModuleLoader.handleResolveId (/app/node_modules/rollup/dist/shared/rollup.js:22508:24)
at /app/node_modules/rollup/dist/shared/rollup.js:22471:26
Build with Vite failed! ❌
我们依靠esbuild-rails对glob导入的支持(import './**/*_controller.js'
)来自动加载Stimulus控制器,但现在,一旦切换到Vite,我们就不再有这个功能了。
幸运的是,我们有import.meta.globEager
,它可以返回路径-模块映射,所以我们可以使用它:
const controllers = import.meta.globEager("./**/*_controller.js");
for (let path in controllers) {
let module = controllers[path];
let name = path.match(/\.\/(.+)_controller\.js$/)[1].replaceAll("/", "--");
application.register(name, module.default);
}
看起来有点黑客的味道。不用担心,我们有stimulus-vit-helpers插件,只需一行代码就能为我们做到这一点:
import { registerControllers } from "stimulus-vite-helpers";
const controllers = import.meta.globEager("./**/*_controller.js");
registerControllers(application, controllers);
很好!就这样:我们刚刚将我们的应用程序迁移到Vite Ruby。但是,你还记得我们当初为什么要这样做吗?
实时重载和HMR
在自动构建模式下,Vite Ruby按需编译资产,每个入口有一个输出文件;就像老式的Sprockets一样:
用Vite Ruby提供自动构建的资产
这就是Ruby主义者可能在开发中使用Vite的方式;然而,Vite的主要卖点是 "即时服务器启动 "和 "闪电式HMR"(HMR代表热模块替换)。我们怎样才能达到这个目的呢?我们应该运行一个Vite开发服务器!
有了Vite Ruby,它就像运行bin/vite dev
一样简单。下面是在开发服务器的帮助下加载的页面:
通过Vite开发服务器提供资产
现在我们加载了许多JavaScript文件:我们所有的依赖和自定义模块(文件)--但只是这个特定页面需要的那些。源代码使用Rollup在后台进行即时处理,第三方(NPM)库也被预先编译(这次是通过esbuild)。但你不需要担心所有这些花哨的前端技术,Vite已经帮你解决了。
这就是 "即时服务器启动 "的由来。那么HMR呢?
热模块替换是一种技术,它使刷新浏览器的JavaScript环境的当前状态成为可能,而无需重新加载整个页面(只需重新加载一个模块)。并非每一段JavaScript代码都能被热重载,但现代框架,如Vue和React,都与这项技术兼容。顺便说一下,Stimulus也是如此。
Vite使用插件来提供HMR功能(捆绑器本身只提供API)。因此,我们需要将Stimulus HMR添加到我们的配置中:
import StimulusHMR from 'vite-plugin-stimulus-hmr'
export default {
plugins: [
StimulusHMR(),
],
};
现在我们可以打开一个带有Stimulus控制器的元素的页面,并尝试使用它:
刺激物HMR演示
刺激物HMR演示
你看到了吗?我们的JavaScript代码被重新加载,控制器被重新连接,而页面内容保持不变(例如,输入框)。这就是热模块替换的作用。
正如我已经提到的,HMR只在兼容的JavaScript代码中工作。如果我们想对,比如说,HTML模板的变化做出反应呢?我们可以通过Vite-plugin-full-reload使用永远可靠的实时重载,这是我们的最终配置:
import { defineConfig } from "vite";
import RubyPlugin from "vite-plugin-ruby";
import StimulusHMR from "vite-plugin-stimulus-hmr";
import FullReload from "vite-plugin-full-reload";
export default defineConfig({
plugins: [
RubyPlugin(),
StimulusHMR(),
// You can specify any paths you want to watch for changes
FullReload(["app/views/**/*.erb"])
],
});
Dockerizing Vite, or not
你可能知道,我正在Docker化环境中构建我的应用程序。设置Vite Ruby以在Docker中工作是非常简单的。
- 我们添加卷来保存Vite资产:
x-backend: &backend
# ...
volumes:
# ...
- vite_dev:/app/public/vite-dev
- vite_test:/app/public/vite-test
volumes:
# ...
vite_dev:
vite_test:
- 我们定义一个新的服务来运行一个Vite开发服务器:
vite:
<<: *backend
command: ./bin/vite dev
volumes:
- ..:/app:cached
- bundle:/usr/local/bundle
- node_modules:/app/node_modules
- vite_dev:/app/public/vite-dev
- vite_test:/app/public/vite-test
ports:
- "3036:3036"
- 最后,我们通过提供
VITE_RUBY_HOST
,将我们的Rails应用 "连接 "到vite
服务上:
x-backend: &backend
environment:
# ...
VITE_RUBY_HOST: ${VITE_HOST:-vite}
现在我们可以运行docker-compose up vite
(或dip up vite
)来运行一个开发服务器。
注意,我在配置中提供了一个不同的Vite主机(${VITE_HOST:-vite}
)。这可以用来建立一个替代性的混合配置。Rails在Docker中运行,Vite在本地运行。
我们主要在重度前端项目中使用Vite,即涉及JavaScript框架和专用前端团队的项目。这通常涉及到先进的DX机器(linters、git hooks、IDE扩展等),在大多数情况下,这与Docker并不兼容。这就是为什么我们让它有可能退回到本地系统开发(仅用于前端)。
但我们使用Ruby gem(vite_ruby
)来管理Vite的配置,所以这是否意味着我们现在必须在本地运行完整的、庞大的Rails应用程序,只是为了一个小小的Vite包装器?当然不是。让我告诉你一个更好的方法。
首先,我们通过为它(以及其他可能的前端依赖项)保留一个单独的Gemfile来隔离 vite_ruby
:
# gemfiles/frontend.gemfile
source "https://rubygems.org"
# https://github.com/ElMassimo/vite_ruby
gem "vite_rails"
我们通过使用eval_gemfile "gemfiles/frontend.gemfile"
,将其纳入我们的主Gemfile(这样我们就可以在Rails应用程序中使用Vite帮助器,或在生产中运行命令)。
然后,我们定义一个自定义的bin/vite
命令,它使用这个frontend.gemfile
:
#!/bin/bash
cd $(dirname $0)/..
export BUNDLE_GEMFILE=./gemfiles/frontend.gemfile
bundle check > /dev/null || bundle install
bundle exec vite $@
这与我用于RuboCop的技巧相同:一个使用自定义Gemfile和自动安装依赖项的bundle exec
包装器。你所需要的只是Ruby(是的,你仍然需要它,但不需要所有其他的系统部署)。
现在,你可以像往常一样启动一个Vite开发服务器:
bin/vite dev
你还可以通过指定VITE_HOST
参数,启动一个 "连接 "到本地运行的服务器的dockerized Rails应用程序:
VITE_HOST=host.docker.internal dip rails s
注意:在config/vite.json
中设置"host": "0.0.0.0"
,使开发服务器可以从Docker容器中访问,这一点很重要。
有了Dip,我们可以更进一步,提供一个有用的捷径,用于混合开发:
# dip.yml
# ...
interaction:
frontend:
description: Frontend development tasks
subcommands:
rails:
description: Run Rails server pointing to a local Vite dev server
service: web
environment:
VITE_HOST: host.docker.internal
compose:
run_options: [ service-ports, use-aliases ]
不,你不需要考虑主机的问题,只要运行dip frontend rails
,就可以了。
收尾工作
所以,你有了它。我们有了一个Ruby Vite的设置,可以进行实时重载、热替换,而且我们对即时满足的需求也得到了完全的恢复欢迎分享这个设置,并在你自己的项目中使用它,我希望它能派上用场。