概述

尽管使用 NodeJS 搭建 HTTP 服务器早已是 Web 开发者们的家常便饭,但结合时下最热门的 TypeScript 却能带来更棒的开发体验。今天,我们就来一起尝试将它们结合起来,快速搭建一个健壮的 Koa 服务。

Koa 基础环境搭建

我们从 Koa 开始,它是一个轻量且富有表现力的 NodeJS Web 框架。

安装核心依赖

安装 Koa 及其常用的中间件 (Middleware):

1
npm install koa koa-bodyparser koa-json koa-logger koa-router

注意: koa-bodyparserkoa-jsonkoa-loggerkoa-router 都属于 Koa 的中间件。

编写基础 Koa 代码

首先,创建 src 目录用于存放源代码,并在其中创建 index.js 文件:

1
2
3
// 文件结构
src
└── index.js

基础 Koa 服务 (index.js):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Koa from 'koa';
import Router from 'koa-router';
import logger from 'koa-logger';
import json from 'koa-json';
import bodyParser from 'koa-bodyparser';

const app = new Koa();
const router = new Router();

// 路由:处理 GET / 请求
router.get('/', async (ctx, next) => {
// 设置响应体,ctx.body 是 ctx.response.body 的快捷方式
ctx.body = { msg: 'Hello World.' };
await next();
});

// 挂载中间件
app.use(json()); // 格式化 JSON 响应
app.use(logger()); // 启用开发模式日志

// 挂载路由
app.use(router.routes()).use(router.allowedMethods());

// 启动服务
app.listen(8000, () => {
console.log('Koa Ready.');
});

Koa 中间件的“洋葱模型”

理解 Koa 的核心在于理解中间件的执行机制,这通常被称为“洋葱模型”

每个请求都会从洋葱的最外层(第一个中间件)进入,然后依次向内层传递,最后从内层外层返回。这意味着请求会与每一个中间件交互两次

在一个中间件函数中:

  • await next() 之前的代码:被视为 **前置操作 (Pre-operation)**,在请求进入下一层之前执行。
  • await next() 之后的代码:被视为 **后置操作 (Post-operation)**,在所有后续中间件执行完毕后,请求返回时执行。

执行顺序示例:

假设你按顺序使用了 middleware_1middleware_2middleware_3

中间件代码片段执行顺序阶段
1console.log(1)1进入 (前置)
1await next()跳入 2
2console.log(2)2进入 (前置)
2await next()跳入 3
3console.log(3)3进入 (前置)
3await next()退出 3 (无后续)
3console.log(4)4退出 (后置)
2console.log(5)5退出 (后置)
1console.log(6)6退出 (后置)

最终输出:1, 2, 3, 4, 5, 6

引入 TypeScript

TypeScript 是 JavaScript 的超集,它通过静态类型检查为我们的项目提供了更强大的保障。

安装 TypeScript 及类型文件

我们需要安装 TypeScript 本身,同时为了消除 Koa 模块的类型报错,还需要安装对应的类型定义文件 (@types/*):

1
2
3
4
5
# 1. 安装 TypeScript
npm install -D typescript

# 2. 安装 Koa 及其相关模块的类型定义
npm install -D @types/node @types/koa @types/koa-router @types/koa-json @types/koa-logger @types/koa-bodyparser

配置 tsconfig.json

使用以下命令生成基础配置文件:

1
npx tsc --init

为了项目的简洁性和兼容性,我们只保留和修改以下关键配置项:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"compilerOptions": {
"target": "es6", /* 指定编译后的 JS 版本为 ES6 */
"module": "commonjs", /* 指定模块代码的生成方式 */
"moduleResolution": "node", /* 指定模块解析策略 */
"outDir": "./build", /* 指定编译后 JS 文件的输出目录 */
"esModuleInterop": true, /* 允许 CommonJS 模块以 ES 模块方式导入 */
"forceConsistentCasingInFileNames": true, /* 确保导入时的文件名大小写一致 */
"strict": true, /* 启用所有严格类型检查选项 */
"skipLibCheck": true /* 跳过 .d.ts 文件的类型检查,加快编译速度 */
},
"include": ["src/**/*"] /* 指定 TS 编译器需要检查和编译的文件 */
}

重点: outDir 设置为 ./build,编译后的 JS 文件会放在该目录下;include 设置为 src/**/*,指定编译器只处理 src 目录下的文件。

使用 Type 增强代码

index.js 重命名为 index.ts,并修改代码,使其接受 POST 请求并从请求体中获取数据,以演示 TypeScript 的类型检查能力。

新的 Koa + TypeScript 代码 (index.ts):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import Koa from 'koa';
import Router from 'koa-router';
import logger from 'koa-logger';
import json from 'koa-json';
import bodyParser from 'koa-bodyparser'; // 用于解析请求体

const app = new Koa();
const router = new Router();

// 1. 定义接口(Interface)来规范请求体的结构
interface RequestBody {
name: string;
}

// 路由:处理 POST / 请求
router.post('/', async (ctx, next) => {
// 2. 使用类型断言明确告诉 TS 请求体的结构
// 此时,requestData 变量的结构就被 RequestBody 严格限定了
const requestData: RequestBody = ctx.request.body;

ctx.body = {
msg: `Hello ${requestData.name}.` // 正常访问 name 属性
};
await next();
});

// 挂载中间件 (确保在路由之前使用 bodyParser)
app.use(json());
app.use(logger());
app.use(bodyParser());

// 挂载路由
app.use(router.routes()).use(router.allowedMethods());

// 启动服务
app.listen(8000, () => {
console.log('Koa Ready.');
});

类型检查的优势:

通过定义 interface RequestBody 并使用类型断言,如果我们错误地尝试访问接口中不存在的属性(例如,将 requestData.name 错写成 requestData.username):

1
2
3
4
5
6
7
8
9
10
11
// 错误示例
router.post('/', async (ctx, next) => {
const requestData: RequestBody = ctx.request.body;

ctx.body = {
msg: `Hello ${requestData.username}.` // 错误! TypeScript会立即报错
};
// 错误提示:Property 'username' does not exist on type 'RequestBody'. ts(2339)

await next();
});

TypeScript 会在编译时(或在 IDE 中)立即给出错误警告,避免了这种低级错误导致的运行时崩溃,极大地提高了代码的健壮性和可维护性。

编译与运行

最后一步是使用 TypeScript 编译器将 TS 代码转译为纯粹的 JavaScript,以便在 Node 环境中运行。

编译 TypeScript

执行编译命令:

1
2
npx tsc 
# 或 tsc (如果已全局安装)

如果没有错误,编译器会根据 tsconfig.json 的配置,在项目根目录下生成 /build 文件夹,其中包含编译后的 index.js 文件。

运行编译后的 JS 文件

进入 /build 目录并启动服务:

1
2
3
cd build
node index.js
# Koa Ready.

使用 Postman 或其他工具向 localhost:8000/ 发送一个 POST 请求,并在请求体中发送 JSON 数据:

1
2
3
{
"name": "Kevin"
}

你将在控制台中看到请求和响应日志:

1
2
3
<-- POST /
Hello Kevin.
--> POST / 200 3ms

至此,一个高效、类型安全的 Koa + TypeScript 后端服务就搭建完成了!