Vite-lizing Rails:用Vite Ruby获得实时重载和热替换的详细指南

454 阅读4分钟

最近,我把我的AnyCable演示应用程序升级到Ruby 3和Rails 7,并使用其新的资产管理工具。结果,assets:precompile ,变得快如闪电,但我们在此过程中失去了一个重要的生产力功能:实时重载。在2022年切换回Webpacker并不是一个好主意。幸运的是,Vite Ruby已经出现在我的视线中很长时间了,所以我决定试一试。

自从引入资产管道(Sprocket)后,Rails已经有了解决资产问题的答案。这对整个世界的Web开发框架来说是重要的一步,不仅仅是对Ruby和Rails。

然后,前端革命开始了,而我们,Rails社区,需要迎头赶上。于是,Webpacker诞生了。尽管它很好地实现了自己的目的,但人们总觉得Webpacker是Rails生态系统中的一个 "异类"。

Rails 7在资产捆绑器的历史上翻开了新的一页。Webpacker已经退役;取而代之的是,我们有一些处理前端的官方方法:导入地图、jsbundling-railscssbundling-railstailwindcss-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_tagstylesheet_link_tag 助手分别替换为vite_javascript_tagvite_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一样:

Serving auto-built assets with Vite Ruby

用Vite Ruby提供自动构建的资产

这就是Ruby主义者可能在开发中使用Vite的方式;然而,Vite的主要卖点是 "即时服务器启动 "和 "闪电式HMR"(HMR代表热模块替换)。我们怎样才能达到这个目的呢?我们应该运行一个Vite开发服务器!

有了Vite Ruby,它就像运行bin/vite dev 一样简单。下面是在开发服务器的帮助下加载的页面:

Serving assets via Vite dev server

通过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的设置,可以进行实时重载、热替换,而且我们对即时满足的需求也得到了完全的恢复欢迎分享这个设置,并在你自己的项目中使用它,我希望它能派上用场。