xfyuan
xfyuan A Chinese software engineer living and working in Chengdu. I love creating the future in digital worlds, big and small.

Monolith的新时代

Monolith的新时代

本文已获得原作者(Svyatoslav KryukovTravis Turner)和 Evil Martians 授权许可进行翻译。原文讲述了 Inertia.js 这个新兴工具在 Rails 中的集成。对 Monolith 架构的促进,并以具体实例进行了演示。

Inertia.js 也是我去年在 RubyConf China 2024 上做的讲演主题的核心内容之一,下面是 B 站和 YouTube 的视频地址,有兴趣的朋友可以去看看:

Bilibili

Youtube

【正文如下】

引言

我们的 CEO Irina Nazarova 在她的 RailsConf 主题演讲“2024 年的 Startups on Rails”中,指出了年轻的 Rails 初创公司的第一个需求——在 Rails 社区中提供适当的 React 支持。因此,我们最近推出了 Turbo Mount,当你的应用只需要几个高度交互的组件时,它简化了场景。但今天,我们更进一步,展示如何简单地将 Inertia.js 与 Rails 集成。

HotwireInertia.js 都是使用 Rails 构建现代 Web 应用的好工具。但是,它们具有不同的用例和目标受众:

  • Hotwire 是一组工具,可使用 Turbo Streams 和 Turbo Frames 实现 HTML 的服务器端渲染和页面的部分更新。

    对于希望在交互性和实时更新方面增强服务端渲染的 Rails 应用而无需编写大量 JavaScript 代码的开发人员来说,这是一个不错的选择。对于应用需要一个或两个高度交互组件的情况,Turbo Mount 可用于将 React、Vue 或 Svelte 组件直接挂载到经典 HTML 页面的一部分中。

Inertia.js 是一个协议和一组库,支持从传统 HTML 模板完全过渡到使用 React、Vue、Svelte 或其他前端框架组件作为视图层的构建块。

这种方案非常适合精通前端框架的团队,他们希望利用自己的技能创建更加动态和响应式的用户界面,同时仍然保持传统的服务端驱动的路由(routing)和控制器(controller)。

这种设置消除了对客户端的路由、API 或通常与拥有单独的前端和后端应用相关的大量 JavaScript 样板的需求。

尽管 inertia_rails 已经存在了一段时间,但它仍然不如 Hotwire 或 StimulusReflex 等其他解决方案受欢迎。然而,我们相信 Inertia.js 非常适合 Rails 应用,它涵盖了许多需要功能齐全的 JavaScript 框架的使用案例。

我们还希望让 Inertia.js 在 Rails 社区中更受欢迎。随着最近将 inertia_rails-contrib 项目上游到核心 inertia_rails gem 中,Rails 开发人员现在可以直接在核心项目中访问增强的工具和特定于 Rails 的文档。这种集成简化了在 Rails 应用程序中使用 Inertia.js 的过程,并加强了对 Rails 生态系统的支持。

在本文中,我们将首先仔细研究 Inertia.js 的工作原理。然后,我们将了解如何使用 inertia_rails 生成器将其集成到 Rails 应用中。

将本文视为快速概览就好,我们不会深入研究。也会分享一些文档链接。最后,我们将讨论 Rails 生态系统中Inertia.js的未来。

How Inertia.js works

要使用 Inertia.js,我们的服务端应用需要实现 Inertia.js 协议。该协议基于为两种类型请求提供服务的理念:经典 HTML 请求和 Inertia.js 请求。让我们更详细地讨论一下它是如何工作的:

  1. 初始页面加载:当用户第一次访问应用时,服务端会返回一个完整的 HTML 响应,就像传统服务端渲染的 Rails 应用一样。此 HTML 响应包括必要的 assets 和一个特殊的根 <div> 元素,该元素具有 data-page 属性,其中包含 JSON 格式的初始页面数据。

  2. 后续页面访问:Inertia.js提供了一个 组件,用于替代标准 <a> 标记。当用户单击 <Link> 时,Inertia.js 会向服务端发送带有 X-Inertia 标头的 AJAX 请求。(另一种选择是使用 Inertia 的 router 以编程方式导航到新页面。

  3. 服务端响应:使用 X-Inertia 标头后,Rails 应用会识别 Inertia 请求并返回一个 JSON 响应,其中包含要呈现的页面组件的名称、该组件的必要数据、新的页面 URL,最后是当前 assets 的构建版本。

    Inertia.js 使用该数据更新页面内容和浏览器地址栏中的 URL,而无需刷新整个页面。

Inertia.js 背后的想法非常简单:不需要 Redux 或 REST API——默认情况下,每个请求都只返回一个 JSON 响应,其中包含渲染页面所需的所有数据。

对于更复杂的用例,Inertia.js 提供了不同的方法来延迟加载数据,或对页面进行部分更新(有关更多信息,请参阅部分重新加载文档)。

Inertia.js Rails integration

让我们看看如何使用 inertia_rails 生成器启动一个新的 带 Inertia.js 的 Rails 应用。首先,我们将设置一个新的 Rails 应用,忽略默认的 JavaScript 和 assets pipeline 设置:

1
2
3
rails new inertia_rails_example --skip-js --skip-asset-pipeline

cd inertia_rails_example

接下来,我们将安装 inertia_rails gem 并运行安装生成器:

1
2
3
bundle add inertia_rails

bin/rails generate inertia:install

生成器会安装 Vite 前端构建工具,可以选择安装 Tailwind CSS,并要求你选择一个前端框架.我们将选择 React。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
$ bin/rails generate inertia:install
Installing Inertia's Rails adapter
Could not find a package.json file to install Inertia to.

Would you like to install Vite Ruby? (y/n) y
         run  bundle add vite_rails from "."
Vite Rails gem successfully installed
         run  bundle exec vite install from "."
Vite Rails successfully installed

Would you like to install Tailwind CSS? (y/n) y
Installing Tailwind CSS
         run  npm add tailwindcss postcss autoprefixer @tailwindcss/forms @tailwindcss/typography @tailwindcss/container-queries --silent from "."
      create  tailwind.config.js
      create  postcss.config.js
      create  app/frontend/entrypoints/application.css
Adding Tailwind CSS to the application layout
      insert  app/views/layouts/application.html.erb
Adding Inertia's Rails adapter initializer
      create  config/initializers/inertia_rails.rb
Installing Inertia npm packages

What framework do you want to use with Inertia? [react, vue, svelte] (react)
         run  npm add @inertiajs/react react react-dom @vitejs/plugin-react --silent from "."
Adding Vite plugin for react
      insert  vite.config.ts
     prepend  vite.config.ts
Copying inertia.js entrypoint
      create  app/frontend/entrypoints/inertia.js
Adding inertia.js script tag to the application layout
      insert  app/views/layouts/application.html.erb
Adding Vite React Refresh tag to the application layout
      insert  app/views/layouts/application.html.erb
        gsub  app/views/layouts/application.html.erb
Copying example Inertia controller
      create  app/controllers/inertia_example_controller.rb
Adding a route for the example Inertia controller
       route  get 'inertia-example', to: 'inertia_example#index'
Copying page assets
      create  app/frontend/pages/InertiaExample.jsx
      create  app/frontend/pages/InertiaExample.module.css
      create  app/frontend/assets/react.svg
      create  app/frontend/assets/inertia.svg
      create  app/frontend/assets/vite_ruby.svg
Copying bin/dev
      create  bin/dev
Inertia's Rails adapter successfully installed

就是这样!生成器已经设置了 Inertia.js Rails 适配器,安装了必要的 NPM 包,安装并配置了 Vite 和 Tailwind CSS,并创建了一个示例页面。此时,你可以通过运行 bin/dev 来启动 Rails 服务器并导航到 http://localhost:3100/inertia-example。应该会看到带有 React 组件的 Inertia.js 页面。

让我们仔细看看生成的 controller 和 component。Controller 位于 app/controllers/inertia_example_controller.rb 文件中:

1
2
3
4
5
6
7
8
class InertiaExampleController < ApplicationController
  def index
    render inertia: "InertiaExample", props: {
      name: params.fetch(:name, "World")
    }
  end
end

请注意,控制器使用 inertia 方法来渲染 Inertia.js 页面。props 包含了将传递给 React 组件的 props。使用 serializers 为前端准备 props 是一种很好的做法,但为了简单起见,我们只从 URL 传递 name 参数。要了解更多信息,请查看 Inertia.js Rails 文档中的惯性方法

React 组件位于文件 app/frontend/pages/InertiaExample.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { Head } from "@inertiajs/react";
import { useState } from "react";

import reactSvg from "/assets/react.svg";
import inertiaSvg from "/assets/inertia.svg";
import viteRubySvg from "/assets/vite_ruby.svg";

import cs from "./InertiaExample.module.css";

export default function InertiaExample({ name }) {
  const [count, setCount] = useState(0);

  return (
    <>
      <Head title="Inertia + Vite Ruby + React Example" />

      <div className={cs.root}>
        <h1 className={cs.h1}>Hello {name}!</h1>

        {/*<div...>*/}

        <h2 className={cs.h2}>Inertia + Vite Ruby + React</h2>

        {/*<div className="card"...>*/}
        {/*<p className={cs.readTheDocs}...>*/}
      </div>
    </>
  );
}

该组件接受从 controller 传递的 name prop。你可以更新 URL 中的 name 参数以查看组件中的更改。

除此之外,我们的页面是一个常规的 React 组件,它使用 useState hook 来管理 count 状态。

Inertia.js generators

为了简化 Inertia.js 集成,我们向 inertia_rails Gem 添加了一组生成器。让我们看看如何生成新的 Inertia.js resource:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
bin/rails generate inertia:scaffold Post title:string body:text published_at:datetime
      invoke  active_record
      create    db/migrate/20240618171615_create_posts.rb
      create    app/models/post.rb
      invoke    test_unit
      create      test/models/post_test.rb
      create      test/fixtures/posts.yml
      invoke  resource_route
       route    resources :posts
      invoke  scaffold_controller
      create    app/controllers/posts_controller.rb
      invoke    inertia_tw_templates
      create      app/frontend/pages/Post
      create      app/frontend/pages/Post/Index.jsx
      create      app/frontend/pages/Post/Edit.jsx
      create      app/frontend/pages/Post/Show.jsx
      create      app/frontend/pages/Post/New.jsx
      create      app/frontend/pages/Post/Form.jsx
      create      app/frontend/pages/Post/Post.jsx
      invoke    resource_route
      invoke    test_unit
      create      test/controllers/posts_controller_test.rb
      create      test/system/posts_test.rb
      invoke    helper
      create      app/helpers/posts_helper.rb
      invoke      test_unit

生成器将创建具有指定属性的新 Post resource。它生成 model、controller、view 和前端组件。

由于生成器会创建大量文件,因此让我们只看一下生成的 controller 中的表单处理,其余的留给你探索。

我们将从 app/controllers/posts_controller.rb 文件中的 editupdate开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class PostsController < ApplicationController
  before_action :set_post, only: %i[ show edit update destroy ]

  inertia_share flash: -> { flash.to_hash }

  # GET /posts/1/edit
  def edit
    render inertia: 'Post/Edit', props: {
      post: serialize_post(@post)
    }
  end

  # PATCH/PUT /posts/1
  def update
    if @post.update(post_params)
      redirect_to @post, notice: "Post was successfully updated."
    else
      redirect_to edit_post_url(@post), inertia: { errors: @post.errors }
    end
  end

  #...
end

首先,让我们检查一下 inertia_share helper 方法。我们使用它来向所有 Inertia.js 响应添加 Flash 消息。在 React 组件中显示 flash[:notice] 消息。

edit行为使用 render: inertia 来渲染包含序列化的 post 数据的 Post/Edit 页面。所有前端组件(pages)都位于 app/frontend/pages/Post 目录中。

最后,如果更新成功,则 update 会更新 post 并重定向到 post show 页面。请注意,如果存在任何验证错误,它不会引发错误,而是重定向回 edit 页面,并在inertia key 中序列化错误。

(另一个值得您花时间的有趣主题是一般的错误处理文档。)

接下来,我们看一下组件 app/frontend/pages/Post/Edit.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { Link, Head } from "@inertiajs/react";
import Form from "./Form";

export default function Edit({ post }) {
  return (
    <>
      <Head title="Editing post" />

      <div className="mx-auto md:w-2/3 w-full px-8 pt-8">
        <h1 className="font-bold text-4xl">Editing post</h1>

        <Form
          post={post}
          onSubmit={(form) => {
            form.transform((data) => ({ post: data }));
            form.patch(`/posts/${post.id}`);
          }}
          submitText="Update post"
        />

        <Link
          href={`/posts/${post.id}`}
          className="ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium"
        >
          Show this post
        </Link>
        <Link
          href="/posts"
          className="ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium"
        >
          Back to posts
        </Link>
      </div>
    </>
  );
}

请注意,我们使用 @inertiajs/reactLink 导航到其他页面,而无需重新加载整个页面。就像默认的 Rails 脚手架一样,我们生成一个处理表单提交的表单组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { useForm } from "@inertiajs/react";

export default function Form({ post, onSubmit, submitText }) {
  const form = useForm({
    title: post.title || "",
    body: post.body || "",
    published_at: post.published_at || "",
  });
  const { data, setData, errors, processing } = form;

  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit(form);
  };

  return (
    <form onSubmit={handleSubmit} className="contents">
      <div className="my-5">
        <label htmlFor="title">Title</label>
        <input
          type="text"
          name="title"
          id="title"
          value={data.title}
          className="block shadow rounded-md border border-gray-400 outline-none px-3 py-2 mt-2 w-full"
          onChange={(e) => setData("title", e.target.value)}
        />
        {errors.title && (
          <div className="text-red-500 px-3 py-2 font-medium">
            {errors.title.join(", ")}
          </div>
        )}
      </div>

      {/* ... */}
    </form>
  );
}

表单组件使用 @inertiajs/react 中的 useForm hook 来处理表单状态和提交。这个 hook 与 Rails 的默认值配合得很好,允许你提交表单并显示验证错误,而无需编写大量样板代码。

总的来说,与 Inertia.js 和 Rails 集成相关的内容有很多值得探索的地方,因此我们再次鼓励去查看完整的 Inertia.js Rails 文档,以了解有关可用功能和最佳实践的更多信息。

The future of Inertia.js in the Rails ecosystem

现在 Rails 生态系统中发生了很多令人兴奋的事情:Hotwire、Turbo 和 StimulusReflex 越来越受欢迎,并改变了我们的构建方式。

而 Inertia.js 是 Rails 工具包的另一个重要补充。我们相信它在 Rails 领域有着光明的未来。它提供了一种简单而优雅的方式来构建现代 Web 应用,而无需复杂的客户端路由和 API。

通过这个项目,我们的目标是让 Inertia.js 更受欢迎,并分享构建优秀应用所需的工具和资源。

如果你有兴趣了解有关 Inertia.js 的更多信息(以及它如何帮助构建更好的 Rails 应用),请前往 Inertia.js Rails 文档。如果你已在 Rails 应用程序中使用 Inertia.js,我们很乐意倾听你的体验以及使用 Inertia.js 的任何提示或技巧!有什么最佳实践、提示或技巧吗?

无论你是想更深入地了解 Inertia.js 还是想参与贡献,都可以加入我们的inertia_rails

Rating:

comments powered by Disqus