Inertia.js 在 PHP 生态中的应用:实现无需编写传统 API 即可构建高性能 React 全栈应用的架构方案

别再写 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 工程师时,你要处理:

  1. Laravel 路由和控制器。
  2. 数据库查询。
  3. API 接口(JSON)。
  4. React 组件。
  5. Axios 请求。
  6. 再次返回 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 工程师最头疼的部分:表单提交。

通常,你的流程是:

  1. 用户填表。
  2. React 发送 Axios POST 请求。
  3. Laravel 接收,验证,保存。
  4. Laravel 返回 JSON { status: 200, message: 'Success' }
  5. 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 组件:

  1. 它是一个高阶组件,它会拦截你的表单提交。
  2. 它会自动帮你构建一个 put 请求(或者 post, delete)。
  3. 它会自动带上 CSRF token。
  4. 最关键的一点: 当后端验证失败时,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 页面在服务器端是完整渲染的。

这意味着:

  1. SEO: Google 会看到一个完整的 HTML。而且,因为 PHP 是模板引擎,你可以利用 Blade 的指令,比如 @if@foreach。如果你的内容是静态的或者需要根据角色展示,PHP 可以直接渲染出来,而 React 只负责处理交互部分。这比 Next.js 的 Server Components 还要彻底。
  2. 首屏渲染(FCP): 对于用户来说,页面加载过程是:
    • 下载 HTML(由 PHP 快速生成)。
    • 下载 JS(由 Vite 快速生成)。
    • React hydrate(水合)。
    • 结果: 用户几乎感觉不到 JS 加载的过程,因为 HTML 已经在那里了。这比那种“白屏等待 JS 下载完成”的体验要好得多。

第八部分:实战演练——构建一个“全栈”博客系统

为了巩固我们的知识,我们来构想一个“微型博客”。

场景:

  1. 首页: 列出所有文章(PHP 获取数据 -> 渲染 React 列表)。
  2. 创建文章: 一个表单,提交后跳转回首页。
  3. 编辑文章: 跳转到编辑页,回填数据,保存后更新。

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 的交互体验。

它的优点总结:

  1. 减少样板代码: 不用写 API 了。
  2. 表单处理: React 表单处理变得像 PHP Blade 表单一样简单(自动重定向、重填)。
  3. SEO 友好: HTML 在服务器生成。
  4. 开发效率: 后端工程师可以直接写前端组件,不用跨域调试。

它的缺点:

  1. SSR 限制: 复杂的服务端逻辑(比如复杂的计算,或者混合了 HTML 字符串和 React 组件)会比较麻烦。你需要用 Livewire 来辅助。
  2. 调试: 如果你习惯了纯前端调试,遇到 PHP 报错可能会不适应。

我的建议:
如果你正在维护一个传统的 PHP 项目,想要给它换一个现代的 UI,但又不想重写整个后端逻辑,或者不想雇佣昂贵的全职前端工程师,Inertia.js 是你的救命稻草。

它让 PHP 变得性感了,也给了 React 一个不需要自己构建整个基础设施的理由。

现在,拿起你的键盘,去安装 Inertia,去享受那种“写 PHP 代码,渲染 React 组件”的快感吧!别再对着 Axios 的 404 Not Found 烦躁了。

谢谢大家!

发表回复

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