跳到主要内容

一切的入口:控制器

访问一个 web 服务的入口就是控制器,通俗点说就是 url 链接,我们没有单独的 路由 配置文件, 所有路由的设置都是与控制器代码绑定在一起的,这让代码维护看起来更清晰自然。

现在我们来创建第一个 "Hello World!"

Hello World!

src 文件夹下创建一个新的文件夹 controller(必须是这个名称,这与代码扫描加载有关,当然如果你想改也不是不行,只是我们推荐先这么约定)

controller 目录下新建一个文件 index.ts

src/controller/index.ts
import { mapping } from "zenweb";

// 导出控制器类,类名称没有要求,你甚至可以导出一个匿名类
// 但是我们不推荐你这么做,毕竟代码是要给人看的,起一个贴合实际的名字不更优雅吗?
// 有了名字也方便后续的日志查看
// 当然你也可以在一个文件中导出多个类,都是可以生效的,但是更推荐一个文件只导出一个控制器类
export class IndexController {
@mapping() // 将类方法注册到路由中,在不指定参数的情况下默认为 GET /{方法名称} (index 名称比较特殊默认为 /)
index() {
return "Hello World!"; // 输出结果
}
}

在你保存文件后应该会发现控制台中信息重新滚动了一次,这是 ts-watch 监测到代码改变而重启了应用,以让新的代码生效。

这时候我们再次打开 http://127.0.0.1:7001 查看是否有:

{"data":"Hello World!"}
为什么默认输出 json 格式

这里我们看到了一段 json 输出,这是 zenweb 的默认输出格式,对于现代 web 开发来说,前后分离已经是一种标配了, 如果你需要传统的服务端渲染可以使用 @zenweb/template 模块,如果你需要直接输出内容可以使用 ctx.body= 这里不做过多介绍。

我可以更改 json 结果的外层包裹吗?

完全可以更改,通过修改 src/index.ts 中的 create({ result: { json: { success } }) 即可实现,详情请查看 result 模块

自定义路由 url 和请求方法

src/controller/admin.ts
import { controller, mapping, Context } from "zenweb";

@controller({
prefix: "/admin", // 统一增加控制器下 url 前缀
})
export class AdminController {
// GET /admin
@mapping()
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
@mapping({ path: ["/sysname", "/system-name"] })
systemName() {
return "admin system";
}

// 路由参数,冒号后面定义参数名称
// 匹配 /admin/user/123 或者 /admin/user/abc 等等
// 如果只想匹配 整数 类型可以指定自定义正则表达式 "/user/:id(\\d+)"
@mapping({ path: "/user/:id" })
userDetail(ctx: Context) { // 控制器方法中定义的参数会被自动注入
return "user id: " + ctx.params.id;
}

// 也可以在一个方法上指定多个不同的 mapping,每个 mapping 使用自己的中间件和方法
@mapping()
@mapping({ method: 'POST', middleware: checkPost })
many() {
}
}

使用中间件

中间件通常用在控制器方法 之前之后 处理业务逻辑

常见的的用途:用户身份认证,数据输出加工,数据输入处理等等

一个中间件处理函数可以接受两个输入参数,分别是:ContextNext

Context 是当前请求上下文,Next 是下一步要处理中间件或控制器方法

下面定义一个典型的中间件代码逻辑:

src/controller/user.ts
import { Context, controller, mapping, 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 {
// 在控制器方法上使用中间件
@mapping({ middleware: 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"}