├── 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 |

<%= msg %>

2 | -------------------------------------------------------------------------------- /test_data/template/test1.pug: -------------------------------------------------------------------------------- 1 | p= 'a = ' + a 2 | p= 'b = ' + b -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: y4ZetDJKbReScCGiHONOL0xIqwffFT1rl 2 | -------------------------------------------------------------------------------- /test_data/template/test1.simple: -------------------------------------------------------------------------------- 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 4 | */ 5 | 6 | import { createServer } from "http"; 7 | 8 | const server = createServer(function (req, res) { 9 | res.end("hello, world"); 10 | }); 11 | server.listen(3000, () => { 12 | console.log("listen on http://localhost:3000"); 13 | }); 14 | -------------------------------------------------------------------------------- /tsconfig.typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 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/lib" 15 | ] 16 | } -------------------------------------------------------------------------------- /src/example/hello2.ts: -------------------------------------------------------------------------------- 1 | import * as web from "../lib"; 2 | 3 | const app = new web.Connect(); 4 | app.templateEngine.initEjs(); 5 | 6 | app.router.get("/", async function (ctx) { 7 | ctx.response.render("index", { msg: "hello, world" }); 8 | }); 9 | 10 | app.router.get("/err", async function (ctx) { 11 | throw new Error(`haha ${new Date()}`); 12 | }); 13 | 14 | app.listen({ port: 3000 }); 15 | -------------------------------------------------------------------------------- /src/lib/module/simple.random.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置模块 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import * as crypto from "crypto"; 7 | 8 | export function randomString(len: number = 16): string { 9 | return crypto.randomBytes(len / 2).toString("hex"); 10 | } 11 | 12 | export function generateSessionId(): string { 13 | return randomString(32); 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 3 | * @author Zongmin Lei 4 | */ 5 | 6 | export * from "./define"; 7 | export * from "./utils"; 8 | export * from "./core"; 9 | export * from "./application"; 10 | export * from "./router"; 11 | export * from "./context"; 12 | export * from "./request"; 13 | export * from "./response"; 14 | 15 | import * as component from "./component"; 16 | export { component }; 17 | -------------------------------------------------------------------------------- /src/lib/component/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置中间件 3 | * @author Zongmin Lei 4 | */ 5 | 6 | export * from "./cookie"; 7 | export * from "./static"; 8 | export * from "./favicon"; 9 | export * from "./cors"; 10 | 11 | export * from "./session"; 12 | export * from "./session.memory"; 13 | export * from "./session.redis"; 14 | 15 | import * as bodyParser from "./body"; 16 | export { bodyParser }; 17 | 18 | export * from "./json.parser"; 19 | 20 | export * from "./proxy"; 21 | -------------------------------------------------------------------------------- /src/benchmark_test/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source="${BASH_SOURCE[0]}" 4 | dir=$(dirname $source) 5 | server="$1" 6 | url="$2" 7 | export NODE_ENV=production 8 | node --require 'ts-node/register' $dir/$server.ts & 9 | pid=$! 10 | echo "server pid: $pid" 11 | 12 | sleep 10 13 | 14 | echo "testing..." 15 | wrk "http://127.0.0.1:3000/$url" \ 16 | -d 2m \ 17 | -c 400 \ 18 | -t 10 \ 19 | | grep 'Requests/sec' \ 20 | | awk -v server="$server" '{ print $2 " Requests/sec - " server }' >> $dir/results.txt 21 | 22 | echo "finish" 23 | echo "kill $pid" 24 | kill $pid 25 | -------------------------------------------------------------------------------- /src/benchmark_test/connect.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 性能测试 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { Application } from "../lib"; 7 | 8 | const app = new Application(); 9 | app.use("/params/:a", function (ctx) { 10 | ctx.response.end(`params: ${ctx.request.params.a}`); 11 | }); 12 | app.use("/url", function (ctx) { 13 | ctx.response.end(`url: ${ctx.request.url}`); 14 | }); 15 | app.use("/", function (ctx) { 16 | ctx.response.end("default"); 17 | }); 18 | 19 | app.listen({ port: 3000 }, () => { 20 | console.log("listen on http://localhost:3000"); 21 | }); 22 | -------------------------------------------------------------------------------- /src/lib/component/static.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置中间件 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { Context } from "../context"; 7 | import { MiddlewareHandle } from "../define"; 8 | import { fromClassicalHandle } from "../utils"; 9 | import * as originServeStatic from "serve-static"; 10 | 11 | export interface ServeStaticOptions extends originServeStatic.ServeStaticOptions {} 12 | 13 | export function serveStatic(root: string, options: ServeStaticOptions = {}): MiddlewareHandle { 14 | return fromClassicalHandle(originServeStatic(root, options) as any); 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/component/favicon.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置中间件 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import * as path from "path"; 7 | import { Context } from "../context"; 8 | import { MiddlewareHandle } from "../define"; 9 | import * as send from "send"; 10 | 11 | export function favicon(filePath: string, options?: send.SendOptions): MiddlewareHandle { 12 | filePath = path.resolve(filePath); 13 | return function (ctx: Context) { 14 | if (ctx.request.path === "/favicon.ico") { 15 | ctx.response.file(filePath, options); 16 | return; 17 | } 18 | ctx.next(); 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/benchmark_test/router.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 性能测试 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { Application, Router } from "../lib"; 7 | 8 | const app = new Application(); 9 | const router = new Router(); 10 | router.get("/params/:a", function (ctx) { 11 | ctx.response.end(`params: ${ctx.request.params.a}`); 12 | }); 13 | router.get("/url", function (ctx) { 14 | ctx.response.end(`url: ${ctx.request.url}`); 15 | }); 16 | router.get("/", function (ctx) { 17 | ctx.response.end("default"); 18 | }); 19 | 20 | app.use("/", router); 21 | app.listen({ port: 3000 }, () => { 22 | console.log("listen on http://localhost:3000"); 23 | }); 24 | -------------------------------------------------------------------------------- /src/lib/component/cookie.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置中间件 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import * as originCookieParser from "cookie-parser"; 7 | import { Context } from "../context"; 8 | import { MiddlewareHandle } from "../define"; 9 | 10 | export interface CookieParserOptions extends originCookieParser.CookieParseOptions {} 11 | 12 | export function cookieParser(secret?: string, options: CookieParserOptions = {}): MiddlewareHandle { 13 | const handler = originCookieParser(secret, options); 14 | return function (ctx: Context) { 15 | handler(ctx.request.req as any, ctx.response.res as any, (err?: any) => ctx.next(err)); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/module/on.writehead.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置模块 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { ServerResponse } from "http"; 7 | 8 | export const WRITE_HEAD = Symbol("origin res.writeHead"); 9 | export const ON_WRITE_HEAD = Symbol("on writeHead event"); 10 | 11 | function onWriteHead(res: ServerResponse, callback: () => void) { 12 | (res as any).once(ON_WRITE_HEAD, callback); 13 | if (WRITE_HEAD in res) return; 14 | (res as any)[WRITE_HEAD] = res.writeHead; 15 | res.writeHead = function (...args: any[]) { 16 | (this as any).emit(ON_WRITE_HEAD); 17 | return (this as any)[WRITE_HEAD](...args); 18 | }; 19 | } 20 | 21 | export default onWriteHead; 22 | -------------------------------------------------------------------------------- /src/lib/module/simple.template.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置模块 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import * as fs from "fs"; 7 | import { TemplateRenderFileCallback, TemplateRenderData } from "../define"; 8 | 9 | export function render(template: string, data: Record) { 10 | return template.replace(/\{\{(.*?)\}\}/g, (match, key) => data[key]); 11 | } 12 | 13 | export function renderFile(fileName: string, data: TemplateRenderData, callback: TemplateRenderFileCallback) { 14 | fs.readFile(fileName, (err, ret) => { 15 | if (err) return callback(new Error(`SimpleTemplate: renderFile fail: ${err.message}`)); 16 | callback(null, render(ret.toString(), data)); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/test/component/component.favicon.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { Application, component } from "../../lib"; 4 | import * as request from "supertest"; 5 | import { expect } from "chai"; 6 | 7 | function readFile(file: string): Promise { 8 | return new Promise((resolve, reject) => { 9 | fs.readFile(file, (err, ret) => { 10 | if (err) return reject(err); 11 | resolve(ret); 12 | }); 13 | }); 14 | } 15 | 16 | const ROOT_DIR = path.resolve(__dirname, "../../.."); 17 | 18 | describe("component.favicon", function () { 19 | it("favicon", async function () { 20 | const file = path.resolve(ROOT_DIR, "test_data/favicon.ico"); 21 | const filedata = await readFile(file); 22 | const app = new Application(); 23 | app.use("/", component.serveStatic(ROOT_DIR)); 24 | app.use("/", component.favicon(file)); 25 | await request(app.server) 26 | .get("/favicon.ico") 27 | .expect("content-type", "image/x-icon") 28 | .expect(200) 29 | .expect((res) => { 30 | expect(res.body).to.deep.equal(filedata); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2021 老雷 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - name: Start Redis 29 | uses: supercharge/redis-github-action@1.2.0 30 | with: 31 | redis-version: ${{ matrix.redis-version }} 32 | - run: npm install 33 | - run: npm run build --if-present 34 | - run: npm run test-cov 35 | - run: npm run coveralls 36 | -------------------------------------------------------------------------------- /src/example/web.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 示例代码 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import * as base from "../lib"; 7 | export * from "../lib"; 8 | 9 | export type MiddlewareHandle = (ctx: Context, err?: base.ErrorReason) => Promise | void; 10 | 11 | export class Application extends base.Application { 12 | protected contextConstructor = Context; 13 | } 14 | 15 | export class Router extends base.Router { 16 | protected contextConstructor = Context; 17 | } 18 | 19 | export class Context extends base.Context { 20 | protected requestConstructor = Request; 21 | protected responseConstructor = Response; 22 | } 23 | 24 | export class Request extends base.Request { 25 | // 扩展 Request 26 | public get remoteIP() { 27 | return String( 28 | this.req.headers["x-real-ip"] || this.req.headers["x-forwarded-for"] || this.req.socket.remoteAddress, 29 | ); 30 | } 31 | } 32 | 33 | export class Response extends base.Response { 34 | // 扩展 Response 35 | public ok(data: any) { 36 | this.json({ data }); 37 | } 38 | public error(error: string) { 39 | this.json({ error }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | dist 61 | benchmark 62 | .vscode 63 | docs 64 | wiki 65 | -------------------------------------------------------------------------------- /src/example/test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 示例代码 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { Application, fromClassicalHandle, Router } from "../lib"; 7 | 8 | function sleep(ms = 1000): Promise { 9 | return new Promise((resolve, reject) => { 10 | setTimeout(() => resolve(), ms); 11 | }); 12 | } 13 | 14 | const app = new Application(); 15 | app.use( 16 | "/sleep", 17 | function (ctx) { 18 | console.log(ctx.request.method, ctx.request.url); 19 | ctx.next(); 20 | }, 21 | async function (ctx) { 22 | console.log(ctx.request.query, ctx.request.hasBody(), ctx.request.body); 23 | await sleep(1000); 24 | ctx.next(); 25 | }, 26 | ); 27 | app.use( 28 | "/", 29 | fromClassicalHandle(function (req, res, next) { 30 | console.log(req.headers); 31 | next(); 32 | }), 33 | ); 34 | 35 | const router = new Router(); 36 | router.use("/", function (ctx) { 37 | console.log("router", ctx.request.method, ctx.request.url); 38 | ctx.next(); 39 | }); 40 | router.get("/hello/:a/:b", function (ctx) { 41 | ctx.response.setHeader("content-type", "application/json"); 42 | ctx.response.end(JSON.stringify(ctx.request.params)); 43 | }); 44 | app.use("/", router); 45 | 46 | console.log(app); 47 | app.listen({ port: 3000 }, () => console.log("listening...")); 48 | -------------------------------------------------------------------------------- /src/example/session.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 示例代码 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { Application, component } from "../lib"; 7 | import * as Redis from "ioredis"; 8 | import { createClient } from "redis"; 9 | import { SimpleRedisClient } from "../lib/module/simple.redis"; 10 | 11 | const redis1 = new Redis(); 12 | const redis2 = createClient(); 13 | const redis3 = new SimpleRedisClient(); 14 | 15 | const app = new Application(); 16 | app.use("/", component.cookieParser("")); 17 | app.use( 18 | "/a", 19 | component.session({ 20 | store: new component.SessionRedisStore({ client: redis1 as any }), 21 | maxAge: 60000, 22 | }), 23 | ); 24 | app.use( 25 | "/b", 26 | component.session({ 27 | store: new component.SessionRedisStore({ client: redis2 }), 28 | maxAge: 60000, 29 | }), 30 | ); 31 | app.use( 32 | "/c", 33 | component.session({ 34 | store: new component.SessionRedisStore({ client: redis3 }), 35 | maxAge: 60000, 36 | }), 37 | ); 38 | 39 | app.use("/", async (ctx) => { 40 | // console.log(ctx.session); 41 | console.log(ctx.request.cookies); 42 | console.log(ctx.request.signedCookies); 43 | ctx.session.data.c = ctx.session.data.c || 0; 44 | ctx.session.data.c++; 45 | // await ctx.session.reload(); 46 | ctx.response.json(ctx.session.data); 47 | }); 48 | 49 | app.listen({ port: 3000 }, () => console.log("listening...")); 50 | -------------------------------------------------------------------------------- /src/lib/component/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置中间件 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import * as path from "path"; 7 | import { Context } from "../context"; 8 | import { MiddlewareHandle } from "../define"; 9 | import { proxyRequest, ProxyTarget, parseProxyTarget } from "../module/proxy.request"; 10 | 11 | /** 12 | * 代理中间件选项 13 | */ 14 | export interface ProxyOptions { 15 | /** 目标地址 */ 16 | target: string | ProxyTarget; 17 | /** 要删除的请求头,默认是 ["host"] */ 18 | removeHeaderNames?: string[]; 19 | } 20 | 21 | /** 默认要删除的代理请求头 */ 22 | export const DEFAULT_PROXY_REMOVE_HEADER_NAMES = ["host"]; 23 | 24 | /** 25 | * proxy 中间件 26 | * 27 | * @param options 28 | */ 29 | export function proxy(options: ProxyOptions): MiddlewareHandle { 30 | const baseTarget = typeof options.target === "string" ? parseProxyTarget(options.target) : { ...options.target }; 31 | const removeHeaderNames = options.removeHeaderNames || DEFAULT_PROXY_REMOVE_HEADER_NAMES; 32 | return function (ctx: Context) { 33 | const originalHeaders = { ...ctx.request.headers }; 34 | for (const n of removeHeaderNames) { 35 | delete originalHeaders[n]; 36 | } 37 | const target: ProxyTarget = { 38 | ...baseTarget, 39 | path: path.join(baseTarget.path, ctx.request.url), 40 | headers: { ...originalHeaders, ...baseTarget.headers }, 41 | }; 42 | proxyRequest(ctx.request.req, ctx.response.res, target).catch((err) => { 43 | ctx.next(err); 44 | }); 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/module/response.gzip.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from "http"; 2 | import { createGzip, createDeflate, createDeflateRaw, Gzip, Deflate, DeflateRaw } from "zlib"; 3 | import { Readable } from "stream"; 4 | 5 | /** 6 | * @leizm/web 中间件基础框架 - 内置模块 7 | * @author Zongmin Lei 8 | */ 9 | 10 | /** 11 | * 响应压缩的内容 12 | * 13 | * @param req 14 | * @param res 15 | * @param data 16 | * @param contentType 17 | */ 18 | export function responseGzip( 19 | req: IncomingMessage, 20 | res: ServerResponse, 21 | data: string | Buffer | Readable, 22 | contentType?: string, 23 | ) { 24 | let zlibStream: Gzip | Deflate | DeflateRaw; 25 | let encoding = req.headers["content-encoding"]; 26 | switch (encoding) { 27 | case "gzip": 28 | zlibStream = createGzip(); 29 | break; 30 | case "deflate": 31 | zlibStream = createDeflate(); 32 | break; 33 | case "deflate-raw": 34 | zlibStream = createDeflateRaw(); 35 | break; 36 | default: 37 | encoding = "gzip"; 38 | zlibStream = createGzip(); 39 | } 40 | if (contentType) { 41 | res.setHeader("Content-Type", contentType); 42 | } 43 | res.setHeader("Content-Encoding", encoding); 44 | zlibStream.pipe(res); 45 | if (typeof data === "string") { 46 | zlibStream.end(data); 47 | } else if (Buffer.isBuffer(data)) { 48 | zlibStream.end(data); 49 | } else { 50 | data.pipe(zlibStream); 51 | } 52 | zlibStream.on("error", (err) => { 53 | res.writeHead(500, { "Content-Type": "text/plain" }); 54 | res.end(`ctx.response.gzip(): ${err.message}`); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/test/core/parseurl.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 单元测试 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { parseUrl, ParsedUrl } from "../../lib/parse_url"; 7 | import { expect } from "chai"; 8 | 9 | describe("parseUrl", function () { 10 | it("relative url, query=false", function () { 11 | expect(parseUrl("/abc/efg?a=1&b=2#345")).to.deep.eq({ 12 | hash: "#345", 13 | host: "", 14 | hostname: "", 15 | password: "", 16 | path: "/abc/efg?a=1&b=2", 17 | pathname: "/abc/efg", 18 | port: "", 19 | protocol: "", 20 | query: null, 21 | search: "?a=1&b=2", 22 | username: "", 23 | } as ParsedUrl); 24 | }); 25 | 26 | it("relative url, query=true", function () { 27 | expect(parseUrl("/abc/efg?a=1&b=2#345", { query: true })).to.deep.eq({ 28 | hash: "#345", 29 | host: "", 30 | hostname: "", 31 | password: "", 32 | path: "/abc/efg?a=1&b=2", 33 | pathname: "/abc/efg", 34 | port: "", 35 | protocol: "", 36 | query: { a: "1", b: "2" }, 37 | search: "?a=1&b=2", 38 | username: "", 39 | } as ParsedUrl); 40 | }); 41 | 42 | it("absolute url", function () { 43 | expect(parseUrl("http://example.com:8080/abc/efg?a=1&b=2#345", { absolute: true })).to.deep.eq({ 44 | hash: "#345", 45 | host: "example.com:8080", 46 | hostname: "example.com", 47 | password: "", 48 | path: "/abc/efg?a=1&b=2", 49 | pathname: "/abc/efg", 50 | port: "8080", 51 | protocol: "http:", 52 | query: null, 53 | search: "?a=1&b=2", 54 | username: "", 55 | } as ParsedUrl); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/lib/parse_url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置模块 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { IncomingMessage } from "http"; 7 | import * as qs from "qs"; 8 | 9 | export interface ParsedUrl { 10 | hash: string; 11 | host: string; 12 | hostname: string; 13 | password: string; 14 | path: string; 15 | pathname: string; 16 | port: string; 17 | protocol: string; 18 | search: string; 19 | query: Record | null; 20 | username: string; 21 | } 22 | 23 | const relativeUrlBase = "http://relative-url"; 24 | 25 | export interface IParseUrlOptions { 26 | /** request instance */ 27 | req?: IncomingMessage; 28 | /** the input url is absolute, defaults to false */ 29 | absolute?: boolean; 30 | /** parse query string, defaults to false */ 31 | query?: boolean; 32 | } 33 | 34 | export function parseUrl(url: string, options: IParseUrlOptions = {}): ParsedUrl { 35 | const absolute = !!(options.absolute || options.req); 36 | const base = 37 | options.req && options.req.headers && options.req.headers.host 38 | ? `http://${options.req.headers.host}` 39 | : relativeUrlBase; 40 | const info = new URL(url, base); 41 | return { 42 | protocol: absolute ? info.protocol : "", 43 | host: absolute ? info.host : "", 44 | hostname: absolute ? info.hostname : "", 45 | port: absolute ? info.port : "", 46 | username: info.username, 47 | password: info.password, 48 | path: info.pathname + info.search, 49 | pathname: info.pathname, 50 | search: info.search, 51 | query: options.query ? (info.search ? qs.parse(info.search.slice(1)) : {}) : null, 52 | hash: info.hash, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/component/session.memory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置中间件 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { 7 | SessionStore, 8 | DEFAULT_SESSION_SERIALIZE as serialize, 9 | DEFAULT_SESSION_DESERIALIZE as deserialize, 10 | } from "./session"; 11 | 12 | export class SessionMemoryStore implements SessionStore { 13 | protected data: Map< 14 | string, 15 | { 16 | expires: Date; 17 | value: string; 18 | } 19 | > = new Map(); 20 | 21 | protected isExpired(expires: Date): boolean { 22 | return expires.getTime() < Date.now(); 23 | } 24 | 25 | public get(sid: string): Promise> { 26 | return new Promise((resolve, reject) => { 27 | const data = this.data.get(sid); 28 | if (!data) return resolve({}); 29 | if (this.isExpired(data.expires)) { 30 | this.data.delete(sid); 31 | return resolve({}); 32 | } 33 | resolve(deserialize(data.value)); 34 | }); 35 | } 36 | 37 | public set(sid: string, data: Record, maxAge: number): Promise { 38 | return new Promise((resolve, reject) => { 39 | const expires = new Date(Date.now() + maxAge); 40 | this.data.set(sid, { expires, value: serialize(data) }); 41 | resolve(); 42 | }); 43 | } 44 | 45 | public destroy(sid: string): Promise { 46 | return new Promise((resolve, reject) => { 47 | this.data.delete(sid); 48 | resolve(); 49 | }); 50 | } 51 | 52 | public touch(sid: string, maxAge: number): Promise { 53 | return new Promise((resolve, reject) => { 54 | const data = this.data.get(sid); 55 | if (data) { 56 | data.expires = new Date(data.expires.getTime() + maxAge); 57 | this.data.set(sid, data); 58 | } 59 | resolve(); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/lib/component/json.parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置中间件 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { Context } from "../context"; 7 | import { MiddlewareHandle } from "../define"; 8 | 9 | export interface JsonParserOptions { 10 | /** 11 | * 允许的最大body字节,当超过此值时将停止解析,并返回错误 12 | */ 13 | limit: number; 14 | } 15 | 16 | export const DEFAULT_JSON_PARSER_OPTIONS: JsonParserOptions = { 17 | /* 默认100k */ 18 | limit: 102400, 19 | }; 20 | 21 | /** 22 | * 快速的 JSON Body 解析中间件 23 | * @param options 选项 24 | */ 25 | export function jsonParser(options: Partial = {}): MiddlewareHandle { 26 | const opts: JsonParserOptions = { ...DEFAULT_JSON_PARSER_OPTIONS, ...options }; 27 | 28 | return function (ctx) { 29 | if (ctx.request.method === "GET" || ctx.request.method === "HEAD") return ctx.next(); 30 | if (String(ctx.request.headers["content-type"]).indexOf("application/json") === -1) return ctx.next(); 31 | 32 | const list: Buffer[] = []; 33 | let size = 0; 34 | let isAborted = false; 35 | 36 | function checkLimit() { 37 | if (size > opts.limit) { 38 | ctx.request.req.pause(); 39 | isAborted = true; 40 | ctx.next(new Error(`jsonParser: out of max body size limit`)); 41 | } 42 | } 43 | 44 | ctx.request.req.on("data", (chunk: Buffer) => { 45 | list.push(chunk); 46 | size += chunk.length; 47 | checkLimit(); 48 | }); 49 | 50 | ctx.request.req.on("end", () => { 51 | checkLimit(); 52 | if (isAborted) return; 53 | 54 | const buf = Buffer.concat(list); 55 | try { 56 | const json = JSON.parse(buf.toString()); 57 | ctx.request.body = json; 58 | return ctx.next(); 59 | } catch (err) { 60 | return ctx.next(new Error(`jsonParser: ${err.message}`)); 61 | } 62 | }); 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/lib/module/body.parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置模块 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { IncomingMessage } from "http"; 7 | import { Readable } from "stream"; 8 | import { createInflate, createGunzip } from "zlib"; 9 | 10 | /** 11 | * 读取指定 Stream 的所有内容 12 | * @param stream 13 | * @param limit 14 | */ 15 | export function readAllBody( 16 | stream: Readable, 17 | limit: number, 18 | ): Promise<{ status?: number; error?: Error; data?: Buffer }> { 19 | return new Promise((resolve, reject) => { 20 | const list: Buffer[] = []; 21 | let length = 0; 22 | let isBreak = false; 23 | stream.on("data", (chunk: Buffer) => { 24 | if (isBreak) { 25 | stream.pause(); 26 | return; 27 | } 28 | list.push(chunk); 29 | length += chunk.length; 30 | checkLength(); 31 | }); 32 | stream.on("end", () => { 33 | if (isBreak) return; 34 | checkLength(); 35 | resolve({ data: Buffer.concat(list) }); 36 | }); 37 | function checkLength() { 38 | if (length > limit) { 39 | isBreak = true; 40 | return resolve({ status: 413, error: new Error(`out of max body size limit`) }); 41 | } 42 | } 43 | }); 44 | } 45 | 46 | /** 47 | * 获得 Request 的 Stream 48 | * @param req 49 | */ 50 | export function getContentStream(req: IncomingMessage) { 51 | const encoding = (req.headers["content-encoding"] || "identity").toLowerCase(); 52 | const length = req.headers["content-length"]; 53 | switch (encoding) { 54 | case "deflate": 55 | return { stream: req.pipe(createInflate()), length }; 56 | case "gzip": 57 | return { stream: req.pipe(createGunzip()), length }; 58 | case "identity": 59 | return { stream: req, length }; 60 | default: 61 | return { status: 415, error: new Error(`unsupported content encoding ${encoding}`) }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ main ] 9 | schedule: 10 | - cron: '0 14 * * 6' 11 | 12 | jobs: 13 | analyse: 14 | name: Analyse 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v1 33 | # Override language selection by uncommenting this and choosing your languages 34 | # with: 35 | # languages: go, javascript, csharp, python, cpp, java 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | - name: Autobuild 40 | uses: github/codeql-action/autobuild@v1 41 | 42 | # ℹ️ Command-line programs to run using the OS shell. 43 | # 📚 https://git.io/JvXDl 44 | 45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 46 | # and modify them (or add more) to build your code if your project 47 | # uses a compiled language 48 | 49 | #- run: | 50 | # make bootstrap 51 | # make release 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v1 55 | -------------------------------------------------------------------------------- /src/test/component/component.cookie.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Application, component } from "../../lib"; 3 | import * as request from "supertest"; 4 | import { sign as signCookie } from "cookie-signature"; 5 | 6 | describe("component.cookie", function () { 7 | it("解析一般的Cookie", function (done) { 8 | const app = new Application(); 9 | app.use("/", component.cookieParser("test")); 10 | app.use("/", function (ctx) { 11 | expect(ctx.request.cookies).to.deep.equal({ 12 | a: "123", 13 | b: "今天的天气真好", 14 | }); 15 | expect(ctx.request.signedCookies).to.deep.equal({}); 16 | ctx.response.cookie("c", { x: 1, y: 2 }); 17 | ctx.response.end("ok"); 18 | }); 19 | request(app.server) 20 | .get("/hello") 21 | .set("cookie", `a=${encodeURIComponent("123")}; b=${encodeURIComponent("今天的天气真好")}`) 22 | .expect(200) 23 | .expect("Set-Cookie", "c=j%3A%7B%22x%22%3A1%2C%22y%22%3A2%7D; Path=/") 24 | .expect("ok", done); 25 | }); 26 | 27 | it("解析签名的Cookie", function (done) { 28 | const app = new Application(); 29 | app.use("/", component.cookieParser("test")); 30 | app.use("/", function (ctx) { 31 | // console.log(ctx.request.cookies, ctx.request.signedCookies); 32 | expect(ctx.request.cookies).to.deep.equal({}); 33 | expect(ctx.request.signedCookies).to.deep.equal({ 34 | a: "123", 35 | b: "今天的天气真好", 36 | }); 37 | ctx.response.cookie("c", { x: 1, y: 2 }, { signed: true }); 38 | ctx.response.end("ok"); 39 | }); 40 | request(app.server) 41 | .get("/hello") 42 | .set( 43 | "cookie", 44 | `a=s:${encodeURIComponent(signCookie("123", "test"))}; b=s:${encodeURIComponent( 45 | signCookie("今天的天气真好", "test"), 46 | )}`, 47 | ) 48 | .expect(200) 49 | .expect( 50 | "Set-Cookie", 51 | `c=${encodeURIComponent(`s:${signCookie(`j:${JSON.stringify({ x: 1, y: 2 })}`, "test")}`)}; Path=/`, 52 | ) 53 | .expect("ok", done); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/lib/module/proxy.request.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置模块 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { IncomingMessage, ServerResponse, OutgoingHttpHeaders, request as httpRequest } from "http"; 7 | import { request as httpsRequest } from "https"; 8 | import { parseUrl } from "../parse_url"; 9 | 10 | export interface ProxyTarget { 11 | /** 协议,默认 http: */ 12 | protocol?: "http:" | "https:"; 13 | /** 端口,默认 80 */ 14 | port?: string | number; 15 | /** 地址 */ 16 | hostname: string; 17 | /** 路径 */ 18 | path: string; 19 | /** 额外的请求头 */ 20 | headers?: OutgoingHttpHeaders; 21 | /** 请求超时时间 ms */ 22 | timeout?: number; 23 | } 24 | 25 | /** 26 | * 解析 URL 27 | * 28 | * @param url 29 | */ 30 | export function parseProxyTarget(url: string): ProxyTarget { 31 | const a = parseUrl(url, { absolute: true }); 32 | return { 33 | protocol: (a.protocol || "http:") as any, 34 | port: a.port || (a.protocol === "https:" ? 443 : 80), 35 | hostname: a.hostname, 36 | path: a.path, 37 | }; 38 | } 39 | 40 | /** 41 | * 代理 HTTP 请求 42 | * 43 | * @param req 44 | * @param res 45 | * @param target 46 | */ 47 | export function proxyRequest(req: IncomingMessage, res: ServerResponse, target: string | ProxyTarget): Promise { 48 | return new Promise((resolve, reject) => { 49 | req.on("error", (err) => reject(err)); 50 | res.on("error", (err) => reject(err)); 51 | const formattedTarget: ProxyTarget = typeof target === "string" ? parseProxyTarget(target) : target; 52 | const remoteReq = (formattedTarget.protocol === "https:" ? httpsRequest : httpRequest)( 53 | { 54 | method: req.method, 55 | ...formattedTarget, 56 | headers: { ...formattedTarget.headers }, 57 | timeout: formattedTarget.timeout, 58 | }, 59 | (remoteRes) => { 60 | remoteRes.on("error", (err) => reject(err)); 61 | res.writeHead(remoteRes.statusCode || 200, remoteRes.headers); 62 | remoteRes.on("data", (chunk) => res.write(chunk)); 63 | remoteRes.on("end", () => { 64 | res.end(); 65 | resolve(); 66 | }); 67 | }, 68 | ); 69 | remoteReq.on("error", (err) => reject(err)); 70 | if (req.method === "GET" || req.method === "HEAD") { 71 | remoteReq.end(); 72 | } else { 73 | req.pipe(remoteReq); 74 | } 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /src/test/component/component.static.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { Application, component } from "../../lib"; 4 | import * as request from "supertest"; 5 | 6 | function readFile(file: string): Promise { 7 | return new Promise((resolve, reject) => { 8 | fs.readFile(file, (err, ret) => { 9 | if (err) return reject(err); 10 | resolve(ret); 11 | }); 12 | }); 13 | } 14 | 15 | const ROOT_DIR = path.resolve(__dirname, "../../.."); 16 | 17 | describe("component.static", function () { 18 | it("作为根路径", async function () { 19 | const file1 = path.resolve(ROOT_DIR, "package.json"); 20 | const file1data = (await readFile(file1)).toString(); 21 | const file2 = path.resolve(ROOT_DIR, "README.md"); 22 | const file2data = (await readFile(file2)).toString(); 23 | const app = new Application(); 24 | app.use("/", component.serveStatic(ROOT_DIR)); 25 | await request(app.server) 26 | .get("/package.json") 27 | .expect("content-type", "application/json; charset=UTF-8") 28 | .expect(200, file1data); 29 | await request(app.server) 30 | .get("/README.md") 31 | .expect("content-type", "text/markdown; charset=UTF-8") 32 | .expect(200, file2data); 33 | }); 34 | 35 | it("作为二级子路径", async function () { 36 | const file1 = path.resolve(ROOT_DIR, "package.json"); 37 | const file1data = (await readFile(file1)).toString(); 38 | const file2 = path.resolve(ROOT_DIR, "README.md"); 39 | const file2data = (await readFile(file2)).toString(); 40 | const app = new Application(); 41 | app.use("/public", component.serveStatic(ROOT_DIR)); 42 | await request(app.server) 43 | .get("/public/package.json") 44 | .expect(200, file1data) 45 | .expect("content-type", "application/json; charset=UTF-8"); 46 | await request(app.server) 47 | .get("/public/README.md") 48 | .expect(200, file2data) 49 | .expect("content-type", "text/markdown; charset=UTF-8"); 50 | }); 51 | 52 | it("作为三级子路径", async function () { 53 | const file1 = path.resolve(ROOT_DIR, "package.json"); 54 | const file1data = (await readFile(file1)).toString(); 55 | const file2 = path.resolve(ROOT_DIR, "README.md"); 56 | const file2data = (await readFile(file2)).toString(); 57 | const app = new Application(); 58 | app.use("/public/assets", component.serveStatic(ROOT_DIR)); 59 | await request(app.server) 60 | .get("/public/assets/package.json") 61 | .expect(200, file1data) 62 | .expect("content-type", "application/json; charset=UTF-8"); 63 | await request(app.server) 64 | .get("/public/assets/README.md") 65 | .expect(200, file2data) 66 | .expect("content-type", "text/markdown; charset=UTF-8"); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/test/core/extends.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 单元测试 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { expect } from "chai"; 7 | import * as request from "supertest"; 8 | import * as bodyParser from "body-parser"; 9 | import { Application, Router, Context, Request, Response, fromClassicalHandle } from "../../lib"; 10 | 11 | //////////////////////////////////////////////////////////////////////// 12 | // 扩展的 Request 对象 13 | class MyRequest extends Request { 14 | public isInited = false; 15 | public getBody() { 16 | return this.body; 17 | } 18 | public inited() { 19 | this.isInited = true; 20 | } 21 | } 22 | 23 | // 扩展的 Response 对象 24 | class MyResponse extends Response { 25 | public isInited = false; 26 | public sendJSON(data: any) { 27 | this.setHeader("content-type", "application/json"); 28 | this.end(JSON.stringify(data)); 29 | } 30 | public inited() { 31 | this.isInited = true; 32 | } 33 | } 34 | 35 | // 扩展 Context 对象 36 | class MyContext extends Context { 37 | protected requestConstructor = MyRequest; 38 | protected responseConstructor = MyResponse; 39 | public isInited = false; 40 | public getHello(msg: string) { 41 | return `hello ${msg}`; 42 | } 43 | public inited() { 44 | this.isInited = true; 45 | } 46 | } 47 | 48 | // 扩展 Application 对象 49 | class MyApplication extends Application { 50 | protected contextConstructor = MyContext; 51 | } 52 | 53 | // 扩展 Router 对象 54 | class MyRouter extends Router { 55 | protected contextConstructor = MyContext; 56 | } 57 | //////////////////////////////////////////////////////////////////////// 58 | 59 | describe("可扩展性", function () { 60 | it("支持扩展 Application", function (done) { 61 | const app = new MyApplication(); 62 | app.use("/", fromClassicalHandle(bodyParser.json() as any)); 63 | app.use("/", function (ctx) { 64 | expect(ctx.getHello("aa")).to.equal("hello aa"); 65 | expect(ctx.request.getBody()).to.deep.equal({ a: 111, b: 222 }); 66 | expect(ctx.isInited).to.equal(true); 67 | expect(ctx.request.isInited).to.equal(true); 68 | expect(ctx.response.isInited).to.equal(true); 69 | ctx.response.sendJSON({ hello: "world" }); 70 | }); 71 | request(app.server).post("/").send({ a: 111, b: 222 }).expect(200).expect({ hello: "world" }, done); 72 | }); 73 | 74 | it("支持扩展 Router", function (done) { 75 | const app = new MyApplication(); 76 | const router = new MyRouter(); 77 | app.use("/", fromClassicalHandle(bodyParser.json() as any)); 78 | router.post("/", function (ctx) { 79 | expect(ctx.getHello("aa")).to.equal("hello aa"); 80 | expect(ctx.request.getBody()).to.deep.equal({ a: 111, b: 222 }); 81 | expect(ctx.isInited).to.equal(true); 82 | expect(ctx.request.isInited).to.equal(true); 83 | expect(ctx.response.isInited).to.equal(true); 84 | ctx.response.sendJSON({ hello: "world" }); 85 | }); 86 | app.use("/", router); 87 | request(app.server).post("/").send({ a: 111, b: 222 }).expect(200).expect({ hello: "world" }, done); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/lib/component/cors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置中间件 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import * as assert from "assert"; 7 | import { parseUrl } from "../parse_url"; 8 | import { Context } from "../context"; 9 | import { MiddlewareHandle } from "../define"; 10 | 11 | export interface CorsOptions { 12 | /** 允许的域名列表 */ 13 | domain?: string[]; 14 | /** 是否允许任意域名,如果为true则总是允许当前请求来源的域名 */ 15 | any?: boolean; 16 | /** 增加额外的响应头 */ 17 | headers?: Record; 18 | /** Access-Control-Max-Age */ 19 | maxAge?: number; 20 | /** Access-Control-Allow-Credentials */ 21 | credentials?: boolean; 22 | /** Access-Control-Allow-Headers 允许的请求头 */ 23 | allowHeaders?: string[]; 24 | /** Access-Control-Allow-Methods 允许的请求方法 */ 25 | allowMethods?: string[]; 26 | } 27 | 28 | export const DEFAULT_CORS_OPTIONS: Required = { 29 | domain: [], 30 | any: false, 31 | headers: {}, 32 | maxAge: 0, 33 | credentials: true, 34 | allowHeaders: ["PUT", "POST", "GET", "HEAD", "DELETE", "OPTIONS", "TRACE"], 35 | allowMethods: ["Origin", "X-Requested-With", "Content-Type", "Content-Length", "Accept", "Authorization", "Cookie"], 36 | }; 37 | 38 | /** 39 | * CORS中间件 40 | */ 41 | export function cors(options: CorsOptions = {}): MiddlewareHandle { 42 | const opts: Required = { ...DEFAULT_CORS_OPTIONS, ...options }; 43 | 44 | if (opts.any) { 45 | assert(opts.any === true, `invalid 'any' option: must be true`); 46 | } else { 47 | assert(Array.isArray(opts.domain), `invalid 'domain' option: must be an array`); 48 | } 49 | 50 | if ("maxAge" in opts) { 51 | opts.headers["Access-Control-Max-Age"] = String(opts.maxAge); 52 | } 53 | if ("credentials" in opts) { 54 | assert(typeof opts.credentials === "boolean", `invalid 'credentials' option: must be true or false`); 55 | opts.headers["Access-Control-Allow-Credentials"] = String(opts.credentials); 56 | } 57 | if ("allowHeaders" in opts) { 58 | assert(Array.isArray(opts.allowHeaders), `invalid 'allowHeaders' option: must be an array`); 59 | opts.headers["Access-Control-Allow-Headers"] = opts.allowHeaders.join(", "); 60 | } 61 | if ("allowMethods" in opts) { 62 | assert(Array.isArray(opts.allowMethods), `invalid 'allowMethods' option: must be an array`); 63 | opts.headers["Access-Control-Allow-Methods"] = opts.allowMethods.join(", "); 64 | } 65 | 66 | function setAdditionalHeaders(ctx: Context) { 67 | if (opts.headers) { 68 | for (const name in opts.headers) { 69 | ctx.response.setHeader(name, opts.headers[name]); 70 | } 71 | } 72 | } 73 | 74 | if (opts.any) { 75 | return function (ctx: Context) { 76 | const origin = ctx.request.headers.origin; 77 | if (origin) { 78 | ctx.response.setHeader("Access-Control-Allow-Origin", origin); 79 | setAdditionalHeaders(ctx); 80 | } 81 | ctx.next(); 82 | }; 83 | } 84 | 85 | return function (ctx: Context) { 86 | const origin = ctx.request.headers.origin; 87 | const info = parseUrl(String(origin), { absolute: true }); 88 | if (origin && opts.domain.indexOf(info.host || "") !== -1) { 89 | ctx.response.setHeader("Access-Control-Allow-Origin", origin); 90 | setAdditionalHeaders(ctx); 91 | } 92 | ctx.next(); 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /src/lib/application.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { Server, IncomingMessage, ServerResponse } from "http"; 7 | import finalHandler from "./final_handler"; 8 | import { Core } from "./core"; 9 | import { Router } from "./router"; 10 | import { ListenOptions, ErrorReason, SYMBOL_APPLICATION, SYMBOL_SERVER } from "./define"; 11 | import { Context } from "./context"; 12 | import { Request } from "./request"; 13 | import { Response } from "./response"; 14 | import { TemplateEngineManager } from "./template"; 15 | import { notifyDeprecated } from "./utils"; 16 | 17 | export class Application> extends Core { 18 | /** 默认Router实例,第一次使用时创建并use() */ 19 | protected defaultRouter?: Router; 20 | 21 | /** http.Server实例 */ 22 | public [SYMBOL_SERVER]: Server; 23 | 24 | /** 模板引擎管理器 */ 25 | public templateEngine: TemplateEngineManager = new TemplateEngineManager(); 26 | 27 | /** 获取默认Router */ 28 | public get router(): Router { 29 | if (!this.defaultRouter) { 30 | this.defaultRouter = new Router(); 31 | this.use("/", this.defaultRouter); 32 | } 33 | return this.defaultRouter; 34 | } 35 | 36 | /** 获取当前http.Server实例 */ 37 | public get server() { 38 | if (!this[SYMBOL_SERVER]) this[SYMBOL_SERVER] = new Server(this.handleRequest.bind(this)); 39 | return this[SYMBOL_SERVER]; 40 | } 41 | 42 | /** 43 | * 监听端口 44 | * 45 | * @param options 监听地址信息 46 | * @param listeningListener 回调函数 47 | */ 48 | public listen(options: ListenOptions, listeningListener?: () => void) { 49 | this.server.listen(options, listeningListener); 50 | } 51 | 52 | /** 53 | * 附加到一个http.Server实例 54 | * 55 | * @param server http.Server实例 56 | */ 57 | public attach(server: Server) { 58 | this[SYMBOL_SERVER] = server; 59 | server.on("request", this.handleRequest.bind(this)); 60 | } 61 | 62 | /** 63 | * 关闭服务器 64 | */ 65 | public async close(): Promise { 66 | return new Promise((resolve, reject) => { 67 | if (this[SYMBOL_SERVER]) { 68 | this[SYMBOL_SERVER]!.close(() => resolve()); 69 | } else { 70 | resolve(); 71 | } 72 | }); 73 | } 74 | 75 | /** 76 | * 处理请求 77 | * 78 | * @param req ServerRequest对象 79 | * @param res ServerResponse对象 80 | * @param done 未处理请求的回调函数 81 | */ 82 | public handleRequest = (req: IncomingMessage, res: ServerResponse, done?: (err?: ErrorReason) => void) => { 83 | done = 84 | done || 85 | function (err?: ErrorReason) { 86 | return finalHandler(req, res)(err); 87 | }; 88 | const ctx = this.createContext(req, res); 89 | this.handleRequestByContext(ctx, done); 90 | }; 91 | 92 | /** 93 | * 创建Context对象 94 | * 95 | * @param req 原始ServerRequest对象 96 | * @param res 原始ServerResponse对象 97 | */ 98 | protected createContext(req: IncomingMessage, res: ServerResponse): C { 99 | const ctx = super.createContext(req, res); 100 | ctx[SYMBOL_APPLICATION] = this as any; 101 | return ctx; 102 | } 103 | } 104 | 105 | export class Connect> extends Application { 106 | constructor() { 107 | super(); 108 | notifyDeprecated("new Connect()", "new Application()", "3.0.0"); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@leizm/web", 3 | "version": "2.7.3", 4 | "description": "现代的 Web 中间件基础框架,完美支持 TypeScript,构建可维护的大型 Web 项目。", 5 | "main": "dist/lib/index.js", 6 | "typings": "dist/lib/index.d.ts", 7 | "files": [ 8 | "dist/lib" 9 | ], 10 | "scripts": { 11 | "test": "npm run format && mocha --require ts-node/register --exit \"src/test/**/*.ts\"", 12 | "test-file": "mocha --require ts-node/register --exit", 13 | "test-core": "mocha --require ts-node/register --exit \"src/test/core/**/*.ts\"", 14 | "test-component": "mocha --require ts-node/register --exit \"src/test/component/**/*.ts\"", 15 | "test-cov": "nyc --reporter=lcov mocha --require ts-node/register --exit \"src/test/**/*.ts\" && nyc report", 16 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 17 | "clean": "rm -rf dist", 18 | "compile": "npm run clean && tsc", 19 | "prepublish": "npm run test && npm run compile", 20 | "format": "prettier --write \"src/**/*.ts\"", 21 | "docs": "typedoc --out ./docs --tsconfig tsconfig.typedoc.json", 22 | "update-wiki": "cd wiki && git pull && node update_index.js && git add . && git commit -m \"update\" && git push" 23 | }, 24 | "nyc": { 25 | "extension": [ 26 | ".ts" 27 | ], 28 | "exclude": [ 29 | "**/*.d.ts" 30 | ] 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/leizongmin/leizm-web.git" 35 | }, 36 | "keywords": [ 37 | "connect", 38 | "express", 39 | "koa", 40 | "restify", 41 | "micro", 42 | "http", 43 | "rawnode", 44 | "hapi", 45 | "feathers", 46 | "sails", 47 | "restful" 48 | ], 49 | "author": "Zongmin Lei ", 50 | "license": "MIT", 51 | "bugs": { 52 | "url": "https://github.com/leizongmin/leizm-web/issues" 53 | }, 54 | "homepage": "https://github.com/leizongmin/leizm-web#readme", 55 | "dependencies": { 56 | "@types/body-parser": "^1.19.1", 57 | "@types/busboy": "^0.2.4", 58 | "@types/cookie": "^0.4.1", 59 | "@types/cookie-parser": "^1.4.1", 60 | "@types/cookie-signature": "^1.0.1", 61 | "@types/mime": "^2.0.1", 62 | "@types/qs": "^6.9.7", 63 | "@types/send": "^0.17.1", 64 | "body-parser": "^1.19.0", 65 | "busboy": "^0.3.1", 66 | "cookie": "^0.4.1", 67 | "cookie-parser": "^1.4.5", 68 | "cookie-signature": "^1.1.0", 69 | "mime": "^2.4.4", 70 | "path-to-regexp": "^6.2.0", 71 | "qs": "^6.9.4", 72 | "send": "^0.17.1", 73 | "serve-static": "^1.14.1" 74 | }, 75 | "devDependencies": { 76 | "@types/chai": "^4.2.21", 77 | "@types/connect": "^3.4.35", 78 | "@types/ejs": "^3.0.7", 79 | "@types/ioredis": "^4.26.6", 80 | "@types/mocha": "^8.2.3", 81 | "@types/node": "^16.3.3", 82 | "@types/nunjucks": "^3.1.5", 83 | "@types/pug": "^2.0.5", 84 | "@types/redis": "^2.8.31", 85 | "@types/supertest": "^2.0.8", 86 | "chai": "^4.2.0", 87 | "connect": "^3.7.0", 88 | "coveralls": "^3.1.1", 89 | "ejs": "^3.0.2", 90 | "ioredis": "^4.27.6", 91 | "mocha": "^9.0.2", 92 | "nunjucks": "^3.2.0", 93 | "nyc": "^15.0.1", 94 | "prettier": "^2.3.2", 95 | "pug": "^3.0.0", 96 | "redis": "^3.1.2", 97 | "supertest": "^6.0.1", 98 | "ts-node": "^10.1.0", 99 | "typedoc": "^0.21.4", 100 | "typescript": "^4.3.5" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/lib/component/session.redis.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置中间件 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { 7 | SessionStore, 8 | SessionDataSerializeFunction, 9 | SessionDataDeserializeFunction, 10 | DEFAULT_SESSION_SERIALIZE, 11 | DEFAULT_SESSION_DESERIALIZE, 12 | } from "./session"; 13 | import { SimpleRedisClientOptions, SimpleRedisClient } from "../module/simple.redis"; 14 | 15 | /** 默认Redis Key前缀 */ 16 | export const DEFAULT_REDIS_PREFIX = "sess:"; 17 | 18 | export interface SessionRedisStoreOptions extends SimpleRedisClientOptions { 19 | /** key前缀 */ 20 | prefix?: string; 21 | /** 客户端实例 */ 22 | client?: RedisCompatibleClient; 23 | /** 数据序列化函数 */ 24 | serialize?: SessionDataSerializeFunction; 25 | /** 数据反序列化函数 */ 26 | deserialize?: SessionDataDeserializeFunction; 27 | } 28 | 29 | /** 30 | * Redis客户端接口 31 | */ 32 | export interface RedisCompatibleClient { 33 | get(key: string, callback: (err: Error | null, ret: any) => void): void; 34 | setex(key: string, ttl: number, data: string, callback: (err: Error | null, ret: any) => void): void; 35 | expire(key: string, ttl: number, callback: (err: Error | null, ret: any) => void): void; 36 | del(key: string, callback: (err: Error | null, ret: any) => void): void; 37 | } 38 | 39 | /** 将毫秒转换为秒 */ 40 | function msToS(ms: number): number { 41 | return Math.ceil(ms / 1000); 42 | } 43 | 44 | export class SessionRedisStore implements SessionStore { 45 | protected keyPrefix: string; 46 | protected client: RedisCompatibleClient; 47 | protected serialize: SessionDataSerializeFunction; 48 | protected deserialize: SessionDataDeserializeFunction; 49 | 50 | constructor(protected readonly options: SessionRedisStoreOptions) { 51 | this.keyPrefix = options.prefix || DEFAULT_REDIS_PREFIX; 52 | this.client = options.client || new SimpleRedisClient(options); 53 | this.serialize = options.serialize || DEFAULT_SESSION_SERIALIZE; 54 | this.deserialize = options.deserialize || DEFAULT_SESSION_DESERIALIZE; 55 | } 56 | 57 | protected getKey(key: string): string { 58 | return this.keyPrefix + key; 59 | } 60 | 61 | public get(sid: string): Promise> { 62 | return new Promise((resolve, reject) => { 63 | this.client.get(this.getKey(sid), (err, ret) => { 64 | if (err) return reject(err); 65 | try { 66 | resolve(this.deserialize(ret)); 67 | } catch (err) { 68 | return reject(err); 69 | } 70 | }); 71 | }); 72 | } 73 | 74 | public set(sid: string, data: Record, maxAge: number): Promise { 75 | return new Promise((resolve, reject) => { 76 | this.client.setex(this.getKey(sid), msToS(maxAge), this.serialize(data), (err, ret) => { 77 | if (err) return reject(err); 78 | resolve(); 79 | }); 80 | }); 81 | } 82 | 83 | public destroy(sid: string): Promise { 84 | return new Promise((resolve, reject) => { 85 | this.client.del(this.getKey(sid), (err, ret) => { 86 | if (err) return reject(err); 87 | resolve(); 88 | }); 89 | }); 90 | } 91 | 92 | public touch(sid: string, maxAge: number): Promise { 93 | return new Promise((resolve, reject) => { 94 | this.client.expire(this.getKey(sid), msToS(maxAge), (err, ret) => { 95 | if (err) return reject(err); 96 | resolve(); 97 | }); 98 | }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/lib/module/crc32.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 内置模块 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import * as assert from "assert"; 7 | 8 | export const TABLE = [ 9 | 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 10 | 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 11 | 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 12 | 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 13 | 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 14 | 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 15 | 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 16 | 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 17 | 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 18 | 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 19 | 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, 20 | 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 21 | 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 22 | 0x206f85b3, 0xb966d409, 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 23 | 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 24 | 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 25 | 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 26 | 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 27 | 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 28 | 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 29 | 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 30 | 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 31 | 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 32 | 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 33 | 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 34 | 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 35 | 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 36 | 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 37 | 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, 38 | ]; 39 | 40 | export function crc32(input: Buffer | string, crc: number = 0) { 41 | crc = crc ^ -1; 42 | const buf = Buffer.isBuffer(input) ? input : Buffer.from(input); 43 | for (let i = 0; i < buf.length; i++) { 44 | crc = TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8); 45 | } 46 | return crc ^ -1; 47 | } 48 | 49 | export default crc32; 50 | 51 | assert(crc32("hello").toString(16), "3610a686"); 52 | assert(crc32("world").toString(16), "3a771143"); 53 | -------------------------------------------------------------------------------- /src/lib/final_handler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import * as http from "http"; 7 | import { parseUrl } from "./parse_url"; 8 | 9 | export default function finalHandler(req: http.IncomingMessage, res: http.ServerResponse) { 10 | return function (err: any) { 11 | if (err) { 12 | writeHead(res, getErrorStatusCode(err) || 500, getErrorHeaders(err)); 13 | if (err instanceof Error) { 14 | writeBody(req, res, createHtmlDocument(err.stack || err.message)); 15 | } else { 16 | writeBody(req, res, createHtmlDocument(String(err))); 17 | } 18 | } else { 19 | writeHead(res, 404); 20 | writeBody(req, res, createHtmlDocument(`Cannot ${req.method} ${getResourceName(req)}`)); 21 | } 22 | }; 23 | } 24 | 25 | const DOUBLE_SPACE_REGEXP = /\x20{2}/g; 26 | const NEWLINE_REGEXP = /\n/g; 27 | const MATCH_HTML_REGEXP = /["'&<>]/; 28 | 29 | function escapeHtml(str: string): string { 30 | const match = MATCH_HTML_REGEXP.exec(str); 31 | if (!match) { 32 | return str; 33 | } 34 | let escape; 35 | let html = ""; 36 | let index = 0; 37 | let lastIndex = 0; 38 | for (index = match.index; index < str.length; index++) { 39 | switch (str.charCodeAt(index)) { 40 | case 34: // " 41 | escape = """; 42 | break; 43 | case 38: // & 44 | escape = "&"; 45 | break; 46 | case 39: // ' 47 | escape = "'"; 48 | break; 49 | case 60: // < 50 | escape = "<"; 51 | break; 52 | case 62: // > 53 | escape = ">"; 54 | break; 55 | default: 56 | continue; 57 | } 58 | if (lastIndex !== index) { 59 | html += str.substring(lastIndex, index); 60 | } 61 | lastIndex = index + 1; 62 | html += escape; 63 | } 64 | return lastIndex !== index ? html + str.substring(lastIndex, index) : html; 65 | } 66 | 67 | function createHtmlDocument(message: string): string { 68 | const body = escapeHtml(message).replace(NEWLINE_REGEXP, "
").replace(DOUBLE_SPACE_REGEXP, "  "); 69 | return ( 70 | "\n" + 71 | '\n' + 72 | "\n" + 73 | '\n' + 74 | "Error\n" + 75 | "\n" + 76 | "\n" + 77 | "
" +
 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 4 | */ 5 | 6 | import { IncomingMessage, ServerResponse, IncomingHttpHeaders } from "http"; 7 | import { Request } from "./request"; 8 | import { Response } from "./response"; 9 | import { Context } from "./context"; 10 | import { Key as RegExpKey } from "path-to-regexp"; 11 | export { Key as RegExpKey, TokensToRegexpOptions as RegExpOptions } from "path-to-regexp"; 12 | import * as cookie from "cookie"; 13 | 14 | /** 出错原因 */ 15 | export type ErrorReason = undefined | null | string | Error | Record; 16 | 17 | /** 编译路由字符串的结果 */ 18 | export interface ParsedRoutePathResult { 19 | /** 正则表达式 */ 20 | regexp: RegExp; 21 | /** 变量信息 */ 22 | keys: RegExpKey[]; 23 | } 24 | 25 | /** 原始路由信息 */ 26 | export interface RawRouteInfo { 27 | /** 请求方法 */ 28 | method: string; 29 | /** 路径 */ 30 | path: string; 31 | } 32 | 33 | /** 中间件处理函数 */ 34 | export interface MiddlewareHandle { 35 | (ctx: C, err?: ErrorReason): Promise | void; 36 | /** 是否为connect中间件 */ 37 | classical?: boolean; 38 | /** 当前中间件注册时的路由前缀 */ 39 | route?: ParsedRoutePathResult; 40 | } 41 | 42 | /** 中间件堆栈的元素 */ 43 | export interface Middleware { 44 | /** 是否为错误处理中间件 */ 45 | handleError: boolean; 46 | /** 路由规则 */ 47 | route?: ParsedRoutePathResult; 48 | /** 中间件处理函数 */ 49 | handle: MiddlewareHandle; 50 | /** 是否排在末尾 */ 51 | atEnd: boolean; 52 | /** 原始路由信息 */ 53 | raw?: RawRouteInfo; 54 | } 55 | 56 | /** next回调函数 */ 57 | export type NextFunction = (err?: ErrorReason) => void; 58 | 59 | /** 监听端口选项 */ 60 | export interface ListenOptions { 61 | /** 端口 */ 62 | port?: number; 63 | /** 地址 */ 64 | host?: string; 65 | backlog?: number; 66 | /** Unix Socket 文件路径 */ 67 | path?: string; 68 | exclusive?: boolean; 69 | } 70 | 71 | /** 经典connect中间件 */ 72 | export type ClassicalMiddlewareHandle = (req: IncomingMessage, res: ServerResponse, next: NextFunction) => void; 73 | /** 经典connect错误处理中间件 */ 74 | export type ClassicalMiddlewareErrorHandle = ( 75 | err: ErrorReason, 76 | req: IncomingMessage, 77 | res: ServerResponse, 78 | next: NextFunction, 79 | ) => void; 80 | 81 | /** Context对象构造器 */ 82 | export interface ContextConstructor { 83 | new (): Context; 84 | } 85 | 86 | /** Request对象构造器 */ 87 | export interface RequestConstructor { 88 | new (req: IncomingMessage, ctx: Context): Request; 89 | } 90 | 91 | /** Response对象构造器 */ 92 | export interface ResponseConstructor { 93 | new (res: ServerResponse, ctx: Context): Response; 94 | } 95 | 96 | /** 请求头 */ 97 | export interface Headers extends IncomingHttpHeaders {} 98 | 99 | /** 扩展的ServerRequest对象 */ 100 | export interface ServerRequestEx extends IncomingMessage { 101 | originalUrl?: string; 102 | query?: Record; 103 | body?: Record; 104 | files?: Record; 105 | params?: Record; 106 | session?: Record; 107 | cookies?: Record; 108 | signedCookies?: Record; 109 | } 110 | 111 | /** 设置Cookie选项 */ 112 | export interface CookieOptions extends cookie.CookieSerializeOptions { 113 | /** 是否签名 */ 114 | signed?: boolean; 115 | } 116 | 117 | /** 模板引擎回调函数 */ 118 | export type TemplateRenderFileCallback = (err: Error | null, data?: string) => void; 119 | 120 | /** 模板引擎渲染文件函数 */ 121 | export type TemplateRenderFileFunction = ( 122 | fileName: string, 123 | data: TemplateRenderData, 124 | callback: TemplateRenderFileCallback, 125 | ) => void; 126 | 127 | /** 模板渲染数据 */ 128 | export type TemplateRenderData = Record; 129 | 130 | //////////////////////////////////////////////////////////////////////////////////////////////////// 131 | 132 | export const SYMBOL_REQUEST = Symbol("request instance"); 133 | export const SYMBOL_RESPONSE = Symbol("response instance"); 134 | export const SYMBOL_APPLICATION = Symbol("parent application instance"); 135 | export const SYMBOL_SERVER = Symbol("http.Server instance"); 136 | export const SYMBOL_SESSION = Symbol("context session instance"); 137 | export const SYMBOL_PUSH_NEXT_HANDLE = Symbol("context.pushNextHandle"); 138 | export const SYMBOL_POP_NEXT_HANDLE = Symbol("context.popNextHandle"); 139 | export const SYMBOL_RAW_ROUTE_INFO = Symbol("context.rawRouteInfo"); 140 | -------------------------------------------------------------------------------- /src/test/component/component.proxy.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.proxy", function () { 12 | const appInstances: Application[] = []; 13 | const remoteApp = new Application(); 14 | let remoteUrl = ""; 15 | let remoteHost = ""; 16 | before(function (done) { 17 | remoteApp.listen({ port: 0 }, () => { 18 | const addr = remoteApp.server.address(); 19 | remoteHost = typeof addr === "string" ? addr : "127.0.0.1:" + ((addr && addr.port) || 80); 20 | remoteUrl = "http://" + remoteHost; 21 | appInstances.push(remoteApp); 22 | done(); 23 | }); 24 | }); 25 | after(async function () { 26 | for (const app of appInstances) { 27 | await app.close(); 28 | } 29 | }); 30 | 31 | remoteApp.router.get("/path/to/some", async function (ctx) { 32 | ctx.response.setHeader("x-proxy", "on"); 33 | ctx.response.json({ headers: ctx.request.headers, url: ctx.request.url }); 34 | }); 35 | 36 | it("component.proxy success", async function () { 37 | const app = new Application(); 38 | appInstances.push(app); 39 | app.use("/", component.proxy({ target: remoteUrl + "/path/to", removeHeaderNames: ["host", "user-agent"] })); 40 | app.use("/", function (ctx) { 41 | ctx.response.end("OK"); 42 | }); 43 | 44 | await request(app.server) 45 | .get("/some") 46 | .expect(200) 47 | .expect("x-proxy", "on") 48 | .expect({ 49 | headers: { 50 | "accept-encoding": "gzip, deflate", 51 | host: remoteHost, 52 | connection: "close", 53 | }, 54 | url: "/path/to/some", 55 | }); 56 | await request(app.server) 57 | .get("/some") 58 | .set("x-aaa-bbb", "ccc") 59 | .expect(200) 60 | .expect("x-proxy", "on") 61 | .expect({ 62 | headers: { 63 | "accept-encoding": "gzip, deflate", 64 | host: remoteHost, 65 | connection: "close", 66 | "x-aaa-bbb": "ccc", 67 | }, 68 | url: "/path/to/some", 69 | }); 70 | }); 71 | 72 | it("ctx.proxy success", async function () { 73 | const app = new Application(); 74 | appInstances.push(app); 75 | let some1Done = false; 76 | let some2Done = false; 77 | app.router.get("/some1", async function (ctx) { 78 | await ctx.proxy(remoteUrl + "/path/to/some"); 79 | some1Done = true; 80 | }); 81 | app.router.get("/some2", async function (ctx) { 82 | await ctx.proxy({ 83 | hostname: "127.0.0.1", 84 | port: remoteHost.split(":")[1], 85 | path: "/path/to/some", 86 | headers: { 87 | "x-abc": "12345678", 88 | }, 89 | }); 90 | some2Done = true; 91 | }); 92 | app.router.get("/some3", function (ctx) { 93 | return ctx.proxyWithHeaders(ctx.request.query!.url); 94 | }); 95 | app.use("/", function (ctx) { 96 | ctx.response.end("OK"); 97 | }); 98 | 99 | await request(app.server) 100 | .get("/some1") 101 | .expect(200) 102 | .expect("x-proxy", "on") 103 | .expect({ 104 | headers: { 105 | host: remoteHost, 106 | connection: "close", 107 | }, 108 | url: "/path/to/some", 109 | }); 110 | expect(some1Done).to.equal(true); 111 | await request(app.server) 112 | .get("/some2") 113 | .set("x-aaa-bbb", "ccc") 114 | .expect(200) 115 | .expect("x-proxy", "on") 116 | .expect({ 117 | headers: { 118 | host: remoteHost, 119 | connection: "close", 120 | "x-abc": "12345678", 121 | }, 122 | url: "/path/to/some", 123 | }); 124 | expect(some2Done).to.equal(true); 125 | await request(app.server) 126 | .get("/some3") 127 | .set("user-agent", "haha") 128 | .query({ 129 | url: remoteUrl + "/path/to/some", 130 | }) 131 | .expect(200) 132 | .expect("x-proxy", "on") 133 | .expect({ 134 | headers: { 135 | "accept-encoding": "gzip, deflate", 136 | host: remoteHost, 137 | connection: "close", 138 | "user-agent": "haha", 139 | }, 140 | url: "/path/to/some", 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /src/lib/router.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { Core } from "./core"; 7 | import { MiddlewareHandle } from "./define"; 8 | import { wrapMiddlewareHandleWithMethod } from "./utils"; 9 | import { Context } from "./context"; 10 | import { Request } from "./request"; 11 | import { Response } from "./response"; 12 | 13 | export class Router> extends Core { 14 | /** 15 | * 处理所有请求方法的请求 16 | * 17 | * @param route 路由规则 18 | * @param handles 处理函数 19 | */ 20 | public all(route: string | RegExp, ...handles: MiddlewareHandle[]) { 21 | this.addToEnd({ method: "ALL", path: route.toString() }, this.parseRoutePath(false, route), ...handles); 22 | } 23 | 24 | /** 25 | * 处理GET请求方法的请求 26 | * 27 | * @param route 路由规则 28 | * @param handles 处理函数 29 | */ 30 | public get(route: string | RegExp, ...handles: MiddlewareHandle[]) { 31 | this.addToEnd( 32 | { method: "GET", path: route.toString() }, 33 | this.parseRoutePath(false, route), 34 | ...handles.map((item) => wrapMiddlewareHandleWithMethod("GET", item)), 35 | ); 36 | } 37 | 38 | /** 39 | * 处理HEAD请求方法的请求 40 | * 41 | * @param route 路由规则 42 | * @param handles 处理函数 43 | */ 44 | public head(route: string | RegExp, ...handles: MiddlewareHandle[]) { 45 | this.addToEnd( 46 | { method: "HEAD", path: route.toString() }, 47 | this.parseRoutePath(false, route), 48 | ...handles.map((item) => wrapMiddlewareHandleWithMethod("HEAD", item)), 49 | ); 50 | } 51 | 52 | /** 53 | * 处理POST请求方法的请求 54 | * 55 | * @param route 路由规则 56 | * @param handles 处理函数 57 | */ 58 | public post(route: string | RegExp, ...handles: MiddlewareHandle[]) { 59 | this.addToEnd( 60 | { method: "POST", path: route.toString() }, 61 | this.parseRoutePath(false, route), 62 | ...handles.map((item) => wrapMiddlewareHandleWithMethod("POST", item)), 63 | ); 64 | } 65 | 66 | /** 67 | * 处理PUT请求方法的请求 68 | * 69 | * @param route 路由规则 70 | * @param handles 处理函数 71 | */ 72 | public put(route: string | RegExp, ...handles: MiddlewareHandle[]) { 73 | this.addToEnd( 74 | { method: "PUT", path: route.toString() }, 75 | this.parseRoutePath(false, route), 76 | ...handles.map((item) => wrapMiddlewareHandleWithMethod("PUT", item)), 77 | ); 78 | } 79 | 80 | /** 81 | * 处理DELETE请求方法的请求 82 | * 83 | * @param route 路由规则 84 | * @param handles 处理函数 85 | */ 86 | public delete(route: string | RegExp, ...handles: MiddlewareHandle[]) { 87 | this.addToEnd( 88 | { method: "DELETE", path: route.toString() }, 89 | this.parseRoutePath(false, route), 90 | ...handles.map((item) => wrapMiddlewareHandleWithMethod("DELETE", item)), 91 | ); 92 | } 93 | 94 | /** 95 | * 处理CONNECT请求方法的请求 96 | * 97 | * @param route 路由规则 98 | * @param handles 处理函数 99 | */ 100 | public connect(route: string | RegExp, ...handles: MiddlewareHandle[]) { 101 | this.addToEnd( 102 | { method: "CONNECT", path: route.toString() }, 103 | this.parseRoutePath(false, route), 104 | ...handles.map((item) => wrapMiddlewareHandleWithMethod("CONNECT", item)), 105 | ); 106 | } 107 | 108 | /** 109 | * 处理OPTIONS请求方法的请求 110 | * 111 | * @param route 路由规则 112 | * @param handles 处理函数 113 | */ 114 | public options(route: string | RegExp, ...handles: MiddlewareHandle[]) { 115 | this.addToEnd( 116 | { method: "OPTIONS", path: route.toString() }, 117 | this.parseRoutePath(false, route), 118 | ...handles.map((item) => wrapMiddlewareHandleWithMethod("OPTIONS", item)), 119 | ); 120 | } 121 | 122 | /** 123 | * 处理TRACE请求方法的请求 124 | * 125 | * @param route 路由规则 126 | * @param handles 处理函数 127 | */ 128 | public trace(route: string | RegExp, ...handles: MiddlewareHandle[]) { 129 | this.addToEnd( 130 | { method: "TRACE", path: route.toString() }, 131 | this.parseRoutePath(false, route), 132 | ...handles.map((item) => wrapMiddlewareHandleWithMethod("TRACE", item)), 133 | ); 134 | } 135 | 136 | /** 137 | * 处理PATCH请求方法的请求 138 | * 139 | * @param route 路由规则 140 | * @param handles 处理函数 141 | */ 142 | public patch(route: string | RegExp, ...handles: MiddlewareHandle[]) { 143 | this.addToEnd( 144 | { method: "PATCH", path: route.toString() }, 145 | this.parseRoutePath(false, route), 146 | ...handles.map((item) => wrapMiddlewareHandleWithMethod("PATCH", item)), 147 | ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/lib/template.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import * as path from "path"; 7 | import * as assert from "assert"; 8 | import { TemplateRenderData, TemplateRenderFileFunction } from "./define"; 9 | import * as simpleTemplate from "./module/simple.template"; 10 | 11 | export class TemplateEngineManager { 12 | protected engines: Map = new Map(); 13 | protected defaultEngine: string = ""; 14 | protected root: string = "./views"; 15 | 16 | /** 模板全局变量 */ 17 | public locals: Record = {}; 18 | 19 | /** 20 | * 设置模板根目录 21 | * @param dir 22 | */ 23 | public setRoot(dir: string): this { 24 | this.root = dir; 25 | return this; 26 | } 27 | 28 | /** 29 | * 注册模板引擎 30 | * @param ext 模板扩展名 31 | * @param renderFile 模板渲染函数 32 | */ 33 | public register(ext: string, renderFile: TemplateRenderFileFunction): this { 34 | if (ext[0] !== ".") ext = "." + ext; 35 | this.engines.set(ext, renderFile); 36 | return this; 37 | } 38 | 39 | /** 40 | * 设置默认渲染引擎 41 | * @param ext 模板扩展名 42 | * @param ignoreIfExists 如果已经设置了默认模板引擎则忽略 43 | */ 44 | public setDefault(ext: string, ignoreIfExists: boolean = false): this { 45 | if (ext[0] !== ".") ext = "." + ext; 46 | assert(this.engines.has(ext), `模板引擎 ${ext} 未注册`); 47 | if (this.defaultEngine && ignoreIfExists) return this; 48 | this.defaultEngine = ext; 49 | return this; 50 | } 51 | 52 | /** 53 | * 渲染模板 54 | * @param name 模板名 55 | * @param data 数据 56 | */ 57 | public render(name: string, data: TemplateRenderData = {}): Promise { 58 | return new Promise((resolve, reject) => { 59 | let ext = path.extname(name); 60 | let fileName = path.resolve(this.root, name); 61 | let renderFile: TemplateRenderFileFunction; 62 | if (ext && this.engines.has(ext)) { 63 | renderFile = this.engines.get(ext)!; 64 | } else { 65 | assert(this.engines.has(this.defaultEngine), `未设置默认模板引擎,无法渲染模板类型 ${ext}`); 66 | if (!ext) { 67 | ext = this.defaultEngine; 68 | fileName += ext; 69 | } 70 | renderFile = this.engines.get(this.defaultEngine)!; 71 | } 72 | renderFile(fileName, { ...this.locals, ...data }, (err, ret) => { 73 | if (err) return reject(err); 74 | resolve(ret!); 75 | }); 76 | }); 77 | } 78 | 79 | /** 80 | * 设置模板全局变量 81 | * @param name 名称 82 | * @param value 值 83 | */ 84 | public setLocals(name: string, value: any): this { 85 | this.locals[name] = value; 86 | return this; 87 | } 88 | 89 | /** 90 | * 初始化自动简单模板引擎 91 | * @param ext 模板扩展名 92 | */ 93 | public initSimple(ext: string = ".html"): this { 94 | this.register(ext, simpleTemplate.renderFile).setDefault(ext, true); 95 | return this; 96 | } 97 | 98 | /** 99 | * 初始化EJS模板引擎 100 | * @param ext 模板扩展名 101 | */ 102 | public initEjs(ext: string = ".html"): this { 103 | try { 104 | const ejs = requireProjectModule("ejs"); 105 | this.register(ext, ejs.renderFile).setDefault(ext, true); 106 | return this; 107 | } catch (err) { 108 | if (err.code === "MODULE_NOT_FOUND") { 109 | throw new Error(`initEjs: 找不到 ejs 模块!请先执行 npm install ejs --save 安装。${err.message}`); 110 | } 111 | throw err; 112 | } 113 | } 114 | 115 | /** 116 | * 初始化Pug模板引擎 117 | * @param ext 模板扩展名 118 | */ 119 | public initPug(ext: string = ".pug"): this { 120 | try { 121 | const pug = requireProjectModule("pug"); 122 | this.register(ext, pug.renderFile).setDefault(ext, true); 123 | return this; 124 | } catch (err) { 125 | if (err.code === "MODULE_NOT_FOUND") { 126 | throw new Error(`initPug: 找不到 pug 模块!请先执行 npm install pug --save 安装。${err.message}`); 127 | } 128 | throw err; 129 | } 130 | } 131 | 132 | /** 133 | * 初始化Nunjucks模板引擎 134 | * @param ext 模板扩展名 135 | */ 136 | public initNunjucks(ext: string = ".html"): this { 137 | try { 138 | const nunjucks = requireProjectModule("nunjucks"); 139 | this.register(ext, nunjucks.render).setDefault(ext, true); 140 | return this; 141 | } catch (err) { 142 | if (err.code === "MODULE_NOT_FOUND") { 143 | throw new Error( 144 | `initNunjucks: 找不到 nunjucks 模块!请先执行 npm install nunjucks --save 安装。${err.message}`, 145 | ); 146 | } 147 | throw err; 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * 加载当前项目运行目录下的指定模块 154 | * @param id 模块名称 155 | */ 156 | function requireProjectModule(id: string): any { 157 | const paths = Array.from( 158 | new Set( 159 | (new Error().stack || "") 160 | .split(/\n/) 161 | .map((v) => v.match(/\((.*)\:\d+\:\d+\)/)) 162 | .filter((v) => v) 163 | .map((v) => path.dirname(v![1])), 164 | ), 165 | ); 166 | const entry = require.resolve(id, { paths: [process.cwd(), ...paths] }); 167 | return require(entry); 168 | } 169 | -------------------------------------------------------------------------------- /src/test/core/connect.compatible.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 - 单元测试 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { expect } from "chai"; 7 | import * as path from "path"; 8 | import * as fs from "fs"; 9 | import { IncomingMessage, ServerResponse } from "http"; 10 | import { Application, fromClassicalHandle, Router, toClassicalHandle } from "../../lib"; 11 | import * as request from "supertest"; 12 | import * as connect from "connect"; 13 | import * as bodyParser from "body-parser"; 14 | import * as serveStatic from "serve-static"; 15 | 16 | export function readFile(file: string): Promise { 17 | return new Promise((resolve, reject) => { 18 | fs.readFile(file, (err, ret) => { 19 | if (err) { 20 | reject(err); 21 | } else { 22 | resolve(ret.toString()); 23 | } 24 | }); 25 | }); 26 | } 27 | 28 | const ROOT_DIR = path.resolve(__dirname, "../../.."); 29 | 30 | describe("兼容 connect 模块", function () { 31 | const appInstances: Application[] = []; 32 | after(async function () { 33 | for (const app of appInstances) { 34 | await app.close(); 35 | } 36 | }); 37 | 38 | it("作为 connect 的中间件", async function () { 39 | const app = connect(); 40 | const app2 = new Application(); 41 | appInstances.push(app2); 42 | app.use(bodyParser.json() as any); 43 | let isCalled = false; 44 | app.use(function (req: IncomingMessage, res: ServerResponse, next: Function) { 45 | isCalled = true; 46 | next(); 47 | }); 48 | app.use(app2.handleRequest); 49 | app2.use("/", function (ctx) { 50 | ctx.response.setHeader("content-type", "application/json"); 51 | ctx.response.end(JSON.stringify(ctx.request.body)); 52 | }); 53 | await request(app) 54 | .post("/") 55 | .send({ 56 | a: 111, 57 | b: 222, 58 | c: 333, 59 | }) 60 | .expect(200) 61 | .expect({ 62 | a: 111, 63 | b: 222, 64 | c: 333, 65 | }); 66 | expect(isCalled).to.equal(true); 67 | }); 68 | 69 | it("转换 connect app 为中间件", async function () { 70 | const app = connect(); 71 | const app2 = new Application(); 72 | appInstances.push(app2); 73 | app.use(bodyParser.json() as any); 74 | let isCalled = false; 75 | app.use(function (req: IncomingMessage, res: ServerResponse, next: Function) { 76 | isCalled = true; 77 | next(); 78 | }); 79 | app2.use("/", fromClassicalHandle(app)); 80 | app2.use("/", function (ctx) { 81 | ctx.response.setHeader("content-type", "application/json"); 82 | ctx.response.end(JSON.stringify(ctx.request.body)); 83 | }); 84 | await request(app2.server) 85 | .post("/") 86 | .send({ 87 | a: 111, 88 | b: 222, 89 | c: 333, 90 | }) 91 | .expect(200) 92 | .expect({ 93 | a: 111, 94 | b: 222, 95 | c: 333, 96 | }); 97 | expect(isCalled).to.equal(true); 98 | }); 99 | 100 | it("Router 作为 connect 中间件", async function () { 101 | const app = connect(); 102 | const app2 = new Application(); 103 | const router = new Router(); 104 | app2.use("/", router); 105 | app.use(app2.handleRequest); 106 | router.get("/a", function (ctx) { 107 | ctx.response.end("this is a"); 108 | }); 109 | router.post("/b", function (ctx) { 110 | ctx.response.end("this is b"); 111 | }); 112 | await request(app2.server).get("/a").expect(200).expect("this is a"); 113 | await request(app2.server).post("/b").expect(200).expect("this is b"); 114 | }); 115 | 116 | it("兼容 serve-static 模块", async function () { 117 | const app = new Application(); 118 | appInstances.push(app); 119 | const app2 = new Application(); 120 | app.use("/public", fromClassicalHandle(serveStatic(ROOT_DIR) as any)); 121 | app.use("/a", app2); 122 | app2.use("/static", fromClassicalHandle(serveStatic(ROOT_DIR) as any)); 123 | await request(app.server) 124 | .get("/public/package.json") 125 | .expect(200) 126 | .expect(await readFile(path.resolve(ROOT_DIR, "package.json"))); 127 | await request(app.server) 128 | .get("/a/static/package.json") 129 | .expect(200) 130 | .expect(await readFile(path.resolve(ROOT_DIR, "package.json"))); 131 | }); 132 | 133 | it("@leizm/web 格式的中间件作为 connect 中间件", async function () { 134 | const app = connect(); 135 | let counter = 0; 136 | app.use( 137 | toClassicalHandle(function (ctx) { 138 | counter++; 139 | ctx.next(); 140 | }), 141 | ); 142 | app.use( 143 | "/a", 144 | toClassicalHandle(function (ctx) { 145 | ctx.response.end("this is a"); 146 | }), 147 | ); 148 | app.use( 149 | "/b", 150 | toClassicalHandle(function (ctx) { 151 | ctx.response.end("this is b"); 152 | }), 153 | ); 154 | await request(app).get("/a").expect(200).expect("this is a"); 155 | await request(app).post("/b").expect(200).expect("this is b"); 156 | expect(counter).to.equal(2); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /src/lib/request.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { IncomingMessage } from "http"; 7 | import { parseUrl, ParsedUrl } from "./parse_url"; 8 | import { Headers, ServerRequestEx } from "./define"; 9 | import { Context } from "./context"; 10 | import { Socket } from "net"; 11 | import { parseMultipart, MultipartParserOptions, FileField } from "./component/body"; 12 | 13 | /** 14 | * @leizm/web 中间件基础框架 15 | * @author Zongmin Lei 16 | */ 17 | 18 | export class Request { 19 | /** 已解析的URL信息 */ 20 | protected parsedUrlInfo: ParsedUrl; 21 | 22 | constructor(public readonly req: IncomingMessage, public readonly ctx: Context) { 23 | const req2 = req as ServerRequestEx; 24 | req2.originalUrl = req2.originalUrl || req.url; 25 | this.parsedUrlInfo = parseUrl(req.url || "", { query: true }); 26 | req2.query = this.parsedUrlInfo.query as any; 27 | } 28 | 29 | /** 30 | * 初始化完成,由 `Context.init()` 自动调用 31 | * 一般用于自定义扩展 Request 时,在此方法中加上自己的祝时候完成的代码 32 | */ 33 | public inited() {} 34 | 35 | /** 获取请求方法 */ 36 | public get method() { 37 | return this.req.method; 38 | } 39 | 40 | /** 获取URL */ 41 | public get url(): string { 42 | return (this.req as ServerRequestEx).url || ""; 43 | } 44 | 45 | /** 更新URL */ 46 | public set url(value: string) { 47 | (this.req as ServerRequestEx).url = this.parsedUrlInfo.path = value; 48 | } 49 | 50 | /** 获取Path(URL不包含查询字符串部分)*/ 51 | public get path(): string { 52 | return this.parsedUrlInfo.pathname || ""; 53 | } 54 | 55 | /** 设置Path(URL不包含查询字符串部分)*/ 56 | public set path(value: string) { 57 | this.parsedUrlInfo.pathname = value; 58 | } 59 | 60 | /** 获取URL查询字符串部分 */ 61 | public get search() { 62 | return this.parsedUrlInfo.search; 63 | } 64 | 65 | /** 获取已解析的URL查询字符串参数 */ 66 | public get query() { 67 | return (this.req as ServerRequestEx).query; 68 | } 69 | 70 | /** 获取当前HTTP版本 */ 71 | public get httpVersion() { 72 | return this.req.httpVersion; 73 | } 74 | 75 | /** 获取所有请求头 */ 76 | public get headers() { 77 | return this.req.headers as Headers; 78 | } 79 | 80 | /** 81 | * 获取请求头 82 | * 83 | * @param name 名称 84 | */ 85 | public getHeader(name: string) { 86 | return this.req.headers[name.toLowerCase()]; 87 | } 88 | 89 | /** 获取URL参数 */ 90 | public get params(): Record { 91 | return (this.req as any).prams || {}; 92 | } 93 | 94 | /** 设置URL参数 */ 95 | public set params(value: Record) { 96 | (this.req as any).prams = value; 97 | } 98 | 99 | /** 判断是否有URL参数 */ 100 | public hasParams() { 101 | return !!(this.req as any).prams; 102 | } 103 | 104 | /** 获取请求Body参数 */ 105 | public get body(): Record { 106 | return (this.req as any).body || {}; 107 | } 108 | 109 | /** 设置请求Body参数 */ 110 | public set body(value: Record) { 111 | (this.req as any).body = value; 112 | } 113 | 114 | /** 判断是否有请求Body参数 */ 115 | public hasBody() { 116 | return !!(this.req as any).body; 117 | } 118 | 119 | /** 获取请求文件参数 */ 120 | public get files(): Record { 121 | return (this.req as any).files || {}; 122 | } 123 | 124 | /** 设置请求文件参数 */ 125 | public set files(value: Record) { 126 | (this.req as any).files = value; 127 | } 128 | 129 | /** 判断是否有请求文件参数 */ 130 | public hasFiles() { 131 | return !!(this.req as any).files; 132 | } 133 | 134 | /** 获取请求Cookies信息 */ 135 | public get cookies(): Record { 136 | return (this.req as any).cookies || {}; 137 | } 138 | 139 | /** 设置请求Cookies信息 */ 140 | public set cookies(value: Record) { 141 | (this.req as any).cookies = value; 142 | } 143 | 144 | /** 判断是否有请求Cookie信息 */ 145 | public hasCookies() { 146 | return !!(this.req as any).cookies; 147 | } 148 | 149 | /** 获取请求signedCookies信息 */ 150 | public get signedCookies(): Record { 151 | return (this.req as any).signedCookies || {}; 152 | } 153 | 154 | /** 设置请求signedCookies信息 */ 155 | public set signedCookies(value: Record) { 156 | (this.req as any).signedCookies = value; 157 | } 158 | 159 | /** 判断是否有请求signedCookies信息 */ 160 | public hasSignedCookies() { 161 | return !!(this.req as any).signedCookies; 162 | } 163 | 164 | /** 获取请求Session信息 */ 165 | public get session(): Record { 166 | return (this.req as any).session || {}; 167 | } 168 | 169 | /** 设置请求Session信息 */ 170 | public set session(value: Record) { 171 | (this.req as any).session = value; 172 | } 173 | 174 | /** 判断是否有请求Session信息 */ 175 | public hasSession() { 176 | return !!(this.req as any).session; 177 | } 178 | 179 | /** 客户端IP地址,来源于req.socket.remoteAddress */ 180 | public get ip() { 181 | return this.req.socket.remoteAddress; 182 | } 183 | 184 | /** 请求的socket对象 */ 185 | public get socket(): Socket { 186 | return this.req.socket; 187 | } 188 | 189 | /** 解析multipart内容 */ 190 | public async parseMultipart(options: MultipartParserOptions = {}): Promise<{ 191 | body: Record; 192 | files: Record; 193 | }> { 194 | await parseMultipart(this.ctx, options); 195 | return { body: this.body, files: this.files }; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/lib/context.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 3 | * @author Zongmin Lei 4 | */ 5 | 6 | import { IncomingMessage, ServerResponse } from "http"; 7 | import { EventEmitter } from "events"; 8 | import { Request } from "./request"; 9 | import { Response } from "./response"; 10 | import { Application } from "./application"; 11 | import { 12 | RawRouteInfo, 13 | NextFunction, 14 | ErrorReason, 15 | RequestConstructor, 16 | ResponseConstructor, 17 | SYMBOL_APPLICATION, 18 | SYMBOL_SESSION, 19 | SYMBOL_PUSH_NEXT_HANDLE, 20 | SYMBOL_POP_NEXT_HANDLE, 21 | SYMBOL_RAW_ROUTE_INFO, 22 | } from "./define"; 23 | import { SessionInstance } from "./component/session"; 24 | import onWriteHead from "./module/on.writehead"; 25 | import { proxyRequest, ProxyTarget, parseProxyTarget } from "./module/proxy.request"; 26 | 27 | export class Context 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

\n

b = 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

\n

b = 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("ejs

a = 123

\n

b = 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("ejs

a = 123

\n

b = 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

\n

b = 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

\n

b = 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

\n

b = 456

"); 142 | await request(app.server).get("/ejs").expect(200).expect("mix

a = 123

\n

b = 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

\n

b = 456

"); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /src/lib/core.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @leizm/web 中间件基础框架 3 | * @author Zongmin Lei 4 | */ 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 | ![Node.js CI](https://github.com/leizongmin/leizm-web/workflows/Node.js%20CI/badge.svg) 3 | [![DeepScan grade](https://deepscan.io/api/projects/2695/branches/18968/badge/grade.svg)](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 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fleizongmin%2Fleizm-web.svg?type=shield)](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 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fleizongmin%2Fleizm-web.svg?type=large)](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

\n

b = 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

\n

b = 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

\n

b = 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 | --------------------------------------------------------------------------------