作者: 李俊才: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
我们上一节搭建了一个基于 TypeScript 地开发环境,TypeScript 是强类型语言,这对于我们地开发提供了强大的类型支持,能够给我们代码很多更加智能化的提示,并且由李云我们后期的改错与维护。在上一节中,我们已经实现了一个基本的 koa 服务器的搭建,但是它还有很多不足,比如,它还没有路由,日志记录也需要进行进一步修改等等。
上一节的服务器时一个单一地址的静态页面,我们这一节的目标就是在上一节的基础上添加路由模块,实现 koa web 的路由功能。
路由 一词的来源与其实工程技术并无关系,它很早就有,仅仅是一个生活中很常见的词汇,含以上表示来自哪里。随着二十世纪电气的到来,电气电子相关技术的蓬勃发展,控制技术、通信工程等相关专业应运而生。不论是从最早在电气控制领域的 工业控制网络 到后来的 计算机网络,随着生产力发展的需要都引入了很多来源于生活的概念,路由 就是其中之一。在网络工程领域,路由(routing)是指分组从源到目的地时,决定端到端路径的网络范围的进程。后来随着互联网技术的发展,从网络工程领域再次借用和引申了 路由 这一概念。在用户界面系统中,比如我们所熟知的 web,路由简单的来说就是根据用户请求的 URL 链接来判断对应的处理程序,并返回处理结果。
生活中我们以各种形式表达道路,如水路、陆路,通过文字描述我们人类从一个位置到达另外一个位置所经过的道路,就是路由。在网络领域也是类似的,同样需要一个方法来表示信息所经过的位置,这个方法就是 统一资源定位系统(URL,uniform resource locator)。
URL 是由一串字母,数字和特殊符号组成的字符所构成的字符串。在不同的使用场景存在不同的 URL 标准协议,比如最常见的有:
URL的一般语法格式为:
protocol :// hostname[:port] / path / [:parameters][?query]#fragment
其中:
'/'
号隔开的字符串,一般用来表示主机上的一个目录或文件地址;'&'
符号隔开,'='
符号隔开;在后文中我们需要以MVC模式关联路由和视图,因此有必要先讲解何谓 MVC。 MVC 是交互系统开发中常用的一种 软件设计模式。 经典MVC架构中,M、V、C 分别表示的是三个开发层级。其中 M(Model)代表 数据模型,V(View)代表视图,C(Controller)则是控制器(也称调度器)。
因此总的来看 MVC 模式将交互系统分层了三个层次,分别是 数据层、视图层 和 调度层。 使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。
与 express 不一样的是,koa 自身连路由系统也没有,需要额外安装路由中间件插件。这给开发者带来了更多的选择,你既可以使用 koa 官方团队提供的路由中间件 koa-router,也可以选择其它的方式实现 Koa Web 的路由。也就是说,是否使用官方的 koa-router 其实并非强制的。
如上所述 koa-router 是一款由 koa 官方以 中间件 形式提供的路由模块,它能完成我们一般的路由功能,并且由于是官方所提供的插件,很多第三方插件更倾向于基于 koa-router 进行推出,因此在本文中也使用它进行路由的讲解,这对于 Web 后端类项目的初学者也更加友好。
另外, koa-router 是 express 路由风格的路由,这大概由于 koa 和 express 是同一个团队进行开发的有较大关系。对于熟悉 express 的读者可以直接入手。
要使用 koa-router 需要先进行安装。目前 koa 团队规范了 路由模块 的项目名称,从旧版的 koa-router 迁移到了新的 @koa/router。因此依据当前最新的文档,你可以使用如下方式对 koa-router 进行安装:
npm i @koa/router
# or
yarn add @koa/router
# or
pnpm i @koa/router
(依据你项目所使用的包管理工具进行选择)
由于我们使用 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
(依据你项目所使用的包管理工具进行选择)
我们用的主要就是 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;
}
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 字符串。
顾名思义,所谓 命名路由 就是给一个路由起别名。也就是说,路由可以有名称,这允许我们在开发过程中生成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
。
顾名思义,所谓 嵌套路由就是在接续一个路由以同样地方式定义子路由。从实现层面上看,路由地使用是通过中间件使用上的嵌套。
例如:
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());
嵌套的子路由自动地被继承其了父路由作为它地前缀,不过还可以手动在实例化 Router 对象时为路由添加前缀。例如:
const router = new Router({
prefix: '/users'
});
router.get('/', ...); // responds to "/users"
router.get('/:id', ...); // responds to "/users/:id"
我们在 2.1.2 统一资源定位符(url) 小节已提到过什么是 url 参数,一旦在需要使用到 url参数 时,koa-router 地动词函数中可以很简单地去用它,例如:
router.get('/:category/:title', (ctx, next) => {
console.log(ctx.params);
// => { category: 'programming', title: 'how-to-node' }
});
一九年的时候我自学了 Django, 这是一款基于 Python 语言的重型 Web 框架。之所以称之为 “重型”框架是因为它具有相当强大、丰富的开箱即用功能,其中就包括路由系统。按照 Django 官方的说法,他是一款 MVT 类型的架构,不过究其本质,实际上还是广义上的 MVC 模式,之所以硬要叫做 MVT 大概是因为 Django 框架中具有强大的 模板系统。所谓模板系统,对于具有前端开发经验的读者来说是不陌生的,因为前端框架最主要部分就是模板语言,比如 vue,他们都以自己的语法形式提供了视图层面开发的模板语言。不过这与本文的主题关系不大,因而不做过多展开讲解。
Django 有一个典型的特点,就是一般而言它是由一个个子应用组成的,甚至为你提供了在创建应用的脚手架(CLI,命令行工具)。在每一个应用中通过一个用作 调度器的 路由模块控将当前应用的路由分配到每一个视图上。一个视图由若干个视图对象构成,这些视图对象既可以是基于类的,也可以是基于函数的,但是都必须通过调度器分配路由并且以某种形式返回一个HTTP响应对象才是有效的。
从整体上看子应用当然是需要集中管理的,这就意味着需要一个 中央调度器,它一般负责从根路由开始将路由分配到各个应用(虽然这也不是强制性的,你可以直接分配视图,但这往往是破坏项目的优雅性和易维护性)。因此从整体上看,不论是 子应用 还是 子路由,看起来都是一个树形结构的。
相对于 Django 而言, Koa 是一款极度轻型的框架(express也是),它不仅没有模板语言、后台系统、权限管理等等相对复杂的功能,甚至连路由都需要额外模块安装并以作为中间件的形式手动引入。在我们配置好 koa-router 后,也可以仿照 Django 的方式搭建围绕应用的路由。
这也就是说,首先我们整个项目管理的主体是应用,每一个应用都是独立的(至少是尽最大可能将应用之间的耦合度降到最低)——去耦合往往也是代码可读性的要求。在每一个应用内,通过一个应用的 调度器 处理 url 和视图之间的关系,这个调度器也就是我们的应用级路由模块。
现在我们在 前一篇文章 的基础上创建一个 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 = '这是 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-router
(已改名为@koa/router
)中间件为我们的 Koa 项目添加了路由。为了使各部分功能看起来更独立,我们使用不同的应用名来区分功能,在每个因公子目录下添加一个路由文件,不过我们目前还并没有要求 根路由所包含的文件必须是src/apps/xxx
下的文件,毕竟这只是为了后期的去耦合,这些将在之后的文章中做更多的处理。目前我们的路由还是 koa-router
的中间件形式,其实这对于视图的调度并不是那么直观,这也将在后续文章中做更多地改进。