用200行代码实现web框架

发布 : 2021-02-20

说明:

  1. 无特殊表达式
  2. 移除类型可以直接作为js使用

已实现多语言 单文件版

实现前提:

  1. ts基本语法
  2. 仅依赖内置http
  3. 利用 Map 实现路由查找

实现内容:

  1. 基础服务
  2. 路由查找
  3. 上下文处理
  4. 超时处理
  5. 中间件
  6. 错误处理

核心内容包含3个类:

  1. Application
  2. Router
  3. Context

1个额外类型定义

  1. type Handler=(ctx: Context) => void|Promise<void>

特殊语法介绍:

  1. 自定义类型 type a=()=>void
  2. 空值判断 var a = id ?? 0
  3. 可选参数 function(a?:any)
  4. 解构参数 function(...arr: string[])

正文

1. Router

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
type Handler=(ctx: Context) => Promise<void> | void;

class Router {
// 默认路由
uri: string = '';
// 子级路由
childrenRoute:Map<string, Router> = new Map<string, Router>();
// 中间件处理函数
middleware:Handler[] = [];

constructor(u: string) {
this.uri = u ?? '';
}
// 挂载自定义路由
hook(r: Router) {
this.childrenRoute.set(r.uri, r);
}
// 挂载中间件
use(...a:Handler[]) {
this.middleware.push(...a);
}
// get请求
get(uri: string, ...params: Handler[]) {
return this.any('GET', uri, ...params);
}
// ...
// 默认路由加载
any(method: string, uri: string, ...handler: Handler[]) {
if (uri.indexOf('/') == 0) uri = uri.substr(1);
const uris = uri.split('/');
let r: Router = this;
// 如果添加的为多级路由, 则拆分挂载
for(let u of uris) {
let nr = new Router(u);
r.childrenRoute.set(u, nr);
r = nr;
}

r.middleware.push(...handler);
return r;
}

// 路由分组
group(prefix: string) {
return this.any('ANY', prefix);
}
// 分组函数别名
prefix(prefix: string) {
return this.any('ANY', prefix);
}

// 实现路由查找及执行
find(ctx: Context, uris?: string[]) {
uris = uris ?? ctx.url.substr(1).split('/');
let r: Router|null = this;
ctx.middleware.push(...r?.middleware ?? []);
// 查询递归执行子路由
for(let i in uris) {
console.log(this.middleware);
// 查询下一级路由
r = r?.childrenRoute.get(uris[i]) ?? null;
ctx.middleware.push(...r?.middleware ?? []);
}
// 如果没有查到最后的路由, 手动添加404处理
if (!r) ctx.middleware.push(async (ctx) => {
ctx.statusCode = 404;
ctx.send('Not found');
});
// 开始依次执行中间件
ctx.next();
}
}

2. Application

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
// 核心app类继承路由, 即为全局根路由
class Application extends Router {
server: http.Server;
timeout: number = 0;
errorHandler: (err: Error, ctx: Context) => Promise<void>;
constructor() {
// 初始化根路由
super("");
// 默认错误处理函数
this.errorHandler = async (err, ctx) => {
ctx.statusCode = 500;
ctx.send(err.stack ?? 'server error');
}
// 创建服务
this.server = http.createServer((req, res) => {
// 初始化上下文
const ctx = new Context(req, res);
// 绑定处理函数
ctx.errorHandler = this.errorHandler;
// 设置超时
if (this.timeout > 0) ctx.timeout(this.timeout);
// 路由查找处理
this.find(ctx);
});
}
// 监听
listen(addr: string) {
this.server.listen(addr);
}
}

3. Context

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import * as http from 'http';

class Context {
// 原生请求
request: http.IncomingMessage;
// 原生响应
response: http.ServerResponse;
query: any;
body: any;
url: string;
method: string;
// 是否已返回请求
isEnd: boolean = false;
// 中间件执行计数
nextInx: number = -1;
// 设置响应码
statusCode:number = 200;
// 当前路由的全部执行函数
middleware:Handler[] = [];
// 错误处理
errorHandler: (err: Error, ctx: Context) => Promise<void>;
responseHeader: Map<String, string> = new Map<String, string>();
// 初始化上下文
constructor(req: http.IncomingMessage, res: http.ServerResponse) {
this.request = req;
this.response = res;
this.url = req.url ?? "";
this.method = req.method?.toLocaleUpperCase() ?? "GET";
this.errorHandler = async (err, ctx) => {};
}
// 超时处理
timeout(time: number){
if (time <= 0) return;
setTimeout(() => {
this.statusCode = 500;
this.send("server timeout");
}, time);
}

// 下一次执行原理
next(info?: any) {
// 如果next有内容, 则证明有显式错误, 及时处理
if (info) {
this.nextInx = this.middleware.length -1;
this.errorHandler(info, this);
return;
}
this.nextInx += 1;
// 如果计数结束/响应结束, 则终止中间件执行
if (this.nextInx >= this.middleware.length || this.isEnd) return;

// 防止中间件错误
try {
this.middleware[this.nextInx](this);
} catch (err) {
this.nextInx = this.middleware.length -1;
this.errorHandler(err, this);
}
}

// 响应封装
send(str: string) {
if (this.isEnd) return;
this.response.setHeader('Content-Type', 'text/plain; charset=utf-8');
for(let key in this.responseHeader.keys()) {
this.response.setHeader(key, this.responseHeader.get(key) ?? '');
}
this.response.statusCode = this.statusCode || 200;
this.response.write(str);
this.response.end();
this.isEnd = true;
}

json(str: Object) {
if (this.isEnd) return;
this.response.setHeader('Content-Type', 'application/json; charset=utf-8');
for(let key in this.responseHeader.keys()) {
this.response.setHeader(key, this.responseHeader.get(key) ?? '');
}
this.response.statusCode = this.statusCode || 200;
this.response.write(str);
this.response.end();
this.isEnd = true;
}

setHeader(key: string, value: string) {
this.responseHeader.set(key, value);
}
}

测试

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
39
40
41
42
43
44
45
46
47
48
49
50
function main() {
const root = new Application();
root.use(async (ctx)=>{
console.log("hello middleware");
ctx.next();
});

const api = root.group('/api');

api.get("/user", (ctx) => {
console.log("hello");
ctx.send("hello ctx");
});

const apiPrefix = api.prefix("/prefix");
// 中间件及执行顺序
apiPrefix.use(async (ctx) => {
console.log("hello next");
// ctx.json(`{"status": 200}`)
ctx.next();
console.log("hello next end");
});
// 测试执行顺序
apiPrefix.get("/hello", async (ctx) => {
console.log("hello prefix start");
ctx.send("hello prefix ctx");
console.log("hello prefix end");
});
// 测试超时
apiPrefix.get("/timeout", async (ctx) => {
console.log("hello timeout start");
});
// 测试错误捕获
apiPrefix.get("/error", async (ctx) => {
console.log("hello error start");
throw new Error("error");
});

// 测试hook
const r = new Router("hello");
root.hook(r);

root.errorHandler = async (err, ctx) => {
ctx.statusCode = 500;
ctx.send("server hello error");
}
root.listen('12345');
}

main();
本文作者 : 萧逸雨
原文链接 : http://qiubo.ink/2021/02/20/用200行代码实现web框架/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!