Inertia.js 架构深度实践:实现 React 前端与 Laravel/NestJS 后端的无缝路由状态同步

各位同学,下午好。

(放下手中的咖啡杯,清清嗓子)

把手机收起来,把代码编辑器打开。今天我们不谈那些虚头巴脑的理论,也不讲那些“高内聚低耦合”的漂亮话。今天我们要聊的是一个能让前端和后端“如胶似漆”的技术——Inertia.js

你们有没有过这样的经历?
写一个 React 应用,明明数据在后端,结果为了配合前端,你还得写一堆 API 接口。写完接口,还得写一堆 fetch 或者 axios,然后处理 loading,处理错误,处理重定向……
“嘿,React!”你说,“我累得要死,能不能别让我再去写 API 了?”
“不好意思,”React 说,“那是 SPA 的宿命。”

然后你转过头去跟 Laravel 说:“嘿,Laravel,你能不能别光给 JSON 了,能不能直接给我把 HTML 渲染出来?”
Laravel 说:“兄弟,我可是后端,我只懂数据。”

这时候,上帝给你递过来一个方案:Inertia.js

这玩意儿就像是给 React 和 Laravel 之间架起了一座免费的滑梯。你不需要写 API,不需要服务器端渲染(SSR)的配置(比如 Webpack 或 Vite 的复杂配置),它就在你的服务器上运行,把 React 组件渲染好,然后直接塞给你的浏览器。就像是你去吃自助餐,厨师在厨房(服务器)做好了,端上来直接给你吃(浏览器),中间不需要你拿个盘子去拿菜(API)。

好了,废话不多说,今天我们深入骨髓,手把手教你怎么把 React、Laravel 和 NestJS 这三兄弟揉在一起,搞出一个既有 SEO 优势,又有 SPA 丝滑体验的怪物。


第一部分:架构本质——当 SSR 走进了 SPA 的房间

首先,我们要理解 Inertia 到底干了什么。很多同学以为 Inertia 是 SSR(服务端渲染),其实严格来说,它是一个“混合模式”

想象一下,普通的 SPA 是什么?你的服务器就是一个传令兵,告诉浏览器:“去 React 那里,自己去干活。” 浏览器加载 React,自己去请求数据。
普通的 SSR 是什么?你的服务器是一个大厨,把菜切好炒熟,直接把 HTML 塞给浏览器。但是浏览器不懂这个菜怎么吃(没 JS)。

Inertia 是什么?
Inertia 就像是那种体贴的服务员。当用户第一次访问网站时,服务器会把 HTML(由 React 组件渲染)直接发送给浏览器。但是! 这个 HTML 里面并没有真正运行 React。它只是放了一些 script 标签。

当用户点击导航时,Inertia.js 会拦截这个请求。它会告诉服务器:“嘿,我想去 /users 页面。”
服务器一看,这不是个 AJAX 请求,这是个正常的 HTTP 请求。于是,服务器把 /users 页面对应的 React 组件重新渲染一遍(或者直接用缓存),把组件的 props(数据)打包成一个 JSON 对象,扔给浏览器。

浏览器收到这个 JSON,再通过 Inertia 的 setup 函数,把 React 组件重新“激活”,填充进刚才渲染好的 HTML 结构里。

这就是核心: 前端路由在浏览器里跑,但是数据获取和组件渲染逻辑在服务器里跑。


第二部分:搭建舞台——React + Laravel + Inertia

我们先用最经典的组合:React + Laravel。

1. 环境准备

首先,你有一个 Laravel 项目。然后你跑:

composer require inertia/inertia
npm install @inertiajs/react @inertiajs/server

2. Laravel 端:中间件与渲染器

Inertia 需要一个中间件来告诉 Laravel:“嘿,如果你收到了一个 HTTP 请求,并且它是给 Inertia 的,你就别直接返回 response() 了,给我用 render() 函数。”

打开 app/Http/Middleware/HandleInertiaRequests.php

<?php

namespace AppHttpMiddleware;

use IlluminateHttpRequest;
use InertiaMiddleware;
use TightenZiggyBladeRouteGenerator;

class HandleInertiaRequests extends Middleware
{
    /**
     * The root template that's loaded on the first page visit.
     *
     * @see https://inertiajs.com/server-side-setup#root-template
     * @return string
     */
    public function rootView(): string
    {
        return 'app'; // 对应 resources/views/app.blade.php
    }

    /**
     * Determines the current asset version.
     *
     * @see https://inertiajs.com/asset-versioning
     * @return string|null
     */
    public function version(Request $request): ?string
    {
        return parent::version($request);
    }

    /**
     * Defines the props that are shared by default.
     *
     * @see https://inertiajs.com/shared-props
     * @return array
     */
    public function share(Request $request): array
    {
        return array_merge(parent::share($request), [
            // 全局共享数据,比如当前用户信息、标题等
            'auth' => fn () => [
                'user' => $request->user(),
            ],
            'ziggy' => fn () => [
                'location' => $request->path(),
                'params' => $request->query(),
                'route_name' => function ($name) {
                    return (new BladeRouteGenerator())->generate($name, []);
                },
            ],
        ]);
    }
}

然后在 bootstrap/app.php(Laravel 11)或者 Http/Kernel.php 里注册中间件。

// Laravel 11 示例
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(appendGroupPrefix: 'inertia');
    $middleware->alias([
        'auth' => AppHttpMiddlewareAuthenticate::class,
    ]);
    // 注册 Inertia 中间件
    $middleware->validateCsrfTokens(except: [
        '/inertia/spa', // 如果你用了 SPA 模式,要排除 CSRF
    ]);
    $middleware->shareInertia();
});

3. React 端:启动 Inertia

现在我们要写 React 了。如果你是用 Vite 创建的 React 项目,其实 Inertia 官方推荐你用 create-inertia-app 脚手架,但为了深度实践,我们手写一遍核心逻辑,这样你才知道它在底层干了什么。

src/main.jsx 或者 src/main.tsx 里:

import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { createInertiaApp } from '@inertiajs/react';
import { resolve } from 'path';

// 定义你的路由配置
const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true });

createInertiaApp({
    title: (title) => `My App - ${title}`,
    resolve: (name) => {
        const page = pages[`./Pages/${name}.jsx`];
        if (!page) {
            throw new Error(`Page not found: ${name}`);
        }
        return page.default;
    },
    setup({ el, App, props }) {
        // 初始化 React
        createRoot(el).render(
            <StrictMode>
                <App {...props} />
            </StrictMode>
        );
    },
});

4. 渲染逻辑

这是最关键的一步。我们要告诉 Laravel,当用户请求你的网站根路径时,到底渲染什么。

打开 resources/views/app.blade.php。这就是你的 HTML 模板。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ isset($title) ? $title : config('app.name') }}</title>

    <!-- 引入 Tailwind CSS (或者你的 CSS 框架) -->
    <script src="{{ asset('css/app.css') }}" defer></script>

    <!-- 引入 Inertia 脚本 -->
    <script src="{{ asset('js/app.js') }}" defer></script>
</head>
<body>
    <!-- 这个 div 是给 Inertia 放 React 组件的 -->
    <div id="app" data-page="{{ json_encode($page) }}"></div>
</body>
</html>

注意那个 data-page 属性!Inertia 会在服务器端渲染组件,把结果扔到这个 div 里。服务器端不会运行 React 代码,只是生成 HTML。这个 HTML 里的 div#app 可能是空的,或者只有初始化状态,因为它还要等浏览器里的 JS 去接管。


第三部分:核心交互——用 usePageuseForm 沟通

光有路由同步还不够,我们得能拿到数据,能提交数据。

1. 获取数据:usePage

在 React 组件里,Inertia 提供了一个 usePage Hook。这可是个神器。

// src/Pages/Dashboard.jsx
import { usePage } from '@inertiajs/react';
import Layout from '@/Layouts/Layout';

export default function Dashboard() {
    const { auth, user } = usePage().props;

    return (
        <Layout>
            <h1>Welcome back, {user.name}</h1>
            <p>Your role is: {auth.user.role}</p>
        </Layout>
    );
}

原理揭秘:
当你在 React 里调用 usePage 时,Inertia 会去读取浏览器内存里刚才接收到的 JSON 数据(也就是 $page 变量)。usePage 返回的是一个 Proxy 对象。如果你在组件里读取了 page.props.user,Inertia 会把这个数据挂载到 React 组件上。如果你没读取,组件就不会重新渲染。

2. 提交数据:useFormInertia.post

这是开发中最爽的地方。你不用写 axios.post('/api/login', data) 了。

import { useForm } from '@inertiajs/react';
import Layout from '@/Layouts/Layout';

export default function Register() {
    const { data, setData, post, processing, errors } = useForm({
        name: '',
        email: '',
        password: '',
        password_confirmation: '',
    });

    const submit = (e) => {
        e.preventDefault();

        // Inertia.post 会自动处理 CSRF token,重定向,错误处理
        post(route('register.store'));
    };

    return (
        <Layout>
            <form onSubmit={submit}>
                <div>
                    <label>Name</label>
                    <input 
                        type="text" 
                        value={data.name} 
                        onChange={e => setData('name', e.target.value)}
                    />
                    {errors.name && <div>{errors.name}</div>}
                </div>
                <div>
                    <label>Email</label>
                    <input 
                        type="email" 
                        value={data.email} 
                        onChange={e => setData('email', e.target.value)}
                    />
                    {errors.email && <div>{errors.email}</div>}
                </div>
                <button disabled={processing}>Register</button>
            </form>
        </Layout>
    );
}

后端怎么写?
在 Laravel 路由里,你只需要写普通的 Form Request 验证和控制器逻辑。

// routes/web.php
Route::post('/register', [RegisteredUserController::class, 'store'])->name('register.store');

// app/Http/Controllers/RegisteredUserController.php
public function store(RegisterRequest $request)
{
    // 1. 验证通过
    // 2. 创建用户
    // 3. Auth::login($user);

    // 4. 重定向到 Inertia 页面
    return redirect()->route('dashboard');
}

注意看,这里没有 return response()->json(...),也没有 return response()->view(...)。Inertia 会自动检测到这是一个 Inertia 请求,把你的 redirect()->route 转换成 HTTP 302 重定向,然后在新的请求中渲染新的页面。


第四部分:进阶玩法——SPA 模式与服务器端路由

刚才我们用的是默认模式:服务器渲染初始 HTML,然后接管路由。

但这还不够变态。如果你有一个超长的页面,每次路由跳转都去服务器渲染一遍,那服务器压力得多大?

Inertia 提供了 SPA 模式

开启 SPA 模式:

在 Laravel 的 routes/web.php 里,定义一个特殊的路由:

use InertiaInertia;

Route::get('/inertia/spa', function () {
    return Inertia::render('Welcome'); // 这里可以随便写个组件,但会被忽略
});

然后在 app/Http/Middleware/HandleInertiaRequests.phpversion 方法或者中间件里,修改逻辑。最简单的方法是在中间件里判断 URL:

// 伪代码逻辑
public function handle($request, Closure $next)
{
    // 如果访问的是 /inertia/spa,说明是 SPA 模式启动
    if ($request->is('/inertia/spa')) {
        return $next($request); // 继续走,不渲染 HTML,直接 200 OK 返回空 HTML 结构
    }

    // 否则是普通的 SSR 模式
    return $next($request);
}

前端怎么改?

src/main.jsx 里,你需要初始化 Inertia 的客户端路由:

createInertiaApp({
    // ... 配置
}).then(({ app, props, initialPage, resolveComponent }) => {

    // 如果是 SPA 模式
    if (window.location.pathname === '/inertia/spa') {
        // 监听浏览器的前进后退
        window.onpopstate = (event) => {
            // 调用 Laravel 的 API,传入当前 URL
            Inertia.get(window.location.pathname, window.location.query, {
                preserveState: true,
                preserveScroll: true,
                replace: true,
            });
        };

        // 处理点击导航
        document.addEventListener('click', (event) => {
            const link = event.target.closest('a');
            if (link && 
                link.getAttribute('href') && 
                link.getAttribute('href').startsWith('/') &&
                !link.getAttribute('href').startsWith('http')
            ) {
                event.preventDefault();
                const href = link.getAttribute('href');

                Inertia.visit(href, {
                    method: link.method || 'get', // 支持数据 POST
                    data: link.data || {},
                });
            }
        });

        // 初始加载
        Inertia.init({
            resolveComponent: resolveComponent,
        });
    } else {
        // SSR 模式:初始化
        createRoot(document.getElementById('app')).render(<App {...props} />);
    }
});

这就是“无缝”的精髓:
用户访问 / -> 服务器渲染 HTML -> 页面加载 JS -> JS 检测到 URL 是 / -> 正常渲染。
用户点击 /users -> JS 拦截点击 -> 发送 AJAX 请求给 Laravel -> Laravel 渲染 Users 组件 -> 返回 JSON -> React 更新 DOM。


第五部分:NestJS 集成——后端的另一种选择

虽然 Inertia 是 Laravel 官方开发的,但它本质上就是一套 HTTP 请求和响应的标准。NestJS 也有现成的适配器:@inertiajs/nestjs-adapter

如果你是用 NestJS 写后端,React 做前端,流程是一样的,只是配置稍微变一下。

1. 安装

npm install @inertiajs/nestjs-adapter @inertiajs/react

2. 配置 Adapter

在 NestJS 的 AppModule 或者一个专门的 InertiaModule 里:

import { Module } from '@nestjs/common';
import { InertiaAdapter } from '@inertiajs/nestjs-adapter';

@Module({
  imports: [],
  providers: [
    {
      provide: 'InertiaAdapter',
      useClass: InertiaAdapter,
    },
  ],
})
export class AppModule {}

3. 创建 Middleware

NestJS 没有中间件文件,我们需要创建一个装饰器或者类来处理 Inertia 逻辑。

import { Injectable, NestMiddleware, Request } from '@nestjs/common';
import { NextFunction, Response } from 'express';
import { Inertia } from '@inertiajs/server';
import { App } from '@inertiajs/server'; // 假设你有一个 App 类来定义路由

@Injectable()
export class InertiaMiddleware implements NestMiddleware {
  constructor(private readonly inertia: Inertia) {}

  use(@Request() req, res: Response, next: NextFunction) {
    // 1. 设置全局共享数据
    this.inertia.share({
      auth: { user: req.user },
    });

    // 2. 如果是 SPA 路由,直接返回
    if (req.path === '/spa') {
      // Inertia 默认行为是返回空 HTML 结构,或者你可以手动渲染一个 layout
      return this.inertia.handle(req, res, App.render(req.url)); 
    }

    // 3. 如果是 SSR 路由,渲染组件
    // 假设我们有一个简单的路由映射表
    const page = this.resolvePage(req.path);
    if (page) {
      return this.inertia.handle(req, res, page);
    }

    next();
  }

  private resolvePage(path: string) {
    // 这里需要你自己实现路由到组件的映射
    // 比如 path === '/dashboard' -> DashboardComponent
    // 简单的例子:
    if (path === '/') return { component: 'Welcome', props: {} };
    if (path === '/dashboard') return { component: 'Dashboard', props: { title: 'Dashboard' } };

    return null;
  }
}

然后在 main.ts 里应用这个中间件:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { InertiaMiddleware } from './inertia/inertia.middleware';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 注册中间件
  app.use(new InertiaMiddleware(app.get('InertiaAdapter')));

  await app.listen(3000);
}
bootstrap();

NestJS 的优势:
你可以把 Inertia 逻辑封装成一个模块,甚至可以配合 Controller 专门处理 Inertia::render 的逻辑,保持业务代码的整洁。


第六部分:实战中的坑与药方

讲了这么多,总得说说实战里你会遇到什么坑。

1. CSRF Token 问题

Inertia 的 Inertia.post 方法会自动处理 CSRF token,所以你通常不需要在 React 里手动加 meta 标签或者 axios interceptor(除非你在用纯 axios)。
但是,如果你用的是传统的 axios 并且手动发送 POST 请求,记得在 Header 里加 X-XSRF-TOKEN

2. 404 页面

如果你的后端路由定义了 Route::get('/user/{id}', ...),但是 Inertia 前端没有对应的组件,或者你访问了一个不存在的路由。

  • 坑: Laravel 默认的 404 页面是一个 HTML 文件,而不是一个 Inertia 组件。这会导致页面直接变白。
  • 药方: 你需要在 Laravel 里定义一个全局的 Inertia 路由来处理 404。
// routes/web.php
Route::any('/{any}', function () {
    return Inertia::render('Errors/404'); // 假设你有这个组件
})->where('any', '.*');

这把所有无法匹配的路由都扔给了 Inertia。

3. 预取

这属于高级优化。Inertia.js 提供了一个 Link 组件,它会自动发送预取请求。

import { Link } from '@inertiajs/react';

export default function Nav() {
    return (
        <nav>
            <Link href="/posts">Posts</Link>
            <Link href="/about" prefetch>About</Link> {/* prefetch 属性让它自动加载 */}
        </nav>
    );
}

prefetch 默认是 viewport(进入视口时加载)。这会让你的 SPA 体验接近原生 App,点哪哪就有数据,根本没有 loading 骨架屏。

4. 状态持久化

当你提交表单后跳转,如果用户按“后退”,数据会丢失。

  • 药方: 配置 preserveState: true
Inertia.visit('/route', {
    method: 'post',
    preserveState: true, // 保持表单数据
    preserveScroll: true, // 保持滚动位置
});

第七部分:最终总结——拥抱 Inertia

回到开头。为什么我们要用 Inertia?

  1. 减少了 CRUD 工作量: 别写 API 了。写后端控制器,写 Form Request,写前端页面。一次编写,到处运行。
  2. SEO 友好: React 的问题在于爬虫看不到内容。SSR 模式下,爬虫能直接抓到渲染好的 HTML。
  3. 开发体验(DX): useFormusePage 让你感觉你是在写一个普通的 PHP + React 的混合应用,而不是在调试两个分离的系统。

当然,它不是银弹。如果你的应用极其复杂,涉及到巨大的状态管理(比如电商后台的复杂库存计算),你需要在前端用 Redux/Zustand 管理全局状态,而在后端用数据库管理持久化状态。Inertia 只是负责把两者在特定页面上同步一下。

如果你厌倦了在 useEffect 里写 useEffect(() => { fetch... }, [id]) 这种噩梦,试试 Inertia。它能让你的代码少写一半,但功能不减。

好了,今天的讲座就到这里。现在,去把你的 API 层删了吧(开玩笑的,先备份)。

谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注