koa开发实践2:为koa项目添加路由模块

nodeJS server-side-develop
koa开发实践2:为koa项目添加路由模块

上一节:《 koa开发实践2:为koa项目添加路由模块 | 下一节:《 koa开发实践3:在koa项目中使用 swagger 文档

作者李俊才:https://blog.csdn.net/qq_28550263?spm=1001.2101.3001.5343
作者邮箱 :291148484@163.com
本文地址:https://blog.csdn.net/qq_28550263/article/details/129828590

请勿转载,仅发布于我的博客:CSDN jclee95 : https://blog.csdn.net/qq_28550263?spm=1001.2101.3001.5343



1. 目标概述

我们上一节搭建了一个基于 TypeScript 地开发环境,TypeScript 是强类型语言,这对于我们地开发提供了强大的类型支持,能够给我们代码很多更加智能化的提示,并且由李云我们后期的改错与维护。在上一节中,我们已经实现了一个基本的 koa 服务器的搭建,但是它还有很多不足,比如,它还没有路由,日志记录也需要进行进一步修改等等。

上一节的服务器时一个单一地址的静态页面,我们这一节的目标就是在上一节的基础上添加路由模块,实现 koa web 的路由功能。

2. 路由 与 koa

2.1 路由的概念

2.1.1 起源

路由 一词的来源与其实工程技术并无关系,它很早就有,仅仅是一个生活中很常见的词汇,含以上表示来自哪里。随着二十世纪电气的到来,电气电子相关技术的蓬勃发展,控制技术、通信工程等相关专业应运而生。不论是从最早在电气控制领域的 工业控制网络 到后来的 计算机网络,随着生产力发展的需要都引入了很多来源于生活的概念,路由 就是其中之一。在网络工程领域,路由routing)是指分组从源到目的地时,决定端到端路径的网络范围的进程。后来随着互联网技术的发展,从网络工程领域再次借用和引申了 路由 这一概念。在用户界面系统中,比如我们所熟知的 web,路由简单的来说就是根据用户请求的 URL 链接来判断对应的处理程序,并返回处理结果。

2.1.2 统一资源定位符(url)

生活中我们以各种形式表达道路,如水路、陆路,通过文字描述我们人类从一个位置到达另外一个位置所经过的道路,就是路由。在网络领域也是类似的,同样需要一个方法来表示信息所经过的位置,这个方法就是 统一资源定位系统URL,uniform resource locator)。

URL 是由一串字母,数字和特殊符号组成的字符所构成的字符串。在不同的使用场景存在不同的 URL 标准协议,比如最常见的有:

  • http Hypertext Transfer Protocol(超文本传输协议);
  • ftp File Transfer protocol(文件传输协议);
  • mailto Electronic mail address(电子邮件地址);
  • file Host-specific file names(特殊主机文件名);

URL的一般语法格式为:

protocol :// hostname[:port] / path / [:parameters][?query]#fragment

其中:

  • protocol 表示协议名,如 http、https、ftp、ed2k 等等;
  • hostname 表示主机名,也可以是域名,只不过域名终将通过 DNS 解析为主机名;
  • port 表示端口号,一般而言,在一个主机上不同的端口号代表了不同的应用,比如 web 应用使用的http服务默认为 80 端口;
  • path 表示具体路径,它是由若干个'/' 号隔开的字符串,一般用来表示主机上的一个目录或文件地址;
  • parameters 表示参数,用于指定特殊参数的可选项,一般通过服务器端程序自行解释,不过目前的前端路由也可以处理参数。
  • query 表示查询,可以有多个。一个查询实际上就是一个 键值对,用于以 url 方式给应用传入相关的参数。每两个查询之间使用'&' 符号隔开,'='符号隔开;
  • fragment 表示信息片断,是一个用于指定网络资源中的片段的字符串。例如一个 Web 中有多个名词解释,可使用fragment直接定位到某一名词解释。在Web的前端路由的 所谓哈希模式中,就是使用 信息片段来区别不同位置的。

2.2 路由的原理

2.3 了解关于 MVC 的概念

在后文中我们需要以MVC模式关联路由和视图,因此有必要先讲解何谓 MVCMVC 是交互系统开发中常用的一种 软件设计模式。 经典MVC架构中,M、V、C 分别表示的是三个开发层级。其中 MModel)代表 数据模型VView)代表视图CController)则是控制器(也称调度器)。

  • Model 他是模型表示业务规则,模型更具体的来说其实就是指数据。
  • View 它是用户看到并与之交互的界面。比如 web 中的html元素组成的网页界面。
  • Controller 它是视图和数据模型之间的桥梁,用于接受用户的输入并调用 Model 和 View 去完成相应的需求。

因此总的来看 MVC 模式将交互系统分层了三个层次,分别是 数据层视图层调度层。 使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。

3. koa-router

3.1 概述

express 不一样的是,koa 自身连路由系统也没有,需要额外安装路由中间件插件。这给开发者带来了更多的选择,你既可以使用 koa 官方团队提供的路由中间件 koa-router,也可以选择其它的方式实现 Koa Web 的路由。也就是说,是否使用官方的 koa-router 其实并非强制的。

如上所述 koa-router 是一款由 koa 官方以 中间件 形式提供的路由模块,它能完成我们一般的路由功能,并且由于是官方所提供的插件,很多第三方插件更倾向于基于 koa-router 进行推出,因此在本文中也使用它进行路由的讲解,这对于 Web 后端类项目的初学者也更加友好。

另外, koa-routerexpress 路由风格的路由,这大概由于 koaexpress 是同一个团队进行开发的有较大关系。对于熟悉 express 的读者可以直接入手。

3.2 配置 koa-router

要使用 koa-router 需要先进行安装。目前 koa 团队规范了 路由模块 的项目名称,从旧版的 koa-router 迁移到了新的 @koa/router。因此依据当前最新的文档,你可以使用如下方式对 koa-router 进行安装:

npm i @koa/router
# or
yarn add @koa/router
# or
pnpm i @koa/router

(依据你项目所使用的包管理工具进行选择)

3.3 路由的 TypeScript 支持

由于我们使用 TypeScript 进行开发,而 koa-router 本身不包含 TypeScript 源码或者相应的类型声明文件。因此为了更好地获得类型的支持,还需要独立安装对应的类型模块。

koa-router 项目名迁移到 @koa/router 后,对应的类型模块项目名为 @types/koa__router。因此你可以通过下面的方式为路由添加 TypeScript 支持:

npm install @types/koa__router -D
# or
yarn add -D @types/koa__router
# or
pnpm install @types/koa__router -D

(依据你项目所使用的包管理工具进行选择)

3.4 koa-router 的编程接口解析

我们用的主要就是 Router 对象,它是一个类,其类型签名如下:

declare class Router<StateT = Koa.DefaultState, ContextT = Koa.DefaultContext> {
    opts: Router.RouterOptions;
    methods: string[];
    params: object;
    stack: Router.Layer[];

    /**
     * 创建新路由器。
     */
    constructor(opt?: Router.RouterOptions);

    /**
     * 使用给定的中间件。
     *
     * 中间件按照 `.use()` 定义的顺序运行。它们被顺序调用,请求从第一个中间件开始,沿着中间件堆栈 "down"(向下) 传递。
     */
    use(...middleware: Array<Router.Middleware<StateT, ContextT>>): Router<StateT, ContextT>;
    /**
     * 使用给定的中间件。
     *
     * 中间件按照 `.use()` 义的顺序运行。它们被顺序调用,请求从第一个中间件开始,沿着中间件堆栈"down"(向下)传递。
     */
    use(
        path: string | string[] | RegExp,
        ...middleware: Array<Router.Middleware<StateT, ContextT>>
    ): Router<StateT, ContextT>;

    /**
     * HTTP get 方法
     */
    get<T = {}, U = {}, B = unknown>(
        name: string,
        path: string | RegExp,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;
    /**
     * HTTP get 方法
     */
    get<T = {}, U = {}, B = unknown>(
        path: string | RegExp | Array<string | RegExp>,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;

    /**
     * HTTP post 方法
     */
    post<T = {}, U = {}, B = unknown>(
        name: string,
        path: string | RegExp,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;
    /**
     * HTTP post 方法
     */
    post<T = {}, U = {}, B = unknown>(
        path: string | RegExp | Array<string | RegExp>,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;

    /**
     * HTTP put 方法
     */
    put<T = {}, U = {}, B = unknown>(
        name: string,
        path: string | RegExp,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;
    /**
     * HTTP put 方法
     */
    put<T = {}, U = {}, B = unknown>(
        path: string | RegExp | Array<string | RegExp>,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;

    /**
     * HTTP link 方法
     */
    link<T = {}, U = {}, B = unknown>(
        name: string,
        path: string | RegExp,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;
    /**
     * HTTP link 方法
     */
    link<T = {}, U = {}, B = unknown>(
        path: string | RegExp | Array<string | RegExp>,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;

    /**
     * HTTP unlink 方法
     */
    unlink<T = {}, U = {}, B = unknown>(
        name: string,
        path: string | RegExp,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;
    /**
     * HTTP unlink 方法
     */
    unlink<T = {}, U = {}, B = unknown>(
        path: string | RegExp | Array<string | RegExp>,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;

    /**
     * HTTP delete 方法
     */
    delete<T = {}, U = {}, B = unknown>(
        name: string,
        path: string | RegExp,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;
    /**
     * HTTP delete 方法
     */
    delete<T = {}, U = {}, B = unknown>(
        path: string | RegExp | Array<string | RegExp>,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;

    /**
     *  `router.delete()` 的别名,因为delete是保留字
     */
    del<T = {}, U = {}, B = unknown>(
        name: string,
        path: string | RegExp,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;
    /**
     *  `router.delete()` 的别名,因为delete是保留字
     */
    del<T = {}, U = {}, B = unknown>(
        path: string | RegExp | Array<string | RegExp>,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;

    /**
     * HTTP head 方法
     */
    head<T = {}, U = {}, B = unknown>(
        name: string,
        path: string | RegExp,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;
    /**
     * HTTP head 方法
     */
    head<T = {}, U = {}, B = unknown>(
        path: string | RegExp | Array<string | RegExp>,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;

    /**
     * HTTP options 方法
     */
    options<T = {}, U = {}, B = unknown>(
        name: string,
        path: string | RegExp,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;
    /**
     * HTTP options 方法
     */
    options<T = {}, U = {}, B = unknown>(
        path: string | RegExp | Array<string | RegExp>,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;

    /**
     * HTTP patch 方法
     */
    patch<T = {}, U = {}, B = unknown>(
        name: string,
        path: string | RegExp,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;
    /**
     * HTTP patch 方法
     */
    patch<T = {}, U = {}, B = unknown>(
        path: string | RegExp | Array<string | RegExp>,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;

    /**
     * 用所有方法注册路由。
     */
    all<T = {}, U = {}, B = unknown>(
        name: string,
        path: string | RegExp,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;
    /**
     * 用所有方法注册路由。
     */
    all<T = {}, U = {}, B = unknown>(
        path: string | RegExp | Array<string | RegExp>,
        ...middleware: Array<Router.Middleware<StateT & T, ContextT & U, B>>
    ): Router<StateT, ContextT>;

    /**
     * 为已经初始化的路由器实例设置路径前缀。
     *
     * @example
     *
     * ```javascript
     * router.prefix('/things/:thing_id')
     * ```
     */
    prefix(prefix: string): Router<StateT, ContextT>;

    /**
     * 返回路由器中间件,它分派与请求匹配的路由。
     */
    routes(): Router.Middleware<StateT, ContextT>;

    /**
     * 返回路由器中间件,它分派与请求匹配的路由。
     */
    middleware(): Router.Middleware<StateT, ContextT>;

    /**
     * 返回单独的中间件,用于响应带有包含允许方法的 `Allow` 请求头的 `OPTIONS` 请求,以及适当地响应“405 方法不允许” 和 “501 未实现”。
     *
     * @example
     *
     * ```javascript
     * var Koa = require('koa');
     * var Router = require('koa-router');
     *
     * var app = new Koa();
     * var router = new Router();
     *
     * app.use(router.routes());
     * app.use(router.allowedMethods());
     * ```
     *
     * **使用[Boom](https://github.com/hapijs/boom)的例子:**
     *
     * ```javascript
     * var Koa = require('koa');
     * var Router = require('koa-router');
     * var Boom = require('boom');
     *
     * var app = new Koa();
     * var router = new Router();
     *
     * app.use(router.routes());
     * app.use(router.allowedMethods({
     *   throw: true,
     *   notImplemented: () => new Boom.notImplemented(),
     *   methodNotAllowed: () => new Boom.methodNotAllowed()
     * }));
     * ```
     */
    allowedMethods(
        options?: Router.RouterAllowedMethodsOptions
    ): Router.Middleware<StateT, ContextT>;

    /**
     * 使用可选的 30x 状态 `code` 将 `source` 重定向到 `destination` URL。
     *
     * `source` 和 `destination` 都可以是路由名.
     *
     * ```javascript
     * router.redirect('/login', 'sign-in');
     * ```
     *
     * 这相当于:
     *
     * ```javascript
     * router.all('/login', ctx => {
     *   ctx.redirect('/sign-in');
     *   ctx.status = 301;
     * });
     * ```
     */
    redirect(source: string, destination: string, code?: number): Router<StateT, ContextT>;

    /**
     * 创建并注册一个 route。
     */
    register(
        path: string | RegExp,
        methods: string[],
        middleware: Router.Middleware<StateT, ContextT> | Array<Router.Middleware<StateT, ContextT>>,
        opts?: Router.LayerOptions,
    ): Router.Layer;

    /**
     * 具有给定 `name` 的 Lookup route。
     */
    route(name: string): Router.Layer | boolean;

    /**
     * 为 route 生成URL。接受命名`params` 的映射或一系列参数(对于正则表达式路由)
     *
     * router = new Router();
     * router.get('user', "/users/:id", ...
     *
     * router.url('user', { id: 3 });
     * // => "/users/3"
     *
     * 可以从第三个参数生成查询(query):
     *
     * router.url('user', { id: 3 }, { query: { limit: 1 } });
     * // => "/users/3?limit=1"
     *
     * router.url('user', { id: 3 }, { query: "limit=1" });
     * // => "/users/3?limit=1"
     *
     */
    url(name: string, params?: any, options?: Router.UrlOptionsQuery): Error | string;

    /**
     * 匹配给定的 `path` 并返回相应的routes。
     */
    match(path: string, method: string): Router.RoutesMatch;

    /**
     * 为命名的路由参数运行中间件。适用于自动加载或验证。
     *
     * @example
     *
     * ```javascript
     * router
     *   .param('user', (id, ctx, next) => {
     *     ctx.user = users[id];
     *     if (!ctx.user) return ctx.status = 404;
     *     return next();
     *   })
     *   .get('/users/:user', ctx => {
     *     ctx.body = ctx.user;
     *   })
     *   .get('/users/:user/friends', ctx => {
     *     return ctx.user.getFriends().then(function(friends) {
     *       ctx.body = friends;
     *     });
     *   })
     *   // /users/3 => {"id": 3, "name": "Alex"}
     *   // /users/3/friends => [{"id": 4, "name": "TJ"}]
     * ```
     */

    param<BodyT = unknown>(param: string, middleware: Router.ParamMiddleware<StateT, ContextT, BodyT>): Router<StateT, ContextT>;

    /**
     * 为路由生成URL。接受一个路径名和一个名为 `params` 的 map。
     *
     * @example
     *
     * ```javascript
     * router.get('user', '/users/:id', (ctx, next) => {
     *   // ...
     * });
     *
     * router.url('user', 3);
     * // => "/users/3"
     *
     * router.url('user', { id: 3 });
     * // => "/users/3"
     *
     * router.use((ctx, next) => {
     *   // 重定向到命名路由
     *   ctx.redirect(ctx.router.url('sign-in'));
     * })
     *
     * router.url('user', { id: 3 }, { query: { limit: 1 } });
     * // => "/users/3?limit=1"
     *
     * router.url('user', { id: 3 }, { query: "limit=1" });
     * // => "/users/3?limit=1"
     * ```
     */
    static url(path: string | RegExp, params: object): string;
 }

3.5 关于路由的一些补充

3.5.1 HTTP 动词(Verbs)和 koa-router verb 方法

HTTP 定义了一组请求方法, 以表明要对给定资源执行的操作。指示针对给定资源要执行的期望动作。HTTP 动词 是用于 HTTP 请求方法中的一系列的值,包括了:

HTTP请求方法动词描述
GETGET 方法请求一个指定资源的表示形式。使用GET的请求应该只被用于获取数据.
HEADHEAD 方法请求一个与GET请求的响应相同的响应,但没有响应体。
POSTPOST 方法用于将实体提交到指定的资源,通常导致状态或服务器上的副作用的更改。
PUTPUT 方法用请求有效载荷替换目标资源的所有当前表示。
DELETEDELETE 方法删除指定的资源。
CONNECTCONNECT 方法建立一个到由目标资源标识的服务器的隧道。
OPTIONSOPTIONS 方法用于描述目标资源的通信选项。
TRACETRACE 方法沿着到目标资源的路径执行一个消息环回测试。
PATCHPATCH 方法用于对资源应用部分修改。

koa-router 插件为我们提供了一些列 router.verb() 方法其中 verb只某个 HTTP 动词,例如:router.get()router.post()。另外 router.all() 方法可以匹配所有的这类方法。详细类型签名参见 3.4 小节。

当路径匹配时,其路径在ctx._matchedRoute处可用。如果路由被命名(参考 3.5.2 命名路由),该名称可在 ctx._matchedRouteName 获得。

koa-router 内部使用 path-to-regexp 模块将路由路径转换为正则表达式。在匹配请求时不会考虑 url 的 query 字符串。

3.5.2 命名路由

顾名思义,所谓 命名路由 就是给一个路由起别名。也就是说,路由可以有名称,这允许我们在开发过程中生成URL和简单地去重命名URL。

get 请求为例,Router 对象上的 get 是两个 重载 的方法,当其中一个重载方法参数为 path...middleware,而另外一个为 namepath...middleware。这就是说我们可以给 get 方法描述的路由在第一个参数的位置多指定一个 name 字符串参数来表示路由名,比如:

router.get('user', '/users/:id', (ctx, next) => {
 // ...
});

这里的 'user' 对应于 name,即该条路由被命名为 usrt。定义了名字自然是为了更方便地来进行使用地。

Router 对象上的 url 方法可以为路由生成URL,它接受的第一个参数就是 命名路由的路由名name 。该方法的类型签名为:

url(name: string, params?: any, options?: Router.UrlOptionsQuery): Error | string;

我们就用它来试一下:

router.url('user', 3);  // => "/users/3"

其中,这里就是把 param 值 (3)赋给了 名字为 user 的路由中后面的 parameter/:id),因此就是/users/3

3.5.3 嵌套路由

顾名思义,所谓 嵌套路由就是在接续一个路由以同样地方式定义子路由。从实现层面上看,路由地使用是通过中间件使用上的嵌套。
例如:

const father = new Router();
const child = new Router();


child.get('/', (ctx, next) => {...});
child.get('/:pid', (ctx, next) => {...});

routers1.use(
  '/father/:fid/child', 
  posts.routes(),
  routers2.allowedMethods()
);

// 响应于 "/father/123/child" 
// 和 "/father/123/child/123"
app.use(routers1.routes());

3.5.4 路由前缀

嵌套的子路由自动地被继承其了父路由作为它地前缀,不过还可以手动在实例化 Router 对象时为路由添加前缀。例如:

const router = new Router({
  prefix: '/users'
});

router.get('/', ...); // responds to "/users"
router.get('/:id', ...); // responds to "/users/:id"

3.5.5 url 参数

我们在 2.1.2 统一资源定位符(url) 小节已提到过什么是 url 参数,一旦在需要使用到 url参数 时,koa-router 地动词函数中可以很简单地去用它,例如:

router.get('/:category/:title', (ctx, next) => {
  console.log(ctx.params);
  // => { category: 'programming', title: 'how-to-node' }
});

4. 搭建应用为中心的路由系统

4.1 来源于 Django 的启发

4.1.1 Django 中的路由调度思路

一九年的时候我自学了 Django, 这是一款基于 Python 语言的重型 Web 框架。之所以称之为 “重型”框架是因为它具有相当强大、丰富的开箱即用功能,其中就包括路由系统。按照 Django 官方的说法,他是一款 MVT 类型的架构,不过究其本质,实际上还是广义上的 MVC 模式,之所以硬要叫做 MVT 大概是因为 Django 框架中具有强大的 模板系统。所谓模板系统,对于具有前端开发经验的读者来说是不陌生的,因为前端框架最主要部分就是模板语言,比如 vue,他们都以自己的语法形式提供了视图层面开发的模板语言。不过这与本文的主题关系不大,因而不做过多展开讲解。

Django 有一个典型的特点,就是一般而言它是由一个个子应用组成的,甚至为你提供了在创建应用的脚手架(CLI,命令行工具)。在每一个应用中通过一个用作 调度器的 路由模块控将当前应用的路由分配到每一个视图上。一个视图由若干个视图对象构成,这些视图对象既可以是基于类的,也可以是基于函数的,但是都必须通过调度器分配路由并且以某种形式返回一个HTTP响应对象才是有效的。

从整体上看子应用当然是需要集中管理的,这就意味着需要一个 中央调度器,它一般负责从根路由开始将路由分配到各个应用(虽然这也不是强制性的,你可以直接分配视图,但这往往是破坏项目的优雅性和易维护性)。因此从整体上看,不论是 子应用 还是 子路由,看起来都是一个树形结构的。

4.1.2 构建基于 Koa 的 MVC 架构体系

相对于 Django 而言, Koa 是一款极度轻型的框架(express也是),它不仅没有模板语言、后台系统、权限管理等等相对复杂的功能,甚至连路由都需要额外模块安装并以作为中间件的形式手动引入。在我们配置好 koa-router 后,也可以仿照 Django 的方式搭建围绕应用的路由。

这也就是说,首先我们整个项目管理的主体是应用,每一个应用都是独立的(至少是尽最大可能将应用之间的耦合度降到最低)——去耦合往往也是代码可读性的要求。在每一个应用内,通过一个应用的 调度器 处理 url 和视图之间的关系,这个调度器也就是我们的应用级路由模块。

4.2 实践

现在我们在 前一篇文章 的基础上创建一个 src/settings.ts 文件用于放项目的一些全局配置、一个 src/libs/url.ts 文件用于放与路由相关的工具,再创建一个 src/apps 目录用于存放应用,现在我们在该文件夹下创建两个应用文件夹,一个为auth,另外一个为 home

现在我们希望自动地获取每个应用下 的 Router 对象合并到一个总的 Router 对象作为 中间件给 Koa 实例使用。

先在 setting.ts 中定义一些常量备用:

import path from "path";

export const devPort = 3000;
export const BASE_DIR = path.resolve(__dirname,'.');
export const APPS_DIR = path.join(BASE_DIR,'apps');

于是在 libs\conf.ts 中编写一个 getRouters 函数:

import path from "path";
import Router from '@koa/router';
import { APPS_DIR } from "../settings";

export declare type URLResolver = { path: string | string[] | RegExp, include: string }

export async function getRouters(urlpatterns: URLResolver[]): Promise<Router> {
    const router = new Router();

    for (let index = 0; index < urlpatterns.length; index++) {
        const item = urlpatterns[index];
        const app_urls = await import(path.resolve(APPS_DIR, ...item.include.split('.')));
        router.use(item.path, app_urls.router.routes(), app_urls.router.allowedMethods());
    }
    
    return router

编写根路由文件 src/urls.ts

import type { URLResolver } from './lib/conf';

const urlpatterns: URLResolver[] = [
  { path: '/', include: 'home.urls' },
  { path: '/auth', include: 'auth.urls' },
]

export {
  urlpatterns
}
  • path 表示以一个路由的值,include 所包含进来的路由文件中定义的路由都将继承于path定义的路由值;
  • include 表示包含的一个子路由文件,. 好分割父子目录。其中最左侧第一个目录是 APPS_DIR 的直接子目录。

接着在homeauth 两个应用下的 urls.ts 文件中简单定义路由:
src/apps/home/urls.ts

import Router from '@koa/router';
const router:Router = new Router();

router.get('/', async (ctx) => {
  ctx.type = 'html';
  ctx.body = '<h1>这是 Home 页!(\'/\')</h1>';
})

export {
  router
}

src/apps/auth/urls.ts

import Router from '@koa/router';

const router:Router = new Router();

router.get("/", async (ctx) => {
  ctx.type = 'html';
  ctx.body = '<h1>这是Auth页(/auth)!</h1>';
})

export {
    router
}

src/app.ts做出相应调整:

import Koa from 'koa';
import { logger } from './logger';
import { urlpatterns } from './urls';
import Boom from 'boom';
import { getRouters } from './lib/conf';
import { devPort } from './settings';

async function bootstrap() {
    const app = new Koa();
    const router = await getRouters(urlpatterns);
    app.use(router.routes());
    app.use(router.allowedMethods({
        throw: true,
        notImplemented: () => Boom.notImplemented(),
        methodNotAllowed: () => Boom.methodNotAllowed()
    }));

    app.listen(devPort,
        ()=>{
            logger.debug(`app started at: http://localhost:${devPort}`);
            logger.debug(`swagger pages at: http://localhost:${devPort}/swagger/index.html`);
        }
    );
}

bootstrap()

现在我们重新运行,并在浏览器中访问:

在这里插入图片描述

在这里插入图片描述

5. 小结

本文在前一篇文章的基础上使用官方的koa-router(已改名为@koa/router)中间件为我们的 Koa 项目添加了路由。为了使各部分功能看起来更独立,我们使用不同的应用名来区分功能,在每个因公子目录下添加一个路由文件,不过我们目前还并没有要求 根路由所包含的文件必须是src/apps/xxx 下的文件,毕竟这只是为了后期的去耦合,这些将在之后的文章中做更多的处理。目前我们的路由还是 koa-router 的中间件形式,其实这对于视图的调度并不是那么直观,这也将在后续文章中做更多地改进。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/7594.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

哪些是真正的全光谱灯品牌呢?推荐五款全光谱护眼灯

所谓全光谱&#xff0c;就是指灯光的色谱成分无限接近太阳光的色谱成分。我们都知道&#xff0c;太阳光不单单只有一束简单的白光&#xff0c;而是有很多种颜色的单色光复合而成&#xff0c;所以它的色彩显色效果非常丰富、真实&#xff0c;这些单色光也成了太阳光的色谱成分。…

浅谈机器学习--聚类

还不了解机器学习&#xff1f;来看&#xff01; 目录 一.聚类 二.k均值聚类算法(k-means) 1.k均值聚类算法的流程 二.k均值算法的改进 1.二分k-means算法 2.k-means算法 3.k-medoids算法 4.Mini Batch k-means算法 三.DBSCAN算法 1.​编辑-邻域 2.核心点和边界点 …

关于TextureRender适配的解决方案

当我们用摄像机渲染出一个图片&#xff0c;显示在UI的时候&#xff0c;会发现&#xff0c;你如果自适配&#xff0c;那么就会拉伸图片&#xff0c;导致人物或者场景变形。 我最近就遇到了这个事&#xff0c;这里我给出几种问题和解决方案&#xff1a; 1 &#xff1a;当我们想…

NSSCTF Round#11 --- 密码个人赛 wp

文章目录ez_encMyMessageMyGameez_signinez_facez_enc 题目&#xff1a; ABAABBBAABABAABBABABAABBABAAAABBABABABAAABAAABBAABBBBABBABBABBABABABAABBAABBABAAABBAABBBABABABAAAABBAAABABAABABBABBBABBAAABBBAABABAABBAAAABBBAAAABAABBBAABBABABAABABAAAAABBBBABAABBBBAAAAB…

开心档之开发入门网-C++ 变量类型

C 变量类型 目录 C 变量类型 C 中的变量定义 C 中的变量声明 实例 实例 C 中的左值&#xff08;Lvalues&#xff09;和右值&#xff08;Rvalues&#xff09; 变量其实只不过是程序可操作的存储区的名称。C 中每个变量都有指定的类型&#xff0c;类型决定了变量存储的大小…

Java多线程:线程组

线程组 可以把线程归属到某一个线程组中&#xff0c;线程组中可以有线程对象&#xff0c;也可以有线程组&#xff0c;组中还可以有线程&#xff0c;这样的组织结构有点类似于树的形式&#xff0c;如图所示&#xff1a; 线程组的作用是&#xff1a;可以批量管理线程或线程组对象…

电脑清理怎么做?5个方法帮你解决电脑空间不足的问题!

案例&#xff1a;电脑清理怎么做&#xff1f; 【求一个电脑清理的好方法&#xff01;电脑垃圾文件太多了又不敢随意删除&#xff0c;怕误删重要的文件&#xff01;哪位友友可以帮我出出主意呀&#xff1f;到底应该怎么清理电脑呢&#xff1f;】 电脑使用的时间长了都会慢慢变…

(链表)合并两个排序的链表

文章目录前言&#xff1a;问题描述&#xff1a;解题思路&#xff1a;代码实现&#xff1a;总结&#xff1a;前言&#xff1a; 此篇是针对链表的经典练习。 问题描述&#xff1a; 输入两个递增的链表&#xff0c;单个链表的长度为n&#xff0c;合并这两个链表并使新链表中的节…

用队列实现栈和用栈实现队列

目录用队列实现栈创建栈实现入栈实现出栈判空取栈顶元素释放用栈实现队列创建队列入队出队返回队列开头的元素判空释放前面我们实现了栈和队列&#xff0c;其实栈和队列之间是可以相互实现的 下面我们来看一下 用 队列实现栈 和 用栈实现队列 用队列实现栈 使用两个队列实现一…

Windows创建用户,添加到管理员组,添加到远程桌面组、RDP

原因&目的 在获得反弹shell后无法得到明文密码&#xff0c;无法远程桌面登录 在目标机器创建新的账号&#xff0c;且为管理员账号&#xff0c;可以远程桌面登录 cmd /c net user gesila 123 /add cmd /c net localgroup Administrators gesila /add cmd /c net localgro…

优思学院 | 质量工程师的职责有哪些?

质量工程师&#xff0c;是一位肩负着质量管理、质量控制和质量改进使命的职业人员。他们身负使命&#xff0c;不断探究、发现、改进&#xff0c;为企业打造出更加卓越、可靠的产品和服务。 在大多数企业中&#xff0c;质量工程师是一个非常重要的职位&#xff0c;他们的职责在…

智能立体车库plc以太网无线应用

一、项目背景 此项目为平面移动类智能停车库&#xff0c;是以传感器网络为支撑的物联网智能停车管理系统。比较于传统的停车场模式&#xff0c;智能立体车库不仅占地少&#xff0c;空间利用率高&#xff0c;智能化程度高&#xff0c;采用集约化系统化的车位管理、收费管理&…

Vue3 集成Element3、Vue-router、Vuex实战

第一步&#xff1a;使用Vite快速搭建Vue 3项目 基础操作总结&#xff1a; npm init vite-app 项目名称cd 项目名称npm installnpm run dev 温馨提示&#xff1a;如果还是Vue 2 环境请参考&#xff1a;Vue 2 升级Vue3 &#xff0c;并且使用vsCode 搭建Vue3 开发环境 Vue 3 …

ARM uboot 启动 Linux 内核

一、编译厂商提供的 uboot 此处&#xff0c;我使用的是九鼎提供的 uboot &#xff1a; 二、烧录 uboot 到 SD 卡 进入 uboot 的 sd_fusing 目录&#xff0c;执行命令烧写 uboot &#xff1a; ./sd_fusing.sh /dev/sdb。 三、将 SD 卡插入开发板&#xff0c;进入 uboot 按任意…

操作系统-内存管理

一、总论 1.1 硬件术语 ​ 为了不让读者懵逼&#xff08;主要是我自己也懵逼&#xff09;&#xff0c;所以特地整理一下在后面会用到术语。 ​ 我们电脑上有个东西叫做内存&#xff0c;他的大小比较小&#xff0c;像我的电脑就是 16 GB 的。它是由 ROM 和 RAM 组成的&#x…

RK3588平台开发系列讲解(同步与互斥篇)信号量介绍

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、信号量介绍二、信号量API1、结构体2、API三、函数调用流程沉淀、分享、成长,让自己和他人都能有所收获!😄 📢上一章我们看了自旋锁的原理,本章我们一起学习下信号量的用法。 一、信号量介绍 和自旋锁一样,…

渗透测试综合实验(迂回渗透,入侵企业内网并将其控制为僵尸网络)

第1节 实验概述 1.1 实验背景概述 本实验为模拟真实企业环境搭建的漏洞靶场&#xff0c;通过网络入侵Web服务器&#xff0c;拿到控制权限后发现有内网网段&#xff0c;建立隧道做内网穿透&#xff0c;接着进一步扫描内网主机&#xff0c;并进行漏洞利用&#xff0c;最终通过域…

java登录页面验证码的生成以及展示

1、代码示例 后端返回前端一个字节数组。 2、gif图面展示 网页中一张图片可以这样显示&#xff1a; <img src“http://www.jwzzsw.com/images/log.gif”/>也可以这样显示&#xff1a; <img src“data:image/gif;base64,R0lGODlhAgACAIAAAP///wAAACwAAAAAAgACAAACAo…

聚会Party

前言 加油 原文 聚会常用会话 ❶ He spun his partner quickly. 他令他的舞伴快速旋转起来。 ❷ She danced without music. 她跳了没有伴乐的舞蹈。 ❸ The attendants of the ball are very polite. 舞会的服务员非常有礼貌。 ❶ Happy birthday to you! 祝你生日快乐!…

Linux--高级IO--poll--0326

1. poll #include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); poll只负责等。 参数介绍 fds 是一个结构体类型的地址&#xff0c;相比于select中的fd_set类型,pollfd结构体可以内部封装一些遍历&#xff0c;解决需要关系那些文件描述符&#…