Inertia
本指南介绍在 AdonisJS 中使用 Inertia 构建单页应用(SPA)。你将学习如何:
- 从控制器和路由渲染 Inertia 页面,并向前端组件传递 props
- 使用
make:page命令脚手架式生成页面组件 - 组织
inertia/目录并理解关键配置文件 - 为页面和共享数据生成端到端类型
- 使用可选(optional)、延迟(deferred)和可合并(mergeable) props 等数据加载模式
- 使用
Link和Form组件构建表单与导航 - 全局共享数据,并使用 error bags 隔离验证错误
- 使用
@inertia和@inertiaHead标签自定义根 Edge 模板 - 控制重定向、浏览器历史记录和加密历史记录
- 启用服务端渲染(SSR)
- 理解 Inertia 应用中的请求生命周期
概述
Inertia 在 AdonisJS 与 React、Vue 等前端框架之间架起了一座桥梁。它通过采用服务端优先(server-first)架构,消除了对客户端路由或复杂状态管理库的需求。你完全可以像在传统的服务端渲染应用中那样编写控制器和路由,但与其返回 HTML 或 JSON,你返回的是由前端框架显示的 Inertia 页面。
这种方式让你两全其美:服务端路由和数据获取的简洁性,加上 React 或 Vue 在视图层带来的丰富交互性。AdonisJS 通过 Inertia 起步套件正式支持这两个框架。
另请参阅:Inertia 官方文档中的 Inertia 工作原理 。
基础示例
让我们端到端地走一遍渲染文章列表的流程。整个流程包含三个部分:一个路由、一个调用 inertia.render() 的控制器,以及 inertia/pages/ 目录中的一个页面组件。
-
注册路由
路由看起来与任何 AdonisJS 路由完全一样。Inertia 没有专门的路由层。
start/routes.tsrouter.get('/posts', [controllers.Posts, 'index']) -
从控制器渲染页面
HTTP 上下文暴露了一个
inertia对象。调用inertia.render()并传入两个参数:页面组件路径(相对于inertia/pages/)以及该组件接收的 props 对象。app/controllers/posts_controller.tsimport Post from '#models/post' import type { HttpContext } from '@adonisjs/core/http' import PostTransformer from '#transformers/post_transformer' export default class PostsController { async index({ inertia }: HttpContext) { const posts = await Post.all() return inertia.render('posts/index', { posts: PostTransformer.transform(posts) }) } }使用 transformer 将模型实例序列化为普通对象。转换器还会在
Data命名空间下生成前端类型,使 props 与后端保持同步。 -
创建页面组件
字符串
'posts/index'会解析为inertia/pages/posts/index.tsx(或.vue)。使用node ace make:page posts/index脚手架式生成该文件。组件会直接接收来自inertia.render()的 props。inertia/pages/posts/index.tsximport { InertiaProps } from '~/types' import { Data } from '@generated/data' type PageProps = InertiaProps<{ posts: Data.Post[] }> export default function PostsIndex({ posts }: PageProps) { return ( <> {posts.map((post) => ( <div key={post.id}> <h2>{post.title}</h2> </div> ))} </> ) }inertia/pages/posts/index.vue<script setup lang="ts"> import { Data } from '@generated/data' defineProps<{ posts: Data.Post[] }>() </script> <template> <div v-for="post in posts" :key="post.id"> <h2>{{ post.title }}</h2> </div> </template>InertiaProps辅助函数将你的页面专属 props 与 共享数据 合并,因此像user或flash这样的全局 props 会与posts一起被类型化。
从路由渲染
对于没有控制器逻辑的页面,可以跳过控制器,直接在路由定义中使用 renderInertia() 渲染。
router.on('/about').renderInertia('about')
router.on('/pricing').renderInertia('marketing/pricing', {
plans: ['starter', 'pro', 'enterprise'],
})
组件名称会针对生成的 InertiaPages 接口进行类型检查,因此拼写错误会在编译时被捕获。
幕后发生了什么
在对 /posts 的首次请求中,Inertia 返回一个 HTML 外壳,其中包含一个根 <div>,该 div 带有页面组件名称和作为 data-page 属性的序列化 props。前端 bundle 读取该属性并启动 React 或 Vue。
在后续的每次导航(点击链接、提交表单)中,Inertia 会发起一个带有 X-Inertia 请求头的 fetch 请求。服务器运行相同的控制器,但返回一个 JSON 页面对象而非 HTML。客户端替换新组件并更新 URL。没有完整的页面刷新,也没有独立的 API。
inertia 目录
inertia/ 目录包含你的前端应用。以下是起步套件创建的目录结构:
inertia/
├── app.tsx (或 app.vue) # 前端应用入口
├── client.ts # Tuyau API 客户端配置
├── ssr.tsx (或 ssr.vue) # SSR 入口(启用时)
├── tsconfig.json # 前端代码的 TypeScript 配置
├── types.ts # 共享类型定义
├── css/
│ └── app.css # 全局样式
├── layouts/ # 可复用的布局组件
│ └── default.tsx
└── pages/ # 由控制器渲染的页面组件
└── home.tsx
pages/ 目录是 Inertia 在调用 inertia.render() 时查找组件的位置。你传入的路径(如 posts/index)直接映射到该目录下的一个文件(inertia/pages/posts/index.tsx)。
app.tsx(或 app.vue)文件是启动前端应用的入口。它使用你的页面组件和任何全局配置来初始化 Inertia。ssr.tsx 文件为服务端渲染提供相同的用途。
随着项目增长,你可以创建额外的目录,例如用于共享 UI 元素的 components/ 或用于自定义 React hooks 的 hooks/。
配置文件
两个配置文件控制 Inertia 在 AdonisJS 应用中的工作方式。
config/inertia.ts 文件定义了 Inertia 适配器设置。
import { defineConfig } from '@adonisjs/inertia'
const inertiaConfig = defineConfig({
rootView: 'inertia_layout',
ssr: {
enabled: false,
entrypoint: 'inertia/ssr.tsx',
},
})
export default inertiaConfig
支持的选项如下:
rootView
渲染初始 HTML 外壳的 Edge 模板。默认为 inertia_layout。传入一个函数可以为每个请求选择不同的模板,例如为未认证用户渲染营销布局。
rootView: (ctx) => ctx.auth.isAuthenticated ? 'app_layout' : 'marketing_layout'
encryptHistory
加密存储在浏览器历史记录状态中的敏感页面 props。默认为 false。请参阅 Inertia 文档中的
历史记录加密
。
assetsVersion
固定用于 资源版本控制 的资源版本字符串。省略时,版本由 Vite manifest 计算。设置此值可使用 git 提交哈希、构建时间戳或任何自定义标识符覆盖默认值。
ssr.enabled
启用服务端渲染。请参阅 服务端渲染 。
ssr.entrypoint
相对于项目根目录的 SSR 入口文件路径。默认为 inertia/ssr.tsx。
ssr.bundle
由 Vite 生成的生产环境 SSR bundle 路径。默认为 ssr/ssr.js。
ssr.pages
将 SSR 限制为页面的一个子集。传入组件名称数组,或传入一个为每页返回布尔值的函数。
ssr: {
enabled: true,
entrypoint: 'inertia/ssr.tsx',
pages: ['home', 'marketing/pricing'],
}
resources/views/inertia_layout.edge 模板渲染初始 HTML 外壳,其中包含前端应用挂载的根 div。可用 Edge 标签请参阅
根模板
。
根模板
在 rootView 下配置的 Edge 模板会对首次请求进行渲染。它包含前端应用挂载的根元素,以及 SSR 输出需要插入其中的任何 HTML。
Inertia 包为该模板注册了两个 Edge 标签。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@inertiaHead()
@vite(['inertia/app.tsx'])
</head>
<body>
@inertia()
</body>
</html>
@inertia 标签渲染一个 div,其 data-page 属性中包含编码后的页面对象。前端读取该属性来启动 SPA。默认情况下,该元素为 <div id="app">。传入一个对象可以覆盖标签、id 或 class。
@inertia({ as: 'main', id: 'app-root', class: 'min-h-screen' })
@inertiaHead 标签输出在服务端渲染期间收集的 head 片段(标题、meta 标签)。只要启用了 SSR 就应包含它。对于非 SSR 响应,它是一个空操作。
向模板传递数据
inertia.render() 的第三个参数会作为视图 props 转发给根模板。对于属于 HTML 外壳而非页面 props 的值(例如页面标题,或非 SSR 页面的 <meta> 标签),可以使用此项。
return inertia.render(
'posts/show',
{ post: PostTransformer.transform(post) },
{ title: post.title, description: post.summary }
)
<head>
<title>{{ title ?? 'My App' }}</title>
@if(description)
<meta name="description" content="{{ description }}">
@end
@inertiaHead()
</head>
生成的类型
AdonisJS 中的 Inertia 是端到端完全类型安全的。这由两件生成的产物驱动:
- 位于
.adonisjs/client/data.d.ts的Data命名空间,它镜像转换器的输出,因此从控制器传递的 props 在页面组件中是类型化的。请参阅 Transformers 。 - 位于
.adonisjs/server/pages.d.ts的InertiaPages接口,它将inertia/pages/中的每个文件映射到其组件 prop 类型。这正是inertia.render('posts/index', { posts })能够对组件名称和 props 进行自动补全和类型检查的原因。
InertiaPages 类型由 indexPages Assembler 钩子生成。在 adonisrc.ts 中注册它,并传入你使用的框架。
import { defineConfig } from '@adonisjs/core/app'
import { indexPages } from '@adonisjs/inertia/index_pages'
export default defineConfig({
hooks: {
onDevServerStarted: [indexPages({ framework: 'react' })],
onBuildStarting: [indexPages({ framework: 'react' })],
},
})
framework 选项接受 'vue3' 或 'react'。传入 source 可以扫描 inertia/pages 以外的目录。
为共享数据添加类型
从 Inertia 中间件返回的共享数据,通过 InertiaProps 辅助函数在每个页面上可用。要使其类型安全,需要使用 share() 方法的推断返回类型来扩展 SharedProps 接口。
import type { HttpContext } from '@adonisjs/core/http'
import type { InferSharedProps } from '@adonisjs/inertia/types'
export default class InertiaMiddleware {
share(ctx: HttpContext) {
return {
user: ctx.auth?.user,
flash: ctx.session?.flashMessages.all(),
}
}
}
declare module '@adonisjs/inertia/types' {
interface SharedProps extends InferSharedProps<InertiaMiddleware> {}
}
扩展后,props.user 和 props.flash 在每个页面组件内部都会是类型化的,无需重新声明。
数据加载模式
Inertia 提供了多种高效加载数据的模式。AdonisJS 在 inertia 对象上暴露了辅助函数以支持每种模式。
可选(optional)props 和延迟(deferred)props 看起来相似,但行为不同。可选 props 仅在前端通过局部重新加载(partial reload)显式请求时才求值。延迟 props 在 Inertia 于页面挂载后自动发起的后续请求中求值。当某个值很少需要(用户可能永远不会点击的标签页)时使用 optional;当某个值始终需要但计算缓慢(仪表盘图表)时使用 defer。
可选 props
可选 props 仅在请求方在前端通过局部重新加载显式请求时才求值。这对于并非每次页面加载都需要的昂贵查询非常有用。
return inertia.render('users/index', {
/**
* 数据库查询仅在请求方在前端发起的
* 局部重新加载中包含 'users' 时才会运行。
*/
users: inertia.optional(async () => {
const users = await User.all()
return UserTransformer.transform(users)
})
})
另请参阅:Inertia 文档中的 局部重新加载 。
Always props
always 辅助函数确保某个 prop 始终包含在响应中,即使在未显式请求的局部重新加载期间也是如此。这与可选 props 正好相反。
return inertia.render('users/index', {
/**
* 无论前端请求什么,权限都会被计算并包含在内。
*/
permissions: inertia.always(async () => {
const permissions = await Permissions.all()
return PermissionTransformer.transform(permissions)
})
})
延迟 props
延迟 props 在初始页面渲染之后加载,使页面能够立即显示,而较慢的数据在后台加载。在前端数据到达之前,会显示加载状态。
return inertia.render('dashboard', {
/**
* 这些 props 在页面渲染后加载。
* 前端可以显示加载指示器。
*/
metrics: inertia.defer(async () => {
return computeMetrics()
}),
newSignups: inertia.defer(async () => {
return getNewSignups()
})
})
你可以对延迟 props 进行分组,使它们在一个请求中一起加载。
return inertia.render('dashboard', {
/**
* 这两个 props 在相同的延迟请求中获取,
* 因为它们共享了 'dashboard' 组名。
*/
metrics: inertia.defer(async () => {
return computeMetrics()
}, 'dashboard'),
newSignups: inertia.defer(async () => {
return getNewSignups()
}, 'dashboard')
})
另请参阅:Inertia 文档中的 延迟 props 。
可合并 props
可合并 props 会与现有的前端数据合并,而不是替换它。这对于无限滚动或向列表追加新项非常有用。
return inertia.render('users/index', {
/**
* 新的通知会与现有通知合并,
* 而不是替换整个数组。
*/
notifications: inertia.merge(await fetchNotifications())
})
默认情况下,数据是浅合并(shallow merge)的。对于需要递归合并的嵌套对象,请改用 deepMerge()。
return inertia.render('users/index', {
/**
* 嵌套的设置会与现有设置深度合并,
* 而不是替换整个对象。
*/
settings: inertia.deepMerge(await fetchSettings())
})
你可以通过将 merge() 或 deepMerge() 方法链式调用,将合并与延迟加载结合起来。
return inertia.render('users/index', {
notifications: inertia.defer(() => {
return fetchNotifications()
}).merge()
})
return inertia.render('users/index', {
settings: inertia.defer(() => {
return fetchSettings()
}).deepMerge()
})
另请参阅:Inertia 文档中的 合并 props 。
Link 和 Form 组件
Inertia 提供了 Link 和 Form 组件用于导航和表单提交。AdonisJS 对这些组件进行了封装,增加了额外的功能,使你可以通过名称引用路由,而不是硬编码 URL。
从 AdonisJS 包而非直接从 Inertia 导入这些组件。
import { Form, Link } from '@inertiajs/react'
import { Form, Link } from '@adonisjs/inertia/react'
<script setup>
import { Form, Link } from '@inertiajs/vue3'
import { Form, Link } from '@adonisjs/inertia/vue'
</script>
创建链接
Link 组件使用你在 AdonisJS 路由中定义的路由名称来创建导航链接。
<Link route="accounts.create">注册</Link>
<Link route="session.create">登录</Link>
创建表单
Form 组件处理表单提交,具备自动 CSRF 保护和错误处理。
import { Form } from '@adonisjs/inertia/react'
export default function EditPost({ post }) {
return (
<Form route="posts.update" routeParams={{ id: post.id }}>
{({ errors }) => (
<>
<div>
<label htmlFor="title">文章标题</label>
<input type="text" name="title" id="title" defaultValue={post.title} />
{errors.title && <div>{errors.title}</div>}
</div>
<button type="submit">更新文章</button>
</>
)}
</Form>
)
}
<script setup lang="ts">
import { Form } from '@adonisjs/inertia/vue'
defineProps<{ post: { id: number; title: string } }>()
</script>
<template>
<Form
route="posts.update"
:params="{ id: post.id }"
v-slot="{ errors }"
>
<div>
<label for="title">文章标题</label>
<input type="text" name="title" id="title" :value="post.title" />
<div v-if="errors.title">{{ errors.title }}</div>
</div>
<button type="submit">更新文章</button>
</Form>
</template>
Form 组件会根据路由名称自动推断 HTTP 方法(POST、PUT、PATCH、DELETE)。你无需传递 method prop——事实上,AdonisJS 封装器从可接受的 props 中省略了 method 和 action,因为两者都派生自路由定义。
当服务器端验证失败时,AdonisJS 会自动将验证错误添加到会话闪现消息中。随后 Inertia 中间件会将这些错误共享给前端,使它们可以通过表单中的 errors 对象使用。
使用 error bags 隔离错误
当一个页面渲染多个独立的表单时,来自一个表单的错误会泄漏到其它表单中,因为它们都从同一个 errors 对象读取。为了隔离它们,请在表单上设置 errorBag prop。Inertia 会在 X-Inertia-Error-Bag 请求头中发送该名称,中间件会将验证错误嵌套在该键下。
<Form route="comments.store" errorBag="newComment">
{({ errors }) => (
/**
* errors.newComment.body 仅保存此表单的错误。
*/
<textarea name="body" />
)}
</Form>
路由参数
Link 和 Form 都接受带有动态段的路由的参数。对象中的键对应于路由中定义的参数名称:
// 单个参数 —— :id
router.get('posts/:id', [PostsController, 'show']).as('posts.show')
// 多个参数 —— :userId 和 :postId
router.get('users/:userId/posts/:postId', [PostsController, 'show']).as('users.posts.show')
将匹配的参数值传递给组件。在 React 中使用 routeParams,在 Vue 中使用 params。
{/* 单个参数 */}
<Link route="posts.show" routeParams={{ id: post.id }}>
{post.title}
</Link>
{/* 多个参数 */}
<Link route="users.posts.show" routeParams={{ userId: user.id, postId: post.id }}>
查看文章
</Link>
<template>
<!-- 单个参数 -->
<Link route="posts.show" :params="{ id: post.id }">
{{ post.title }}
</Link>
<!-- 多个参数 -->
<Link route="users.posts.show" :params="{ userId: user.id, postId: post.id }">
查看文章
</Link>
</template>
TypeScript 强制要求你提供所有必需参数且名称正确。缺失或拼写错误的参数会在编译时被捕获。
查询参数
Link 和 Form 组件使用 route prop 进行类型安全的导航,但它们不直接接受查询参数。要添加查询参数(例如 ?page=2),请使用 urlFor 生成 URL,并作为 href prop 传入:
import { urlFor } from '~/client'
<Link href={urlFor('posts.index', {}, { qs: { page: 2, status: 'published' } })}>
第 2 页
</Link>
使用 href 时,你会失去 route prop 提供的类型安全路由名称检查。标准导航请使用带有路由参数的 route,仅在需要查询参数时回退到使用 urlFor 的 href。
共享数据
共享数据对你的应用中的每个页面都可用,而无需从每个控制器显式传递。这对于全局数据(如已认证用户、闪现消息或应用设置)非常有用。
InertiaMiddleware 定义了哪些数据被共享。该中间件存放在 app/middleware/inertia_middleware.ts,其中包含一个返回共享数据的 share 方法。
import type { HttpContext } from '@adonisjs/core/http'
import UserTransformer from '#transformers/user_transformer'
export default class InertiaMiddleware {
share(ctx: HttpContext) {
/**
* share 方法可能在所有中间件运行之前被调用。
* 例如,在 404 响应期间。始终将上下文属性
* 视为可能未定义。
*/
const { session, auth } = ctx as Partial<HttpContext>
const error = session?.flashMessages.get('error')
const success = session?.flashMessages.get('success')
return {
/**
* 使用 always() 确保这些 props 被包含,
* 即使在局部重新加载期间也是如此。
*/
errors: ctx.inertia.always(this.getValidationErrors(ctx)),
flash: ctx.inertia.always({
error,
success,
}),
user: ctx.inertia.always(
auth?.user ? UserTransformer.transform(auth.user) : undefined
),
}
}
}
share 方法可能在请求经过所有中间件或到达控制器之前被调用。这发生在渲染错误页面或提前中止请求时。在访问上下文属性前,始终检查它们是否存在。
访问共享数据
共享数据会自动包含在每页的 props 中。在 React 中,当你使用 InertiaProps 类型辅助函数定义页面 props 时,它同时包含你的页面专属 props 和所有共享数据。在 Vue 中,使用 usePage。
import { InertiaProps } from '~/types'
import { Data } from '@generated/data'
type PageProps = InertiaProps<{
posts: Data.Post[]
}>
export default function PostsIndex(props: PageProps) {
/**
* 与页面专属 props 一起访问共享数据。
*/
if (props.flash.error) {
console.log('Error:', props.flash.error)
}
return (
<div>
{props.user && <p>欢迎,{props.user.name}</p>}
{/* 渲染文章 */}
</div>
)
}
<script setup lang="ts">
import { computed, watch } from 'vue'
import { usePage } from '@inertiajs/vue3'
import { Data } from '@generated/data'
defineProps<{
posts: Data.Post[]
}>()
/**
* 访问共享数据。
*/
const page = usePage<Data.SharedProps>()
const user = computed(() => page.props.user)
watch(
() => page.props.flash,
(flashMessages) => {
if (flashMessages.error) {
console.log('Error:', flashMessages.error)
}
},
{ immediate: true }
)
</script>
<template>
<p v-if="user">欢迎,{{ user.name }}</p>
<!-- 渲染文章 -->
</template>
分页
Inertia 应用中的分页需要控制器、转换器和前端组件之间的协调。以下是使用文章列表的完整示例。
控制器
使用转换器的 paginate 方法序列化数据和分页元数据,然后将所有内容传递给 inertia.render():
import Post from '#models/post'
import type { HttpContext } from '@adonisjs/core/http'
import PostTransformer from '#transformers/post_transformer'
export default class PostsController {
async index({ request, inertia }: HttpContext) {
const page = request.input('page', 1)
const posts = await Post.query().paginate(page, 10)
return inertia.render('posts/index', {
posts: PostTransformer.paginate(posts.all(), posts.getMeta()),
})
}
}
前端组件
使用 Data 命名空间对分页 props 进行类型化。分页元数据包含 currentPage、lastPage 以及可用于渲染分页控件的其他字段:
import { Link } from '@adonisjs/inertia/react'
import { urlFor } from '~/client'
import { InertiaProps } from '~/types'
import { Data } from '@generated/data'
type PageProps = InertiaProps<{
posts: {
data: Data.Post[]
metadata: {
total: number
perPage: number
currentPage: number
lastPage: number
firstPage: number
}
}
}>
export default function PostsIndex({ posts }: PageProps) {
const { data, metadata } = posts
return (
<div>
{data.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
<nav>
{metadata.currentPage > 1 && (
<Link href={urlFor('posts.index', {}, { qs: { page: metadata.currentPage - 1 } })}>
上一页
</Link>
)}
{metadata.currentPage < metadata.lastPage && (
<Link href={urlFor('posts.index', {}, { qs: { page: metadata.currentPage + 1 } })}>
下一页
</Link>
)}
</nav>
</div>
)
}
分页链接使用带有 qs 选项的 urlFor 来生成像 /posts?page=2 这样的 URL。有关 paginate 方法以及元数据对象形状的详情,请参阅
Transformers
。
CSRF 保护
Inertia 起步套件已自动配置好 CSRF 保护。config/shield.ts 中的 enableXsrfCookie 选项设置了一个 cookie,Inertia 会读取它并在每次请求中附带。你无需手动为表单添加 CSRF token。
另请参阅: Shield ,了解更多关于 CSRF 保护的详情。
资源版本控制
资源版本控制告诉前端你的 JavaScript 或 CSS bundle 何时发生了变化,从而触发完整的页面重新加载,而不是局部更新。这确保用户在部署后始终运行最新版本的前端代码。
默认情况下,AdonisJS 会计算 .vite/manifest.json 文件(在你构建前端资源时创建)的哈希值,并将其用作版本标识符。要将版本固定为自定义值,请在 config/inertia.ts 中将 assetsVersion 设置为 git 提交哈希、构建时间戳或你控制的任何其他标识符。
const inertiaConfig = defineConfig({
assetsVersion: process.env.RELEASE_SHA,
})
Inertia 会在每次请求中通过 X-Inertia-Version 请求头发送当前资源版本。当服务器在 GET 请求上检测到不匹配时,它会以 409 响应并指示客户端在同一 URL 上执行完整的页面重新加载。闪现消息会自动重新闪现,以便在重新加载后仍然保留。
重定向与历史记录
Inertia 的重定向和历史记录行为与传统的服务端渲染应用不同,因为导航是通过 fetch 进行的。inertia 对象在 HTTP 上下文上暴露了一些辅助函数,用于处理框架无法自动处理的场景。
来自 mutating 请求的重定向
当 PUT、PATCH 或 DELETE 请求之后跟随一个 302 重定向时,浏览器会针对新 URL 重放原始方法。Inertia 中间件会自动将这些重定向升级为 303,以便浏览器发出 GET 请求。你无需自己设置状态码。
async update({ request, response }: HttpContext) {
await Post.updateOrFail(request.param('id'), request.all())
return response.redirect().toRoute('posts.index')
}
外部重定向
Inertia 无法通过 fetch 跟随指向不同源的重定向。使用 inertia.location() 向客户端发送一个带有 X-Inertia-Location 请求头的 409 响应,这会触发到目标 URL 的完整浏览器导航。
async checkout({ inertia }: HttpContext) {
const session = await stripe.createCheckoutSession()
return inertia.location(session.url)
}
清除浏览器历史记录
在渲染之前调用 inertia.clearHistory() 以清除客户端历史记录栈。这在退出登录后非常有用,因为你不想让用户导航回已认证的页面。
async destroy({ inertia, auth }: HttpContext) {
await auth.use('web').logout()
inertia.clearHistory()
return inertia.location('/')
}
加密历史记录状态
Inertia 将每次访问的页面对象存储在浏览器的历史记录状态中,以支持前进/后退导航。对于包含敏感数据(账户设置、账单详情)的页面,请启用加密,使数据无法通过历史 API 读取。
在调用 render() 之前,针对每个请求切换加密:
async settings({ inertia, auth }: HttpContext) {
inertia.encryptHistory()
return inertia.render('account/settings', {
user: UserTransformer.transform(auth.user),
})
}
或者通过
encryptHistory
配置选项全局启用。
有关权衡取舍,请参阅 Inertia 文档中的 历史记录加密 。
服务端渲染
服务端渲染(SSR)在服务器上生成初始 HTML,从而提升感知性能和 SEO。启用 SSR 需要在 Vite 和 AdonisJS 中同时进行配置。
首先,在 Vite 配置中启用 SSR。这会告诉 Vite 使用你的 ssr.tsx 或 ssr.vue 入口点创建一个单独的 SSR bundle。
export default defineConfig({
plugins: [
inertia({
ssr: {
enabled: true,
entrypoint: 'inertia/ssr.tsx'
}
}),
],
})
然后在 AdonisJS 配置中启用 SSR,使服务器知道使用 SSR bundle 进行渲染。
import { defineConfig } from '@adonisjs/inertia'
const inertiaConfig = defineConfig({
ssr: {
enabled: true,
entrypoint: 'inertia/ssr.tsx',
},
})
export default inertiaConfig
请求生命周期
理解请求如何流经 Inertia 应用,有助于调试或扩展默认行为。
当用户首次访问你的应用时,请求遵循以下路径:
- 请求命中你的 AdonisJS 路由,并由控制器处理
- 控制器调用
inertia.render(),传入页面组件和 props - Inertia 中间件的
share()方法将共享数据添加到 props 中 - 由于这是首次访问,Inertia 返回一个完整的 HTML 响应,其中包含外壳布局,该布局带有一个保存序列化页面组件名称和 props 的
div - 前端 bundle 启动,从
div读取 props,并渲染 React 或 Vue 组件
对于后续导航(点击链接或提交表单):
- Inertia 拦截导航,并发起一个带有
X-Inertia请求头的fetch请求 - 请求像之前一样流经路由、控制器和中间件
- 由于存在
X-Inertia请求头,Inertia 返回一个仅包含页面组件名称和 props 的 JSON 响应 - 前端接收到 JSON,并用新组件替换当前组件,在不重新加载整个页面的情况下更新 URL
这种架构让你拥有了传统服务端渲染应用的开发体验,以及现代 SPA 的用户体验。