别再写 API 了!用 Inertia.js 让 PHP 和 React 谈一场轰轰烈烈的恋爱
各位同事、各位后端转型的全栈工程师、各位被业务需求逼到不得不写前端的 PHP 工程师们,大家好。
今天我们坐在这儿,不谈什么晦涩的微服务架构,也不聊什么 Kubernetes 的编排艺术。我们聊点实际的,聊点能让你少掉几根头发、少写几百行代码,还能让老板眼前一亮的话题。
那就是:如何用 PHP(特别是 Laravel 这种好用的)配合 Inertia.js,打造一个“全栈”应用,而且这全栈不需要你像个傻瓜一样在两个项目之间来回切换。
第一部分:为什么我们会对着屏幕抓狂?
在开始之前,我们先来聊聊痛点。
曾经,我们开发一个网站,PHP 是大脑,负责逻辑;HTML 是皮肤,负责展示。后来,我们觉得 HTML 不够灵活,于是我们引入了 React。React 很好,它让页面变得交互丰富,像苹果公司的产品一样丝滑。
但是,React 也很“贱”。它通常需要你先把 PHP 的东西全部吐出来变成 JSON,扔给前端去解析,前端解析完再吐回 PHP,PHP 再吐回前端。这就像是剥香蕉,你非得把香蕉皮和香蕉肉都剥下来,只为了吃中间那一点点果肉。
于是,我们发明了 API-first。于是,我们有了成千上万个 get_user_list, create_order, update_profile 的接口。
当你是一个后端 PHP 工程师时,你要处理:
- Laravel 路由和控制器。
- 数据库查询。
- API 接口(JSON)。
- React 组件。
- Axios 请求。
- 再次返回 Laravel 时的表单验证和重定向逻辑。
你感觉自己像个精分的特工,左手写 PHP,右手写 JavaScript,中间还要用 Axios 捡数据。你的代码里充满了 axios.get('/api/...') 和 setUsers(data),这简直是对人类智商的侮辱。
有没有一种办法,让 React 像直接操作 DOM 一样简单,同时又能享受到 PHP 的路由、验证和重定向能力?
有!这就是 Inertia.js。
第二部分:Inertia.js 到底是个什么妖魔鬼怪?
Inertia.js 不是框架,它是一个库。它不碰你的浏览器 DOM,它也不试图接管你的路由。它的核心思想非常简单,甚至有点“偷懒”:
它让前端框架(React, Vue, Svelte)直接渲染 PHP 控制器返回的模板。
想象一下,你依然使用 Laravel 的路由。当你访问 /users 时,Laravel 的控制器不再返回一个 .blade.php 文件,而是返回一个 JSON 对象,里面包含了你需要的所有数据。然后,这个 JSON 被一个服务器端的代理(通常是 Vite)注入到你的 React 应用中,React 就把它当作 props 渲染出来。
关键点来了: 在浏览器看来,这就是一个完整的 HTML 页面。所以,SEO(搜索引擎优化)依然是完美的!因为 Google 抓取的是一个完整的 HTML 文档,而不是一堆乱七八糟的 JavaScript 源码。
第三部分:环境准备与“痛苦”的安装
好,理论说完了,我们开始实战。为了演示方便,假设你的环境里已经有一个 Laravel 项目。如果没有?那就跑一下 composer create-project laravel/laravel,别在这浪费时间。
首先,安装 Laravel 的 Inertia 集成包:
composer require inertia/inertia
然后,你需要告诉 Laravel 在开发环境下使用 Vite(如果你不用 Vite 也可以用 Webpack,但我强烈建议你用 Vite,因为生活已经够苦了,别用 Webpack 那个老古董了)。
在 config/app.php 中注册服务提供者:
InertiaInertiaServiceProvider::class,
接下来,最关键的一步。打开你的 routes/web.php。
use IlluminateSupportFacadesRoute;
use InertiaInertia;
Route::get('/dashboard', function () {
return Inertia::render('Dashboard', [
'user' => Auth::user()
]);
})->middleware(['auth', 'verified'])->name('dashboard');
看这段代码!这简直是后端工程师的福音。你没有返回 view('dashboard'),也没有返回 json()。你返回的是 Inertia::render。
第二个参数 'user' 是我们需要传递给 React 的数据。
现在,你需要告诉 Inertia 去哪里找 React 组件。在 vite.config.js(Vite 配置)中,我们需要添加一个 resolve.alias,把 @ 指向 resources/js,并且添加 inertia-react 插件。
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
react(),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './resources/js'),
},
},
})
第四部分:React 端的写法
现在,我们去写 React 代码。打开 resources/js/Pages/Dashboard.jsx(当然,你也可以用 TypeScript,但我这里用 JSX 方便理解)。
import React from 'react'
import { Head, Link } from '@inertiajs/react'
export default function Dashboard({ auth, canLogin, canRegister }) {
return (
<React.Fragment>
<Head title="Dashboard" />
<div className="relative flex justify-center min-h-screen bg-gray-100 overflow-hidden">
{/* 简单的欢迎卡片 */}
<div className="relative max-w-7xl mx-auto z-10 px-4 pt-10 pb-12 sm:px-6 lg:px-8">
<div className="sm:text-center lg:text-left">
<h1 className="text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl md:text-6xl">
<span className="block xl:inline">欢迎回来,</span>
<span className="block text-indigo-600 xl:inline">{auth.user.name}</span>
</h1>
<p className="mt-3 text-base text-gray-500 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0">
这里是你的控制中心。不需要刷新页面,所有数据都由 Laravel 实时注入。
</p>
<div className="mt-5 sm:mt-8 sm:flex sm:justify-center lg:justify-start">
<div className="rounded-md shadow">
<Link
href={route('profile.edit')}
className="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 md:py-4 md:text-lg"
>
编辑个人资料
</Link>
</div>
</div>
</div>
</div>
</div>
</React.Fragment>
)
}
注意看那个 <Head /> 组件。这就是 Inertia 的魔法。它告诉浏览器:“嘿,把标题改成 Dashboard”。而且,它不会破坏你的页面渲染,它会在服务器端处理,然后无缝地塞进 <head> 标签里。这对 SEO 和用户体验来说,简直太棒了。
第五部分:处理表单——这是 Inertia 的杀手锏
这是 PHP 工程师最头疼的部分:表单提交。
通常,你的流程是:
- 用户填表。
- React 发送 Axios POST 请求。
- Laravel 接收,验证,保存。
- Laravel 返回 JSON
{ status: 200, message: 'Success' }。 - React 看到状态码 200,显示成功提示,然后可能还要手动跳转。
在 Inertia 中,这一切变得极其优雅。
首先,你需要安装 Inertia 的 React 适配包(如果你是用 JSX 的话,或者直接用官方的 @inertiajs/react)。
npm install @inertiajs/react
在 React 组件中,我们需要使用 <InertiaForm>。
假设我们有一个编辑用户的页面。
PHP 端 (routes/web.php):
Route::put('/profile', function (Request $request) {
$request->user()->fill($request->only('name', 'email'));
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return back()->with(['status' => 'Profile updated!']);
})->middleware('auth');
React 端 (resources/js/Pages/EditProfile.jsx):
import React from 'react'
import { Head, Inertia, InertiaLink, Form } from '@inertiajs/react'
export default function EditProfile({ auth, mustVerifyEmail, status }) {
return (
<React.Fragment>
<Head title="Edit Profile" />
<h1>编辑个人资料</h1>
{status && (
<div className="mb-4 font-medium text-sm text-green-600">
{status}
</div>
)}
<Form method="put" action={route('profile.update')} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Name</label>
<input
id="name"
type="text"
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
defaultValue={auth.user.name}
name="name"
required
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label>
<input
id="email"
type="email"
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
defaultValue={auth.user.email}
name="email"
required
/>
</div>
<button type="submit" className="bg-indigo-600 text-white px-4 py-2 rounded">
保存更改
</button>
</Form>
</React.Fragment>
)
}
注意看这个 Form 组件:
- 它是一个高阶组件,它会拦截你的表单提交。
- 它会自动帮你构建一个
put请求(或者post,delete)。 - 它会自动带上 CSRF token。
- 最关键的一点: 当后端验证失败时,Laravel 会重定向回这个页面。而 Inertia 会自动把验证错误(
$errors)和原来的数据(old)填充回你的表单中。
你的表单根本不需要 useEffect 去处理重置状态!Inertia 帮你做了这一切。当你重定向回来时,数据还在那里,错误信息也在那里。你就像从来没有离开过这个页面一样!
第六部分:共享状态与中间件
在传统的 SPA 中,如果你想在所有页面共享状态(比如当前的登录用户),你得用 Redux、Context,或者全局变量。
在 Inertia 中,你可以直接在中间件里把数据“共享”出去。
修改 app/Http/Middleware/HandleInertiaRequests.php:
public function share(Request $request): array
{
return array_merge(parent::share($request), [
// 共享当前用户
'auth' => [
'user' => $request->user() ? [
'name' => $request->user()->name,
'avatar' => $request->user()->avatar_url, // 假设你有个头像字段
] : null,
],
// 共享应用信息,比如导航菜单
'app' => [
'name' => config('app.name'),
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
],
// 共享一个时间戳,防止浏览器缓存静态资源(可选但推荐)
'time' => time(),
]);
}
现在,你的 React 组件可以通过 usePage() 钩子获取这些数据。
import { usePage } from '@inertiajs/react'
export default function Layout({ children }) {
const { auth } = usePage().props;
return (
<div>
<nav>
{auth.user ? <span>欢迎, {auth.user.name}</span> : <Link href={route('login')}>登录</Link>}
</nav>
<main>{children}</main>
</div>
)
}
第七部分:性能与 SEO 的真相
有同事可能会问:“但是,Inertia 的页面是不是像普通的 SPA 一样,第一次加载很慢?”
这是一个非常好的问题。
是的,第一次加载时,你的浏览器需要下载 React 库、你的 JavaScript 代码。但是,请注意,Inertia 页面在服务器端是完整渲染的。
这意味着:
- SEO: Google 会看到一个完整的 HTML。而且,因为 PHP 是模板引擎,你可以利用 Blade 的指令,比如
@if,@foreach。如果你的内容是静态的或者需要根据角色展示,PHP 可以直接渲染出来,而 React 只负责处理交互部分。这比 Next.js 的 Server Components 还要彻底。 - 首屏渲染(FCP): 对于用户来说,页面加载过程是:
- 下载 HTML(由 PHP 快速生成)。
- 下载 JS(由 Vite 快速生成)。
- React hydrate(水合)。
- 结果: 用户几乎感觉不到 JS 加载的过程,因为 HTML 已经在那里了。这比那种“白屏等待 JS 下载完成”的体验要好得多。
第八部分:实战演练——构建一个“全栈”博客系统
为了巩固我们的知识,我们来构想一个“微型博客”。
场景:
- 首页: 列出所有文章(PHP 获取数据 -> 渲染 React 列表)。
- 创建文章: 一个表单,提交后跳转回首页。
- 编辑文章: 跳转到编辑页,回填数据,保存后更新。
1. 首页 (routes/web.php):
Route::get('/', function () {
// 假设我们有这个模型
$posts = AppModelsPost::latest()->get();
return Inertia::render('Home', [
'posts' => $posts->map(fn ($post) => [
'id' => $post->id,
'title' => $post->title,
'excerpt' => $post->excerpt,
'created_at' => $post->created_at->diffForHumans(),
]),
]);
})->name('home');
2. React 首页 (resources/js/Pages/Home.jsx):
import React from 'react'
import { Link } from '@inertiajs/react'
export default function Home({ posts }) {
return (
<div className="max-w-4xl mx-auto py-10">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">最新文章</h1>
<Link
href={route('posts.create')}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition"
>
+ 写文章
</Link>
</div>
<div className="space-y-6">
{posts.map((post) => (
<div key={post.id} className="bg-white shadow rounded-lg p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-2">
<Link href={route('posts.show', post.id)} className="hover:text-blue-600">
{post.title}
</Link>
</h2>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
<span className="text-sm text-gray-400">{post.created_at}</span>
</div>
))}
</div>
</div>
)
}
3. 创建文章页 (routes/web.php):
Route::get('/posts/create', function () {
return Inertia::render('Posts/Create');
})->middleware('auth')->name('posts.create');
Route::post('/posts', function (Request $request) {
// 1. 验证
$validated = $request->validate([
'title' => 'required|max:255',
'content' => 'required',
]);
// 2. 保存
AppModelsPost::create([
'title' => $validated['title'],
'content' => $validated['content'],
'user_id' => auth()->id(),
]);
// 3. 重定向并带提示
return redirect()->route('home')->with('success', '文章发布成功!');
})->middleware('auth')->name('posts.store');
4. React 表单 (resources/js/Pages/Posts/Create.jsx):
import React from 'react'
import { Inertia, InertiaLink, Form } from '@inertiajs/react'
export default function CreatePost() {
return (
<div className="max-w-2xl mx-auto py-10">
<h1 className="text-3xl font-bold text-gray-900 mb-6">创建新文章</h1>
<Form method="post" action={route('posts.store')} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700">标题</label>
<input
type="text"
name="title"
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm"
autoFocus
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">内容</label>
<textarea
name="content"
rows="10"
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm"
></textarea>
</div>
<div className="flex justify-end gap-4">
<InertiaLink
href={route('home')}
className="text-gray-700 hover:text-gray-900"
>
取消
</InertiaLink>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
发布
</button>
</div>
</Form>
</div>
)
}
看,这有多简洁?没有 Axios 的 try-catch,没有手动管理 URL。PHP 处理所有的逻辑,React 只管 UI。
第九部分:服务端事件
Inertia 还有一个非常高级的功能,叫做 Server Events。这让你可以在 PHP 端向 React 端发送消息,不需要任何 WebSocket 的设置。
场景:一个后台管理面板,当管理员添加了一个新用户,其他正在浏览页面的用户(如果你使用类似 Laravel Echo 的服务)应该能看到提示。
虽然这通常结合 Laravel Echo 使用,但 Inertia 也支持直接共享一个简单的通知对象。
// 在控制器中
return back()->with('flash', [
'type' => 'success',
'message' => '操作成功'
]);
然后在 React 中:
import { usePage } from '@inertiajs/react'
export default function FlashMessages() {
const { flash } = usePage().props;
useEffect(() => {
if (flash) {
alert(flash.message); // 或者用更好的 Toast 库
}
}, [flash]);
return null;
}
第十部分:进阶话题——SSR 渲染与预渲染
如果你真的需要极致的 SEO,或者你想让初始加载更快,Inertia 的生态里有一些东西。
比如 Inertia SSR。这允许你使用 PHP 来完全渲染 React 组件(使用 Livewire 的那种思路,或者用一个 React 组件作为 PHP 的模板)。
但这通常比较复杂,属于“过度设计”的范畴。对于 90% 的应用来说,客户端渲染 + PHP 渲染模板 是完美的平衡点。
第十一部分:总结(或者说,退场)
好了,各位听众。
我们今天探讨了 Inertia.js。它没有像 Redux 那样复杂,也没有像 Next.js 那样激进地完全抛弃 PHP 的渲染逻辑。
它只是用一种非常“React”的方式,调用 PHP 的控制器。它利用了 PHP 的成熟生态(路由、中间件、验证、Session),同时保留了 React 的交互体验。
它的优点总结:
- 减少样板代码: 不用写 API 了。
- 表单处理: React 表单处理变得像 PHP Blade 表单一样简单(自动重定向、重填)。
- SEO 友好: HTML 在服务器生成。
- 开发效率: 后端工程师可以直接写前端组件,不用跨域调试。
它的缺点:
- SSR 限制: 复杂的服务端逻辑(比如复杂的计算,或者混合了 HTML 字符串和 React 组件)会比较麻烦。你需要用 Livewire 来辅助。
- 调试: 如果你习惯了纯前端调试,遇到 PHP 报错可能会不适应。
我的建议:
如果你正在维护一个传统的 PHP 项目,想要给它换一个现代的 UI,但又不想重写整个后端逻辑,或者不想雇佣昂贵的全职前端工程师,Inertia.js 是你的救命稻草。
它让 PHP 变得性感了,也给了 React 一个不需要自己构建整个基础设施的理由。
现在,拿起你的键盘,去安装 Inertia,去享受那种“写 PHP 代码,渲染 React 组件”的快感吧!别再对着 Axios 的 404 Not Found 烦躁了。
谢谢大家!