构建 DevShow —— 一个社区展示网站
在本教程中,你将构建 DevShow。DevShow 是一个小型的社区展示网站,用户可以在这里分享自己构建的作品。 每个用户都能创建账户、发布一条"展示条目"(一个项目、工具、实验,或任何他们引以为傲的东西),并浏览他人创建的条目。
概述
本教程采用动手实践的方式,从零到一构建一个真实的应用。你不会孤立地学习各个功能,而是能看到 AdonisJS 与 React 中的一切是如何协同工作的:路由、控制器、模型、验证、身份验证、转换器与 React 组件汇聚在一起,构成一个可运行的 Web 应用。
到本教程结束时,你将构建出:
- 帖子列表与详情页 - 展示所有帖子以及带评论的单篇帖子详情
- 帖子的创建与编辑 - 通过表单创建和更新帖子,并带验证
- 评论系统 - 允许用户对帖子发表评论
- 授权 - 确保用户只能编辑/删除自己的帖子与评论
- 导航与样式 - 带有页面间合理导航的精致 UI
身份验证系统(注册、登录、登出)已包含在入门套件中,且功能完整。
理解入门套件
我们以 AdonisJS Inertia + React 入门套件为起点,它已经内置了身份验证。让我们通过打开路由文件,看看手头有哪些可用资源。
import { middleware } from '#start/kernel'
import { controllers } from '#generated/controllers'
import router from '@adonisjs/core/services/router'
router.on('/').renderInertia('home')
/**
* 注册和登录路由 - 仅对访客开放
*/
router
.group(() => {
router.get('signup', [controllers.NewAccount, 'create'])
router.post('signup', [controllers.NewAccount, 'store'])
router.get('login', [controllers.Session, 'create'])
router.post('login', [controllers.Session, 'store'])
})
.use(middleware.guest())
/**
* 登出路由 - 仅对已登录用户开放
*/
router
.group(() => {
router.post('logout', [controllers.Session, 'destroy'])
})
.use(middleware.auth())
入门套件为我们提供了用户注册、登录与登出的路由。注意 middleware.guest() 如何确保只有未登录用户才能访问注册/登录页,而 middleware.auth() 则保护登出路由。
在整个教程中,我们都会使用 auth 中间件来保护需要身份验证的路由。
控制器如何配合 Inertia 工作
让我们看看注册控制器,了解请求是如何在 Inertia 应用中流转的。
import User from '#models/user'
import { signupValidator } from '#validators/user'
import type { HttpContext } from '@adonisjs/core/http'
export default class NewAccountController {
async create({ inertia }: HttpContext) {
return inertia.render('auth/signup')
}
async store({ request, response, auth }: HttpContext) {
/**
* 验证提交的数据
*/
const payload = await request.validateUsing(signupValidator)
/**
* 在数据库中创建新用户
*/
const user = await User.create(payload)
/**
* 自动登录
*/
await auth.use('web').login(user)
/**
* 重定向到首页
*/
response.redirect().toRoute('home')
}
}
每个控制器方法都以 HTTP 上下文对象作为第一个参数。该上下文包含了当前请求的一切信息:请求数据、响应对象、认证状态、Inertia 渲染器,以及更多。我们只需解构出需要的属性(inertia 用于渲染 React 组件,request 用于表单数据,response 用于重定向,auth 用于身份验证)。
create 方法使用 inertia.render() 渲染注册表单。与传统的服务端渲染应用返回 HTML 不同,Inertia 发送一个包含组件名称及任意 props 的 JSON 响应。你的 React 前端接收到它并渲染对应的组件。
store 方法承担了主要工作:它验证数据、创建用户、登录并重定向到首页。这种将验证器、模型与认证汇集在一起的模式,将会贯穿整个教程。
你可能会注意到控制器引用了 User 模型和一个 signupValidator。入门套件已经包含了它们。我们将在
数据库与模型
章节了解模型如何工作,在
表单与验证
章节了解验证器。
关于 Inertia 与 React
如果你使用过 Next.js 或 Remix 这类元框架,这个入门套件可能看起来有些不同。我们的大部分代码位于 AdonisJS(后端)中,而 React 仅用于渲染视图。 没有前端路由,没有在服务端和客户端同时运行的同构代码,也没有复杂的状态管理库。
这是有意为之。Inertia 的理念很简单:让你的后端与前端保持分离,但让它们无缝协作。
- 你的后端(AdonisJS)负责路由、身份验证、数据库查询、验证与业务逻辑
- 你的前端(React)负责渲染与用户交互
- Inertia 充当粘合剂,将来自控制器的 JSON 响应发送给你的 React 组件
如果你是第一次听说 Inertia,可以访问 inertiajs.com 进一步了解其理念。或者直接跟着本教程走下去,亲眼看看它与元框架那套复杂组合相比有多简单。
下面是请求在 AdonisJS + Inertia 应用中的流转方式:
浏览器请求
↓
AdonisJS 路由器
↓
控制器(验证、查询数据库等)
↓
inertia.render('组件名', props)
↓
React 组件(通过 Vite 渲染)
↓
浏览器响应
注册表单如何工作
当控制器调用 inertia.render('auth/signup') 时,Inertia 会在 inertia/pages/auth/signup.tsx 查找一个 React 组件并渲染它。让我们看看那个组件。
import { Form } from '@adonisjs/inertia/react'
export default function Signup() {
return (
<div className="form-container">
<div>
<h1>Signup</h1>
<p>Enter your details below to create your account</p>
</div>
<div>
<Form route="new_account.store">
{({ errors }) => (
<>
<div>
<label htmlFor="fullName">Full name</label>
<input
type="text"
name="fullName"
id="fullName"
data-invalid={errors.fullName ? 'true' : undefined}
/>
{errors.fullName && <div>{errors.fullName}</div>}
</div>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
name="email"
id="email"
autoComplete="email"
data-invalid={errors.email ? 'true' : undefined}
/>
{errors.email && <div>{errors.email}</div>}
</div>
<div>
<label htmlFor="password">Password</label>
<input
type="password"
name="password"
id="password"
autoComplete="new-password"
data-invalid={errors.password ? 'true' : undefined}
/>
{errors.password && <div>{errors.password}</div>}
</div>
<div>
<label htmlFor="passwordConfirmation">Confirm password</label>
<input
type="password"
name="passwordConfirmation"
id="passwordConfirmation"
autoComplete="new-password"
data-invalid={errors.passwordConfirmation ? 'true' : undefined}
/>
{errors.passwordConfirmation && <div>{errors.passwordConfirmation}</div>}
</div>
<div>
<button type="submit" className="button">
Sign up
</button>
</div>
</>
)}
</Form>
</div>
</div>
)
}
页面组件位于 inertia/pages 目录中。来自 @adonisjs/inertia/react 的 Form 组件负责处理表单提交。它接受一个 route 属性(要提交到的命名路由),并通过 render prop 模式提供一个 errors 对象。当你提交表单时,Inertia 会将请求发送到你的后端,并自动处理响应,包括展示验证错误。
试着创建一个账户
在继续之前,先用 node ace serve --hmr 启动你的开发服务器,并尝试创建一个账户。先熟悉一下入门套件的工作方式,我们将在此基础上继续构建。