nodeJS server-side-developkoa开发实践2:为koa项目添加路由模块
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模式关联路由和视图,因此有必要先讲解何谓 MVC。 MVC 是交互系统开发中常用的一种 软件设计模式。 经典MVC架构中,M、V、C 分别表示的是三个开发层级。其中 M(Model)代表 数据模型,V(View)代表视图,C(Controller)则是控制器(也称调度器)。
- 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-router 是 express 路由风格的路由,这大概由于 koa 和 express 是同一个团队进行开发的有较大关系。对于熟悉 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
,而另外一个为 name
、path
、...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
的直接子目录。
接着在home
、auth
两个应用下的 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
的中间件形式,其实这对于视图的调度并不是那么直观,这也将在后续文章中做更多地改进。