一切的入口:控制器
访问一个 web 服务的入口就是控制器,通俗点说就是 url 链接,我们没有单独的 路由 配置文件,
所有路由的设置都是与控制器代码绑定在一起的,这让代码维护看起来更清晰自然。
现在我们来创建第一个 "Hello World!"
Hello World!
在 src 文件夹下创建一个新的文件夹 controller。 (必须是这个名称,这与代码扫描加载有关,当然如果你想改也不是不行,只是我们推荐先这么约定)
在 controller 目录下新建一个文件 index.ts
- TypeScript
- JavaScript
import { Get } from "zenweb";
// 导出控制器类,类名称没有要求,你甚至可以导出一个匿名类
// 但是我们不推荐你这么做,毕竟代码是要给人看的,起一个贴合实际的名字不更优雅吗?
// 有了名字也方便后续的日志查看
// 当然你也可以在一个文件中导出多个类,都是可以生效的,但是更推荐一个文件只导出一个控制器类
export class IndexController {
@Get() // 将类方法注册到路由中,在不指定参数的情况下默认为 GET /{方法名称} (index 名称比较特殊默认为 /)
index() {
return "Hello World!"; // 输出结果
}
}
const { Get } = require("zenweb");
class IndexController {
index() {
return "Hello World!";
}
}
// 依赖注入需要手动处理声明,显然没有 TypeScript 优雅
Get()(IndexController.prototype, 'index');
// 别忘了导出类
exports = {
IndexController,
};
在你保存文件后应该会发现控制台中信息重新滚动了一次,这是 ts-watch 监测到代码改变而重启了应用,以让新的代码生效。
这时候我们再次打开 http://127.0.0.1:7001 查看是否有:
{"data":"Hello World!"}
这里我们看到了一段 json 输出,这是 zenweb 的默认输出格式,对于现代 web 开发来说,前后分离已经是一种标配了,
如果你需要传统的服务端渲染可以使用 @zenweb/template 模块,如果你需要直接输出内容可以使用 ctx.body= 这里不做过多介绍。
完全可以更改,通过修改 src/index.ts 中的 create({ result: { json: { success } }) 即可实现,详情请查看 result 模块
自定义路由 url 和请求方法
import { Controller, Get, Post, Mapping, Context } from "zenweb";
@Controller({
prefix: "/admin", // 统一增加控制器下 url 前缀
})
export class AdminController {
// GET /admin
@Get()
index() {
return "index";
}
// 可以指定多个请求方法
// POST,PUT /admin/login-by-username
@Mapping({ path: "/login-by-username", method: ["POST", "PUT"] })
loginByUsername() {
return "login-by-username";
}
// 可以指定多个 url
// /admin/sysname
// /admin/system-name
@Get(["/sysname", "/system-name"])
systemName() {
return "admin system";
}
// 路由参数,冒号后面定义参数名称
// 匹配 /admin/user/123 或者 /admin/user/abc 等等
// 如果只想匹配 整数 类型可以指定自定义正则表达式 "/user/:id(\\d+)"
@Get("/user/:id")
userDetail(ctx: Context) { // 控制器方法中定义的参数会被自动注入
return "user id: " + ctx.params.id;
}
// 也可以在一个方法上指定多个不同的 mapping,每个 mapping 使用自己的中间件和方法
@Get()
@Post(checkPost)
many() {
}
}
使用中间件
中间件通常用在控制器方法 之前 或 之后 处理业务逻辑
常见的的用途:用户身份认证,数据输出加工,数据输入处理等等
一个中间件处理函数可以接受两个输入参数,分别是:Context 和 Next
Context 是当前请求上下文,Next 是下一步要处理中间件或控制器方法
下面定义一个典型的中间件代码逻辑:
import { Context, Controller, Get, Middleware, fail } from "zenweb";
// `之前` 类型演示
// 中间件推荐定义成一个函数返回一个中间件方法
// 统一为这样的风格,方便后续添加参数
function userCheckMiddleware(/* 一些参数 */): Middleware {
// ctx 当前请求上下文
// next 下一步中间件或控制器方法
return function (ctx, next) {
// 从 请求 header 头中取得 token 信息
const token = ctx.get('token');
// 验证 token 是否正确
if (token !== 'demo') {
// token 不正确,抛出异常,终止代码继续执行
fail('need-login');
}
// 设置一些信息到状态上,传递给后续代码使用
ctx.state.username = 'Bob';
// 当业务逻辑没有问题,需要返回下一个
// 此种类型的中间件就属于 `之前` 类型
return next();
}
}
// `之后` 类型演示
function userActionLog(): Middleware {
return async function (ctx, next) {
// 等待之后的所有中间件和控制器执行
await next();
// 打印输出
ctx.log.info('user: %s body: %s', ctx.state.username, ctx.body)
}
}
@Controller({
// 可以将中间件定义在整个控制器类上,控制器下每个方法都会经过中间件处理
middleware: userActionLog(),
})
export class UserController {
// 在控制器方法上使用中间件
@Get(userCheckMiddleware())
my(ctx: Context) {
return "my name is: " + ctx.state.username;
}
}
访问测试:
curl http://127.0.0.1:7001/my
# 输出: {"message":"请登陆!"}
# 添加需要的头信息再次访问
curl -H token:demo http://127.0.0.1:7001/my
# 输出: {"data":"my name is: Bob"}
这些装饰器的第一个参数可以是路径或中间件:
import { Controller, Get, Post, Delete, Middleware, Context } from 'zenweb';
@Controller({ prefix: '/api/user' })
export class UserApiController {
// GET /api/user
@Get()
list() {
return [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
}
// POST /api/user
@Post()
create() {
return { success: true };
}
// DELETE /api/user/:id
@Delete('/:id')
remove(ctx: Context) {
return { removed: ctx.params.id };
}
// 第一个参数也可以直接传中间件
@Get(authMiddleware())
profile(ctx: Context) {
return { name: ctx.state.username };
}
// 或者同时指定路径和中间件
@Get('/detail', logMiddleware())
detail() {
return { detail: true };
}
}
路由自动发现
zenweb 会自动扫描 src/controller 目录下的文件并注册路由。你可以通过配置修改扫描行为:
import { create } from 'zenweb';
create({
controller: {
// 扫描目录,默认 ['./app/controller']
discoverPaths: ['./src/controller'],
// 自动将文件路径映射为路由前缀
// 例如 src/controller/user/profile.ts → /user/profile
autoControllerPrefix: true,
},
});
autoControllerPrefix 映射规则
当 autoControllerPrefix: true 时,文件路径会自动映射为控制器的路由前缀:
| 文件路径 | 路由前缀 |
|---|---|
src/controller/index.ts | / |
src/controller/user.ts | /user |
src/controller/user/profile.ts | /user/profile |
src/controller/admin/log.ts | /admin/log |
方法名默认路径规则
在不指定 path 的情况下,方法名会自动映射为路径:
| 方法名 | 默认路径 |
|---|---|
index | / |
| 其他名称 | /{方法名} |