在 Rails 中集成 JavaScript:Import Maps、构建器与 Turbo 实战指南
本指南基于 Ruby on Rails 官方文档编写,系统讲解在 Rails 应用中集成 JavaScript 的完整方案,涵盖如何以 import maps 免去 Node.js/Yarn 构建步骤、如何选用 Bun/esbuild/Rollup/Webpack 等传统打包器,以及 Turbo(Turbo Drive、Turbo Frames、Turbo Streams)驱动的 HTML 快速渲染方式。读完本文后,你将掌握从 rails new 开始为应用选定 JavaScript 方案、运行官方安装任务,并利用 turbo-rails 提供的 HTML 与服务端助手实现页面局部更新、内联编辑与实时广播的关键技术。
概览:Rails 的 JavaScript 集成选项
本指南解决的核心问题是:在 Rails 应用中集成 JavaScript 有哪些可行路径,各自适用什么场景。整体上包含三类选择:
- Import Maps:用逻辑名称在浏览器中直接导入按版本映射的 JavaScript 模块,无需转译与打包。Rails 7 起默认采用该方案。
- JavaScript Bundlers:以 Bun、esbuild、Rollup 或 Webpack 做传统打包,适用于需要 JSX/TypeScript 转译、Webpack loader 或 tree-shaking 等能力的场景。
- Turbo:无论选择 import maps 还是打包器,Rails 都内置 Turbo(配合 turbo-rails gem),通过服务端直接交付 HTML,显著减少需手写的 JavaScript 量。
从仓库源码可见,新应用默认的 JavaScript 方案由 rails new 生成器统一管控。app_generator.rb 定义了 --javascript(别名 -j/--js)选项,默认值为 "importmap",而允许取值来自 app_base.rb 中的常量:
JAVASCRIPT_OPTIONS = %w( importmap bun webpack esbuild rollup ).freeze
也就是说,本仓库所指的“JavaScript 五选一”与生成器 enum 严格对应。另外,若选择打包器,会经由 jsbundling-rails gem 与资源管道(asset pipeline)集成;Turbo 与 Stimulus 则通过 hotwire_gemfile_entry 与 run_hotwire(执行 turbo:install stimulus:install)随新应用自动装配(见 app_base.rb)。
使用 Import Maps:无需 Node.js、Yarn 与构建步骤
Import maps 让你在浏览器中直接使用逻辑名称导入 JavaScript 模块,这些名称被映射到有版本号的文件上。应用采用 import maps 后:
- 不需要 Node.js 或 Yarn 即可运行;
- 无需独立的构建流程——直接执行
bin/rails server启动服务器即可开发。
因此,如果你打算用 importmap-rails 管理 JavaScript 依赖,完全没有必要安装 Node.js 或 Yarn。
安装 importmap-rails
Rails 7+ 的新应用会自动包含 importmap-rails;对已有应用,可手动安装:
$ bundle add importmap-rails
然后运行安装任务:
$ bin/rails importmap:install
使用 import map 的默认布局模板同样内置了配套的基础设施。查看新应用的默认布局 application.html.erb.tt,可以看到 csrf_meta_tags、csp_meta_tag 以及带 data-turbo-track: "reload" 的 stylesheet_link_tag 已被自动生成;而 application_controller.rb.tt 在 import map 场景下会额外生成 stale_when_importmap_changes 注释行,提示 importmap 变更会使 HTML 响应 ETag 失效——这正是“改 importmap 配置即触发浏览器重新拉取资源”的实现基础。
用 importmap-rails 添加 npm 包
向基于 import map 的应用添加新包,在终端运行 bin/importmap pin:
$ bin/importmap pin react react-dom
随后像往常一样在 application.js 中导入即可:
import React from "react"
import ReactDOM from "react-dom"
pin 的本质是为包生成 importmap 条目,把逻辑名映射到 CDN 上的精确版本文件;浏览器端 import map 会替浏览器解析这些映射,因此你拿到了绝大多数 npm 包的能力,却不需要转译和打包。
使用 JavaScript Bundlers 添加 npm 包
Import maps 是默认方案,但如果你偏好传统打包流程,可以用 rails new 的 --javascript(或 -j)选项直接创建使用 Bun、esbuild、Webpack 或 Rollup 的应用:
$ rails new my_new_app --javascript=bun
# 等价写法
$ rails new my_new_app -j bun
这些打包选项都带有简洁的配置,并通过 jsbundling-rails gem 与资源管道集成。使用打包方案时,开发环境下用 bin/dev 同时启动 Rails 服务器并构建 JavaScript。
注意与 CSS 方案的交互:生成器源码在 app_base.rb 中通过 using_importmap?、using_js_runtime?、using_node? 与 using_bun? 判断运行时需求——当选择非 importmap 的 JS 方案,或 CSS 选择需要转译的处理器时,会自动推导出需要 Node.js 或 Bun 作为 JS 运行时,并在 CI 模板、Dockerfile(通过 dockerfile_build_packages 注入 node-gyp 或 unzip 等包)中体现。
安装 JavaScript 运行时
- 使用 esbuild、Rollup 或 Webpack 打包:必须安装 Node.js 与 Yarn。
- 使用 Bun:只需安装 Bun,因为它既是 JavaScript 运行时又是打包器。
安装 Bun
参考 Bun 官方安装指引,并用以下命令验证是否安装正确且在 PATH 中:
$ bun --version
应打印出 Bun 运行时版本号,例如 1.0.0,即表示安装成功。若未输出,可能需要重新在当前目录安装 Bun 或重启终端。
安装 Node.js 与 Yarn
参考 Node.js 官方下载页安装,并验证:
$ node --version
打印出的版本号需大于 8.16.0。接着按 Yarn 官网指引安装并验证:
$ yarn --version
若输出类似 1.22.0,说明 Yarn 已正确安装。从仓库源码看,生成器在探测环境时正是解析 node --version、yarn --version 与 bun --version 的输出来确定运行时(见 app_base.rb 的 node_version、dockerfile_yarn_version、dockerfile_bun_version),因此这些命令的可用性直接决定打包方案能否跑通。
在 Import Maps 与 JavaScript Bundler 之间做选择
创建新 Rails 应用时需要在两种路线中决策。由于大型复杂应用在方案间迁移可能耗时巨大,请结合自身需求仔细权衡。
Import maps 成为默认选项,是因为 Rails 团队看重它在降低复杂度、改善开发者体验、带来性能提升上的潜力。适合长期采用 import maps 的典型场景是:应用主要依赖 Hotwire 技术栈满足 JavaScript 需求。可参考 DHH 关于“Rails 7 在 2021 年对 JavaScript 给出三个优秀答案”的说明,了解将 import maps 设为默认的完整理由。
而以下需求则提示你应选择传统打包器:
- 代码需要转译步骤,例如 JSX 或 TypeScript;
- 需要使用包含 CSS、或依赖 Webpack loaders 的 JavaScript 库;
- 明确需要 tree-shaking(摇树优化)能力;
- 计划通过
cssbundling-railsgem 安装 Bootstrap、Bulma、PostCSS 或 Dart CSS——该 gem 除 Tailwind 与 Sass 外的所有选项,若不在rails new中另行指定,都会自动为你安装 esbuild。
生成器源码可印证这套决策逻辑:using_js_runtime? 的判定条件是“非 importmap”,或在 importmap 基础上叠加了 Tailwind/Sass 之外的 CSS 处理器(app_base.rb),而 CSS 选项映射到 tailwindcss-rails、dartsass-rails 或 cssbundling-rails 的规则也在 css_gemfile_entry 中(同文件第 650 行起)。
Turbo:让服务端直接交付 HTML
无论选择 import maps 还是打包器,Rails 都内置 Turbo 来加速应用,同时大幅减少需要手写的 JavaScript。Turbo 让服务端直接交付 HTML,是对主流前端框架把 Rails 服务端降格为 JSON API 这一趋势的直接回应。Rails 通过 turbo-rails gem 提供配套的 HTML 与服务端助手。
Turbo Drive
Turbo Drive 通过避免每次导航请求都做整页拆除与重建来加速页面加载,是 Turbolinks 的改进与替代品。其效果直接体现在默认模板里:新应用布局的样式表标签默认附带 "data-turbo-track": "reload"(见 application.html.erb.tt),让 Turbo 在导航时追踪并替换发生变化的资源引用,从而实现“无整页刷新”的资源热更新。
Turbo Frames
Turbo Frames 允许只更新页面中预定义的局部区域,而不影响页面其余内容。利用它,你可以不写任何自定义 JavaScript 就实现就地编辑(in-place editing)、懒加载内容,以及轻松构建服务端渲染的选项卡式界面。
通过 turbo-rails gem,用 turbo_frame_tag 助手即可添加一个 Turbo Frame:
<%= turbo_frame_tag dom_id(post) do %>
<div>
<%= link_to post.title, post_path(post) %>
</div>
<% end %>
工作原理是:Turbo 只会接管页面内对应 frame(以 dom_id(post) 作为唯一标识)的导航响应并替换该 frame 中的内容,形成可预测的局部刷新边界。
Turbo Streams
Turbo Streams 以自执行 <turbo-stream> 元素包裹的 HTML 片段来交付页面变更。它允许你通过 WebSocket 广播其他用户做出的变更,也可以在表单提交后更新页面的部分内容而无需整页加载。
通过 turbo-rails gem,可在控制器动作中渲染 Turbo Stream。Rails 会自动查找对应的 .turbo_stream.erb 视图文件并渲染:
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.turbo_stream
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
Turbo Stream 响应也可以直接在控制器动作内联渲染:
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.turbo_stream { render turbo_stream: turbo_stream.prepend("posts", partial: "post") }
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
最后,Turbo Streams 还可以从模型或后台任务中通过内置助手发起。这些广播经由 WebSocket 连接将内容更新推送给所有用户,让页面内容保持鲜活。
在模型中结合回调进行广播:
class Post < ApplicationRecord
after_create_commit { broadcast_append_to("posts") }
end
并在应接收更新的页面上建立 WebSocket 连接:
<%= turbo_stream_from "posts" %>
turbo_frame_tag 与 turbo_stream_from 正是 turbo-rails 注入 Rails 的典型视图助手;而 WebSocket 侧的通道基础设施在 actioncable 中实现,<turbo-stream> 更新正是通过 Action Cable 建立的连接推送到客户端,从而实现多人实时协同的场景。
Rails/UJS 功能的替代方案
Rails 6 曾内置 UJS(Unobtrusive JavaScript,非侵入式 JavaScript),用于重写 <a> 标签的 HTTP 请求方法、在执行动作前添加确认对话框等。UJS 是 Rails 7 之前的默认方案,现在官方推荐改用 Turbo 实现同等能力。
请求方法(Method)
点击链接永远产生 HTTP GET 请求。若应用遵循 RESTful 约定,部分“链接”实为会修改服务端数据的动作,应使用非 GET 请求执行。data-turbo-method 属性允许为这类链接显式标注方法,例如 "post"、"put" 或 "delete"。Turbo 会扫描 <a> 标签中的 turbo-method data 属性,存在时按指定方法发起请求,覆盖默认的 GET 行为。
例如:
<%= link_to "Delete post", post_path(post), data: { turbo_method: "delete" } %>
生成:
<a data-turbo-method="delete" href="...">Delete post</a>
data-turbo-method 的替代方案是使用 Rails 的 button_to 助手。出于无障碍(accessibility)考虑,任何非 GET 动作都应优先使用真正的按钮与表单。Rails 的 link_to 与 button_to 助手实现位于 navigation_helper.rb,其中按钮形式会内部渲染出携带隐藏方法字段的表单,天然比伪装成链接的删除动作更可访问。
确认对话框(Confirmations)
可以通过在链接与表单上添加 data-turbo-confirm 属性,向用户请求额外确认。点击链接或提交表单时,用户会看到包含该属性文本的 JavaScript confirm() 对话框;若用户选择取消,动作不会执行。
配合 link_to 助手的示例:
<%= link_to "Delete post", post_path(post), data: { turbo_method: "delete", turbo_confirm: "Are you sure?" } %>
生成:
<a href="..." data-turbo-confirm="Are you sure?" data-turbo-method="delete">Delete post</a>
用户点击 “Delete post” 链接时,将看到 “Are you sure?” 确认对话框。
该属性同样可用于 button_to 助手,但必须添加到 button_to 内部渲染的表单上:
<%= button_to "Delete post", post, method: :delete, form: { data: { turbo_confirm: "Are you sure?" } } %>
Ajax 请求
从 JavaScript 发起非 GET 请求时,必须携带 X-CSRF-Token 请求头;缺少该头,请求将不被 Rails 接受。此令牌是 Rails 为防御跨站请求伪造(Cross-Site Request Forgery,CSRF)攻击所必需的,详见 安全指南。
Rails Request.JS 封装了添加 Rails 所需请求头的逻辑。导入其中的 FetchRequest 类,实例化时传入请求方法、url 与选项,然后调用 await request.perform() 处理响应即可:
import { FetchRequest } from '@rails/request.js'
....
async function myMethod () {
const request = new FetchRequest('post', 'http://localhost:3000/posts', {
body: JSON.stringify({ name: 'Request.JS' })
})
const response = await request.perform()
if (response.ok) {
const body = await response.text
}
}
使用其他库发起 Ajax 调用时,需要自行把安全令牌加为默认请求头。要获取令牌,查看应用视图里由 csrf_meta_tags 打印的 <meta name='csrf-token' content='THE-TOKEN'> 标签,可以这样读取:
document.head.querySelector("meta[name=csrf-token]")?.content
令牌的产生机制可从源码确认:csrf_helper.rb 中的 csrf_meta_tags 在开启 protect_against_forgery? 时输出 csrf-param 与 csrf-token 两个 meta 标签,其值分别来自 request_forgery_protection_token 与 form_authenticity_token。这正是所有常规表单隐藏字段、以及上文 Ajax 请求头所使用的同一套 CSRF 保护体系——普通表单无需处理,因为会自行生成隐藏字段,而动态 JavaScript 请求则需按本文方式显式读取并回传该令牌。新应用的默认布局已包含 csrf_meta_tags(见 application.html.erb.tt),因此多数基于该模板创建的应用无需额外改动即可配合上述 JavaScript 代码工作。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00