├── src ├── example │ ├── views │ │ └── index.html │ ├── hello.ts │ ├── hello2.ts │ ├── web.ts │ ├── test.ts │ └── session.ts ├── benchmark_test │ ├── inspect │ ├── http.ts │ ├── run │ ├── connect.ts │ └── router.ts ├── lib │ ├── module │ │ ├── simple.random.ts │ │ ├── on.writehead.ts │ │ ├── simple.template.ts │ │ ├── response.gzip.ts │ │ ├── body.parser.ts │ │ ├── proxy.request.ts │ │ ├── crc32.ts │ │ └── simple.redis.ts │ ├── index.ts │ ├── component │ │ ├── index.ts │ │ ├── static.ts │ │ ├── favicon.ts │ │ ├── cookie.ts │ │ ├── proxy.ts │ │ ├── session.memory.ts │ │ ├── json.parser.ts │ │ ├── cors.ts │ │ ├── session.redis.ts │ │ ├── session.ts │ │ └── body.ts │ ├── parse_url.ts │ ├── application.ts │ ├── final_handler.ts │ ├── define.ts │ ├── router.ts │ ├── template.ts │ ├── request.ts │ ├── context.ts │ ├── core.ts │ ├── utils.ts │ └── response.ts └── test │ ├── component │ ├── component.favicon.test.ts │ ├── component.cookie.test.ts │ ├── component.static.test.ts │ ├── component.proxy.test.ts │ ├── component.cors.test.ts │ ├── component.body.test.ts │ └── component.session.test.ts │ └── core │ ├── parseurl.test.ts │ ├── extends.test.ts │ ├── connect.compatible.test.ts │ ├── template.compatible.test.ts │ ├── router.test.ts │ ├── connect.test.ts │ └── context.request.response.test.ts ├── test_data ├── template │ ├── test1.pug │ ├── test1.simple │ ├── test1.nunjucks │ └── test1.ejs └── favicon.ico ├── .coveralls.yml ├── .prettierrc.js ├── tsconfig.json ├── tsconfig.typedoc.json ├── LICENSE ├── .github └── workflows │ ├── node.js.yml │ └── codeql-analysis.yml ├── .gitignore ├── package.json └── README.md /src/example/views/index.html: -------------------------------------------------------------------------------- 1 |
a = {{a}}
2 |b = {{b}}
-------------------------------------------------------------------------------- /test_data/template/test1.nunjucks: -------------------------------------------------------------------------------- 1 |a = {{a}}
2 |b = {{b}}
-------------------------------------------------------------------------------- /test_data/template/test1.ejs: -------------------------------------------------------------------------------- 1 | <%= type %>a = <%= a %>
2 |b = <%= b %>
-------------------------------------------------------------------------------- /test_data/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leizongmin/leizm-web/HEAD/test_data/favicon.ico -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // .prettierrc.js 2 | module.exports = { 3 | printWidth: 120, 4 | trailingComma: "all", 5 | parser: "typescript" 6 | }; 7 | -------------------------------------------------------------------------------- /src/benchmark_test/inspect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source="${BASH_SOURCE[0]}" 4 | dir=$(dirname $source) 5 | server="$1" 6 | export NODE_ENV=production 7 | node --inspect --require 'ts-node/register' $dir/$server.ts 8 | -------------------------------------------------------------------------------- /src/example/hello.ts: -------------------------------------------------------------------------------- 1 | import * as web from "../lib"; 2 | 3 | const app = new web.Application(); 4 | app.templateEngine.initEjs(); 5 | 6 | app.router.get("/", async function (ctx) { 7 | ctx.response.render("index", { msg: "hello, world" }); 8 | }); 9 | 10 | app.listen({ port: 3000 }); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "alwaysStrict": true, 9 | "declaration": true, 10 | "strict": true, 11 | "noUnusedLocals": true 12 | }, 13 | "include": [ 14 | "src" 15 | ] 16 | } -------------------------------------------------------------------------------- /src/benchmark_test/http.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 性能测试 3 | * @author Zongmin Lei" + 78 | body + 79 | "\n" + 80 | "\n" + 81 | "\n" 82 | ); 83 | } 84 | 85 | function writeHead(res: http.ServerResponse, statusCode: number, headers?: http.OutgoingHttpHeaders): void { 86 | if (res.headersSent) return; 87 | res.writeHead(statusCode, { 88 | "Content-Security-Policy": "default-src 'self'", 89 | "X-Content-Type-Options": "nosniff", 90 | "Content-Type": "text/html; charset=utf-8", 91 | ...headers, 92 | }); 93 | } 94 | 95 | function writeBody(req: http.IncomingMessage, res: http.ServerResponse, body: string): void { 96 | if (req.method === "HEAD") return; 97 | if (res.finished) return; 98 | res.end(body); 99 | } 100 | 101 | function getErrorStatusCode(err: any): number | undefined { 102 | if (typeof err.status === "number" && err.status >= 400 && err.status < 600) { 103 | return err.status; 104 | } 105 | if (typeof err.statusCode === "number" && err.statusCode >= 400 && err.statusCode < 600) { 106 | return err.statusCode; 107 | } 108 | return undefined; 109 | } 110 | 111 | function getErrorHeaders(err: any): http.OutgoingHttpHeaders { 112 | if (!err.headers || typeof err.headers !== "object") { 113 | return {}; 114 | } 115 | const headers = Object.create(null); 116 | const keys = Object.keys(err.headers); 117 | for (let i = 0; i < keys.length; i++) { 118 | const key = keys[i]; 119 | headers[key] = err.headers[key]; 120 | } 121 | return headers; 122 | } 123 | 124 | function getResourceName(req: http.IncomingMessage): string { 125 | return parseUrl((req as any).originalUrl || req.url).pathname || "/"; 126 | } 127 | -------------------------------------------------------------------------------- /src/lib/define.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 3 | * @author Zongmin Lei
extends EventEmitter { 28 | /** 原始ServerRequest对象 */ 29 | protected _request?: Q; 30 | /** 原始ServerResponse对象 */ 31 | protected _response?: S; 32 | /** 用于存储next函数的堆栈 */ 33 | protected readonly nextHandleStack: NextFunction[] = []; 34 | /** Request对象的构造函数 */ 35 | protected requestConstructor: RequestConstructor = Request; 36 | /** Response对象的构造函数 */ 37 | protected responseConstructor: ResponseConstructor = Response; 38 | 39 | /** 父 Application 实例 */ 40 | public [SYMBOL_APPLICATION]: Application | undefined; 41 | 42 | /** 原始 Session对象 */ 43 | public [SYMBOL_SESSION]: SessionInstance; 44 | /** Session对象 */ 45 | public get session(): SessionInstance { 46 | if (this[SYMBOL_SESSION]) return this[SYMBOL_SESSION]; 47 | throw new Error(`ctx.session: please use component.session() middleware firstly`); 48 | } 49 | 50 | /** 原始路由信息 */ 51 | public [SYMBOL_RAW_ROUTE_INFO]: RawRouteInfo | null; 52 | 53 | /** 其他可任意挂载在Context上的数据 */ 54 | public data: Record= {}; 55 | 56 | /** 57 | * 创建Request对象 58 | * 59 | * @param req 原始ServerRequest对象 60 | */ 61 | protected createRequest(req: IncomingMessage): Q { 62 | return new this.requestConstructor(req, this) as Q; 63 | } 64 | 65 | /** 66 | * 创建Response对象 67 | * 68 | * @param res 原始ServerResponse对象 69 | */ 70 | protected createResponse(res: ServerResponse): S { 71 | return new this.responseConstructor(res, this) as S; 72 | } 73 | 74 | /** 75 | * 初始化 76 | * 77 | * @param req 原始ServerRequest对象 78 | * @param res 原始ServerResponse对象 79 | */ 80 | public init(req: IncomingMessage, res: ServerResponse) { 81 | this._request = this.createRequest(req); 82 | this._request.inited(); 83 | this._response = this.createResponse(res); 84 | this._response.inited(); 85 | this.response.setHeader("X-Powered-By", "@leizm/web"); 86 | res.once("finish", () => this.emit("finish")); 87 | onWriteHead(res, () => this.emit("writeHead")); 88 | this.inited(); 89 | return this; 90 | } 91 | 92 | /** 93 | * 初始化完成,由 `Context.init()` 自动调用 94 | * 一般用于自定义扩展 Context 时,在此方法中加上自己的祝时候完成的代码 95 | */ 96 | public inited() {} 97 | 98 | /** 99 | * 获得路由信息 100 | */ 101 | public get route(): RawRouteInfo { 102 | if (this[SYMBOL_RAW_ROUTE_INFO]) { 103 | return this[SYMBOL_RAW_ROUTE_INFO]!; 104 | } 105 | return { method: this.request.method || "", path: this.request.path }; 106 | } 107 | 108 | /** 109 | * 获取Request对象 110 | */ 111 | public get request(): Q { 112 | return this._request as Q; 113 | } 114 | 115 | /** 116 | * 获取Response对象 117 | */ 118 | public get response(): S { 119 | return this._response as S; 120 | } 121 | 122 | /** 123 | * 转到下一个中间件 124 | * 125 | * @param err 出错信息 126 | */ 127 | public next(err?: ErrorReason) { 128 | const next = this.nextHandleStack[this.nextHandleStack.length - 1]; 129 | if (next) { 130 | next(err); 131 | } 132 | } 133 | 134 | /** 135 | * next函数堆栈入栈 136 | * 137 | * @param next 回调函数 138 | */ 139 | public [SYMBOL_PUSH_NEXT_HANDLE](next: NextFunction) { 140 | this.nextHandleStack.push(next); 141 | } 142 | 143 | /** 144 | * next函数出栈 145 | */ 146 | public [SYMBOL_POP_NEXT_HANDLE](): NextFunction | void { 147 | return this.nextHandleStack.pop(); 148 | } 149 | 150 | /** 151 | * 注册中间件执行出错时的事件监听 152 | * 153 | * @param callback 回调函数 154 | */ 155 | public onError(callback: (err: ErrorReason) => void) { 156 | this.on("error", callback); 157 | } 158 | 159 | /** 160 | * 注册响应结束时的事件监听 161 | * 162 | * @param callback 回调函数 163 | */ 164 | public onFinish(callback: () => void) { 165 | this.on("finish", callback); 166 | } 167 | 168 | /** 169 | * 注册准备输出响应头时的事件监听 170 | * 171 | * @param callback 回调函数 172 | */ 173 | public onWriteHead(callback: () => void) { 174 | this.on("writeHead", callback); 175 | } 176 | 177 | /** 178 | * 代理请求 179 | * 180 | * @param target 181 | */ 182 | public async proxy(target: string | ProxyTarget) { 183 | return proxyRequest(this.request.req, this.response.res, target); 184 | } 185 | 186 | /** 187 | * 代理请求 188 | * 189 | * @param url 目标地址 190 | * @param removeHeaderNames 需要删除的原始请求头列表 191 | */ 192 | public async proxyWithHeaders(url: string, removeHeaderNames: string[] = ["host"]) { 193 | const target = parseProxyTarget(url); 194 | const originalHeaders = { ...this.request.headers }; 195 | for (const n of removeHeaderNames) { 196 | delete originalHeaders[n]; 197 | } 198 | target.headers = originalHeaders; 199 | return proxyRequest(this.request.req, this.response.res, target); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/lib/component/session.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置中间件 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { Context } from "../context"; 7 | import { MiddlewareHandle, CookieOptions, SYMBOL_SESSION } from "../define"; 8 | import { SessionMemoryStore } from "./session.memory"; 9 | import { generateSessionId } from "../module/simple.random"; 10 | import crc32 from "../module/crc32"; 11 | 12 | /** 13 | * Session中间件 14 | * 注意:需要依赖cookieParser中间件,否则无法正确取得sessionId 15 | */ 16 | export function session(options: SessionOptions = {}): MiddlewareHandle { 17 | const opts: Required = { ...DEFAULT_SESSION_OPTIONS, ...options }; 18 | const isSigned = !!(opts.cookie && opts.cookie.signed); 19 | const cookieName = opts.name; 20 | opts.cookie.maxAge = opts.maxAge; 21 | 22 | return async function (ctx: Context) { 23 | const currentSid: string = (isSigned ? ctx.request.signedCookies : ctx.request.cookies)[cookieName]; 24 | const sid = currentSid || opts.genid(ctx); 25 | const sess = (ctx[SYMBOL_SESSION] = new SessionInstance(ctx, sid, opts)); 26 | ctx.request.session = sess.data; 27 | if (currentSid) { 28 | // 旧的session,需要载入其数据 29 | await sess.reload(); 30 | } 31 | ctx.onWriteHead(() => { 32 | ctx.response.cookie(sess.cookieName, sess.id, opts.cookie); 33 | sess.save(); 34 | }); 35 | ctx.next(); 36 | }; 37 | } 38 | 39 | /** 用于生成SessionId的函数 */ 40 | export type GenerateSessionIdFunction = (ctx: Context) => string; 41 | 42 | /** Session中间件初始化选项 */ 43 | export interface SessionOptions { 44 | /** 存储引擎实例 */ 45 | store?: SessionStore; 46 | /** Cookie名称 */ 47 | name?: string; 48 | /** Cookie选项 */ 49 | cookie?: CookieOptions; 50 | /** 生成SessionId的函数 */ 51 | genid?: GenerateSessionIdFunction; 52 | /** Session有效时间(单位:毫秒),此参数会覆盖cookie中的maxAge */ 53 | maxAge?: number; 54 | } 55 | 56 | export interface SessionStore { 57 | /** 58 | * 获取session 59 | * @param sid 60 | */ 61 | get(sid: string): Promise >; 62 | 63 | /** 64 | * 设置session 65 | * @param sid 66 | * @param data 67 | * @param maxAge 68 | */ 69 | set(sid: string, data: Record , maxAge: number): Promise ; 70 | 71 | /** 72 | * 销毁Session 73 | * @param sid 74 | */ 75 | destroy(sid: string): Promise ; 76 | 77 | /** 78 | * 保持session激活 79 | * @param sid 80 | * @param maxAge 81 | */ 82 | touch(sid: string, maxAge: number): Promise ; 83 | } 84 | 85 | export class SessionInstance { 86 | public readonly cookieName: string; 87 | public readonly store: Required ; 88 | public readonly maxAge: number; 89 | protected _data: Record = {}; 90 | protected _hash: string = ""; 91 | protected _isDestroy: boolean = false; 92 | 93 | constructor(public readonly ctx: Context, public readonly id: string, options: Required ) { 94 | this.store = options.store; 95 | this.cookieName = options.name; 96 | this.maxAge = options.maxAge; 97 | } 98 | 99 | public get data() { 100 | return this._data; 101 | } 102 | 103 | public set data(v) { 104 | this._data = this.ctx.request.session = v; 105 | } 106 | 107 | public regenerate(): Promise { 108 | return this.destroy().then(() => { 109 | this.data = {}; 110 | this._isDestroy = false; 111 | }); 112 | } 113 | 114 | public destroy(): Promise { 115 | return this.store.destroy(this.id).then(() => { 116 | this.data = {}; 117 | this._hash = ""; 118 | this._isDestroy = true; 119 | }); 120 | } 121 | 122 | public reload(): Promise { 123 | return this.store.get(this.id).then((data) => { 124 | this.data = data; 125 | this._hash = getDataHash(this.data); 126 | }); 127 | } 128 | 129 | public save(): Promise { 130 | if (this._isDestroy) return Promise.resolve(); 131 | const hash = getDataHash(this.data); 132 | if (hash === this._hash) { 133 | // 如果内容没有改变,则只执行touch 134 | return this.touch(); 135 | } 136 | return this.forceSave(); 137 | } 138 | 139 | public forceSave(): Promise { 140 | return this.store.set(this.id, this.data, this.maxAge); 141 | } 142 | 143 | public touch(): Promise { 144 | return this.store.touch(this.id, this.maxAge); 145 | } 146 | } 147 | 148 | export function getDataHash(data: any): string { 149 | return crc32(JSON.stringify(data)).toString(16); 150 | } 151 | 152 | /** 默认生成SessionId的函数 */ 153 | export const DEFAULT_SESSION_GENID: GenerateSessionIdFunction = (ctx: Context) => generateSessionId(); 154 | 155 | /** 默认SessionId存储于Cookie的名称 */ 156 | export const DEFAULT_SESSION_NAME = "web.sid"; 157 | 158 | /** 默认Cookie选项 */ 159 | export const DEFAULT_SESSION_COOKIE: CookieOptions = { path: "/", httpOnly: true }; 160 | 161 | /** 默认Session MaxAge */ 162 | export const DEFAULT_SESSION_MAX_AGE = 0; 163 | 164 | /** 默认Session中间件选项 */ 165 | export const DEFAULT_SESSION_OPTIONS: Required = { 166 | cookie: DEFAULT_SESSION_COOKIE, 167 | genid: DEFAULT_SESSION_GENID, 168 | name: DEFAULT_SESSION_NAME, 169 | store: new SessionMemoryStore(), 170 | maxAge: DEFAULT_SESSION_MAX_AGE, 171 | }; 172 | 173 | /** Session数据序列化函数 */ 174 | export type SessionDataSerializeFunction = (data: any) => string; 175 | 176 | /** Session数据反序列化函数 */ 177 | export type SessionDataDeserializeFunction = (data: string) => any; 178 | 179 | /** 默认Session数据序列化函数 */ 180 | export const DEFAULT_SESSION_SERIALIZE = (data: any) => JSON.stringify(data || {}); 181 | 182 | /** 默认Session数据反序列化函数 */ 183 | export const DEFAULT_SESSION_DESERIALIZE = (data: string) => JSON.parse(data) || {}; 184 | -------------------------------------------------------------------------------- /src/test/component/component.cors.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 单元测试 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { Application, component } from "../../lib"; 7 | import * as request from "supertest"; 8 | import { expect } from "chai"; 9 | import { IncomingMessage } from "http"; 10 | 11 | describe("component.cors", function () { 12 | const appInstances: Application[] = []; 13 | after(async function () { 14 | for (const app of appInstances) { 15 | await app.close(); 16 | } 17 | }); 18 | 19 | describe("any = true", function () { 20 | it("http", async function () { 21 | const app = new Application(); 22 | appInstances.push(app); 23 | app.use("/", component.cors({ any: true })); 24 | app.use("/", function (ctx) { 25 | ctx.response.end("OK"); 26 | }); 27 | await request(app.server) 28 | .get("/test") 29 | .set("Origin", "http://example.com") 30 | .expect("Access-Control-Allow-Origin", "http://example.com") 31 | .expect(200, "OK"); 32 | }); 33 | 34 | it("https", async function () { 35 | const app = new Application(); 36 | appInstances.push(app); 37 | app.use("/", component.cors({ any: true })); 38 | app.use("/", function (ctx) { 39 | ctx.response.end("OK"); 40 | }); 41 | await request(app.server) 42 | .get("/test") 43 | .set("Origin", "https://example.com") 44 | .expect("Access-Control-Allow-Origin", "https://example.com") 45 | .expect(200, "OK"); 46 | }); 47 | }); 48 | 49 | describe("domain = list", function () { 50 | it("in list", async function () { 51 | const app = new Application(); 52 | appInstances.push(app); 53 | app.use("/", component.cors({ domain: ["example.com"] })); 54 | app.use("/", function (ctx) { 55 | ctx.response.end("OK"); 56 | }); 57 | await request(app.server) 58 | .get("/test") 59 | .set("Origin", "http://example.com") 60 | .expect("Access-Control-Allow-Origin", "http://example.com") 61 | .expect(200); 62 | }); 63 | 64 | it("not in list", async function () { 65 | const app = new Application(); 66 | appInstances.push(app); 67 | app.use("/", component.cors({ domain: ["example.com"] })); 68 | app.use("/", function (ctx) { 69 | ctx.response.end("OK"); 70 | }); 71 | await request(app.server) 72 | .get("/test") 73 | .set("Origin", "http://ucdok.com") 74 | .expect((res: IncomingMessage) => { 75 | expect(res.headers).to.not.have.property("access-control-allow-origin"); 76 | }) 77 | .expect(200, "OK"); 78 | }); 79 | }); 80 | 81 | describe("其他选项", function () { 82 | it("credentials", async function () { 83 | const app = new Application(); 84 | appInstances.push(app); 85 | app.use("/", component.cors({ any: true, credentials: true })); 86 | app.use("/", function (ctx) { 87 | ctx.response.end("OK"); 88 | }); 89 | await request(app.server) 90 | .get("/test") 91 | .set("Origin", "http://example.com") 92 | .expect("Access-Control-Allow-Origin", "http://example.com") 93 | .expect("Access-Control-Allow-Credentials", "true") 94 | .expect(200, "OK"); 95 | }); 96 | 97 | it("maxAge", async function () { 98 | const app = new Application(); 99 | appInstances.push(app); 100 | app.use("/", component.cors({ any: true, maxAge: 100 })); 101 | app.use("/", function (ctx) { 102 | ctx.response.end("OK"); 103 | }); 104 | await request(app.server) 105 | .get("/test") 106 | .set("Origin", "http://example.com") 107 | .expect("Access-Control-Allow-Origin", "http://example.com") 108 | .expect("Access-Control-Max-Age", "100") 109 | .expect(200, "OK"); 110 | }); 111 | 112 | it("allowHeaders", async function () { 113 | const app = new Application(); 114 | appInstances.push(app); 115 | app.use("/", component.cors({ any: true, allowHeaders: ["A", "B"] })); 116 | app.use("/", function (ctx) { 117 | ctx.response.end("OK"); 118 | }); 119 | await request(app.server) 120 | .get("/test") 121 | .set("Origin", "http://example.com") 122 | .expect("Access-Control-Allow-Origin", "http://example.com") 123 | .expect("Access-Control-Allow-Headers", "A, B") 124 | .expect(200, "OK"); 125 | }); 126 | 127 | it("allowMethods", async function () { 128 | const app = new Application(); 129 | appInstances.push(app); 130 | app.use("/", component.cors({ any: true, allowMethods: ["A", "B"] })); 131 | app.use("/", function (ctx) { 132 | ctx.response.end("OK"); 133 | }); 134 | await request(app.server) 135 | .get("/test") 136 | .set("Origin", "http://example.com") 137 | .expect("Access-Control-Allow-Origin", "http://example.com") 138 | .expect("Access-Control-Allow-Methods", "A, B") 139 | .expect(200, "OK"); 140 | }); 141 | 142 | it("headers", async function () { 143 | const app = new Application(); 144 | appInstances.push(app); 145 | app.use("/", component.cors({ any: true, headers: { A: "12345", B: "67890" } })); 146 | app.use("/", function (ctx) { 147 | ctx.response.end("OK"); 148 | }); 149 | await request(app.server) 150 | .get("/test") 151 | .set("Origin", "http://example.com") 152 | .expect("Access-Control-Allow-Origin", "http://example.com") 153 | .expect("A", "12345") 154 | .expect("B", "67890") 155 | .expect(200, "OK"); 156 | }); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /src/lib/component/body.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置中间件 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { Context } from "../context"; 7 | import { MiddlewareHandle } from "../define"; 8 | import * as os from "os"; 9 | import * as fs from "fs"; 10 | import * as path from "path"; 11 | import * as bodyParser from "body-parser"; 12 | import { fromClassicalHandle } from "../utils"; 13 | import * as Busboy from "busboy"; 14 | import { randomString } from "../module/simple.random"; 15 | 16 | export interface JsonParserOptions extends bodyParser.OptionsJson {} 17 | 18 | export interface TextParserOptions extends bodyParser.OptionsText {} 19 | 20 | export interface UrlencodedParserOptions extends bodyParser.OptionsUrlencoded {} 21 | 22 | export interface RawParserOptions extends bodyParser.Options {} 23 | 24 | export interface MultipartParserOptions { 25 | /** 字段名称长度,默认 100 */ 26 | fieldNameSize?: number; 27 | /** 字段值长度,默认 100KB */ 28 | fieldSize?: number; 29 | /** 字段数量,默认 Infinity */ 30 | fields?: number; 31 | /** 文件大小,默认 Infinity */ 32 | fileSize?: number; 33 | /** 文件数量,默认 Infinity */ 34 | files?: number; 35 | /** 字段和文件总数,默认 Infinity */ 36 | parts?: number; 37 | /** 字段属性对数量,默认 2000 */ 38 | headerPairs?: number; 39 | /** 小文件尺寸,当文件尺寸小于此尺寸时则只保存到内存中,默认 0 */ 40 | smallFileSize?: number; 41 | } 42 | 43 | export const DEFAULT_MULTIPART_PARSER_OPTIONS: Required = { 44 | fieldNameSize: 100, 45 | fieldSize: 1024 * 100, 46 | fields: Infinity, 47 | fileSize: Infinity, 48 | files: Infinity, 49 | parts: Infinity, 50 | headerPairs: 2000, 51 | smallFileSize: 0, 52 | }; 53 | 54 | export interface FileField { 55 | /** 原始文件名 */ 56 | originalName: string; 57 | /** 编码 */ 58 | encoding: string; 59 | /** MIME 类型 */ 60 | mimeType: string; 61 | /** 文件大小 */ 62 | size: number; 63 | /** 临时文件路径 */ 64 | path?: string; 65 | /** 文件内容 */ 66 | buffer?: Buffer; 67 | } 68 | 69 | export function json(options: JsonParserOptions = {}): MiddlewareHandle { 70 | const fn = bodyParser.json(options); 71 | return fromClassicalHandle(fn); 72 | } 73 | 74 | export function text(options: TextParserOptions = {}): MiddlewareHandle { 75 | const fn = bodyParser.text(options); 76 | return fromClassicalHandle(fn); 77 | } 78 | 79 | export function urlencoded(options: UrlencodedParserOptions = {}): MiddlewareHandle { 80 | const fn = bodyParser.urlencoded(options); 81 | return fromClassicalHandle(fn); 82 | } 83 | 84 | export function raw(options: RawParserOptions = {}): MiddlewareHandle { 85 | const fn = bodyParser.raw(options); 86 | return fromClassicalHandle(fn); 87 | } 88 | 89 | export function multipart(options: MultipartParserOptions = {}): MiddlewareHandle { 90 | return async function (ctx: Context) { 91 | await parseMultipart(ctx, options); 92 | ctx.next(); 93 | }; 94 | } 95 | 96 | export function parseMultipart(ctx: Context, options: MultipartParserOptions = {}): Promise { 97 | return new Promise((resolve, reject) => { 98 | const method = ctx.request.method; 99 | if (method === "GET" || method === "HEAD") return resolve(); 100 | const contentType = ctx.request.headers["content-type"]; 101 | if (!contentType) return resolve(); 102 | if (contentType.indexOf("multipart/form-data") === -1) return resolve(); 103 | 104 | const opts: Required = { ...DEFAULT_MULTIPART_PARSER_OPTIONS, ...options }; 105 | 106 | const busboy = new Busboy({ headers: ctx.request.headers, limits: opts }); 107 | const fields: Record = {}; 108 | const files: Record = {}; 109 | const asyncTasks: Promise [] = []; 110 | 111 | busboy.on("file", (fieldName, file, originalName, encoding, mimeType) => { 112 | asyncTasks.push( 113 | new Promise((resolve, reject) => { 114 | let buf: Buffer[] = []; 115 | let size = 0; 116 | let fileStream: fs.WriteStream | null = null; 117 | let filePath = ""; 118 | file.on("data", (chunk: Buffer) => { 119 | size += chunk.length; 120 | if (fileStream) { 121 | fileStream.write(chunk); 122 | } else { 123 | buf.push(chunk); 124 | if (size > opts.smallFileSize) { 125 | filePath = path.resolve(os.tmpdir(), `multipart-tmp-${randomString(32)}`); 126 | fileStream = fs.createWriteStream(filePath); 127 | fileStream.on("error", (err) => reject(err)); 128 | fileStream.write(Buffer.concat(buf)); 129 | buf = []; 130 | } 131 | } 132 | }); 133 | file.on("end", () => { 134 | files[fieldName] = { 135 | originalName: originalName || "", 136 | encoding: encoding || "", 137 | mimeType: mimeType || "", 138 | size, 139 | }; 140 | if (fileStream) { 141 | fileStream!.end(() => resolve()); 142 | files[fieldName].path = filePath; 143 | } else { 144 | files[fieldName].buffer = Buffer.concat(buf); 145 | resolve(); 146 | } 147 | }); 148 | }), 149 | ); 150 | }); 151 | busboy.on("field", (fieldName, val, fieldNameTruncated, valTruncated) => { 152 | fields[fieldName] = val; 153 | }); 154 | busboy.on("finish", () => { 155 | Promise.all(asyncTasks) 156 | .then(() => { 157 | ctx.request.body = fields; 158 | ctx.request.files = files; 159 | resolve(); 160 | }) 161 | .catch(reject); 162 | }); 163 | busboy.on("error", (err: Error) => reject(err)); 164 | ctx.request.req.pipe(busboy); 165 | }); 166 | } 167 | -------------------------------------------------------------------------------- /src/test/core/template.compatible.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 单元测试 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import * as path from "path"; 7 | import { Application, Router } from "../../lib"; 8 | import * as simpleTemplate from "../../lib/module/simple.template"; 9 | import * as request from "supertest"; 10 | import * as ejs from "ejs"; 11 | import * as pug from "pug"; 12 | import * as nunjucks from "nunjucks"; 13 | 14 | const ROOT_DIR = path.resolve(__dirname, "../../.."); 15 | 16 | describe("模板引擎兼容性", function () { 17 | it("使用 simple 渲染", function (done) { 18 | const app = new Application(); 19 | const router = new Router(); 20 | app.templateEngine 21 | .register(".simple", simpleTemplate.renderFile) 22 | .setDefault(".simple") 23 | .setRoot(path.resolve(ROOT_DIR, "test_data/template")); 24 | app.use("/", router); 25 | router.use("/", function (ctx) { 26 | ctx.response.render("test1", { a: 123, b: 456 }); 27 | }); 28 | request(app.server).get("/").expect(200).expect(" a = 123
\nb = 456
", done); 29 | }); 30 | 31 | it("使用 simple 渲染 - initSimple()", function (done) { 32 | const app = new Application(); 33 | const router = new Router(); 34 | app.templateEngine.initSimple(".simple").setRoot(path.resolve(ROOT_DIR, "test_data/template")); 35 | app.use("/", router); 36 | router.use("/", function (ctx) { 37 | ctx.response.render("test1", { a: 123, b: 456 }); 38 | }); 39 | request(app.server).get("/").expect(200).expect("a = 123
\nb = 456
", done); 40 | }); 41 | 42 | it("使用 ejs 渲染", function (done) { 43 | const app = new Application(); 44 | const router = new Router(); 45 | app.templateEngine 46 | .register(".ejs", ejs.renderFile) 47 | .setDefault(".ejs") 48 | .setRoot(path.resolve(ROOT_DIR, "test_data/template")) 49 | .setLocals("type", "ejs"); 50 | app.use("/", router); 51 | router.use("/", function (ctx) { 52 | ctx.response.render("test1", { a: 123, b: 456 }); 53 | }); 54 | request(app.server).get("/").expect(200).expect("ejsa = 123
\nb = 456
", done); 55 | }); 56 | 57 | it("使用 ejs 渲染 - initEjs()", function (done) { 58 | const app = new Application(); 59 | const router = new Router(); 60 | app.templateEngine.initEjs(".ejs").setRoot(path.resolve(ROOT_DIR, "test_data/template")).setLocals("type", "ejs"); 61 | app.use("/", router); 62 | router.use("/", function (ctx) { 63 | ctx.response.render("test1", { a: 123, b: 456 }); 64 | }); 65 | request(app.server).get("/").expect(200).expect("ejsa = 123
\nb = 456
", done); 66 | }); 67 | 68 | it("使用 pug 渲染", function (done) { 69 | const app = new Application(); 70 | const router = new Router(); 71 | app.templateEngine 72 | .register(".pug", pug.renderFile) 73 | .setDefault(".pug") 74 | .setRoot(path.resolve(ROOT_DIR, "test_data/template")); 75 | app.use("/", router); 76 | router.use("/", function (ctx) { 77 | ctx.response.render("test1", { a: 123, b: 456 }); 78 | }); 79 | request(app.server).get("/").expect(200).expect("a = 123
b = 456
", done); 80 | }); 81 | 82 | it("使用 pug 渲染 - initPug()", function (done) { 83 | const app = new Application(); 84 | const router = new Router(); 85 | app.templateEngine.initPug(".pug").setRoot(path.resolve(ROOT_DIR, "test_data/template")); 86 | app.use("/", router); 87 | router.use("/", function (ctx) { 88 | ctx.response.render("test1", { a: 123, b: 456 }); 89 | }); 90 | request(app.server).get("/").expect(200).expect("a = 123
b = 456
", done); 91 | }); 92 | 93 | it("使用 nunjucks 渲染", function (done) { 94 | const app = new Application(); 95 | const router = new Router(); 96 | app.templateEngine 97 | .register(".nunjucks", nunjucks.render) 98 | .setDefault(".nunjucks") 99 | .setRoot(path.resolve(ROOT_DIR, "test_data/template")); 100 | app.use("/", router); 101 | router.use("/", function (ctx) { 102 | ctx.response.render("test1", { a: 123, b: 456 }); 103 | }); 104 | request(app.server).get("/").expect(200).expect("a = 123
\nb = 456
", done); 105 | }); 106 | 107 | it("使用 nunjucks 渲染 - initNunjucks", function (done) { 108 | const app = new Application(); 109 | const router = new Router(); 110 | app.templateEngine.initNunjucks(".nunjucks").setRoot(path.resolve(ROOT_DIR, "test_data/template")); 111 | app.use("/", router); 112 | router.use("/", function (ctx) { 113 | ctx.response.render("test1", { a: 123, b: 456 }); 114 | }); 115 | request(app.server).get("/").expect(200).expect("a = 123
\nb = 456
", done); 116 | }); 117 | 118 | it("多个模板引擎混合", async function () { 119 | const app = new Application(); 120 | const router = new Router(); 121 | app.templateEngine 122 | .initSimple(".simple") 123 | .initEjs(".ejs") 124 | .initPug(".pug") 125 | .initNunjucks(".nunjucks") 126 | .setRoot(path.resolve(ROOT_DIR, "test_data/template")) 127 | .setLocals("type", "mix"); 128 | app.use("/", router); 129 | router.use("/simple", function (ctx) { 130 | ctx.response.render("test1", { a: 123, b: 456 }); 131 | }); 132 | router.use("/ejs", function (ctx) { 133 | ctx.response.render("test1.ejs", { a: 123, b: 456 }); 134 | }); 135 | router.use("/pug", function (ctx) { 136 | ctx.response.render("test1.pug", { a: 123, b: 456 }); 137 | }); 138 | router.use("/nunjucks", function (ctx) { 139 | ctx.response.render("test1.nunjucks", { a: 123, b: 456 }); 140 | }); 141 | await request(app.server).get("/simple").expect(200).expect("a = 123
\nb = 456
"); 142 | await request(app.server).get("/ejs").expect(200).expect("mixa = 123
\nb = 456
"); 143 | await request(app.server).get("/pug").expect(200).expect("a = 123
b = 456
"); 144 | await request(app.server).get("/nunjucks").expect(200).expect("a = 123
\nb = 456
"); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /src/lib/core.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 3 | * @author Zongmin Lei4 | */ 5 | 6 | import { IncomingMessage, ServerResponse } from "http"; 7 | import { Context } from "./context"; 8 | import { 9 | RawRouteInfo, 10 | Middleware, 11 | MiddlewareHandle, 12 | ErrorReason, 13 | NextFunction, 14 | ContextConstructor, 15 | ParsedRoutePathResult, 16 | RegExpOptions, 17 | SYMBOL_PUSH_NEXT_HANDLE, 18 | SYMBOL_POP_NEXT_HANDLE, 19 | SYMBOL_RAW_ROUTE_INFO, 20 | } from "./define"; 21 | import { 22 | testRoutePath, 23 | parseRoutePath, 24 | getRouteParams, 25 | isMiddlewareErrorHandle, 26 | execMiddlewareHandle, 27 | getRouteMatchPath, 28 | } from "./utils"; 29 | import { Request } from "./request"; 30 | import { Response } from "./response"; 31 | 32 | export class Core > { 33 | /** 中间件堆栈 */ 34 | protected readonly stack: Middleware [] = []; 35 | /** Context对象构造函数 */ 36 | protected contextConstructor: ContextConstructor = Context; 37 | /** 解析路由选项 */ 38 | protected readonly routeOptions: RegExpOptions = { 39 | sensitive: true, 40 | strict: true, 41 | end: true, 42 | delimiter: "/", 43 | }; 44 | /** use()当前中间件时的路由规则 */ 45 | protected route?: ParsedRoutePathResult; 46 | /** use()当前中间件时的字符串路径 */ 47 | protected originalPath: string = ""; 48 | 49 | protected get path() { 50 | return this.originalPath; 51 | } 52 | 53 | protected set path(s: string) { 54 | if (s) { 55 | if (s.slice(-1) === "/") { 56 | s = s.slice(0, -1); 57 | } 58 | this.originalPath = s; 59 | } 60 | } 61 | 62 | /** 63 | * 创建Context对象 64 | * 65 | * @param req 原始ServerRequest对象 66 | * @param res 原始ServerResponse对象 67 | */ 68 | protected createContext(req: IncomingMessage, res: ServerResponse): C { 69 | return new this.contextConstructor().init(req, res) as C; 70 | } 71 | 72 | /** 73 | * 解析路由规则 74 | * 75 | * @param isPrefix 是否为前缀模式 76 | * @param route 路由规则 77 | */ 78 | protected parseRoutePath(isPrefix: boolean, route: string | RegExp): ParsedRoutePathResult | undefined { 79 | if (isPrefix && (!route || route === "/")) { 80 | return; 81 | } 82 | return parseRoutePath(route, { 83 | ...this.routeOptions, 84 | end: !isPrefix, 85 | }); 86 | } 87 | 88 | /** 89 | * 生成中间件 90 | */ 91 | public toMiddleware() { 92 | const self = this; 93 | return function (ctx: C) { 94 | let removedPath = ""; 95 | if (self.route) { 96 | removedPath = getRouteMatchPath(ctx.request.path, self.route); 97 | if (removedPath) { 98 | ctx.request.url = ctx.request.url.slice(removedPath.length); 99 | ctx.request.path = ctx.request.path.slice(removedPath.length); 100 | } 101 | } 102 | self.handleRequestByContext(ctx, function (err) { 103 | if (removedPath) { 104 | ctx.request.url = removedPath + ctx.request.url; 105 | ctx.request.path = removedPath + ctx.request.path; 106 | } 107 | ctx.next(err); 108 | }); 109 | }; 110 | } 111 | 112 | /** 113 | * 注册中间件 114 | * 115 | * @param route 路由规则 116 | * @param handles 中间件对象或处理函数 117 | */ 118 | public use(route: string | RegExp, ...handles: Array | Core >): this { 119 | const parsedRoute = this.parseRoutePath(true, route); 120 | this.add( 121 | parsedRoute, 122 | ...handles.map((item) => { 123 | if (item instanceof Core) { 124 | item.path = route.toString(); 125 | if (this.path) { 126 | item.path = this.path + item.path; 127 | } 128 | item.route = parsedRoute; 129 | return item.toMiddleware(); 130 | } 131 | item.route = parsedRoute; 132 | return item; 133 | }), 134 | ); 135 | return this; 136 | } 137 | 138 | /** 139 | * 注册中间件 140 | * 141 | * @param route 路由 142 | * @param handles 中间件对象或处理函数 143 | */ 144 | protected add(route: ParsedRoutePathResult | undefined, ...handles: MiddlewareHandle []) { 145 | for (const handle of handles) { 146 | const item: Middleware = { 147 | route, 148 | handle, 149 | handleError: isMiddlewareErrorHandle(handle), 150 | atEnd: false, 151 | }; 152 | const i = this.stack.findIndex((v) => v.atEnd); 153 | if (i === -1) { 154 | this.stack.push(item); 155 | } else { 156 | this.stack.splice(i, -1, item); 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * 添加中间件到末尾 163 | * 164 | * @param raw 原始路由信息 165 | * @param route 路由 166 | * @param handles 中间件对象或处理函数 167 | */ 168 | protected addToEnd(raw: RawRouteInfo, route: ParsedRoutePathResult | undefined, ...handles: MiddlewareHandle []) { 169 | for (const handle of handles) { 170 | const item: Middleware = { 171 | route, 172 | handle, 173 | handleError: isMiddlewareErrorHandle(handle), 174 | atEnd: true, 175 | raw, 176 | }; 177 | this.stack.push(item); 178 | } 179 | } 180 | 181 | /** 182 | * 通过原始ServerRequest和ServerResponse对象处理请求 183 | * @param req 原始ServerRequest对象 184 | * @param res 原始ServerResponse对象 185 | * @param done 未处理请求回调函数 186 | */ 187 | protected handleRequestByRequestResponse( 188 | req: IncomingMessage, 189 | res: ServerResponse, 190 | done: (err?: ErrorReason) => void, 191 | ) { 192 | this.handleRequestByContext(this.createContext(req, res), done); 193 | } 194 | 195 | /** 196 | * 通过Context对象处理请求 197 | * 198 | * @param ctx Context对象 199 | * @param done 未处理请求回调函数 200 | */ 201 | protected handleRequestByContext(ctx: C, done: (err?: ErrorReason) => void) { 202 | let index = 0; 203 | type GetMiddlewareHandle = () => void | Middleware ; 204 | 205 | const getNextHandle: GetMiddlewareHandle = () => { 206 | const handle = this.stack[index++]; 207 | if (!handle) return; 208 | if (handle.handleError) return getNextHandle(); 209 | return handle; 210 | }; 211 | 212 | const getNextErrorHandle: GetMiddlewareHandle = () => { 213 | const handle = this.stack[index++]; 214 | if (!handle) return; 215 | if (!handle.handleError) return getNextErrorHandle(); 216 | return handle; 217 | }; 218 | 219 | const next: NextFunction = (err) => { 220 | const handle = err ? getNextErrorHandle() : getNextHandle(); 221 | err = err || null; 222 | if (err && ctx.listenerCount("error") > 0) { 223 | ctx.emit("error", err); 224 | } 225 | if (!handle) { 226 | ctx[SYMBOL_POP_NEXT_HANDLE](); 227 | return done(err || null); 228 | } 229 | if (!testRoutePath(ctx.request.path, handle.route)) { 230 | return next(err); 231 | } 232 | if (handle.raw) { 233 | // 如果有匹配到路由则更新,没有匹配到则保留上一个 234 | let path = handle.raw.path; 235 | if (this.path) { 236 | path = this.path + path; 237 | } 238 | ctx[SYMBOL_RAW_ROUTE_INFO] = { method: handle.raw.method, path }; 239 | } 240 | ctx.request.params = getRouteParams(ctx.request.path, handle.route); 241 | execMiddlewareHandle(handle.handle, ctx, err, next); 242 | }; 243 | 244 | ctx[SYMBOL_PUSH_NEXT_HANDLE](next); 245 | ctx.next(); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { pathToRegexp as pathToRegExp } from "path-to-regexp"; 7 | import { 8 | MiddlewareHandle, 9 | ClassicalMiddlewareHandle, 10 | ClassicalMiddlewareErrorHandle, 11 | ErrorReason, 12 | RegExpOptions, 13 | RegExpKey, 14 | ParsedRoutePathResult, 15 | ContextConstructor, 16 | SYMBOL_PUSH_NEXT_HANDLE, 17 | } from "./define"; 18 | import { Context } from "./context"; 19 | import { IncomingMessage, ServerResponse } from "http"; 20 | import finalHandler from "./final_handler"; 21 | 22 | /** 23 | * 判断是否为Promise对象 24 | * 25 | * @param p 要判断的对象 26 | */ 27 | export function isPromise(p: Promise ): boolean { 28 | return p && typeof p.then === "function" && typeof p.catch === "function"; 29 | } 30 | 31 | /** 32 | * 解析路由字符串 33 | * 34 | * @param route 路由字符串 35 | * @param options 选项 36 | */ 37 | export function parseRoutePath(route: string | RegExp, options: RegExpOptions): ParsedRoutePathResult { 38 | if (route instanceof RegExp) { 39 | return { regexp: route, keys: [] }; 40 | } 41 | const keys: RegExpKey[] = []; 42 | const regexp = pathToRegExp(route, keys, options); 43 | return { regexp, keys }; 44 | } 45 | 46 | /** 47 | * 判断路由规则是否匹配 48 | * 49 | * @param pathname 当前路径 50 | * @param route 当前路由规则 51 | */ 52 | export function testRoutePath(pathname: string, route: ParsedRoutePathResult | undefined): boolean { 53 | if (!route) { 54 | return true; 55 | } 56 | route.regexp.lastIndex = 0; 57 | return route.regexp.test(pathname); 58 | } 59 | 60 | /** 61 | * 获取当前匹配路由规则对应的URL参数 62 | * 63 | * @param pathname 当前路径 64 | * @param route 当前路由规则 65 | */ 66 | export function getRouteParams(pathname: string, route: ParsedRoutePathResult | undefined): Record { 67 | const params: Record = {}; 68 | if (route) { 69 | route.regexp.lastIndex = 0; 70 | const values = route.regexp.exec(pathname); 71 | if (values && route.keys) { 72 | route.keys.forEach((k, i) => { 73 | params[k.name] = values[i + 1]; 74 | }); 75 | } 76 | } 77 | return params; 78 | } 79 | 80 | /** 81 | * 获取当前匹配路由规则对应的URL前缀 82 | * 83 | * @param pathname 当前路径 84 | * @param route 当前路由规则 85 | */ 86 | export function getRouteMatchPath(pathname: string, route: ParsedRoutePathResult | null): string { 87 | if (!route) return ""; 88 | route.regexp.lastIndex = 0; 89 | const values = route.regexp.exec(pathname); 90 | return (values && values[0]) || ""; 91 | } 92 | 93 | /** 94 | * 转换经典的connect中间件 95 | * 96 | * @param fn 处理函数 97 | */ 98 | export function fromClassicalHandle (fn: ClassicalMiddlewareHandle): MiddlewareHandle { 99 | const handle: MiddlewareHandle = function (ctx: C) { 100 | let removedPath = ""; 101 | if (handle.route) { 102 | removedPath = getRouteMatchPath(ctx.request.path, handle.route); 103 | if (removedPath) { 104 | ctx.request.url = ctx.request.url.slice(removedPath.length); 105 | ctx.request.path = ctx.request.path.slice(removedPath.length); 106 | } 107 | } 108 | fn(ctx.request.req, ctx.response.res, (err?: ErrorReason) => { 109 | if (removedPath) { 110 | ctx.request.url = removedPath + ctx.request.url; 111 | ctx.request.path = removedPath + ctx.request.path; 112 | } 113 | ctx.next(err); 114 | }); 115 | }; 116 | handle.classical = true; 117 | return handle; 118 | } 119 | 120 | /** 121 | * 转换为经典的connect中间件 122 | * 123 | * @param fn 处理函数 124 | */ 125 | export function toClassicalHandle( 126 | fn: MiddlewareHandle , 127 | contextConstructor: ContextConstructor = Context, 128 | ): ClassicalMiddlewareHandle { 129 | return function (req: IncomingMessage, res: ServerResponse, next: (err: ErrorReason) => void) { 130 | const ctx = new contextConstructor().init(req, res); 131 | if (typeof next !== "function") next = finalHandler(req, res); 132 | ctx[SYMBOL_PUSH_NEXT_HANDLE](next); 133 | const ret = fn(ctx) as any; 134 | if (isPromise(ret)) { 135 | (ret as Promise ).catch(next); 136 | } 137 | }; 138 | } 139 | 140 | /** 141 | * 转换经典的connect错误处理中间件 142 | * 143 | * @param fn 处理函数 144 | */ 145 | export function fromClassicalErrorHandle (fn: ClassicalMiddlewareErrorHandle): MiddlewareHandle { 146 | const handle: MiddlewareHandle = function (ctx: Context, err?: ErrorReason) { 147 | let removedPath = ""; 148 | if (handle.route) { 149 | removedPath = getRouteMatchPath(ctx.request.path, handle.route); 150 | if (removedPath) { 151 | ctx.request.url = ctx.request.url.slice(removedPath.length); 152 | ctx.request.path = ctx.request.path.slice(removedPath.length); 153 | } 154 | } 155 | fn(err, ctx.request.req, ctx.response.res, (err?: ErrorReason) => { 156 | if (removedPath) { 157 | ctx.request.url = removedPath + ctx.request.url; 158 | ctx.request.path = removedPath + ctx.request.path; 159 | } 160 | ctx.next(err); 161 | }); 162 | }; 163 | handle.classical = true; 164 | return handle; 165 | } 166 | 167 | /** 168 | * 判断是否为错误处理中间件 169 | * 170 | * @param handle 处理函数 171 | */ 172 | export function isMiddlewareErrorHandle (handle: MiddlewareHandle ): boolean { 173 | return handle.length > 1; 174 | } 175 | 176 | /** 177 | * 给当前中间件包装请求方法限制 178 | * 179 | * @param method 请求方法 180 | * @param handle 处理函数 181 | */ 182 | export function wrapMiddlewareHandleWithMethod ( 183 | method: string, 184 | handle: MiddlewareHandle , 185 | ): MiddlewareHandle { 186 | function handleRequest(ctx: C, err?: ErrorReason) { 187 | if (ctx.request.method !== method) return ctx.next(err); 188 | execMiddlewareHandle(handle, ctx, err, (err2) => ctx.next(err2)); 189 | } 190 | if (isMiddlewareErrorHandle(handle)) { 191 | return function (ctx: C, err?: ErrorReason) { 192 | handleRequest(ctx, err); 193 | }; 194 | } 195 | return function (ctx: C) { 196 | handleRequest(ctx); 197 | }; 198 | } 199 | 200 | /** 201 | * 执行中间件 202 | * 203 | * @param handle 处理函数 204 | * @param ctx 当前Context对象 205 | * @param err 出错信息 206 | * @param callback 回调函数 207 | */ 208 | export function execMiddlewareHandle ( 209 | handle: MiddlewareHandle , 210 | ctx: C, 211 | err: ErrorReason, 212 | onError: (err: ErrorReason) => void, 213 | ) { 214 | process.nextTick(function () { 215 | let p: Promise | void; 216 | try { 217 | p = handle(ctx, err); 218 | } catch (err) { 219 | return onError(err); 220 | } 221 | if (p && isPromise(p)) { 222 | p.catch(onError); 223 | } 224 | }); 225 | } 226 | 227 | const notifiedDeprecatedMap: Record = {}; 228 | 229 | /** 230 | * 提示接口更改 231 | * @param old 旧方法 232 | * @param current 新方法 233 | * @param since 开始完全弃用的版本 234 | */ 235 | export function notifyDeprecated(old: string, current: string, since: string): void { 236 | const msg = `[deprecated] @leizm/web模块:${old} 已更改为 ${current},旧的使用方法将会在 v${since} 版本之后弃用,请及时更新您的代码。`; 237 | if (notifiedDeprecatedMap[msg]) { 238 | notifiedDeprecatedMap[msg]++; 239 | } else { 240 | notifiedDeprecatedMap[msg] = 1; 241 | console.error(colorRed(msg)); 242 | } 243 | } 244 | 245 | /** 246 | * 返回红色ansi文本 247 | * @param str 248 | */ 249 | export function colorRed(str: string): string { 250 | return `\u001b[31m${str}\u001b[39m`; 251 | } 252 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM version][npm-image]][npm-url] 2 |  3 | [](https://deepscan.io/dashboard#view=project&pid=2695&bid=18968) 4 | [![Test coverage][coveralls-image]][coveralls-url] 5 | [![David deps][david-image]][david-url] 6 | [![node version][node-image]][node-url] 7 | [![npm download][download-image]][download-url] 8 | [![npm license][license-image]][download-url] 9 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fleizongmin%2Fleizm-web?ref=badge_shield) 10 | 11 | [npm-image]: https://img.shields.io/npm/v/@leizm/web.svg?style=flat-square 12 | [npm-url]: https://npmjs.org/package/@leizm/web 13 | [coveralls-image]: https://img.shields.io/coveralls/leizongmin/leizm-web.svg?style=flat-square 14 | [coveralls-url]: https://coveralls.io/r/leizongmin/leizm-web?branch=master 15 | [david-image]: https://img.shields.io/david/leizongmin/leizm-web.svg?style=flat-square 16 | [david-url]: https://david-dm.org/leizongmin/leizm-web 17 | [node-image]: https://img.shields.io/badge/node.js-%3E=_10-green.svg?style=flat-square 18 | [node-url]: http://nodejs.org/download/ 19 | [download-image]: https://img.shields.io/npm/dm/@leizm/web.svg?style=flat-square 20 | [download-url]: https://npmjs.org/package/@leizm/web 21 | [license-image]: https://img.shields.io/npm/l/@leizm/web.svg 22 | 23 | # @leizm/web 24 | 25 | 现代的 Web 中间件基础框架,完美支持 TypeScript,构建可维护的大型 Web 项目。 26 | 27 | 本框架参考了 connect、express 和 koa 等主流框架,具有以下特点: 28 | 29 | * 兼容 connect 中间件,可以通过内置的函数转换 connect 中间件,使用 NPM 上大量的模块资源 30 | * 可将本框架的实例转换为 connect 中间件,与其他项目模块紧密合作 31 | * 支持直接操作原生 req 和 res 对象 32 | * 简单没有歧义的接口参数,内置 TypeScript 支持,强大的代码自动提示支持 33 | * 内置路由功能及众多常用的中间件,无需借助第三方模块 34 | * 性能优于主流框架 Koa 和 Express 35 | * 代码库轻盈,依赖模块少 36 | 37 | ----- 38 | 39 | 内置中间件列表: 40 | 41 | * `bodyParser` 请求体解析: 42 | * `json` 解析 application/json,基于模块[body-parser](https://www.npmjs.com/package/body-parser) 43 | * `text` 解析 text/plain,基于模块 [body-parser](https://www.npmjs.com/package/body-parser) 44 | * `urlencoded` 解析 application/x-www-form-urlencoded,基于模块 [body-parser](https://www.npmjs.com/package/body-parser) 45 | * `raw` 解析 application/octet-stream,基于模块 [body-parser](https://www.npmjs.com/package/body-parser) 46 | * `multipart` 解析 multipart/form-data ,基于模块 [busboy](https://www.npmjs.com/package/busboy) 47 | * `cookieParser` 解析 Cookie,基于模块 [cookie-parser](https://www.npmjs.com/package/cookie-parser) 48 | * `serveStatic` 静态文件服务,基于模块 [serve-static](https://www.npmjs.com/package/serve-static) 49 | * `favicon` Favicon中间件,使用方法类似于 [serve-favicon](https://github.com/expressjs/serve-favicon) 50 | * `cors` 设置 CORS 51 | * `session` 提供多存储引擎的 Session 支持: 52 | * `SessionMemoryStore` 内存存储引擎 53 | * `SessionRedisStore` Redis 存储引擎,通过传入 Redis 客户端实例实现存储,支持 [ioredis](https://www.npmjs.com/package/ioredis) 和 [redis](https://www.npmjs.com/package/redis) 模块 54 | * `SimpleRedisClientOptions` 简单 Redis 客户端,可以不依赖第三方模块的情况下实现 Redis 存储,直接在 `SessionRedisStore` 初始化时指定 `{ host, port, db }` 来代替 `client` 参数即可 55 | 56 | **🌟🌟🌟🌟详细使用说明可阅读 [Wiki](https://github.com/leizongmin/leizm-web/wiki)🌟🌟🌟🌟** 57 | 58 | ## 安装 59 | 60 | ```bash 61 | npm i @leizm/web -S 62 | ``` 63 | 64 | ## Hello, world 65 | 66 | ```typescript 67 | import * as web from "@leizm/web"; 68 | 69 | // 创建app实例 70 | const app = new web.Application(); 71 | // 快速初始化 ejs 模板,需要手动安装 ejs 模块 72 | app.templateEngine.initEjs(); 73 | 74 | app.router.get("/a", async function(ctx) { 75 | // 渲染模板,模板文件为 views/index.html 76 | ctx.response.render("index", { msg: "hello, world" }); 77 | }); 78 | 79 | app.router.get("/b", async function(ctx) { 80 | // 返回JSON 81 | ctx.response.json({ msg: "hello, world" }); 82 | }); 83 | 84 | // 监听端口 85 | app.listen({ port: 3000 }); 86 | ``` 87 | 88 | ## 基本使用方法 89 | 90 | ```typescript 91 | import * as web from "@leizm/web"; 92 | 93 | const app = new web.Application(); 94 | const router = new web.Router(); 95 | 96 | // 使用内置中间件 97 | app.use("/", web.component.cookieParser()); 98 | 99 | // 基本的中间件 100 | app.use("/", function(ctx) { 101 | console.log("hello, world"); 102 | ctx.next(); 103 | }); 104 | 105 | // 支持 async function 106 | app.use("/", async function(ctx) { 107 | console.log("async function"); 108 | await sleep(1000); 109 | ctx.next(); 110 | }); 111 | 112 | // 路由中间件 113 | router.get("/hello/:a/:b", function(ctx) { 114 | console.log("a=%s, b=%s", ctx.request.params.a, ctx.request.params.b); 115 | ctx.response.html("it works"); 116 | }); 117 | app.use("/", router); 118 | 119 | // 错误处理 120 | app.use("/", function(ctx, err) { 121 | ctx.response.json({ message: err.message }); 122 | }); 123 | 124 | // 监听端口 125 | app.listen({ port: 3000 }, () => { 126 | console.log("server started"); 127 | }); 128 | ``` 129 | 130 | ## 扩展 131 | 132 | 扩展 Request 与 Response 对象的方法:[参考单元测试程序](https://github.com/leizongmin/leizm-web/blob/master/src/test/extends.test.ts) 133 | 134 | 模板文件 `web.ts`(自己的项目中引用此文件中的 `Application` 和 `Router`,而不是来自 `@leizm/web` 的): 135 | 136 | ```typescript 137 | import * as base from "@leizm/web"; 138 | export * from "@leizm/web"; 139 | 140 | export type MiddlewareHandle = (ctx: Context, err?: base.ErrorReason) => Promise | void; 141 | 142 | export class Application extends base.Application { 143 | protected contextConstructor = Context; 144 | } 145 | 146 | export class Router extends base.Router { 147 | protected contextConstructor = Context; 148 | } 149 | 150 | export class Context extends base.Context { 151 | protected requestConstructor = Request; 152 | protected responseConstructor = Response; 153 | } 154 | 155 | export class Request extends base.Request { 156 | // 扩展 Request 157 | public get remoteIP() { 158 | return String(this.req.headers["x-real-ip"] || this.req.headers["x-forwarded-for"] || this.req.socket.remoteAddress); 159 | } 160 | } 161 | 162 | export class Response extends base.Response { 163 | // 扩展 Response 164 | public ok(data: any) { 165 | this.json({ data }); 166 | } 167 | public error(error: string) { 168 | this.json({ error }); 169 | } 170 | } 171 | ``` 172 | 173 | ## 性能 174 | 175 | [性能测试程序](https://github.com/leizongmin/leizm-web-benchmark) 结果(性能略低于主流框架 **koa** 的 -5%,高于 **express** 的 +235%): 176 | 177 | ------------------------------------------------------------------------ 178 | 179 | ### connection: close 方式请求: 180 | 181 | - 8370 Requests/sec - micro.js 182 | - 8185 Requests/sec - http.js 183 | - **7612 Requests/sec - koa.js** 184 | - **7302 Requests/sec - @leizm/web🌟🌟** 185 | - 5871 Requests/sec - restify.js 186 | - 5800 Requests/sec - hapi.js 187 | - **3602 Requests/sec - express.js** 188 | 189 | ------------------------------------------------------------------------ 190 | 191 | ### connection: keep-alive 方式请求: 192 | 193 | - 22780 Requests/sec - http.js 194 | - 18899 Requests/sec - micro.js 195 | - **17704 Requests/sec - koa.js** 196 | - **16793 Requests/sec - @leizm/web🌟🌟** 197 | - 11603 Requests/sec - restify.js 198 | - 11428 Requests/sec - hapi.js 199 | - **5012 Requests/sec - express.js** 200 | 201 | 202 | ## 授权协议 203 | 204 | ```text 205 | MIT License 206 | 207 | Copyright (c) 2017-2021 老雷 208 | 209 | Permission is hereby granted, free of charge, to any person obtaining a copy 210 | of this software and associated documentation files (the "Software"), to deal 211 | in the Software without restriction, including without limitation the rights 212 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 213 | copies of the Software, and to permit persons to whom the Software is 214 | furnished to do so, subject to the following conditions: 215 | 216 | The above copyright notice and this permission notice shall be included in all 217 | copies or substantial portions of the Software. 218 | 219 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 220 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 221 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 222 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 223 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 224 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 225 | SOFTWARE. 226 | ``` 227 | 228 | 229 | ## License 230 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fleizongmin%2Fleizm-web?ref=badge_large) -------------------------------------------------------------------------------- /src/lib/module/simple.redis.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置模块 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { EventEmitter } from "events"; 7 | import { createConnection, Socket } from "net"; 8 | import { RedisCompatibleClient } from "../component/session.redis"; 9 | 10 | class RedisParser { 11 | /** 已初步解析出来的行 */ 12 | protected _lines: string[] = []; 13 | /** 剩余不能构成一行的文本 */ 14 | protected _text: string = ""; 15 | /** 解析结果 */ 16 | public result: any; 17 | 18 | /** 19 | * 将收到的数据添加到缓冲区 20 | */ 21 | public push(text: string | Buffer) { 22 | // 将结果按照\r\n 分隔 23 | const lines = (this._text + text.toString()).split("\r\n"); 24 | // 如果结尾是\r\n,那么数组最后一个元素肯定是一个空字符串 25 | // 否则,我们应该将剩余的部分跟下一个 data 事件接收到的数据连起来 26 | this._text = lines.pop() || ""; 27 | this._lines = this._lines.concat(...lines); 28 | } 29 | 30 | /** 31 | * 解析下一个结果,如果没有则返回 null 32 | */ 33 | public next() { 34 | const lines = this._lines; 35 | const first = lines[0]; 36 | 37 | // 去掉指定数量的行,并且返回结果 38 | const popResult = (lineNumber: number, result: { data?: any; error?: string }) => { 39 | this._lines = this._lines.slice(lineNumber); 40 | return (this.result = result); 41 | }; 42 | 43 | // 返回空结果 44 | const popEmpty = () => { 45 | return (this.result = false); 46 | }; 47 | 48 | if (lines.length < 1) return popEmpty(); 49 | 50 | switch (first[0]) { 51 | case "+": 52 | return popResult(1, { data: first.slice(1) }); 53 | 54 | case "-": 55 | return popResult(1, { error: first.slice(1) }); 56 | 57 | case ":": 58 | return popResult(1, { data: Number(first.slice(1)) }); 59 | 60 | case "$": { 61 | const n = Number(first.slice(1)); 62 | if (n === -1) { 63 | // 如果是 $-1 表示空结果 64 | return popResult(1, { data: null }); 65 | } else { 66 | // 否则取后面一行作为结果 67 | const second = lines[1]; 68 | if (typeof second !== "undefined") { 69 | return popResult(2, { data: second }); 70 | } else { 71 | return popEmpty(); 72 | } 73 | } 74 | } 75 | 76 | case "*": { 77 | const n = Number(first.slice(1)); 78 | if (n === 0) { 79 | return popResult(1, { data: [] }); 80 | } else { 81 | const array = []; 82 | let i = 1; 83 | for (; i < lines.length && array.length < n; i++) { 84 | const a = lines[i]; 85 | const b = lines[i + 1]; 86 | if (a.slice(0, 3) === "$-1") { 87 | array.push(null); 88 | } else if (a[0] === ":") { 89 | array.push(Number(a.slice(1))); 90 | } else { 91 | if (typeof b !== "undefined") { 92 | array.push(b); 93 | i++; 94 | } else { 95 | return popEmpty(); 96 | } 97 | } 98 | } 99 | if (array.length === n) { 100 | return popResult(i, { data: array }); 101 | } else { 102 | return popEmpty(); 103 | } 104 | } 105 | } 106 | 107 | default: 108 | return popEmpty(); 109 | } 110 | } 111 | } 112 | 113 | export interface SimpleRedisClientOptions { 114 | /** Redis服务器地址 */ 115 | host?: string; 116 | /** Redis服务器端口 */ 117 | port?: number; 118 | /** Redis服务器数据库号 */ 119 | db?: number; 120 | /** Redis服务器密码 */ 121 | password?: string; 122 | } 123 | 124 | export const DEFAULT_REDIS_OPTIONS: Required = { 125 | host: "127.0.0.1", 126 | port: 6379, 127 | db: 0, 128 | password: "", 129 | }; 130 | 131 | export class SimpleRedisClient extends EventEmitter implements RedisCompatibleClient { 132 | protected _parser: RedisParser = new RedisParser(); 133 | /** 回调函数列表 */ 134 | protected _callbacks: Array<(err: Error | null, ret: any) => void> = []; 135 | /** 待发送数据列表 */ 136 | protected _sendBuffers: Array = []; 137 | /** 连接状态 */ 138 | protected _isConnected: boolean = false; 139 | protected _isConnecting: boolean = false; 140 | /** 参数 */ 141 | protected readonly options: Required ; 142 | /** 连接实例 */ 143 | public socket?: Socket; 144 | 145 | constructor(options: SimpleRedisClientOptions = {}) { 146 | super(); 147 | 148 | this.options = { 149 | ...DEFAULT_REDIS_OPTIONS, 150 | ...options, 151 | }; 152 | } 153 | 154 | public get connected() { 155 | return this._isConnected; 156 | } 157 | 158 | /** 159 | * 接收到数据,循环结果 160 | */ 161 | protected _pushData(data: Buffer) { 162 | this._parser.push(data); 163 | 164 | while (this._parser.next()) { 165 | const result = this._parser.result; 166 | const cb = this._callbacks.shift(); 167 | if (result.error) { 168 | cb!(new Error(result.error), null); 169 | } else { 170 | cb!(null, result.data); 171 | } 172 | } 173 | } 174 | 175 | /** 176 | * 连接 177 | */ 178 | protected _connect() { 179 | if (this._isConnecting) return; 180 | 181 | this._isConnected = false; 182 | this.socket = createConnection(this.options.port, this.options.host, () => { 183 | this._isConnected = true; 184 | this._isConnecting = false; 185 | this.emit("connect"); 186 | if (this.options.password) { 187 | this.preCommand(["AUTH", this.options.password], (err) => { 188 | if (err) { 189 | this.emit("error", new Error(`auth failed: ${err.message}`)); 190 | } 191 | }); 192 | } 193 | if (this.options.db > 0) { 194 | this.preCommand(["SELECT", this.options.db], (err) => { 195 | if (err) { 196 | this.emit("error", new Error(`select database failed: ${err.message}`)); 197 | } 198 | }); 199 | } 200 | this._sendBuffers.forEach((data) => this.socket!.write(data)); 201 | this._sendBuffers = []; 202 | }); 203 | this._isConnecting = true; 204 | this.socket.on("error", (err) => { 205 | this._isConnecting = false; 206 | this.emit("error", err); 207 | }); 208 | 209 | this.socket.on("close", () => { 210 | this._isConnecting = false; 211 | delete this.socket; 212 | // 处理未完成的任务和回调 213 | const callbacks = this._callbacks.slice(); 214 | this._callbacks = []; 215 | this._sendBuffers = []; 216 | this._parser = new RedisParser(); 217 | callbacks.forEach((callback) => callback(new Error(`connection has been closed`), null)); 218 | this.emit("close"); 219 | }); 220 | this.socket.on("end", () => { 221 | this.emit("end"); 222 | }); 223 | this.socket.on("data", (data) => { 224 | this._pushData(data); 225 | }); 226 | } 227 | 228 | /** 229 | * 发送命令给服务器 230 | * @param cmd 231 | * @param callback 232 | */ 233 | public command(cmd: Array , callback: (err: Error | null, ret: any) => void) { 234 | setImmediate(() => { 235 | this._callbacks.push(callback); 236 | const data = `${cmd.map(stringify).join(" ")}\r\n`; 237 | if (this.socket) { 238 | this.socket.write(data); 239 | } else { 240 | this._sendBuffers.push(data); 241 | this._connect(); 242 | } 243 | }); 244 | } 245 | 246 | protected preCommand(cmd: Array , callback: (err: Error | null, ret: any) => void) { 247 | this._callbacks.push(callback); 248 | const data = `${cmd.map(stringify).join(" ")}\r\n`; 249 | if (this.socket) { 250 | this.socket.write(data); 251 | } 252 | } 253 | 254 | /** 255 | * 关闭连接 256 | */ 257 | public end() { 258 | if (this.socket) { 259 | this.socket.destroy(); 260 | } 261 | } 262 | 263 | public get(key: string, callback: (err: Error | null, ret: any) => void): void { 264 | return this.command(["GET", key], callback); 265 | } 266 | public setex(key: string, ttl: number, data: string, callback: (err: Error | null, ret: any) => void): void { 267 | return this.command(["SETEX", key, ttl, data], callback); 268 | } 269 | public expire(key: string, ttl: number, callback: (err: Error | null, ret: any) => void): void { 270 | return this.command(["EXPIRE", key, ttl], callback); 271 | } 272 | public del(key: string, callback: (err: Error | null, ret: any) => void): void { 273 | return this.command(["DEL", key], callback); 274 | } 275 | } 276 | 277 | function stringify(v: string | number | boolean): string { 278 | return JSON.stringify(v); 279 | } 280 | 281 | // const conn = new SimpleRedisClient({ db: 5 }); 282 | // setInterval(() => { 283 | // console.log(conn); 284 | // conn.command(["set", "abc", Date.now()], (err, ret) => { 285 | // console.log(err, ret); 286 | // }); 287 | // }, 2000); 288 | // conn.on("error", err => console.log("redis error", err)); 289 | -------------------------------------------------------------------------------- /src/test/component/component.body.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 单元测试 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { Application, component } from "../../lib"; 7 | import * as request from "supertest"; 8 | import { expect } from "chai"; 9 | import * as fs from "fs"; 10 | import * as path from "path"; 11 | 12 | function readFile(file: string): Promise { 13 | return new Promise((resolve, reject) => { 14 | fs.readFile(file, (err, ret) => { 15 | if (err) return reject(err); 16 | resolve(ret); 17 | }); 18 | }); 19 | } 20 | 21 | // const ROOT_DIR = path.resolve(__dirname, "../../.."); 22 | 23 | describe("component.body", function () { 24 | const appInstances: Application[] = []; 25 | after(async function () { 26 | for (const app of appInstances) { 27 | await app.close(); 28 | } 29 | }); 30 | 31 | it("json", async function () { 32 | const app = new Application(); 33 | appInstances.push(app); 34 | app.use("/", component.bodyParser.json()); 35 | app.use("/", function (ctx) { 36 | ctx.response.setHeader("content-type", "application/json"); 37 | ctx.response.end(JSON.stringify(ctx.request.body)); 38 | }); 39 | await request(app.server) 40 | .post("/") 41 | .send({ 42 | a: 111, 43 | b: 222, 44 | c: 333, 45 | }) 46 | .expect(200) 47 | .expect({ 48 | a: 111, 49 | b: 222, 50 | c: 333, 51 | }); 52 | }); 53 | 54 | describe("fast json parser", function () { 55 | it("json", async function () { 56 | const app = new Application(); 57 | appInstances.push(app); 58 | app.use("/", component.jsonParser()); 59 | app.use("/", function (ctx) { 60 | ctx.response.setHeader("content-type", "application/json"); 61 | ctx.response.end(JSON.stringify(ctx.request.body)); 62 | }); 63 | await request(app.server) 64 | .post("/") 65 | .send({ 66 | a: 111, 67 | b: 222, 68 | c: 333, 69 | }) 70 | .expect(200) 71 | .expect({ 72 | a: 111, 73 | b: 222, 74 | c: 333, 75 | }); 76 | }); 77 | 78 | it("out of limit", async function () { 79 | const app = new Application(); 80 | appInstances.push(app); 81 | app.use("/", component.jsonParser({ limit: 1024 })); 82 | app.use("/", function (ctx) { 83 | ctx.response.setHeader("content-type", "application/json"); 84 | ctx.response.end(JSON.stringify(ctx.request.body)); 85 | }); 86 | const data = { 87 | a: "a".repeat(1024), 88 | b: "b".repeat(1024), 89 | c: "c".repeat(1024), 90 | }; 91 | await request(app.server) 92 | .post("/") 93 | .send(data) 94 | .expect(500) 95 | .expect(/out of max body size limit/); 96 | }); 97 | }); 98 | 99 | it("urlencoded", async function () { 100 | const app = new Application(); 101 | appInstances.push(app); 102 | app.use("/", component.bodyParser.urlencoded({ extended: false })); 103 | app.use("/", function (ctx) { 104 | ctx.response.setHeader("content-type", "application/json"); 105 | ctx.response.end(JSON.stringify(ctx.request.body)); 106 | }); 107 | await request(app.server) 108 | .post("/") 109 | .set("content-type", "application/x-www-form-urlencoded") 110 | .send({ 111 | a: "111", 112 | b: "222", 113 | c: "333", 114 | }) 115 | .expect(200) 116 | .expect({ 117 | a: "111", 118 | b: "222", 119 | c: "333", 120 | }); 121 | }); 122 | 123 | it("text", async function () { 124 | const app = new Application(); 125 | appInstances.push(app); 126 | app.use("/", component.bodyParser.text()); 127 | app.use("/", function (ctx) { 128 | ctx.response.setHeader("content-type", "application/json"); 129 | ctx.response.end(JSON.stringify(ctx.request.body)); 130 | }); 131 | await request(app.server) 132 | .post("/") 133 | .set("content-type", "text/plain") 134 | .send("hello, world") 135 | .expect(200) 136 | .expect(`"hello, world"`); 137 | }); 138 | 139 | it("raw", async function () { 140 | const app = new Application(); 141 | appInstances.push(app); 142 | app.use("/", component.bodyParser.raw()); 143 | app.use("/", function (ctx) { 144 | ctx.response.setHeader("content-type", "application/json"); 145 | ctx.response.end(JSON.stringify((ctx.request.body as Buffer).toJSON())); 146 | }); 147 | await request(app.server) 148 | .post("/") 149 | .set("content-type", "application/octet-stream") 150 | .send("hello, world") 151 | .expect(200) 152 | .expect(JSON.stringify(Buffer.from("hello, world").toJSON())); 153 | }); 154 | 155 | describe("multipart", function () { 156 | it("smallFileSize=Infinity 文件存储于内存中", async function () { 157 | const app = new Application(); 158 | appInstances.push(app); 159 | app.use("/", component.bodyParser.multipart({ smallFileSize: Infinity })); 160 | app.use("/", function (ctx) { 161 | ctx.response.json({ ...ctx.request.body, ...ctx.request.files }); 162 | }); 163 | const c = await readFile(__filename); 164 | const d = Buffer.from("456"); 165 | await request(app.server) 166 | .post("/") 167 | .field("a", 123) 168 | .field("b", __dirname) 169 | .attach("c", __filename) 170 | .field("d", d) 171 | .expect(200) 172 | .expect((res: any) => { 173 | expect(res.body.a).to.equal("123"); 174 | expect(res.body.b).to.equal(__dirname); 175 | expect(res.body.c).includes({ 176 | originalName: path.basename(__filename), 177 | size: c.length, 178 | }); 179 | expect(res.body.c.buffer).to.deep.equal(JSON.parse(JSON.stringify(c))); 180 | expect(res.body.d).include({ originalName: "", size: d.length }); 181 | expect(res.body.d.buffer).to.deep.equal(JSON.parse(JSON.stringify(d))); 182 | }); 183 | }); 184 | 185 | it("smallFileSize=100 文件存储于临时文件中", async function () { 186 | const app = new Application(); 187 | appInstances.push(app); 188 | app.use("/", component.bodyParser.multipart({ smallFileSize: 100 })); 189 | app.use("/", function (ctx) { 190 | ctx.response.json({ ...ctx.request.body, ...ctx.request.files }); 191 | }); 192 | const c = await readFile(__filename); 193 | const d = Buffer.from("456"); 194 | await request(app.server) 195 | .post("/") 196 | .field("a", 123) 197 | .field("b", __dirname) 198 | .attach("c", __filename) 199 | .field("d", d) 200 | .expect(200) 201 | .expect(async (res: any) => { 202 | expect(res.body.a).to.equal("123"); 203 | expect(res.body.b).to.equal(__dirname); 204 | expect(res.body.c).includes({ 205 | originalName: path.basename(__filename), 206 | size: c.length, 207 | }); 208 | expect(res.body.c.path).to.be.exist; 209 | expect(await readFile(res.body.c.path)).to.deep.equal(c); 210 | expect(res.body.d).include({ originalName: "", size: d.length }); 211 | expect(res.body.d.buffer).to.deep.equal(JSON.parse(JSON.stringify(d))); 212 | }); 213 | }); 214 | 215 | it("smallFileSize=0 通过 ctx.request.parseMultipart() 解析", async function () { 216 | const app = new Application(); 217 | appInstances.push(app); 218 | app.use("/", async function (ctx) { 219 | expect(ctx.request.body).to.deep.equal({}); 220 | expect(ctx.request.files).to.deep.equal({}); 221 | const { body, files } = await ctx.request.parseMultipart({ smallFileSize: 0 }); 222 | expect(body).to.equal(ctx.request.body); 223 | expect(files).to.equal(ctx.request.files); 224 | ctx.response.json({ ...body, ...files }); 225 | }); 226 | const c = await readFile(__filename); 227 | const d = Buffer.from("456"); 228 | let res: any; 229 | await request(app.server) 230 | .post("/") 231 | .field("a", 123) 232 | .field("b", __dirname) 233 | .attach("c", __filename) 234 | .field("d", d) 235 | .expect(200) 236 | .expect((r: any) => { 237 | res = r; 238 | }); 239 | expect(res.body.a).to.equal("123"); 240 | expect(res.body.b).to.equal(__dirname); 241 | expect(res.body.c).includes({ 242 | originalName: path.basename(__filename), 243 | size: c.length, 244 | }); 245 | expect(res.body.c.path).to.be.exist; 246 | expect(await readFile(res.body.c.path)).to.deep.equal(c); 247 | expect(res.body.d).include({ originalName: "", size: d.length }); 248 | expect(res.body.d.path).to.be.exist; 249 | expect(await readFile(res.body.d.path)).to.deep.equal(d); 250 | }); 251 | }); 252 | }); 253 | -------------------------------------------------------------------------------- /src/lib/response.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import * as path from "path"; 7 | import { ServerResponse } from "http"; 8 | import { Context } from "./context"; 9 | import { sign as signCookie } from "cookie-signature"; 10 | import * as cookie from "cookie"; 11 | import * as send from "send"; 12 | import * as mime from "mime"; 13 | import { CookieOptions, TemplateRenderData, SYMBOL_APPLICATION } from "./define"; 14 | import { notifyDeprecated } from "./utils"; 15 | import { Readable } from "stream"; 16 | import { responseGzip } from "./module/response.gzip"; 17 | 18 | export class Response { 19 | constructor(public readonly res: ServerResponse, public readonly ctx: Context) {} 20 | 21 | /** 22 | * 初始化完成,由 `Context.init()` 自动调用 23 | * 一般用于自定义扩展 Response 时,在此方法中加上自己的祝时候完成的代码 24 | */ 25 | public inited() {} 26 | 27 | /** 28 | * 响应状态码 29 | */ 30 | get statusCode(): number { 31 | return this.res.statusCode; 32 | } 33 | 34 | /** 35 | * 响应状态消息 36 | */ 37 | get statusMessage(): string { 38 | return this.res.statusMessage; 39 | } 40 | 41 | /** 42 | * 响应头是否已发送 43 | */ 44 | get headersSent(): boolean { 45 | return this.res.headersSent; 46 | } 47 | 48 | /** 49 | * 是否已响应完成 50 | */ 51 | get finished(): boolean { 52 | return this.res.finished; 53 | } 54 | 55 | /** 56 | * 设置响应状态码(弃用,请使用 status 代替) 57 | * 58 | * @param statusCode 响应状态码 59 | */ 60 | public setStatus(statusCode: number): this { 61 | notifyDeprecated("response.setStatus(code)", "response.status(code)", "3.0.0"); 62 | return this.status(statusCode); 63 | } 64 | 65 | /** 66 | * 设置响应状态码 67 | * 68 | * @param statusCode 响应状态码 69 | */ 70 | public status(statusCode: number): this { 71 | this.res.statusCode = statusCode; 72 | return this; 73 | } 74 | 75 | /** 76 | * 获取响应头 77 | * 78 | * @param name 名称 79 | */ 80 | public getHeader(name: string): string | string[] | number | undefined { 81 | return this.res.getHeader(name); 82 | } 83 | 84 | /** 85 | * 获取所有响应头 86 | * 87 | * @param name 名称 88 | */ 89 | public getHeaders(): Record { 90 | return (this.res.getHeaders ? this.res.getHeaders() : (this.res as any)._headers) || {}; 91 | } 92 | 93 | /** 94 | * 设置响应头 95 | * 96 | * @param name 名称 97 | * @param value 值 98 | */ 99 | public setHeader(name: string, value: string | string[] | number): this { 100 | this.res.setHeader(name, value); 101 | return this; 102 | } 103 | 104 | /** 105 | * 添加响应头 106 | * 107 | * @param name 名称 108 | * @param value 值 109 | */ 110 | public appendHeader(name: string, value: string | string[] | number): this { 111 | let header = this.getHeader(name) as any[]; 112 | if (!header) { 113 | header = []; 114 | } else if (!Array.isArray(header)) { 115 | header = [header]; 116 | } 117 | if (Array.isArray(value)) { 118 | header = header.concat(value); 119 | } else { 120 | header.push(value); 121 | } 122 | this.setHeader(name, header); 123 | return this; 124 | } 125 | 126 | /** 127 | * 设置响应头 128 | * 129 | * @param headers 响应头 130 | */ 131 | public setHeaders(headers: Record ): this { 132 | for (const name in headers) { 133 | this.setHeader(name, headers[name]); 134 | } 135 | return this; 136 | } 137 | 138 | /** 139 | * 删除响应头 140 | * 141 | * @param name 名称 142 | */ 143 | public removeHeader(name: string): this { 144 | this.res.removeHeader(name); 145 | return this; 146 | } 147 | 148 | /** 149 | * 写响应头 150 | * 151 | * @param statusCode 响应状态码 152 | * @param headers 响应头 153 | */ 154 | public writeHead(statusCode: number, headers: Record ): this { 155 | this.res.writeHead(statusCode, headers); 156 | return this; 157 | } 158 | 159 | /** 160 | * 根据文件名或文件后缀设置 Content-Type 161 | * 162 | * @param fileName 163 | */ 164 | public type(fileName: string): this { 165 | const type = mime.getType(fileName); 166 | if (type) { 167 | this.setHeader("Content-Type", type); 168 | } 169 | return this; 170 | } 171 | 172 | /** 173 | * 输出数据 174 | * 175 | * @param data 要输出的数据 176 | * @param encoding 字符编码 177 | * @param callback 回调函数 178 | */ 179 | public write(data: string | Buffer | Uint8Array, encoding?: string, callback?: () => void): boolean { 180 | return this.res.write.apply(this.res, arguments as any); 181 | } 182 | 183 | /** 184 | * 输出数据并结束 185 | * 186 | * @param data 要输出的数据 187 | * @param encoding 字符编码 188 | * @param callback 回调函数 189 | */ 190 | public end(data?: string | Buffer | Uint8Array, encoding?: string, callback?: () => void): void { 191 | return this.res.end.apply(this.res, arguments as any); 192 | } 193 | 194 | /** 195 | * 响应JSON 196 | * @param data 数据 197 | */ 198 | public json(data: any): void { 199 | this.setHeader("Content-Type", "application/json"); 200 | this.end(JSON.stringify(data)); 201 | } 202 | 203 | /** 204 | * 响应HTML页面 205 | * @param str 内容 206 | */ 207 | public html(str: Buffer | string): void { 208 | this.setHeader("Content-Type", "text/html; charset=utf-8"); 209 | this.end(str); 210 | } 211 | 212 | /** 213 | * 响应文件内容 214 | * @param file 文件名 215 | * @param options 216 | */ 217 | public file(file: string, options?: send.SendOptions) { 218 | send(this.ctx.request.req, path.resolve(file), options) 219 | .on("error", (err) => { 220 | this.res.statusCode = err.status || 500; 221 | this.res.end(err.message); 222 | }) 223 | .pipe(this.res); 224 | } 225 | 226 | /** 227 | * HTTP 302 临时重定向(弃用,请使用 redirectTemporary 代替) 228 | * @param url 网址 229 | * @param content 内容 230 | */ 231 | public temporaryRedirect(url: string, content: string = ""): void { 232 | notifyDeprecated("response.temporaryRedirect()", "response.redirectTemporary()", "3.0.0"); 233 | return this.redirectTemporary(url, content); 234 | } 235 | 236 | /** 237 | * HTTP 302 临时重定向 238 | * @param url 网址 239 | * @param content 内容 240 | */ 241 | public redirectTemporary(url: string, content: string = ""): void { 242 | this.writeHead(302, { Location: url }); 243 | this.end(content); 244 | } 245 | 246 | /** 247 | * HTTP 301 永久重定向(弃用,请使用 redirectPermanent 代替) 248 | * @param url 网址 249 | * @param content 内容 250 | */ 251 | public permanentRedirect(url: string, content: string = ""): void { 252 | notifyDeprecated("response.permanentRedirect()", "response.redirectPermanent()", "3.0.0"); 253 | return this.redirectPermanent(url, content); 254 | } 255 | 256 | /** 257 | * HTTP 301 永久重定向 258 | * @param url 网址 259 | * @param content 内容 260 | */ 261 | public redirectPermanent(url: string, content: string = ""): void { 262 | this.writeHead(301, { Location: url }); 263 | this.end(content); 264 | } 265 | 266 | /** 267 | * 删除Cookie 268 | * @param name 名称 269 | * @param options 选项 270 | */ 271 | public clearCookie(name: string, options: CookieOptions = {}) { 272 | this.cookie(name, "", { expires: new Date(1), path: "/", ...options }); 273 | } 274 | 275 | /** 276 | * 设置Cookie 277 | * @param name 名称 278 | * @param value 值 279 | * @param options 选项 280 | */ 281 | public cookie(name: string, value: any, options: CookieOptions = {}) { 282 | const opts = { ...options }; 283 | const secret = (this.ctx.request.req as any).secret; 284 | if (opts.signed && !secret) { 285 | throw new Error('cookieParser("secret") required for signed cookies'); 286 | } 287 | let val = typeof value === "object" ? "j:" + JSON.stringify(value) : String(value); 288 | if (opts.signed) { 289 | val = "s:" + signCookie(val, secret); 290 | } 291 | if ("maxAge" in opts && opts.maxAge) { 292 | opts.expires = new Date(Date.now() + opts.maxAge); 293 | opts.maxAge /= 1000; 294 | } 295 | if (opts.path == null) { 296 | opts.path = "/"; 297 | } 298 | this.appendHeader("Set-Cookie", cookie.serialize(name, String(val), opts)); 299 | } 300 | 301 | /** 302 | * 渲染模板 303 | * @param name 模板名称 304 | * @param data 模板数据 305 | */ 306 | public async render(name: string, data: TemplateRenderData = {}): Promise { 307 | try { 308 | const html = await this.ctx[SYMBOL_APPLICATION]!.templateEngine.render(name, data); 309 | if (!this.getHeader("Content-Type")) { 310 | this.setHeader("Content-Type", "text/html; charset=utf-8"); 311 | } 312 | this.end(html); 313 | } catch (err) { 314 | this.ctx.next(err); 315 | } 316 | } 317 | 318 | /** 319 | * 响应压缩的内容 320 | * @param data 321 | * @param contentType 322 | */ 323 | public async gzip(data: string | Buffer | Readable, contentType?: string) { 324 | responseGzip(this.ctx.request.req, this.res, data, contentType); 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/test/core/router.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 单元测试 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { expect } from "chai"; 7 | import { Application, Router, Context, ErrorReason } from "../../lib"; 8 | import * as request from "supertest"; 9 | 10 | const METHODS = ["get", "head", "post", "put", "delete", "connect", "options", "trace", "patch"]; 11 | 12 | describe("Router", function () { 13 | it("可以在 Application.use 中直接使用", function (done) { 14 | const app = new Application(); 15 | const router = new Router(); 16 | router.post("/ok", function (ctx) { 17 | ctx.response.end("yes"); 18 | }); 19 | app.use("/", router); 20 | request(app.server).post("/ok").expect(200).expect("yes", done); 21 | }); 22 | 23 | it("可以通过 Router.use 嵌套 Router", function (done) { 24 | const status: any = {}; 25 | const app = new Application(); 26 | const router = new Router(); 27 | const router2 = new Router(); 28 | router2.post("/haha", function (ctx) { 29 | status.a = true; 30 | ctx.next(); 31 | }); 32 | router.use("/", router2); 33 | router.get("/haha", function (ctx) { 34 | status.b = true; 35 | ctx.next(); 36 | }); 37 | app.use("/", router); 38 | app.use("/", function (ctx) { 39 | ctx.response.end("ok"); 40 | }); 41 | request(app.server) 42 | .post("/haha") 43 | .expect(200) 44 | .expect("ok", function () { 45 | expect(status).to.deep.equal({ 46 | a: true, 47 | }); 48 | done(); 49 | }); 50 | }); 51 | 52 | it("可拦截出错信息,并响应200", function (done) { 53 | const app = new Application(); 54 | const router = new Router(); 55 | router.get( 56 | "/xx", 57 | function (ctx) { 58 | throw new Error("test error"); 59 | }, 60 | function (ctx, err) { 61 | expect(err).to.instanceof(Error); 62 | expect(err).property("message").to.equal("test error"); 63 | ctx.response.end("ok"); 64 | }, 65 | function (ctx, err) { 66 | throw new Error("不可能执行到此处"); 67 | }, 68 | ); 69 | app.use("/", router); 70 | request(app.server).get("/xx").expect(200).expect("ok", done); 71 | }); 72 | 73 | it("all 响应所有请求", async function () { 74 | const app = new Application(); 75 | const router = new Router(); 76 | router.get("/", function (ctx) { 77 | ctx.response.end("不应该执行到此处"); 78 | }); 79 | router.all("/ok", function (ctx) { 80 | ctx.response.end("yes"); 81 | }); 82 | app.use("/", router); 83 | for (const method of METHODS) { 84 | if (method === "connect") continue; 85 | await (request(app.server) as any) 86 | [method]("/ok") 87 | .expect(200) 88 | .expect(method === "head" ? undefined : "yes"); 89 | } 90 | }); 91 | 92 | it("注册各种请求方法并正确处理请求", async function () { 93 | const app = new Application(); 94 | const router = new Router(); 95 | function generateHandle(msg: string) { 96 | return function (ctx: Context) { 97 | ctx.response.end(msg); 98 | }; 99 | } 100 | function generateErrorHandle(msg: string) { 101 | return function (ctx: Context, err?: ErrorReason) { 102 | ctx.response.end(msg); 103 | }; 104 | } 105 | for (const method of METHODS) { 106 | (router as any)[method]("/xyz", generateErrorHandle("不可能执行到此处"), generateHandle(`this is not ${method}`)); 107 | } 108 | for (const method of METHODS) { 109 | (router as any)[method]("/abc", generateErrorHandle("不可能执行到此处"), generateHandle(`this is ${method}`)); 110 | } 111 | app.use("/", router); 112 | for (const method of METHODS) { 113 | if (method === "connect") continue; 114 | await (request(app.server) as any) 115 | [method]("/abc") 116 | .expect(200) 117 | .expect(method === "head" ? undefined : `this is ${method}`); 118 | } 119 | }); 120 | 121 | it("注册各种请求方法并正确处理出错的请求 (async function)", async function () { 122 | const app = new Application(); 123 | const router = new Router(); 124 | function generateHandle(msg: string) { 125 | return function (ctx: Context) { 126 | ctx.response.end(msg); 127 | }; 128 | } 129 | function generateErrorHandle(msg: string) { 130 | return function (ctx: Context, err?: ErrorReason) { 131 | expect(err).to.instanceof(Error); 132 | expect(err).property("message").to.equal(msg); 133 | ctx.response.end(msg); 134 | }; 135 | } 136 | for (const method of METHODS) { 137 | (router as any)[method]("/xyz", generateErrorHandle("不可能执行到此处"), generateHandle(method)); 138 | } 139 | for (const method of METHODS) { 140 | (router as any)[method]("/abc", generateErrorHandle("不可能执行到此处"), generateHandle(method)); 141 | } 142 | app.use("/", router); 143 | for (const method of METHODS) { 144 | if (method === "connect") continue; 145 | await (request(app.server) as any) 146 | [method]("/abc") 147 | .expect(200) 148 | .expect(method === "head" ? undefined : method); 149 | } 150 | }); 151 | 152 | it("use() 的中间件始终在 get()、post() 等方法前面", async function () { 153 | const app = new Application(); 154 | const router = new Router(); 155 | const numbers: number[] = []; 156 | router.post("/ok", function (ctx) { 157 | ctx.response.end("yes"); 158 | }); 159 | router.post("/not_ok", function (ctx) { 160 | ctx.response.end("no"); 161 | }); 162 | router.use("/", function (ctx) { 163 | numbers.push(123); 164 | ctx.next(); 165 | }); 166 | router.use("/", function (ctx) { 167 | numbers.push(456); 168 | ctx.next(); 169 | }); 170 | router.get("/", function (ctx) { 171 | ctx.response.end("home"); 172 | }); 173 | app.use("/", router); 174 | 175 | await request(app.server).post("/ok").expect(200).expect("yes"); 176 | expect(numbers).to.deep.equal([123, 456]); 177 | 178 | await request(app.server).post("/not_ok").expect(200).expect("no"); 179 | expect(numbers).to.deep.equal([123, 456, 123, 456]); 180 | 181 | await request(app.server).get("/").expect(200).expect("home"); 182 | expect(numbers).to.deep.equal([123, 456, 123, 456, 123, 456]); 183 | }); 184 | 185 | it("ctx.route 获得原始路由信息", async function () { 186 | const app = new Application(); 187 | const router = new Router(); 188 | const router2 = new Router(); 189 | const router3 = new Router(); 190 | const histories: any[] = []; 191 | app.use("/", function (ctx) { 192 | // console.log(ctx.route); 193 | histories.push(ctx.route); 194 | ctx.next(); 195 | }); 196 | app.use("/", router); 197 | app.use("/test", router2); 198 | app.use("/", function (ctx, err) { 199 | expect(err).instanceof(Error); 200 | expect((err as any).message).to.equal("xxxx"); 201 | histories.push(ctx.route); 202 | ctx.response.json(ctx.route); 203 | }); 204 | router2.use("/test3", router3); 205 | 206 | router.post("/ok", function (ctx) { 207 | const route = { method: "POST", path: "/ok" }; 208 | expect(ctx.route).to.deep.equal(route); 209 | ctx.response.json(ctx.route); 210 | }); 211 | router.get("/say/:msg", function (ctx) { 212 | const route = { method: "GET", path: "/say/:msg" }; 213 | expect(ctx.route).to.deep.equal(route); 214 | ctx.response.json(ctx.route); 215 | }); 216 | router.get("/error/:msg", function (ctx) { 217 | const route = { method: "GET", path: "/error/:msg" }; 218 | expect(ctx.route).to.deep.equal(route); 219 | ctx.next(new Error(ctx.request.params.msg)); 220 | }); 221 | 222 | router2.all("/user/:user_id/reply/:info", function (ctx) { 223 | const route = { method: "ALL", path: "/test/user/:user_id/reply/:info" }; 224 | expect(ctx.route).to.deep.equal(route); 225 | ctx.response.json(ctx.route); 226 | }); 227 | 228 | router3.all("/hahaha/:name", function (ctx) { 229 | const route = { method: "ALL", path: "/test/test3/hahaha/:name" }; 230 | expect(ctx.route).to.deep.equal(route); 231 | ctx.response.json(ctx.route); 232 | }); 233 | 234 | await request(app.server).post("/ok").expect(200).expect({ method: "POST", path: "/ok" }); 235 | await request(app.server).get("/say/hello").expect(200).expect({ method: "GET", path: "/say/:msg" }); 236 | await request(app.server).get("/say/hi").expect(200).expect({ method: "GET", path: "/say/:msg" }); 237 | await request(app.server) 238 | .delete("/test/user/123/reply/abc") 239 | .expect(200) 240 | .expect({ method: "ALL", path: "/test/user/:user_id/reply/:info" }); 241 | await request(app.server) 242 | .delete("/test/test3/hahaha/abc") 243 | .expect(200) 244 | .expect({ method: "ALL", path: "/test/test3/hahaha/:name" }); 245 | await request(app.server).get("/error/xxxx").expect(200).expect({ method: "GET", path: "/error/:msg" }); 246 | expect(histories).to.deep.equal([ 247 | { method: "POST", path: "/ok" }, 248 | { method: "GET", path: "/say/hello" }, 249 | { method: "GET", path: "/say/hi" }, 250 | { method: "DELETE", path: "/test/user/123/reply/abc" }, 251 | { method: "DELETE", path: "/test/test3/hahaha/abc" }, 252 | { method: "GET", path: "/error/xxxx" }, 253 | { method: "GET", path: "/error/:msg" }, 254 | ]); 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /src/test/component/component.session.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 单元测试 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { Application, component } from "../../lib"; 7 | import { SimpleRedisClient } from "../../lib/module/simple.redis"; 8 | import * as request from "supertest"; 9 | import { expect } from "chai"; 10 | import * as Redis from "ioredis"; 11 | import { createClient } from "redis"; 12 | 13 | function sleep(ms: number) { 14 | return new Promise((resolve) => { 15 | setTimeout(resolve, ms); 16 | }); 17 | } 18 | 19 | describe("component.session", function () { 20 | this.timeout(10000); 21 | const appInstances: Application[] = []; 22 | after(async function () { 23 | for (const app of appInstances) { 24 | await app.close(); 25 | } 26 | }); 27 | 28 | describe("多存储引擎", function () { 29 | it("SessionMemoryStore", async function () { 30 | const app = new Application(); 31 | appInstances.push(app); 32 | app.use("/", component.cookieParser()); 33 | app.use("/", component.session({ store: new component.SessionMemoryStore(), maxAge: 1000 })); 34 | app.use("/", function (ctx) { 35 | ctx.session.data.counter = ctx.session.data.counter || 0; 36 | ctx.session.data.counter++; 37 | ctx.response.json(ctx.session.data); 38 | }); 39 | const agent = request.agent(app.server); 40 | await agent.get("/").expect(200, { counter: 1 }); 41 | await agent.get("/").expect(200, { counter: 2 }); 42 | await agent.get("/").expect(200, { counter: 3 }); 43 | await agent.get("/").expect(200, { counter: 4 }); 44 | await agent.get("/").expect(200, { counter: 5 }); 45 | await sleep(1500); 46 | await agent.get("/").expect(200, { counter: 1 }); 47 | await agent.get("/").expect(200, { counter: 2 }); 48 | }); 49 | 50 | it("SessionRedisStore 基于 ioredis 模块", async function () { 51 | const app = new Application(); 52 | appInstances.push(app); 53 | app.use("/", component.cookieParser()); 54 | const client = new Redis(); 55 | const prefix = `test:sess:${Date.now()}:${Math.random()}:`; 56 | app.use( 57 | "/", 58 | component.session({ 59 | store: new component.SessionRedisStore({ client: client as any, prefix }), 60 | maxAge: 1000, 61 | }), 62 | ); 63 | app.use("/", function (ctx) { 64 | ctx.session.data.counter = ctx.session.data.counter || 0; 65 | ctx.session.data.counter++; 66 | ctx.response.json(ctx.session.data); 67 | }); 68 | const agent = request.agent(app.server); 69 | await agent.get("/").expect(200, { counter: 1 }); 70 | await agent.get("/").expect(200, { counter: 2 }); 71 | await agent.get("/").expect(200, { counter: 3 }); 72 | await agent.get("/").expect(200, { counter: 4 }); 73 | await agent.get("/").expect(200, { counter: 5 }); 74 | await sleep(1500); 75 | await agent.get("/").expect(200, { counter: 1 }); 76 | await agent.get("/").expect(200, { counter: 2 }); 77 | }); 78 | 79 | it("SessionRedisStore 基于 redis 模块", async function () { 80 | const app = new Application(); 81 | appInstances.push(app); 82 | app.use("/", component.cookieParser()); 83 | const client = createClient(); 84 | const prefix = `test:sess:${Date.now()}:${Math.random()}:`; 85 | app.use( 86 | "/", 87 | component.session({ 88 | store: new component.SessionRedisStore({ client, prefix }), 89 | maxAge: 2000, 90 | }), 91 | ); 92 | app.use("/", function (ctx) { 93 | ctx.session.data.counter = ctx.session.data.counter || 0; 94 | ctx.session.data.counter++; 95 | ctx.response.json(ctx.session.data); 96 | }); 97 | const agent = request.agent(app.server); 98 | await agent.get("/").expect(200, { counter: 1 }); 99 | await agent.get("/").expect(200, { counter: 2 }); 100 | await agent.get("/").expect(200, { counter: 3 }); 101 | await agent.get("/").expect(200, { counter: 4 }); 102 | await agent.get("/").expect(200, { counter: 5 }); 103 | await sleep(3000); 104 | await agent.get("/").expect(200, { counter: 1 }); 105 | await agent.get("/").expect(200, { counter: 2 }); 106 | }); 107 | 108 | it("SessionRedisStore 基于内置 SimpleRedisClient 模块", async function () { 109 | const app = new Application(); 110 | appInstances.push(app); 111 | app.use("/", component.cookieParser()); 112 | const client = new SimpleRedisClient(); 113 | const prefix = `test:sess:${Date.now()}:${Math.random()}:`; 114 | app.use( 115 | "/", 116 | component.session({ 117 | store: new component.SessionRedisStore({ client, prefix }), 118 | maxAge: 2000, 119 | }), 120 | ); 121 | app.use("/", function (ctx) { 122 | ctx.session.data.counter = ctx.session.data.counter || 0; 123 | ctx.session.data.counter++; 124 | ctx.response.json(ctx.session.data); 125 | }); 126 | const agent = request.agent(app.server); 127 | await agent.get("/").expect(200, { counter: 1 }); 128 | await agent.get("/").expect(200, { counter: 2 }); 129 | await agent.get("/").expect(200, { counter: 3 }); 130 | await agent.get("/").expect(200, { counter: 4 }); 131 | await agent.get("/").expect(200, { counter: 5 }); 132 | await sleep(3000); 133 | await agent.get("/").expect(200, { counter: 1 }); 134 | await agent.get("/").expect(200, { counter: 2 }); 135 | }); 136 | 137 | it("SessionRedisStore 不指定 Redis 客户端,使用内置 SimpleRedisClient 模块", async function () { 138 | const app = new Application(); 139 | appInstances.push(app); 140 | app.use("/", component.cookieParser()); 141 | const prefix = `test:sess:${Date.now()}:${Math.random()}:`; 142 | app.use( 143 | "/", 144 | component.session({ 145 | store: new component.SessionRedisStore({ prefix }), 146 | maxAge: 2000, 147 | }), 148 | ); 149 | app.use("/", function (ctx) { 150 | ctx.session.data.counter = ctx.session.data.counter || 0; 151 | ctx.session.data.counter++; 152 | ctx.response.json(ctx.session.data); 153 | }); 154 | const agent = request.agent(app.server); 155 | await agent.get("/").expect(200, { counter: 1 }); 156 | await agent.get("/").expect(200, { counter: 2 }); 157 | await agent.get("/").expect(200, { counter: 3 }); 158 | await agent.get("/").expect(200, { counter: 4 }); 159 | await agent.get("/").expect(200, { counter: 5 }); 160 | await sleep(3000); 161 | await agent.get("/").expect(200, { counter: 1 }); 162 | await agent.get("/").expect(200, { counter: 2 }); 163 | }); 164 | }); 165 | 166 | describe("session操作相关方法", function () { 167 | it("session.reload()", async function () { 168 | const app = new Application(); 169 | appInstances.push(app); 170 | app.use("/", component.cookieParser()); 171 | app.use("/", component.session({ maxAge: 2000 })); 172 | app.use("/a", async function (ctx) { 173 | ctx.session.data.yes = false; 174 | await ctx.session.save(); 175 | ctx.response.json(ctx.session.data); 176 | }); 177 | app.use("/b", async function (ctx) { 178 | expect(ctx.session.data).to.deep.equal({ yes: false }); 179 | ctx.session.data.yes = true; 180 | expect(ctx.session.data).to.deep.equal({ yes: true }); 181 | await ctx.session.reload(); 182 | expect(ctx.session.data).to.deep.equal({ yes: false }); 183 | ctx.response.json(ctx.session.data); 184 | }); 185 | const agent = request.agent(app.server); 186 | await agent.get("/a").expect(200, { yes: false }); 187 | await agent.get("/b").expect(200, { yes: false }); 188 | }); 189 | 190 | it("session.regenerate()", async function () { 191 | const app = new Application(); 192 | appInstances.push(app); 193 | app.use("/", component.cookieParser()); 194 | app.use("/", component.session({ maxAge: 2000 })); 195 | app.use("/a", async function (ctx) { 196 | ctx.session.data.yes = false; 197 | await ctx.session.save(); 198 | ctx.response.json(ctx.session.data); 199 | }); 200 | app.use("/b", async function (ctx) { 201 | expect(ctx.session.data).to.deep.equal({ yes: false }); 202 | ctx.session.data.yes = true; 203 | expect(ctx.session.data).to.deep.equal({ yes: true }); 204 | await ctx.session.regenerate(); 205 | expect(ctx.session.data).to.deep.equal({}); 206 | ctx.session.data.no = true; 207 | ctx.response.json(ctx.session.data); 208 | }); 209 | app.use("/c", async function (ctx) { 210 | ctx.response.json(ctx.session.data); 211 | }); 212 | const agent = request.agent(app.server); 213 | await agent.get("/a").expect(200, { yes: false }); 214 | await agent.get("/b").expect(200, { no: true }); 215 | await agent.get("/c").expect(200, { no: true }); 216 | }); 217 | 218 | it("session.destroy()", async function () { 219 | const app = new Application(); 220 | appInstances.push(app); 221 | app.use("/", component.cookieParser()); 222 | app.use("/", component.session({ maxAge: 2000 })); 223 | app.use("/a", async function (ctx) { 224 | ctx.session.data.yes = false; 225 | await ctx.session.save(); 226 | ctx.response.json(ctx.session.data); 227 | }); 228 | app.use("/b", async function (ctx) { 229 | expect(ctx.session.data).to.deep.equal({ yes: false }); 230 | await ctx.session.destroy(); 231 | expect(ctx.session.data).to.deep.equal({}); 232 | ctx.response.json(ctx.session.data); 233 | }); 234 | app.use("/c", async function (ctx) { 235 | expect(ctx.session.data).to.deep.equal({}); 236 | ctx.response.json(ctx.session.data); 237 | }); 238 | const agent = request.agent(app.server); 239 | await agent.get("/a").expect(200, { yes: false }); 240 | await agent.get("/b").expect(200, {}); 241 | await agent.get("/c").expect(200, {}); 242 | }); 243 | 244 | it("session.touch()", async function () { 245 | const app = new Application(); 246 | appInstances.push(app); 247 | app.use("/", component.cookieParser()); 248 | app.use("/", component.session({ maxAge: 2000 })); 249 | app.use("/a", async function (ctx) { 250 | ctx.session.data.yes = false; 251 | await ctx.session.save(); 252 | ctx.response.json(ctx.session.data); 253 | }); 254 | app.use("/b", async function (ctx) { 255 | ctx.response.json(ctx.session.data); 256 | }); 257 | const agent = request.agent(app.server); 258 | await agent.get("/a").expect(200, { yes: false }); 259 | await agent.get("/b").expect(200, { yes: false }); 260 | await sleep(800); 261 | await agent.get("/b").expect(200, { yes: false }); 262 | await sleep(800); 263 | await agent.get("/b").expect(200, { yes: false }); 264 | await sleep(2000); 265 | await agent.get("/b").expect(200, {}); 266 | }).timeout(5000); 267 | }); 268 | 269 | describe("其他选项", function () { 270 | it("自定义Cookie名称", async function () { 271 | const app = new Application(); 272 | appInstances.push(app); 273 | app.use("/", component.cookieParser()); 274 | app.use("/", component.session({ maxAge: 2000, name: "hello" })); 275 | app.use("/a", async function (ctx) { 276 | ctx.session.data.yes = false; 277 | ctx.response.json(ctx.session.data); 278 | }); 279 | app.use("/b", async function (ctx) { 280 | expect(ctx.session.id).to.equal(ctx.request.cookies.hello); 281 | ctx.response.json(ctx.session.data); 282 | }); 283 | const agent = request.agent(app.server); 284 | await agent.get("/a").expect(200, { yes: false }); 285 | await agent.get("/b").expect(200, { yes: false }); 286 | }); 287 | 288 | it("自定义Cookie选项:{ signed: true }", async function () { 289 | const app = new Application(); 290 | appInstances.push(app); 291 | app.use("/", component.cookieParser("secret key")); 292 | app.use("/", component.session({ maxAge: 2000, name: "hello", cookie: { signed: true } })); 293 | app.use("/a", async function (ctx) { 294 | ctx.session.data.yes = false; 295 | ctx.response.json(ctx.session.data); 296 | }); 297 | app.use("/b", async function (ctx) { 298 | expect(ctx.session.id).to.equal(ctx.request.signedCookies.hello); 299 | ctx.response.json(ctx.session.data); 300 | }); 301 | const agent = request.agent(app.server); 302 | await agent.get("/a").expect(200, { yes: false }); 303 | await agent.get("/b").expect(200, { yes: false }); 304 | }); 305 | }); 306 | }); 307 | -------------------------------------------------------------------------------- /src/test/core/connect.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 单元测试 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { expect } from "chai"; 7 | import { Server } from "http"; 8 | import { Application, Router, fromClassicalHandle, fromClassicalErrorHandle } from "../../lib"; 9 | import * as request from "supertest"; 10 | import * as bodyParser from "body-parser"; 11 | 12 | function sleep(ms: number): Promise { 13 | return new Promise((resolve, reject) => { 14 | setTimeout(() => resolve(ms), ms); 15 | }); 16 | } 17 | 18 | describe("Application", function () { 19 | const appInstances: Application[] = []; 20 | after(async function () { 21 | for (const app of appInstances) { 22 | await app.close(); 23 | } 24 | }); 25 | 26 | it("封装了 http.Server", function (done) { 27 | const app = new Application(); 28 | appInstances.push(app); 29 | app.use("/", function (ctx) { 30 | ctx.response.end("ok"); 31 | }); 32 | expect(app.server).to.instanceof(Server); 33 | request(app.server).get("/").expect(200).expect("ok", done); 34 | }); 35 | 36 | it("调用 listen 监听端口成功", function (done) { 37 | const app = new Application(); 38 | appInstances.push(app); 39 | app.use("/", function (ctx) { 40 | ctx.response.end("ok"); 41 | }); 42 | app.listen({ port: 0 }); 43 | request(app.server).get("/").expect(200).expect("ok", done); 44 | }); 45 | 46 | it("支持在 http.createServer 内使用", function (done) { 47 | const app = new Application(); 48 | appInstances.push(app); 49 | app.use("/", function (ctx) { 50 | ctx.response.end("ok"); 51 | }); 52 | const server = new Server(function (req, res) { 53 | app.handleRequest(req, res); 54 | }); 55 | request(server).get("/").expect(200).expect("ok", done); 56 | }); 57 | 58 | it("支持在 http.createServer 内使用(自动绑定 this)", function (done) { 59 | const app = new Application(); 60 | appInstances.push(app); 61 | app.use("/", function (ctx) { 62 | ctx.response.end("ok"); 63 | }); 64 | const server = new Server(app.handleRequest); 65 | request(server).get("/").expect(200).expect("ok", done); 66 | }); 67 | 68 | it("可以 attach http.Server", function (done) { 69 | const app = new Application(); 70 | appInstances.push(app); 71 | app.use("/", function (ctx) { 72 | ctx.response.end("ok"); 73 | }); 74 | const server = new Server(); 75 | app.attach(server); 76 | request(server).get("/").expect(200).expect("ok", done); 77 | }); 78 | 79 | it("支持 async function", function (done) { 80 | const app = new Application(); 81 | appInstances.push(app); 82 | app.use("/", async function (ctx) { 83 | await sleep(300); 84 | ctx.response.end("ok"); 85 | }); 86 | const t = process.uptime(); 87 | request(app.server) 88 | .get("/") 89 | .expect(200) 90 | .expect("ok", function () { 91 | expect(process.uptime() - t).to.greaterThan(0.3); 92 | done(); 93 | }); 94 | }); 95 | 96 | it("如果没有中间件响应结果,调用 done 回调函数", function (done) { 97 | const app = new Application(); 98 | appInstances.push(app); 99 | const server = new Server(function (req, res) { 100 | app.handleRequest(req, res, function (err) { 101 | expect(err).to.equal(null); 102 | res.end("hello"); 103 | }); 104 | }); 105 | request(server).get("/").expect(200).expect("hello", done); 106 | }); 107 | 108 | it("如果没有中间件响应 Error 结果,调用 done 回调函数时传递 Error 信息", function (done) { 109 | const app = new Application(); 110 | appInstances.push(app); 111 | app.use("/", function (ctx) { 112 | ctx.next("test error"); 113 | }); 114 | const server = new Server(function (req, res) { 115 | app.handleRequest(req, res, function (err) { 116 | expect(err).to.equal("test error"); 117 | res.end("hello"); 118 | }); 119 | }); 120 | request(server).get("/").expect(200).expect("hello", done); 121 | }); 122 | 123 | it("如果没有中间件响应结果,且不传递 done 回调函数时,返回 404", function (done) { 124 | const app = new Application(); 125 | appInstances.push(app); 126 | const server = new Server(function (req, res) { 127 | app.handleRequest(req, res); 128 | }); 129 | request(server).get("/").expect(404, done); 130 | }); 131 | 132 | it("如果没有中间件响应 Error 结果,且不传递 done 回调函数时,返回 500", function (done) { 133 | const app = new Application(); 134 | appInstances.push(app); 135 | app.use("/", function (ctx) { 136 | ctx.next("test error"); 137 | }); 138 | const server = new Server(function (req, res) { 139 | app.handleRequest(req, res); 140 | }); 141 | request(server) 142 | .get("/") 143 | .expect(500) 144 | .expect(/test error/, done); 145 | }); 146 | 147 | it("支持捕捉中间件抛出的异常,并传递给出错处理中间件", function (done) { 148 | const app = new Application(); 149 | appInstances.push(app); 150 | app.use("/", function (ctx) { 151 | throw new Error("oh error"); 152 | }); 153 | app.use("/", function (ctx) { 154 | throw new Error("不可能执行到此处"); 155 | }); 156 | app.use("/", function (ctx, err) { 157 | expect(err).to.instanceof(Error); 158 | expect(err).property("message").to.equal("oh error"); 159 | ctx.response.end("no error"); 160 | }); 161 | request(app.server).get("/").expect(200).expect("no error", done); 162 | }); 163 | 164 | it("支持捕捉 async function 中间件抛出的异常,并传递给出错处理中间件", function (done) { 165 | const app = new Application(); 166 | appInstances.push(app); 167 | app.use("/", async function (ctx) { 168 | throw new Error("oh error"); 169 | }); 170 | app.use("/", function (ctx, err) { 171 | expect(err).to.instanceof(Error); 172 | expect(err).property("message").to.equal("oh error"); 173 | ctx.response.end("no error"); 174 | }); 175 | request(app.server).get("/").expect(200).expect("no error", done); 176 | }); 177 | 178 | it("支持 URL 字符串前缀", function (done) { 179 | const app = new Application(); 180 | appInstances.push(app); 181 | app.use("/a", function (ctx) { 182 | ctx.response.end("this is a"); 183 | }); 184 | app.use("/b", function (ctx) { 185 | ctx.response.end("this is b"); 186 | }); 187 | request(app.server).get("/b").expect(200).expect("this is b", done); 188 | }); 189 | 190 | it("支持 URL 正则前缀", function (done) { 191 | const app = new Application(); 192 | appInstances.push(app); 193 | app.use(/a/, function (ctx) { 194 | ctx.response.end("this is a"); 195 | }); 196 | app.use(/b/, function (ctx) { 197 | ctx.response.end("this is b"); 198 | }); 199 | request(app.server).get("/b").expect(200).expect("this is b", done); 200 | }); 201 | 202 | it("use 支持多个中间件", function (done) { 203 | const app = new Application(); 204 | appInstances.push(app); 205 | const status: any = {}; 206 | app.use( 207 | "/", 208 | function (ctx) { 209 | status.a = true; 210 | ctx.next(); 211 | }, 212 | function (ctx) { 213 | status.b = true; 214 | ctx.response.end("ok"); 215 | }, 216 | function (ctx) { 217 | status.c = true; 218 | throw new Error("不应该执行到此处"); 219 | }, 220 | ); 221 | request(app.server) 222 | .get("/") 223 | .expect(200) 224 | .expect("ok", function () { 225 | expect(status).to.deep.equal({ 226 | a: true, 227 | b: true, 228 | }); 229 | done(); 230 | }); 231 | }); 232 | 233 | it("use 支持嵌套使用另一个 Application 实例", function (done) { 234 | const app = new Application(); 235 | const app2 = new Application(); 236 | appInstances.push(app); 237 | appInstances.push(app2); 238 | app2.use("/aaa", function (ctx) { 239 | ctx.response.end("aaa"); 240 | }); 241 | app.use("/", app2); 242 | app.use("/aaa", function (ctx) { 243 | throw new Error("不可能执行到此处"); 244 | }); 245 | request(app.server).get("/aaa").expect(200).expect("aaa", done); 246 | }); 247 | 248 | it("所有中间件按照顺序执行,不符合执行条件会被跳过", function (done) { 249 | const app = new Application(); 250 | appInstances.push(app); 251 | const status: any = {}; 252 | app.use("/", function (ctx) { 253 | status.x = true; 254 | ctx.next(); 255 | }); 256 | app.use("/", function (ctx, err) { 257 | throw new Error("不可能执行到此处"); 258 | }); 259 | app.use("/a", function (ctx) { 260 | status.a = true; 261 | ctx.next(); 262 | }); 263 | app.use("/a/b", function (ctx) { 264 | status.ab = true; 265 | ctx.next(); 266 | }); 267 | app.use("/b", function (ctx) { 268 | status.b = true; 269 | ctx.next(); 270 | }); 271 | app.use("/b/b", function (ctx) { 272 | status.bb = true; 273 | ctx.next(); 274 | }); 275 | app.use("/b/b", function (ctx, err) { 276 | throw new Error("不可能执行到此处"); 277 | }); 278 | app.use("/c", function (ctx) { 279 | status.c = true; 280 | ctx.next(); 281 | }); 282 | app.use("/", function (ctx) { 283 | status.d = true; 284 | ctx.response.end("end"); 285 | }); 286 | request(app.server) 287 | .get("/b/b") 288 | .expect(200) 289 | .expect("end", function () { 290 | expect(status).to.deep.equal({ 291 | x: true, 292 | b: true, 293 | bb: true, 294 | d: true, 295 | }); 296 | done(); 297 | }); 298 | }); 299 | 300 | it("支持 ctx.request.params 参数", function (done) { 301 | const app = new Application(); 302 | appInstances.push(app); 303 | app.use("/prefix/:x/:y/:z", function (ctx) { 304 | expect(ctx.request.params).to.deep.equal({ 305 | x: "hello", 306 | y: "world", 307 | z: "ok", 308 | }); 309 | ctx.next(); 310 | }); 311 | app.use("/prefix/:a/:b/:c", function (ctx) { 312 | expect(ctx.request.params).to.deep.equal({ 313 | a: "hello", 314 | b: "world", 315 | c: "ok", 316 | }); 317 | ctx.response.setHeader("content-type", "application/json"); 318 | ctx.response.end(JSON.stringify(ctx.request.params)); 319 | }); 320 | request(app.server).get("/prefix/hello/world/ok").expect(200).expect( 321 | { 322 | a: "hello", 323 | b: "world", 324 | c: "ok", 325 | }, 326 | done, 327 | ); 328 | }); 329 | 330 | it("支持使用 connect/express 中间件", function (done) { 331 | const app = new Application(); 332 | appInstances.push(app); 333 | app.use("/", fromClassicalHandle(bodyParser.json() as any)); 334 | app.use("/", function (ctx) { 335 | ctx.response.setHeader("content-type", "application/json"); 336 | ctx.response.end(JSON.stringify(ctx.request.body)); 337 | }); 338 | request(app.server) 339 | .post("/") 340 | .send({ 341 | a: 111, 342 | b: 222, 343 | c: 333, 344 | }) 345 | .expect(200) 346 | .expect( 347 | { 348 | a: 111, 349 | b: 222, 350 | c: 333, 351 | }, 352 | done, 353 | ); 354 | }); 355 | 356 | it("支持使用 connect/express 错误处理中间件", function (done) { 357 | const app = new Application(); 358 | appInstances.push(app); 359 | app.use("/", function (ctx) { 360 | throw new Error("test error"); 361 | }); 362 | app.use( 363 | "/", 364 | fromClassicalErrorHandle(function (err, req, res, next) { 365 | expect(err).to.instanceof(Error); 366 | expect(err).property("message").to.equal("test error"); 367 | res.end("no error"); 368 | }), 369 | ); 370 | request(app.server).get("/").expect(200).expect("no error", done); 371 | }); 372 | 373 | it("use Application 和 Router 实例时,request.url & request.path 的值正确", async function () { 374 | const app = new Application(); 375 | appInstances.push(app); 376 | const status = { 377 | a: false, 378 | b: false, 379 | c: false, 380 | }; 381 | const router = new Router(); 382 | app.use("/abc", function (ctx) { 383 | expect(ctx.request.path).to.equal("/abc/123/xx"); 384 | expect(ctx.request.url).to.equal("/abc/123/xx?hello=world"); 385 | status.a = true; 386 | ctx.next(); 387 | }); 388 | router.use("/123", function (ctx) { 389 | expect(ctx.request.path).to.equal("/123/xx"); 390 | expect(ctx.request.url).to.equal("/123/xx?hello=world"); 391 | status.b = true; 392 | ctx.next(); 393 | }); 394 | router.get("/123/:code", function (ctx) { 395 | expect(ctx.request.path).to.equal("/123/xx"); 396 | expect(ctx.request.url).to.equal("/123/xx?hello=world"); 397 | expect(ctx.request.params).to.deep.equal({ code: "xx" }); 398 | expect(ctx.request.query).to.deep.equal({ hello: "world" }); 399 | status.c = true; 400 | ctx.next(); 401 | }); 402 | app.use("/abc", router); 403 | app.use("/", function (ctx) { 404 | expect(ctx.request.path).to.equal("/abc/123/xx"); 405 | expect(ctx.request.url).to.equal("/abc/123/xx?hello=world"); 406 | ctx.response.end("it works"); 407 | }); 408 | await request(app.server).get("/abc/123/xx?hello=world").expect(200).expect("it works"); 409 | expect(status).to.deep.equal({ a: true, b: true, c: true }); 410 | }); 411 | 412 | it("默认 Router", async function () { 413 | const app = new Application(); 414 | appInstances.push(app); 415 | let isOk = false; 416 | app.use("/", function (ctx) { 417 | isOk = true; 418 | ctx.next(); 419 | }); 420 | app.router.get("/hello", function (ctx) { 421 | expect(isOk).to.equal(true); 422 | ctx.response.json({ ok: true }); 423 | }); 424 | await request(app.server).get("/hello").expect(200).expect({ ok: true }); 425 | }); 426 | }); 427 | -------------------------------------------------------------------------------- /src/test/core/context.request.response.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 单元测试 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import * as fs from "fs"; 7 | import * as path from "path"; 8 | import { expect } from "chai"; 9 | import { Application, fromClassicalHandle } from "../../lib"; 10 | import * as request from "supertest"; 11 | import * as cookieParser from "cookie-parser"; 12 | import { sign as signCookie } from "cookie-signature"; 13 | import * as simpleTemplate from "../../lib/module/simple.template"; 14 | 15 | function readFile(file: string): Promise { 16 | return new Promise((resolve, reject) => { 17 | fs.readFile(file, (err, ret) => { 18 | if (err) return reject(err); 19 | resolve(ret); 20 | }); 21 | }); 22 | } 23 | 24 | const ROOT_DIR = path.resolve(__dirname, "../../.."); 25 | 26 | describe("Request", function () { 27 | it("正确解析 query, url, path, search, httpVersion 等基本信息", function (done) { 28 | const app = new Application(); 29 | app.use("/", function (ctx) { 30 | expect(ctx.request.query).to.deep.equal({ 31 | a: "123", 32 | b: "456", 33 | }); 34 | expect(ctx.request.url).to.equal("/hello?a=123&b=456"); 35 | expect(ctx.request.method).to.equal("GET"); 36 | expect(ctx.request.path).to.equal("/hello"); 37 | expect(ctx.request.search).to.equal("?a=123&b=456"); 38 | expect(ctx.request.httpVersion).to.be.oneOf(["1.0", "1.1", "2.0"]); 39 | ctx.response.end("ok"); 40 | }); 41 | request(app.server).get("/hello?a=123&b=456").expect(200).expect("ok", done); 42 | }); 43 | 44 | it("正确获取 params 信息", function (done) { 45 | const app = new Application(); 46 | app.use("/:a/:b/ccc", function (ctx) { 47 | expect(ctx.request.hasParams()).to.equal(true); 48 | expect(ctx.request.params).to.deep.equal({ 49 | a: "aaa", 50 | b: "bbb", 51 | }); 52 | ctx.response.end("ok"); 53 | }); 54 | request(app.server).get("/aaa/bbb/ccc").expect(200).expect("ok", done); 55 | }); 56 | 57 | it("正确获取 headers, getHeader", function (done) { 58 | const app = new Application(); 59 | app.use("/", function (ctx) { 60 | expect(ctx.request.headers).property("user-agent").includes("superagent"); 61 | expect(ctx.request.getHeader("USER-agent")).includes("superagent"); 62 | ctx.response.end("ok"); 63 | }); 64 | request(app.server).get("/hello").set("user-agent", "superagent").expect("ok").expect(200, done); 65 | }); 66 | 67 | it("可以设置、获取、判断 body, files, cookies, session 等可选数据", function (done) { 68 | const app = new Application(); 69 | app.use("/", function (ctx) { 70 | { 71 | expect(ctx.request.body).to.deep.equal({}); 72 | expect(ctx.request.hasBody()).to.equal(false); 73 | ctx.request.body = { a: 111 }; 74 | expect(ctx.request.body).to.deep.equal({ a: 111 }); 75 | expect(ctx.request.hasBody()).to.equal(true); 76 | } 77 | { 78 | expect(ctx.request.files).to.deep.equal({}); 79 | expect(ctx.request.hasFiles()).to.equal(false); 80 | ctx.request.files = { a: 111 }; 81 | expect(ctx.request.files).to.deep.equal({ a: 111 }); 82 | expect(ctx.request.hasFiles()).to.equal(true); 83 | } 84 | { 85 | expect(ctx.request.cookies).to.deep.equal({}); 86 | expect(ctx.request.hasCookies()).to.equal(false); 87 | ctx.request.cookies = { a: "111" }; 88 | expect(ctx.request.cookies).to.deep.equal({ a: "111" }); 89 | expect(ctx.request.hasCookies()).to.equal(true); 90 | } 91 | { 92 | expect(ctx.request.session).to.deep.equal({}); 93 | expect(ctx.request.hasSession()).to.equal(false); 94 | ctx.request.session = { a: 111 }; 95 | expect(ctx.request.session).to.deep.equal({ a: 111 }); 96 | expect(ctx.request.hasSession()).to.equal(true); 97 | } 98 | ctx.response.end("ok"); 99 | }); 100 | request(app.server).get("/hello").expect(200).expect("ok", done); 101 | }); 102 | }); 103 | 104 | describe("Response", function () { 105 | it("正确响应 setStatus, setHeader, setHeaders, writeHead, write, end", function (done) { 106 | const app = new Application(); 107 | app.use("/", function (ctx) { 108 | { 109 | ctx.response.status(100); 110 | expect(ctx.response.res.statusCode).to.equal(100); 111 | } 112 | ctx.response.setHeader("aaa", "hello aaa").setHeaders({ 113 | bbb: "hello bbb", 114 | ccc: "hello ccc", 115 | }); 116 | { 117 | ctx.response.appendHeader("t111", 1).appendHeader("t111", "hello").appendHeader("t111", ["a", "123"]); 118 | expect(ctx.response.getHeader("t111")).to.deep.equal([1, "hello", "a", "123"]); 119 | ctx.response.setHeader("t222", ["a", "b"]).appendHeader("t222", "c"); 120 | expect(ctx.response.getHeader("t222")).to.deep.equal(["a", "b", "c"]); 121 | expect(ctx.response.getHeaders()).to.deep.include({ 122 | aaa: "hello aaa", 123 | bbb: "hello bbb", 124 | ccc: "hello ccc", 125 | t111: [1, "hello", "a", "123"], 126 | t222: ["a", "b", "c"], 127 | }); 128 | } 129 | { 130 | ctx.response.setHeader("xxx", 123); 131 | expect(ctx.response.getHeader("XXX")).to.equal(123); 132 | ctx.response.removeHeader("xxx"); 133 | expect(ctx.response.getHeader("xxx")).to.equal(undefined); 134 | } 135 | ctx.response 136 | .writeHead(404, { 137 | bbb: "xxx", 138 | ddd: "xxxx", 139 | }) 140 | .write("123"); 141 | ctx.response.end("456"); 142 | }); 143 | request(app.server) 144 | .get("/hello") 145 | .expect("123456") 146 | .expect(404) 147 | .expect("aaa", "hello aaa") 148 | .expect("bbb", "xxx") 149 | .expect("ccc", "hello ccc") 150 | .expect("ddd", "xxxx") 151 | .end(done); 152 | }); 153 | 154 | it("json() 和 html()", async function () { 155 | const app = new Application(); 156 | app.use("/json", function (ctx) { 157 | ctx.response.json({ a: 123, b: 456 }); 158 | }); 159 | app.use("/html", function (ctx) { 160 | ctx.response.html("hello, world"); 161 | }); 162 | 163 | await request(app.server).get("/json").expect(200, { a: 123, b: 456 }); 164 | await request(app.server).get("/html").expect(200, "hello, world"); 165 | }); 166 | 167 | it("type()", async function () { 168 | const app = new Application(); 169 | app.use("/jpg", function (ctx) { 170 | ctx.response.type("jpg").end(); 171 | }); 172 | app.use("/png", function (ctx) { 173 | ctx.response.type("png").end(); 174 | }); 175 | 176 | await request(app.server).get("/jpg").expect(200).expect("content-type", "image/jpeg"); 177 | await request(app.server).get("/png").expect(200).expect("content-type", "image/png"); 178 | }); 179 | 180 | it("file()", async function () { 181 | const file1 = path.resolve(ROOT_DIR, "package.json"); 182 | const file1data = (await readFile(file1)).toString(); 183 | const file2 = path.resolve(ROOT_DIR, "README.md"); 184 | const file2data = (await readFile(file2)).toString(); 185 | const app = new Application(); 186 | app.use("/file1", function (ctx) { 187 | ctx.response.file(file1); 188 | }); 189 | app.use("/file2", function (ctx) { 190 | ctx.response.file(file2); 191 | }); 192 | await request(app.server) 193 | .get("/file1") 194 | .expect("content-type", "application/json; charset=UTF-8") 195 | .expect(200, file1data); 196 | await request(app.server) 197 | .get("/file2") 198 | .expect("content-type", "text/markdown; charset=UTF-8") 199 | .expect(200, file2data); 200 | }); 201 | 202 | describe("cookie()", function () { 203 | it("解析一般的Cookie", function (done) { 204 | const app = new Application(); 205 | app.use("/", fromClassicalHandle(cookieParser("test") as any)); 206 | app.use("/", function (ctx) { 207 | expect(ctx.request.cookies).to.deep.equal({ 208 | a: "123", 209 | b: "今天的天气真好", 210 | }); 211 | expect(ctx.request.signedCookies).to.deep.equal({}); 212 | ctx.response.cookie("c", { x: 1, y: 2 }); 213 | ctx.response.end("ok"); 214 | }); 215 | request(app.server) 216 | .get("/hello") 217 | .set("cookie", `a=${encodeURIComponent("123")}; b=${encodeURIComponent("今天的天气真好")}`) 218 | .expect(200) 219 | .expect("Set-Cookie", "c=j%3A%7B%22x%22%3A1%2C%22y%22%3A2%7D; Path=/") 220 | .expect("ok", done); 221 | }); 222 | 223 | it("解析签名的Cookie", function (done) { 224 | const app = new Application(); 225 | app.use("/", fromClassicalHandle(cookieParser("test") as any)); 226 | app.use("/", function (ctx) { 227 | // console.log(ctx.request.cookies, ctx.request.signedCookies); 228 | expect(ctx.request.cookies).to.deep.equal({}); 229 | expect(ctx.request.signedCookies).to.deep.equal({ 230 | a: "123", 231 | b: "今天的天气真好", 232 | }); 233 | ctx.response.cookie("c", { x: 1, y: 2 }, { signed: true }); 234 | ctx.response.end("ok"); 235 | }); 236 | request(app.server) 237 | .get("/hello") 238 | .set( 239 | "cookie", 240 | `a=s:${encodeURIComponent(signCookie("123", "test"))}; b=s:${encodeURIComponent( 241 | signCookie("今天的天气真好", "test"), 242 | )}`, 243 | ) 244 | .expect(200) 245 | .expect( 246 | "Set-Cookie", 247 | `c=${encodeURIComponent(`s:${signCookie(`j:${JSON.stringify({ x: 1, y: 2 })}`, "test")}`)}; Path=/`, 248 | ) 249 | .expect("ok", done); 250 | }); 251 | }); 252 | 253 | describe("redirectXXX()", function () { 254 | it("临时重定向", function (done) { 255 | const app = new Application(); 256 | app.use("/", function (ctx) { 257 | ctx.response.redirectTemporary("/a", "ok"); 258 | }); 259 | request(app.server).get("/").expect(302).expect("Location", "/a").expect("ok", done); 260 | }); 261 | 262 | it("永久重定向", function (done) { 263 | const app = new Application(); 264 | app.use("/", function (ctx) { 265 | ctx.response.redirectPermanent("/a", "ok"); 266 | }); 267 | request(app.server).get("/").expect(301).expect("Location", "/a").expect("ok", done); 268 | }); 269 | }); 270 | 271 | describe("gzip()", function () { 272 | it("gzip", async function () { 273 | const app = new Application(); 274 | app.use("/a", function (ctx) { 275 | ctx.response.gzip("今天的天气真好", "text/plain"); 276 | }); 277 | app.use("/b", function (ctx) { 278 | ctx.response.gzip(Buffer.from("今天的天气真好"), "text/plain"); 279 | }); 280 | await request(app.server) 281 | .get("/a") 282 | .expect("Content-Encoding", "gzip") 283 | .expect("Content-Type", "text/plain") 284 | .expect(200, "今天的天气真好"); 285 | await request(app.server) 286 | .get("/b") 287 | .expect("Content-Encoding", "gzip") 288 | .expect("Content-Type", "text/plain") 289 | .expect(200, "今天的天气真好"); 290 | }); 291 | }); 292 | 293 | describe("render()", function () { 294 | it("带后缀名", function (done) { 295 | const app = new Application(); 296 | app.templateEngine 297 | .register(".simple", simpleTemplate.renderFile) 298 | .setDefault(".simple") 299 | .setRoot(path.resolve(ROOT_DIR, "test_data/template")); 300 | app.use("/", function (ctx) { 301 | ctx.response.render("test1.simple", { a: 123, b: 456 }); 302 | }); 303 | request(app.server).get("/").expect(200).expect(" a = 123
\nb = 456
", done); 304 | }); 305 | }); 306 | 307 | it("省略后缀名", function (done) { 308 | const app = new Application(); 309 | app.templateEngine 310 | .register(".simple", simpleTemplate.renderFile) 311 | .setDefault(".simple") 312 | .setRoot(path.resolve(ROOT_DIR, "test_data/template")); 313 | app.use("/", function (ctx) { 314 | ctx.response.render("test1", { a: 123, b: 456 }); 315 | }); 316 | request(app.server).get("/").expect(200).expect("a = 123
\nb = 456
", done); 317 | }); 318 | 319 | it("完整文件路径", function (done) { 320 | const app = new Application(); 321 | app.templateEngine 322 | .register(".simple", simpleTemplate.renderFile) 323 | .setDefault(".simple") 324 | .setRoot(path.resolve(ROOT_DIR, "test_data/template")); 325 | app.use("/", function (ctx) { 326 | ctx.response.render(path.resolve("test_data/template/test1.simple"), { a: 123, b: 456 }); 327 | }); 328 | request(app.server).get("/").expect(200).expect("a = 123
\nb = 456
", done); 329 | }); 330 | }); 331 | 332 | describe("Context", function () { 333 | it("ctx.request.ctx 和 ctx.response.ctx", function (done) { 334 | const app = new Application(); 335 | app.use("/", function (ctx) { 336 | expect(ctx.request.ctx).to.equal(ctx); 337 | expect(ctx.response.ctx).to.equal(ctx); 338 | ctx.response.end("ok"); 339 | }); 340 | request(app.server).get("/hello").expect(200).expect("ok", done); 341 | }); 342 | 343 | it("支持 ctx.onFinsh()", function (done) { 344 | const app = new Application(); 345 | let isFinish = false; 346 | app.use("/", function (ctx) { 347 | ctx.onFinish(() => (isFinish = true)); 348 | ctx.next(); 349 | }); 350 | app.use("/", function (ctx) { 351 | ctx.response.end("ok"); 352 | }); 353 | request(app.server) 354 | .get("/hello") 355 | .expect(200) 356 | .expect("ok", (err) => { 357 | expect(isFinish).to.equal(true); 358 | done(err); 359 | }); 360 | }); 361 | 362 | it("支持 ctx.onError() -- throw new Error", function (done) { 363 | const app = new Application(); 364 | let isError = false; 365 | app.use("/", function (ctx) { 366 | ctx.onError((err) => { 367 | isError = true; 368 | expect(err).to.instanceof(Error); 369 | expect((err as any).message).to.equal("haha"); 370 | }); 371 | ctx.next(); 372 | }); 373 | app.use("/", async function (ctx) { 374 | throw new Error("haha"); 375 | }); 376 | request(app.server) 377 | .get("/hello") 378 | .expect(500, (err) => { 379 | expect(isError).to.equal(true); 380 | done(err); 381 | }); 382 | }); 383 | 384 | it("支持 ctx.onError() -- ctx.next(new Error())", function (done) { 385 | const app = new Application(); 386 | let isError = false; 387 | app.use("/", function (ctx) { 388 | ctx.onError((err) => { 389 | isError = true; 390 | expect(err).to.instanceof(Error); 391 | expect((err as any).message).to.equal("haha"); 392 | }); 393 | ctx.next(); 394 | }); 395 | app.use("/", function (ctx) { 396 | ctx.next(new Error("haha")); 397 | }); 398 | request(app.server) 399 | .get("/hello") 400 | .expect(500, (err) => { 401 | expect(isError).to.equal(true); 402 | done(err); 403 | }); 404 | }); 405 | }); 406 | --------------------------------------------------------------------------------