在前面章节中介绍的静态资源服务器在渲染目录页的时候使用了部分动态特性——模板渲染,而路由功能是动态服务器的基础

路由简单来讲就是根据客户端的请求路径和方法(get、post 等)调用不同的功能

最简单的路由

使用中间件可以轻松实现一个极简的路由

为了方便 response 内容输出,先写一个最简单的处理 response 的中间件 src/middleware/body.js

  1. module.exports = async function (ctx, next) {
  2. await next();
  3. const { body } = ctx;
  4. if (body !== null && body !== undefined) {
  5. if (typeof ctx.body === 'string') {
  6. ctx.res.end(ctx.body);
  7. } else {
  8. ctx.body.pipe(ctx.res);
  9. }
  10. }
  11. };

然后是最简单的路由实现,对响应的请求处理有限

  1. 只能响应 GET 方法
  2. 路径以 file/ 或者 /data/ 开头

src/middleware/router.js

  1. async function router(ctx, next) {
  2. await next();
  3. const { path, method } = ctx;
  4. if (/^\/file\/.+/.test(path) && method === 'GET') {
  5. ctx.body = `get file: ${path}`;
  6. } else if (/^\/data\/.+/.test(path) && method === 'GET') {
  7. ctx.body = `get data: ${path}`;
  8. } else {
  9. ctx.body = 'error!';
  10. }
  11. }
  12. module.exports = router;

start() 方法中注册两个中间件 src/index.js

  1. this.server = http.createServer((req, res) => {
  2. const { url, method } = req;
  3. // 准备中间件的执行环境
  4. const ctx = {
  5. req,
  6. res,
  7. config: this.config,
  8. path: url,
  9. method,
  10. };
  11. // 按顺序调用中间件
  12. compose([body, router])(ctx);
  13. }).listen(port, () => {
  14. console.log(`Dynamic server started at port ${port}`);
  15. });

完整代码:https://github.com/Samaritan89/dynamic-server/tree/v1

koa-router

这样的实现问题很明显,当路由多起来后代码中充斥大量的 if-else 不方便管理,koa-router 通过简洁的 api 让路由管理非常简单

  1. const Koa = require('koa');
  2. const KoaRouter = require('koa-router');
  3. const app = new Koa();
  4. // 创建 router 实例对象
  5. const router = new KoaRouter();
  6. //注册路由
  7. router.get('/', async (ctx, next) => {
  8. console.log('index');
  9. ctx.body = 'index';
  10. });
  11. app.use(router.routes()); // 添加路由中间件
  12. app.listen(3000);

通过 router.get(path, middleware) 就可以注册一个路径对应的处理函数,而且支持链式调用,最后通过 router.routes() 获取所有路径处理函数,转成一个中间件注册到 koa

实现

koa-router 功能很多,支持携带 param 参数的路由,了解其原理可以实现一个最基础的 router
src/util/router.js

  1. class Router {
  2. constructor() {
  3. const methods = ['get', 'post'];
  4. this.stack = {};
  5. methods.forEach(method => {
  6. // router.get(path, middleware), router.post(path, middleware)
  7. // router.register(path, method, middleware) 快捷方式
  8. Router.prototype[method] = function (path, middleware) {
  9. this.register(path, method, middleware);
  10. // 支持链式调用
  11. return this;
  12. };
  13. // 每种方法单独维护一个数组,提升路由匹配性能
  14. this.stack[method] = [];
  15. });
  16. }
  17. register(path, method, middleware) {
  18. // 注册一个处理函数导 this.stack
  19. this.stack[method].push({
  20. path,
  21. middleware,
  22. });
  23. }
  24. routes() {
  25. // 导出 koa 中间件
  26. }
  27. }
  28. module.exports = Router;

register(path, method, middleware) 用来代替之前 if(path === xxx && method === xxxx),为了方便使用在构造函数处理成了

  1. router.get(path, middleware)
  2. router.post(path, middleware)

routes() 方法用于最终请求来了匹配路径和method,选择合适的中间件执行

  1. routes() {
  2. const stack = this.stack;
  3. return async function (ctx, next) {
  4. const middlewareList = stack[ctx.method.toLowerCase()];
  5. let targetMiddleware;
  6. for (let i = 0; i < middlewareList.length; i++) {
  7. const { path, middleware } = middlewareList[i];
  8. // 忽略了 url 参数等影响
  9. const isPathMatch = path instanceof RegExp ? path.test(ctx.path) : path === ctx.path;
  10. if (isPathMatch) {
  11. targetMiddleware = middleware;
  12. // 使用匹配到的第一个路由,不再继续尝试
  13. break;
  14. }
  15. }
  16. if (targetMiddleware) {
  17. await targetMiddleware(ctx, next);
  18. } else {
  19. await next();
  20. }
  21. }
  22. }

这样就可以和 koa-router 一样写路径处理函数了
src/route.js

  1. const Router = require('./util/router');
  2. const router = new Router();
  3. router
  4. .get(/^\/file\/.+/, async (ctx, next) => {
  5. await next();
  6. const { path } = ctx;
  7. ctx.body = `get file: ${path}`;
  8. })
  9. .get(/^\/data\/.+/, async (ctx, next) => {
  10. await next();
  11. const { path } = ctx;
  12. ctx.body = `get data: ${path}`;
  13. })
  14. .get(/.*/, async (ctx, next) => {
  15. await next();
  16. const { path } = ctx;
  17. ctx.body = `error: ${path}`;
  18. });
  19. module.exports = router.routes();

完整代码:https://github.com/Samaritan89/dynamic-server/tree/v2