Skip to content

Inertia

Inertia 是一种与框架无关的方式来创建单页应用程序,而无需现代 SPA 的大部分复杂性。

它是传统服务端渲染应用程序(使用模板引擎)和现代 SPA(使用客户端路由和状态管理)之间的良好折中方案。

使用 Inertia 将允许您使用您喜欢的前端框架(Vue.js、React、Svelte 或 Solid.js)创建 SPA,而无需创建单独的 API。

:::codegroup

ts
// title: app/controllers/users_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class UsersController {
  async index({ inertia }: HttpContext) {
    const users = await User.all()

    return inertia.render('users/index', { users })
  }
}
vue
// title: inertia/pages/users/index.vue
<script setup lang="ts">
import { Link, Head } from '@inertiajs/vue3'

defineProps<{
  users: SerializedUser[]
}>()
</script>

<template>
  <Head title="Users" />

  <div v-for="user in users" :key="user.id">
    <Link :href="`/users/${user.id}`">
      {{ user.name }}
    </Link>
    <div>{{ user.email }}</div>
  </div>
</template>

:::

安装

TIP

您正在启动一个新项目并想使用 Inertia 吗?查看 Inertia 入门套件

从 npm 注册表安装包:

:::codegroup

sh
// title: npm
npm i @adonisjs/inertia

:::

完成后,运行以下命令来配置包。

sh
node ace configure @adonisjs/inertia

:::disclosure

  1. adonisrc.ts 文件中注册以下服务提供器和命令。

    ts
    {
      providers: [
        // ...其他提供器
        () => import('@adonisjs/inertia/inertia_provider')
      ]
    }
  2. start/kernel.ts 文件中注册以下中间件

    ts
    router.use([() => import('@adonisjs/inertia/inertia_middleware')])
  3. 创建 config/inertia.ts 文件。

  4. 复制一些存根到您的应用程序中,以帮助您快速开始。每个复制的文件都适应于先前选择的前端框架。

  5. 创建 ./resources/views/inertia_layout.edge 文件,用于渲染用于启动 Inertia 的 HTML 页面。

  6. 创建 ./inertia/css/app.css 文件,其中包含样式化 inertia_layout.edge 视图所需的内容。

  7. 创建 ./inertia/tsconfig.json 文件以区分服务器端和客户端的 TypeScript 配置。

  8. 创建 ./inertia/app/app.ts 用于引导 Inertia 和您的前端框架。

  9. 创建 ./inertia/pages/home.{tsx|vue|svelte} 文件来渲染应用程序的主页。

  10. 创建 ./inertia/pages/server_error.{tsx|vue|svelte}./inertia/pages/not_found.{tsx|vue|svelte} 文件来渲染错误页面。

  11. vite.config.ts 文件中添加正确的 vite 插件来编译您的前端框架。

  12. 在您的 start/routes.ts 文件中添加 / 处的虚拟路由,以使用 Inertia 作为示例渲染主页。

  13. 根据所选的前端框架安装包。

:::

完成后,您应该可以在 AdonisJS 应用程序中使用 Inertia 了。启动您的开发服务器,并访问 localhost:3333 以查看使用您选择的前端框架通过 Inertia 渲染的主页。

TIP

阅读 Inertia 官方文档

Inertia 是一个后端无关的库。我们只是创建了一个适配器使其与 AdonisJS 一起工作。在阅读本文档之前,您应该熟悉 Inertia 的概念。

我们只会在本文档中介绍 AdonisJS 特定的部分。

客户端入口点

如果您使用了 configureadd 命令,该包将在 inertia/app/app.ts 创建一个入口点文件,因此您可以跳过此步骤。

基本上,此文件将是您前端应用程序的主入口点,用于引导 Inertia 和您的前端框架。此文件应该是由根 Edge 模板使用 @vite 标签加载的入口点。

:::codegroup

ts
// title: Vue
import { createApp, h } from 'vue'
import type { DefineComponent } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import { resolvePageComponent } from '@adonisjs/inertia/helpers'

const appName = import.meta.env.VITE_APP_NAME || 'AdonisJS'

createInertiaApp({
  title: (title) => {{ `${title} - ${appName}` }},
  resolve: (name) => {
    return resolvePageComponent(
      `../pages/${name}.vue`,
      import.meta.glob<DefineComponent>('../pages/**/*.vue'),
    )
  },
  setup({ el, App, props, plugin }) {
    createApp({ render: () => h(App, props) })
      .use(plugin)
      .mount(el)
  },
})
tsx
// title: React
import { createRoot } from 'react-dom/client';
import { createInertiaApp } from '@inertiajs/react';
import { resolvePageComponent } from '@adonisjs/inertia/helpers'

const appName = import.meta.env.VITE_APP_NAME || 'AdonisJS'

createInertiaApp({
  progress: { color: '#5468FF' },

  title: (title) => `${title} - ${appName}`,

  resolve: (name) => {
    return resolvePageComponent(
      `./pages/${name}.tsx`,
      import.meta.glob('./pages/**/*.tsx'),
    )
  },

  setup({ el, App, props }) {
    const root = createRoot(el);
    root.render(<App {...props} />);
  },
});
ts
// title: Svelte
import { createInertiaApp } from '@inertiajs/svelte'
import { resolvePageComponent } from '@adonisjs/inertia/helpers'

const appName = import.meta.env.VITE_APP_NAME || 'AdonisJS'

createInertiaApp({
  progress: { color: '#5468FF' },

  title: (title) => `${title} - ${appName}`,

  resolve: (name) => {
    return resolvePageComponent(
      `./pages/${name}.svelte`,
      import.meta.glob('./pages/**/*.svelte'),
    )
  },

  setup({ el, App, props }) {
    new App({ target: el, props })
  },
})
ts
// title: Solid
import { render } from 'solid-js/web'
import { createInertiaApp } from 'inertia-adapter-solid'
import { resolvePageComponent } from '@adonisjs/inertia/helpers'

const appName = import.meta.env.VITE_APP_NAME || 'AdonisJS'

createInertiaApp({
  progress: { color: '#5468FF' },

  title: (title) => `${title} - ${appName}`,

  resolve: (name) => {
    return resolvePageComponent(
      `./pages/${name}.tsx`,
      import.meta.glob('./pages/**/*.tsx'),
    )
  },

  setup({ el, App, props }) {
    render(() => <App {...props} />, el)
  },
})

:::

此文件的作用是创建 Inertia 应用并解析页面组件。使用 inertia.render 时编写的页面组件将被传递给 resolve 函数,此函数的作用是返回需要渲染的组件。

渲染页面

在配置您的包时,inertia_middleware 已在 start/kernel.ts 文件中注册。此中间件负责在 HttpContext 上设置 inertia 对象。

要使用 Inertia 渲染视图,请使用 inertia.render 方法。该方法接受视图名称和要作为 props 传递给组件的数据。

ts
// title: app/controllers/home_controller.ts
export default class HomeController {
  async index({ inertia }: HttpContext) {
    // highlight-start
    return inertia.render('home', { user: { name: 'julien' } })
    // highlight-end
  }
}

看到传递给 inertia.render 方法的 home 了吗?它应该是相对于 inertia/pages 目录的组件文件路径。这里我们渲染 inertia/pages/home.(vue,tsx) 文件。

您的前端组件将接收 user 对象作为 prop:

:::codegroup

vue
// title: Vue
<script setup lang="ts">
defineProps<{
  user: { name: string }
}>()
</script>

<template>
  <p>你好 {{ user.name }}</p>
</template>
tsx
// title: React
export default function Home(props: { user: { name: string } }) {
  return <p>你好 {props.user.name}</p>
}
svelte
// title: Svelte
<script lang="ts">
export let user: { name: string }
</script>

<Layout>
  <p>你好 {user.name}</p>
</Layout>
jsx
// title: Solid
export default function Home(props: { user: { name: string } }) {
  return <p>你好 {props.user.name}</p>
}

:::

就是这么简单。

WARNING

在向前端传递数据时,所有内容都会被序列化为 JSON。不要期望传递模型实例、日期或其他复杂对象。

根 Edge 模板

根模板是一个常规的 Edge 模板,将在您应用程序的第一次页面访问时加载。这是您应该包含 CSS 和 Javascript 文件的地方,也是您应该包含 @inertia 标签的地方。典型的根模板如下所示:

:::codegroup

edge
// title: Vue
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title inertia>AdonisJS x Inertia</title>

  @inertiaHead()
  @vite(['inertia/app/app.ts', `inertia/pages/${page.component}.vue`])
</head>

<body>
  @inertia()
</body>

</html>
edge
// title: React
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title inertia>AdonisJS x Inertia</title>

  @inertiaHead()
  @viteReactRefresh()
  @vite(['inertia/app/app.tsx', `inertia/pages/${page.component}.tsx`])
</head>

<body>
  @inertia()
</body>

</html>
edge
// title: Svelte
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title inertia>AdonisJS x Inertia</title>

  @inertiaHead()
  @vite(['inertia/app/app.ts', `inertia/pages/${page.component}.svelte`])
</head>

<body>
  @inertia()
</body>

</html>
edge
// title: Solid
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title inertia>AdonisJS x Inertia</title>

  @inertiaHead()
  @vite(['inertia/app/app.tsx', `inertia/pages/${page.component}.tsx`])
</head>

<body>
  @inertia()
</body>

</html>

:::

您可以在 config/inertia.ts 文件中配置根模板路径。默认情况下,它假设您的模板位于 resources/views/inertia_layout.edge

ts
import { defineConfig } from '@adonisjs/inertia'

export default defineConfig({
  // 相对于 `resources/views` 目录的根模板路径
  rootView: 'app_root', 
})

如果需要,您可以将函数传递给 rootView 属性以动态决定应使用哪个根模板。

ts
import { defineConfig } from '@adonisjs/inertia'
import type { HttpContext } from '@adonisjs/core/http'

export default defineConfig({
  rootView: ({ request }: HttpContext) => {
    if (request.url().startsWith('/admin')) {
      return 'admin_root'
    }

    return 'app_root'
  }
})

根模板数据

您可能想要与根 Edge 模板共享数据。例如,用于添加 meta 标题或 open graph 标签。您可以使用 inertia.render 方法的第 3 个参数来做到这一点:

ts
// title: app/controllers/posts_controller.ts
export default class PostsController {
  async index({ inertia }: HttpContext) {
    return inertia.render('posts/details', post, {
      // highlight-start
      title: post.title,
      description: post.description
      // highlight-end
    })
  }
}

titledescription 现在将在根 Edge 模板中可用:

edge
// title: resources/views/root.edge
<html>
  <title>{{ title }}</title>
  <meta name="description" content="{{ description }}">

  <body>
    @inertia()
  </body>
</html

重定向

在 AdonisJS 中应该这样做:

ts
export default class UsersController {
  async store({ response }: HttpContext) {
    await User.create(request.body())

    // 👇 您可以使用标准的 AdonisJS 重定向
    return response.redirect().toRoute('users.index')
  }

  async externalRedirect({ inertia }: HttpContext) {
    // 👇 或使用 inertia.location 进行外部重定向
    return inertia.location('https://adonisjs.com')
  }
}

有关更多信息,请参阅官方文档

与所有视图共享数据

有时,您可能需要在多个视图之间共享相同的数据。例如,我们与所有视图共享当前用户信息。每个控制器都这样做可能会变得繁琐。幸运的是,我们有两种解决此问题的方法。

sharedData

config/inertia.ts 文件中,您可以定义一个 sharedData 对象。此对象允许您定义应与所有视图共享的数据。

ts
import { defineConfig } from '@adonisjs/inertia'

export default defineConfig({
  sharedData: {
    appName: 'My App', // 👈 这将在所有视图中可用
    user: (ctx) => ctx.auth?.user, // 👈 作用域限定于当前请求
  },
})

从中间件共享

有时,从中间件而不是 config/inertia.ts 文件共享数据可能更方便。您可以使用 inertia.share 方法来做到这一点:

ts
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'

export default class MyMiddleware {
  async handle({ inertia, auth }: HttpContext, next: NextFn) {
    inertia.share({
      appName: 'My App',
      user: (ctx) => ctx.auth?.user
    })
  }
}

部分重新加载和延迟数据评估

首先阅读官方文档以了解什么是部分重新加载以及它们是如何工作的。

关于延迟数据评估,以下是它在 AdonisJS 中的工作方式:

ts
export default class UsersController {
  async index({ inertia }: HttpContext) {
    return inertia.render('users/index', {
      // 始终在首次访问时包含。
      // 可选地在部分重新加载时包含。
      // 始终评估
      users: await User.all(),

      // 始终在首次访问时包含。
      // 可选地在部分重新加载时包含。
      // 仅在需要时评估
      users: () => User.all(),

      // 从不在首次访问时包含。
      // 可选地在部分重新加载时包含。
      // 仅在需要时评估
      users: inertia.optional(() => User.all())
    }),
  }
}

类型共享

通常,您会想要共享传递给前端页面组件的数据类型。一个简单的方法是使用 InferPageProps 类型。

:::codegroup

ts
// title: app/controllers/users_controller.ts
export class UsersController {
  index() {
    return inertia.render('users/index', {
      users: [
        { id: 1, name: 'julien' },
        { id: 2, name: 'virk' },
        { id: 3, name: 'romain' },
      ]
    })
  }
}
tsx
// title: inertia/pages/users/index.tsx
import { InferPageProps } from '@adonisjs/inertia/types'
import type { UsersController } from '../../controllers/users_controller.ts'

export function UsersPage(
  // 👇 它将根据您在控制器中传递给
  // inertia.render 的内容正确地进行类型推断
  props: InferPageProps<UsersController, 'index'>
) {
  return (
    // ...
  )
}

:::

如果您使用 Vue,您必须在 defineProps 中手动定义每个属性。这是 Vue 的一个恼人限制,有关更多信息,请参阅此问题

vue
<script setup lang="ts">
import { InferPageProps } from '@adonisjs/inertia/types'

defineProps<{
  // 👇 您必须手动定义每个 prop
  users: InferPageProps<UsersController, 'index'>['users'],
  posts: InferPageProps<PostsController, 'index'>['posts'],
}>()

</script>

引用指令

由于您的 Inertia 应用程序是一个单独的 TypeScript 项目(有自己的 tsconfig.json),您需要帮助 TypeScript 理解某些类型。我们的许多官方包使用模块扩充来为您的 AdonisJS 项目添加某些类型。

例如,HttpContext 上的 auth 属性及其类型仅在您将 @adonisjs/auth/initialize_auth_middleware 导入到您的项目时才可用。现在,问题是我们没有在 Inertia 项目中导入此模块,因此如果您尝试从使用 auth 的控制器推断页面 props,那么您很可能会收到 TypeScript 错误或无效类型。

要解决此问题,您可以使用引用指令来帮助 TypeScript 理解某些类型。为此,您需要在 inertia/app/app.ts 文件中添加以下行:

ts
/// <reference path="../../adonisrc.ts" />

根据您使用的类型,您可能需要添加其他引用指令,例如对某些也使用模块扩充的配置文件的引用。

ts
/// <reference path="../../adonisrc.ts" />
/// <reference path="../../config/ally.ts" />
/// <reference path="../../config/auth.ts" />

类型级序列化

关于 InferPageProps 需要知道的一件重要事情是它会"在类型级别序列化"您传递的数据。例如,如果您将 Date 对象传递给 inertia.render,来自 InferPageProps 的结果类型将是 string

:::codegroup

ts
// title: app/controllers/users_controller.ts
export default class UsersController {
  async index({ inertia }: HttpContext) {
    const users = [
      { id: 1, name: 'John Doe', createdAt: new Date() }
    ]

    return inertia.render('users/index', { users })
  }
}
tsx
// title: inertia/pages/users/index.tsx
import type { InferPageProps } from '@adonisjs/inertia/types'

export function UsersPage(
  props: InferPageProps<UsersController, 'index'>
) {
  props.users
  //     ^? { id: number, name: string, createdAt: string }[]
}

:::

这完全合理,因为日期在通过 JSON 网络传递时会被序列化为字符串。

模型序列化

记住上一点,另一件需要知道的重要事情是,如果您将 AdonisJS 模型传递给 inertia.render,那么来自 InferPageProps 的结果类型将是 ModelObject:一个几乎不包含任何信息的类型。这可能会有问题。要解决此问题,您有几个选项:

  • 在将模型传递给 inertia.render 之前将其转换为简单对象:
  • 使用 DTO(数据传输对象)系统在将模型传递给 inertia.render 之前将其转换为简单对象。

:::codegroup

ts
// title: 转换
class UsersController {
  async edit({ inertia, params }: HttpContext) {
    const user = users.serialize() as {
        id: number
        name: string 
    }

    return inertia.render('user/edit', { user })
  }
}
ts
// title: DTOs
class UserDto {
  constructor(private user: User) {}

  toJson() {
    return {
      id: this.user.id,
      name: this.user.name
    }
  }
}

class UsersController {
  async edit({ inertia, params }: HttpContext) {
    const user = await User.findOrFail(params.id)
    return inertia.render('user/edit', { user: new UserDto(user).toJson() })
  }
}

:::

您现在将在前端组件中拥有准确的类型。

共享 Props

要在组件中拥有共享数据的类型,请确保您已在 config/inertia.ts 文件中执行了模块扩充,如下所示:

ts
// file: config/inertia.ts
const inertiaConfig = defineConfig({
  sharedData: {
    appName: 'My App',
  },
});

export default inertiaConfig;

declare module '@adonisjs/inertia/types' {
  export interface SharedProps extends InferSharedProps<typeof inertiaConfig> {
    // 如有必要,您也可以手动添加一些共享 props,
    // 例如从中间件共享的那些
    propsSharedFromAMiddleware: number;
  }
}

此外,请确保在您的 inertia/app/app.ts 文件中添加此引用指令

ts
/// <reference path="../../config/inertia.ts" />

完成后,您将可以通过 InferPageProps 在组件中访问共享 props。InferPageProps 将包含您的共享 props 和 inertia.render 传递的 props 的类型:

tsx
// file: inertia/pages/users/index.tsx

import type { InferPageProps } from '@adonisjs/inertia/types'

export function UsersPage(
  props: InferPageProps<UsersController, 'index'>
) {
  props.appName
  //     ^? string
  props.propsSharedFromAMiddleware
  //     ^? number
}

如果需要,您可以通过 SharedProps 类型仅访问共享 props 的类型:

tsx
import type { SharedProps } from '@adonisjs/inertia/types'

const page = usePage<SharedProps>()

CSRF

如果您为应用程序启用了 CSRF 保护,请在 config/shield.ts 文件中启用 enableXsrfCookie 选项。

启用此选项将确保在客户端设置 XSRF-TOKEN cookie,并在每个请求中将其发送回服务器。

无需额外配置即可使 Inertia 与 CSRF 保护一起工作。

资源版本控制

重新部署应用程序时,您的用户应始终获得最新版本的客户端资源。这是 Inertia 协议和 AdonisJS 开箱即用支持的功能。

默认情况下,@adonisjs/inertia 包将为 public/assets/manifest.json 文件计算哈希并将其用作资源的版本。

如果您想调整此行为,您可以编辑 config/inertia.ts 文件。assetsVersion 属性定义资源的版本,可以是字符串或函数。

ts
import { defineConfig } from '@adonisjs/inertia'

export default defineConfig({
  assetsVersion: 'v1'
})

有关更多信息,请阅读官方文档

SSR

启用 SSR

Inertia 入门套件开箱即用地支持服务端渲染(SSR)。因此,如果您想为应用程序启用 SSR,请确保使用它。

如果您在未启用 SSR 的情况下启动应用程序,您可以稍后按照以下步骤启用它:

添加服务器入口点

我们需要添加一个与客户端入口点非常相似的服务器入口点。此入口点将在服务器上而不是在浏览器中渲染首次页面访问。

您必须创建一个 inertia/app/ssr.ts,它默认导出一个类似这样的函数:

:::codegroup

ts
// title: Vue 
import { createInertiaApp } from '@inertiajs/vue3'
import { renderToString } from '@vue/server-renderer'
import { createSSRApp, h, type DefineComponent } from 'vue'

export default function render(page) {
  return createInertiaApp({
    page,
    render: renderToString,
    resolve: (name) => {
      const pages = import.meta.glob<DefineComponent>('../pages/**/*.vue')
      return pages[`../pages/${name}.vue`]()
    },

    setup({ App, props, plugin }) {
      return createSSRApp({ render: () => h(App, props) }).use(plugin)
    },
  })
}
tsx
// title: React
import ReactDOMServer from 'react-dom/server'
import { createInertiaApp } from '@inertiajs/react'

export default function render(page) {
  return createInertiaApp({
    page,
    render: ReactDOMServer.renderToString,
    resolve: (name) => {
      const pages = import.meta.glob('./pages/**/*.tsx', { eager: true })
      return pages[`./pages/${name}.tsx`]
    },
    setup: ({ App, props }) => <App {...props} />,
  })
}
ts
// title: Svelte
import { createInertiaApp } from '@inertiajs/svelte'
import createServer from '@inertiajs/svelte/server'

export default function render(page) {
  return createInertiaApp({
    page,
    resolve: name => {
      const pages = import.meta.glob('./pages/**/*.svelte', { eager: true })
      return pages[`./pages/${name}.svelte`]
    },
  })
}
tsx
// title: Solid
import { hydrate } from 'solid-js/web'
import { createInertiaApp } from 'inertia-adapter-solid'

export default function render(page: any) {
  return createInertiaApp({
    page,
    resolve: (name) => {
      const pages = import.meta.glob('./pages/**/*.tsx', { eager: true })
      return pages[`./pages/${name}.tsx`]
    },
    setup({ el, App, props }) {
      hydrate(() => <App {...props} />, el)
    },
  })
}

:::

更新配置文件

转到 config/inertia.ts 文件并更新 ssr 属性以启用它。如果您使用不同的路径,还请指向您的服务器入口点。

ts
import { defineConfig } from '@adonisjs/inertia'

export default defineConfig({
  // ...
  ssr: {
    enabled: true,
    entrypoint: 'inertia/app/ssr.ts'
  }
})

更新 Vite 配置

首先,确保您已注册 inertia vite 插件。完成后,如果您使用不同的路径,应在 vite.config.ts 文件中更新服务器入口点的路径。

ts
import { defineConfig } from 'vite'
import inertia from '@adonisjs/inertia/client'

export default defineConfig({
  plugins: [
    inertia({
      ssr: {
        enabled: true,
        entrypoint: 'inertia/app/ssr.ts'
      }
    })
  ]
})

您现在应该能够在服务器上渲染首次页面访问,然后继续客户端渲染。

SSR 允许列表

使用 SSR 时,您可能不想在服务端渲染所有组件。例如,您正在构建一个需要身份验证的管理仪表板,因此这些路由没有理由在服务器上渲染。但是在同一个应用程序中,您可能有一个可以从 SSR 中受益以改善 SEO 的登陆页面。

因此,您可以在 config/inertia.ts 文件中添加应在服务器上渲染的页面。

ts
import { defineConfig } from '@adonisjs/inertia'

export default defineConfig({
  ssr: {
    enabled: true,
    pages: ['home']
  }
})

您还可以将函数传递给 pages 属性以动态决定哪些页面应在服务器上渲染。

ts
import { defineConfig } from '@adonisjs/inertia'

export default defineConfig({
  ssr: {
    enabled: true,
    pages: (ctx, page) => !page.startsWith('admin')
  }
})

测试

有几种方法可以测试您的前端代码:

  • 端到端测试。您可以使用 Browser Client,这是 Japa 和 Playwright 之间的无缝集成。
  • 单元测试。我们建议使用适合前端生态系统的测试工具,特别是 Vitest

最后,您还可以测试您的 Inertia 端点以确保它们返回正确的数据。为此,我们在 Japa 中提供了一些测试助手。

首先,如果您尚未这样做,请确保在 test/bootsrap.ts 文件中配置 inertiaApiClientapiClient 插件:

ts
// title: tests/bootstrap.ts
import { assert } from '@japa/assert'
import app from '@adonisjs/core/services/app'
import { pluginAdonisJS } from '@japa/plugin-adonisjs'
// highlight-start
import { apiClient } from '@japa/api-client'
import { inertiaApiClient } from '@adonisjs/inertia/plugins/api_client'
// highlight-end

export const plugins: Config['plugins'] = [
  assert(), 
  pluginAdonisJS(app),
  // highlight-start
  apiClient(),
  inertiaApiClient(app)
  // highlight-end
]

接下来,我们可以使用 withInertia() 请求我们的 Inertia 端点,以确保数据以 JSON 格式正确返回。

ts
test('returns correct data', async ({ client }) => {
  const response = await client.get('/home').withInertia()

  response.assertStatus(200)
  response.assertInertiaComponent('home/main')
  response.assertInertiaProps({ user: { name: 'julien' } })
})

让我们看看可用于测试端点的各种断言:

withInertia()

向请求添加 X-Inertia 头。它确保数据以 JSON 格式正确返回。

assertInertiaComponent()

检查服务器返回的组件是否是预期的。

ts
test('returns correct data', async ({ client }) => {
  const response = await client.get('/home').withInertia()

  response.assertInertiaComponent('home/main')
})

assertInertiaProps()

检查服务器返回的 props 是否与作为参数传递的完全相同。

ts
test('returns correct data', async ({ client }) => {
  const response = await client.get('/home').withInertia()

  response.assertInertiaProps({ user: { name: 'julien' } })
})

assertInertiaPropsContains()

检查服务器返回的 props 是否包含作为参数传递的一些 props。它在底层使用 containsSubset

ts
test('returns correct data', async ({ client }) => {
  const response = await client.get('/home').withInertia()

  response.assertInertiaPropsContains({ user: { name: 'julien' } })
})

其他属性

除此之外,您可以在 ApiResponse 上访问以下属性:

ts
test('returns correct data', async ({ client }) => {
  const response = await client.get('/home').withInertia()

  // 👇 服务器返回的组件
  console.log(response.inertiaComponent) 

  // 👇 服务器返回的 props
  console.log(response.inertiaProps)
})

常见问题

为什么我更新前端代码时服务器不断重新加载?

假设您使用的是 React。每次更新前端代码时,服务器都会重新加载,浏览器会刷新。您没有享受到热模块替换(HMR)功能。

您需要从根 tsconfig.json 文件中排除 inertia/**/* 才能使其工作。

jsonc
{
  "compilerOptions": {
    // ...
  },
  "exclude": ["inertia/**/*"]
}

因为负责重启服务器的 AdonisJS 进程正在监视 tsconfig.json 文件中包含的文件。

为什么我的生产构建不工作?

如果您遇到如下错误:

X [ERROR] Failed to load url inertia/app/ssr.ts (resolved id: inertia/app/ssr.ts). Does the file exist?

一个常见问题是您只是忘记在运行生产构建时设置 NODE_ENV=production

shell
NODE_ENV=production node build/server.js

Top-level await is not available...

如果您遇到如下错误:

X [ERROR] Top-level await is not available in the configured target environment ("chrome87", "edge88", "es2020", "firefox78", "safari14" + 2 overrides)

    node_modules/@adonisjs/core/build/services/hash.js:15:0:
      15 │await app.booted(async () => {
         │~~~~~

那么您很可能将后端代码导入到前端。仔细查看由 Vite 生成的错误,我们看到它正在尝试编译 node_modules/@adonisjs/core 中的代码。因此,这意味着我们的后端代码将最终进入前端包。这可能不是您想要的。

通常,当您尝试与前端共享类型时会发生此错误。如果这就是您正在尝试实现的目标,请确保始终仅通过 import type 而不是 import 导入此类型:

ts
// ✅ 正确
import type { User } from '#models/user'

// ❌ 错误
import { User } from '#models/user'