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
作者邮箱 :[email protected]
本文地址:https://blog.csdn.net/qq_28550263/article/details/129828590

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


目 录

  • 1. 目标概述
  • 2. 路由 与 koa
    • 2.1 路由的概念
      • 2.1.1 起源
      • 2.1.2 统一资源定位符(url)
    • 2.2 路由的原理
    • 2.3 了解关于 MVC 的概念
  • 3. koa-router
    • 3.1 概述
    • 3.2 配置 koa-router
    • 3.3 路由的 TypeScript 支持
    • 3.4 koa-router 的编程接口解析
    • 3.5 关于路由的一些补充
      • 3.5.1 HTTP 动词(Verbs)和 koa-router verb 方法
      • 3.5.2 命名路由
      • 3.5.3 嵌套路由
      • 3.5.4 路由前缀
      • 3.5.5 url 参数
  • 4. 搭建应用为中心的路由系统
    • 4.1 来源于 Django 的启发
      • 4.1.1 Django 中的路由调度思路
      • 4.1.2 构建基于 Koa 的 MVC 架构体系
    • 4.2 实践
  • 5. 小结


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请求方法动词 描述
GET GET 方法请求一个指定资源的表示形式。使用GET的请求应该只被用于获取数据.
HEAD HEAD 方法请求一个与GET请求的响应相同的响应,但没有响应体。
POST POST 方法用于将实体提交到指定的资源,通常导致状态或服务器上的副作用的更改。
PUT PUT 方法用请求有效载荷替换目标资源的所有当前表示。
DELETE DELETE 方法删除指定的资源。
CONNECT CONNECT 方法建立一个到由目标资源标识的服务器的隧道。
OPTIONS OPTIONS 方法用于描述目标资源的通信选项。
TRACE TRACE 方法沿着到目标资源的路径执行一个消息环回测试。
PATCH PATCH 方法用于对资源应用部分修改。

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

这是 Home 页!(\'/\')

'
; }) 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 = '

这是Auth页(/auth)!

'
; }) 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()

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

koa开发实践2:为koa项目添加路由模块_第1张图片

koa开发实践2:为koa项目添加路由模块_第2张图片

5. 小结

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

你可能感兴趣的:(koa,koa-router,node.js,服务器,web)