选择你的技术路线
本指南介绍 AdonisJS 的前端开发思路,以及你可以选择的三种主要技术栈。你将了解:
- 为何 AdonisJS 以后端为先,却在前端上保持灵活。
- 理解 Hypermedia、Inertia 与纯 API 这三种方案的区别。
- 了解视图层在每种技术栈中的运作方式。
- 了解为每种方案提供"约定式"初始配置的入门套件(starter kit)。
概述
AdonisJS 对后端有着强烈的约定——它内置了身份验证、授权、验证、数据库工具链等,但对前端刻意保持灵活。这种"后端为先"的理念意味着,在由你决定如何构建用户界面的同时,你能获得一套健壮的服务器端逻辑基础。
你可以使用同一个后端框架,创建传统的服务端渲染应用、现代的单页应用(SPA),或介于两者之间的任何形态。营销网站的需求不同于管理后台,也不同于移动端应用的 API 后端。AdonisJS 不会把你强行限定在单一方案上,而是让你选择最适合需求的前端技术栈。
三种方案
AdonisJS 支持三种构建前端的主要方案。每种方案代表了你思考应用架构中"视图层"的不同方式。
Hypermedia
Hypermedia 应用在服务端生成完整的 HTML 页面并发送给浏览器。你使用模板引擎(AdonisJS 提供 Edge )来构建界面,并在需要时使用 Alpine.js 或 HTMX/Unpoly 这类轻量 JavaScript 库来增强交互。
"Hypermedia" 一词指的是将 HTML 作为构建交互式应用的媒介——由服务端驱动应用状态,客户端(浏览器)负责展示。如果你对这个概念还不熟悉,HTMX 项目有一篇精彩的论文深入讲解了 超媒体驱动的应用 。
在 Hypermedia 应用中:
- 服务端负责渲染你的视图。
- 你的控制器返回 HTML 而非 JSON。
- 页面间的导航通过传统的整页加载或渐进增强的请求完成。
这种方案拥抱 Web 的原生能力,并将应用的大部分逻辑保留在服务端,让你拥有完全的控制权。
当你希望由服务端掌控应用,或希望尽量减少用户下载的 JavaScript 体积时,选择此方案。Hypermedia 应用借助 Alpine.js 和 HTMX 等库可以实现高度交互,同时保持前端代码精简、部署简单。
Inertia(React 或 Vue)
Inertia.js 在服务端渲染模板与 SPA 之间提供了一条中间路线。你使用 React 或 Vue 组件作为视图,同时保留服务端路由与控制器。AdonisJS 官方支持通过 Inertia 使用 React 或 Vue 构建应用,让你获得现代前端框架的组件化开发体验,而无需维护一个独立的单页应用所带来的复杂性。
使用 Inertia 时:
- 你的后端路由直接映射到前端组件,消除了双路由系统的复杂性。
- 你的控制器向 Inertia 组件返回数据,而非渲染模板或返回 JSON。
- 导航体验如单页应用般流畅顺滑,但你的路由逻辑仍留在服务端,更易于保护与维护。
Inertia 还简化了表单提交与数据获取,同时让你的应用保持单体部署。你无需构建和维护独立的 API 层,即可获得现代、响应式的用户体验。
当你想使用 React 或 Vue 但偏好服务端路由、希望避免前端与后端分离部署的复杂性,或想要紧密集成的全栈开发体验时,选择此方案。访问 inertiajs.com 可进一步了解 Inertia 如何弥合服务端框架与客户端框架之间的鸿沟。
纯 API
你可以使用 AdonisJS 构建一个 JSON API 后端,而前端则存在于完全独立代码库中。这种方案形成了清晰的职责分离:AdonisJS 处理所有后端逻辑并通过 API 端点暴露数据,而你的前端应用(用任何框架构建)消费这些端点。
在纯 API 架构中:
- 你的控制器返回 JSON 响应。
- 前端与后端是独立的部署,各自拥有构建流程、代码仓库(或 monorepo)与部署流水线。
- 两者仅通过发往你 API 端点的 HTTP 请求进行通信。
在 monorepo 中,你可以使用 类型安全的 API 客户端 实现后端与前端之间真正的端到端类型。 Transformers(转换器) 也能生成可复用、独立的响应类型,让你的 UI 可以直接依赖序列化后的 API 契约。
这种方案覆盖面很广:可用于移动应用(iOS、Android)的 API、用任意前端框架构建的 Web 应用、桌面应用,甚至多个前端(Web 与移动端)共用同一套 API。这种分离让你能够灵活地独立部署和扩展每一层。
当你正在构建一个服务于多个客户端应用、团队偏好使用独立的前端和后端代码仓库、需要对前后端进行独立部署与扩展,或正在构建外部开发者会消费的公开 API 时,选择此方案。
同一个控制器,三种不同的返回
在下面的示例中,你会看到我们使用的是相同的路由、相同的控制器、相同的数据获取逻辑,只有 return 语句不同。
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router.get('posts', [controllers.Posts, 'index'])
import Post from '#models/post'
import { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
async index({ view }: HttpContext) {
const posts = await Post.all()
return view.render('posts/index', { posts })
}
}
@each(post in posts)
<div>
<h2>{{ post.title }}</h2>
<div>
@end
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router.get('posts', [controllers.Posts, 'index'])
import Post from '#models/post'
import { 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)
})
}
}
import { 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>
))
}
</>
}
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router.get('posts', [controllers.Posts, 'index'])
import Post from '#models/post'
import { HttpContext } from '@adonisjs/core/http'
import PostTransformer from '#transformers/post_transformer'
export default class PostsController {
async index({ serialize }: HttpContext) {
const posts = await Post.all()
return serialize(PostTransformer.transform(posts))
}
}
{
"data": [
{
"id": 1,
"title": "AdonisJS In 30",
"description": "In this series, we'll ...",
}
]
}
不要期待什么
如果你来自元框架(meta-framework),AdonisJS 中有一些模式是你找不到的。这些差异是有意为之,目的是让应用的运作方式更加清晰。
AdonisJS 的设计定位是"为你的前端应用提供动力的后端",而非元框架的替代品。你可以将 AdonisJS 与 TanStack Start、Nuxt、Next.js 等框架配合使用,两者之间边界清晰、API 契约明确。
没有基于文件的路由:AdonisJS 使用路由文件中显式的路由定义。路由通过 router API 声明,让你对应用的 URL 结构拥有完全的控制与可见性。这让你可以在一个地方查看所有路由,并按需应用中间件或约束。
没有带 "use" 指令的编译魔法:你不会看到像 "use server" 或 "use client" 这样模糊代码执行位置的魔法语句。AdonisJS 在后端代码(运行于服务端)与前端代码(运行于浏览器)之间保持着清晰的分离。
不混合服务端与客户端的代码:你的后端逻辑位于控制器、模型与服务中;前端代码位于模板、组件或独立的前端应用中。代码在哪里运行没有任何歧义,这让调试变得直观,也有助于团队在明确边界下协作。
这种分离让代码在何处执行一目了然,使调试可预测,并让团队能在前后端之间以清晰的契约协作。当你的前端需要从后端获取数据时,你使用的是显式的 API 调用或模板数据传递,而非在编译期模糊边界的"魔法"机制。
入门套件(Starter kits)
AdonisJS 为每种方案都提供了入门套件,这些套件已内置约定式的配置与最佳实践。你无需从零开始配置一切。
这些入门套件包含正确配置好的构建工具、身份验证脚手架,以及所有必要的集成。当你创建一个新的 AdonisJS 应用时,可以根据所选方案挑选对应的入门套件。
这带来了一种**"灵活但不孤立无援"**的体验:你可以选择自己的技术栈,但一旦选定,你就能获得开箱即用、完整配置好的初始环境。
下一步
既然你已经了解了 AdonisJS 支持的所有方案,就可以着手创建你的第一个应用了。 安装指南 将引导你通过入门套件,用所选技术栈搭建一个新项目。