各位同学,下午好。
(放下手中的咖啡杯,清清嗓子)
把手机收起来,把代码编辑器打开。今天我们不谈那些虚头巴脑的理论,也不讲那些“高内聚低耦合”的漂亮话。今天我们要聊的是一个能让前端和后端“如胶似漆”的技术——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 去接管。
第三部分:核心交互——用 usePage 和 useForm 沟通
光有路由同步还不够,我们得能拿到数据,能提交数据。
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. 提交数据:useForm 和 Inertia.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.php 的 version 方法或者中间件里,修改逻辑。最简单的方法是在中间件里判断 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?
- 减少了 CRUD 工作量: 别写 API 了。写后端控制器,写 Form Request,写前端页面。一次编写,到处运行。
- SEO 友好: React 的问题在于爬虫看不到内容。SSR 模式下,爬虫能直接抓到渲染好的 HTML。
- 开发体验(DX):
useForm和usePage让你感觉你是在写一个普通的 PHP + React 的混合应用,而不是在调试两个分离的系统。
当然,它不是银弹。如果你的应用极其复杂,涉及到巨大的状态管理(比如电商后台的复杂库存计算),你需要在前端用 Redux/Zustand 管理全局状态,而在后端用数据库管理持久化状态。Inertia 只是负责把两者在特定页面上同步一下。
如果你厌倦了在 useEffect 里写 useEffect(() => { fetch... }, [id]) 这种噩梦,试试 Inertia。它能让你的代码少写一半,但功能不减。
好了,今天的讲座就到这里。现在,去把你的 API 层删了吧(开玩笑的,先备份)。
谢谢大家!