教你从写一个迷你koa-router到浏览koa-router源码

本盘算教一步步完成koa-router,由于要诠释的太多了,所以先简化成mini版本,从完成部份功能到浏览源码,愿望能让你好明白一些。
愿望你之前有读过koa源码,没有的话,给你链接

最中心需求-路由婚配

router最主要的就是路由婚配,我们就从最中心的入手

router.get('/string',async (ctx, next) => {
  ctx.body = 'koa2 string'
})

router.get('/json',async (ctx, next) => {
  ctx.body = 'koa2 json'
})

我们愿望

  • 途径接见 /string 页面显现 ‘koa2 string’
  • 途径接见 /json 页面显现 ‘koa2 json’

先剖析

网络开发者输入的信息设置

1.我们须要一个数组,数组里每一个都是一个对象,每一个对象包含途径,要领,函数,传参等信息
这个数组我们起个名字叫stack

const stack = []

2.关于每一个对象,我们起名叫layer
我们把它定义成一个函数

function Layer() {
    
}

我们把页面比方成一个箱子,箱子是对外的,箱子须要有进口,须要包容。把每一个router比作放在箱子里的物件,物件是内部的

定义两个js页面,router.js做为进口,关于当前页面的接见的处置惩罚,layer.js包含开发者已商定好的划定规矩

router.js

module.exports = Router;

function Router(opts) {
  // 包容layer层
  this.stack = [];
};

layer.js

module.exports = Layer;

function Layer() {

};

我们在Router要放上许多要领,我们能够在Router内部挂载要领,也能够在原型上挂载函数

然则要斟酌多能够Router要被屡次实例化,如许内里都要拓荒一份新的空间,挂载在原型就是统一份空间。
终究决议挂载在原型上

要领有许多,我们先完成商定几个经常运用的吧

const methods = [
  'get',
  'post',
  'put',
  'head',
  'delete',
  'options',
];
methods.forEach(function(method) {
  Router.prototype[method] = function(path,middleware){
    // 关于path,middleware,我们须要把它交给layer,拿到layer返回的效果
    // 这里交给另一个函数来是完成,我们叫它register就是暂存的意义
    this.register(path, [method], middleware);
    // 由于get还能够继承get,我们返回this
    return this
  };
});

完成layer的沟通

Router.prototype.register = function (path, methods, middleware) {
  let stack = this.stack;
  let route = new Layer(path, methods, middleware);
  stack.push(route);
  
  return route
};

这里我们先去写layer


const pathToRegExp = require('path-to-regexp');

function Layer(path, methods, middleware) {
  // 把要领称号放到methods数组里
  this.methods = [];
  // stack盛放中间件函数
  this.stack = Array.isArray(middleware) ? middleware : [middleware];
  // 途径
  this.path = path;
  // 关于这个途径天生婚配划定规矩,这里借助第三方
  this.regexp = pathToRegExp(path);
  // methods
  methods.forEach(function(method) {
    this.methods.push(method.toUpperCase());
    // 绑定layer的this,不然匿名函数的this指向window
  }, this);

};
// 给一个原型要领match婚配返回true
Layer.prototype.match = function (path) {
  return this.regexp.test(path);
};

回到router层

定义match要领,依据Developer传入的path, method返回 一个对象(包含是不是婚配,婚配胜利layer,和婚配胜利的要领)

Router.prototype.match = function (path, method) {
  const layers = this.stack;
  let layer;
  const matched = {
    path: [],
    pathAndMethod: [],
    route: false
  };
   //轮回寄放好的stack层的每一个layer
  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i];

    //layer是提早存好的途径, path是过来的path
    if (layer.match(path)) {
      // layer放入path,为何不把path传入,一是path已没用了,婚配了就够了,layer含有更多信息须要用
      matched.path.push(layer);
      //假如methods什么也没写,或许假如要领里含有你的过来的要领,那末把layer放入pathAndMethod
      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer);
        // 途径婚配,而且有要领
        if (layer.methods.length) matched.route = true;
      }
    }
  }

  return matched;
};

给Developer一个要领

app.use(index.routes())

这里不斟酌传多个id,和屡次婚配状况,拿到婚配的函数

Router.prototype.routes = function(){
  var router = this;

  const dispatch = function dispatch(ctx, next) {
    const path = ctx.path
    const method = ctx.method
    const matched = router.match(path, ctx.method);

    if (!matched.route) return next();
    const matchedLayers = matched.pathAndMethod
    // 先不斟酌多matchedLayers多stack状况
    return matchedLayers[0].stack[0](ctx, next);
  }

  return dispatch
}

此时一个迷你koa-router已完成了

读源码

需求完成

完成婚配

要领名婚配,途径婚配,还要满足动态参数的通报

而且还要给很懒的开发者一个router.all()
也就是说不必辨别要领了🙄

 router
    .get('/', (ctx, next) => {
          ctx.body = 'Hello World!';
    })
    .post('/users', (ctx, next) => {
         // ...
    })
    .put('/users/:id', (ctx, next) => {
        // ...
    })
    .del('/users/:id', (ctx, next) => {
        // ...
    })
    .all('/users/:id', (ctx, next) => {
        // ...
   });

写法的多样性

为了轻易浩瀚的开发者运用

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

以下写法
都是一个途径

// => "/users/3"

支撑中间件

router.get(
    '/users/:id',
    (ctx, next) => {
        return User.findOne(ctx.params.id).then(function(user)  
            ctx.user = user;
            next();
       });
    },
    ctx => {
        console.log(ctx.user);
        // => { id: 17, name: "Alex" }
    })

多层嵌套

 var forums = new Router();
 var posts = new Router();
 posts.get('/', (ctx, next) => {...});
 posts.get('/:pid', (ctx, next) => {...});
 forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());
 //responds to "/forums/123/posts" and "/forums/123/posts/123"
 app.use(forums.routes());

途径前缀(Router prefixes)

var router = new Router({
     prefix: '/users'
});
router.get('/', ...); 
// responds to "/users"
router.get('/:id', ...); 
// responds to "/users/:id"

URL parameters

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

router.js

methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    var middleware;

    if (typeof path === 'string' || path instanceof RegExp) {
      // 第二个参数是不是是途径,假如是途径字符串那末从下表[2]最先才是中间件
      middleware = Array.prototype.slice.call(arguments, 2);
    } else {
      middleware = Array.prototype.slice.call(arguments, 1);
      path = name;
      name = null;
    }

    this.register(path, [method], middleware, {
      name: name
    });

    return this;
  };
});

//别号
Router.prototype.del = Router.prototype['delete'];

methods援用第三方包含

function getBasicNodeMethods() {
  return [
    'get',
    'post',
    'put',
    'head',
    'delete',
    'options',
    'trace',
    'copy',
    'lock',
    'mkcol',
    'move',
    'purge',
    'propfind',
    'proppatch',
    'unlock',
    'report',
    'mkactivity',
    'checkout',
    'merge',
    'm-search',
    'notify',
    'subscribe',
    'unsubscribe',
    'patch',
    'search',
    'connect'
  ];
}
Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this;

  var dispatch = function dispatch(ctx, next) {
    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;

    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      ctx.matched = matched.path;
    }
    // ctx挂载router
    ctx.router = router;

    if (!matched.route) return next();
    // 拿到既婚配到途径又婚配到要领的layer
    var matchedLayers = matched.pathAndMethod
    // 掏出末了一个layer
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
    // 挂载_matchedRoute属性
    ctx._matchedRoute = mostSpecificLayer.path;
    // 假如有name,既以下写法会有name, name是string
    // router.get('/string','/string/:1',async (ctx, next) => {
    //   ctx.body = 'koa2 string'
    // })
    if (mostSpecificLayer.name) {
      // 挂载_matchedRouteName属性
      ctx._matchedRouteName = mostSpecificLayer.name;
    }
    // layerChain就是中间件数组,现在是两个函数
    layerChain = matchedLayers.reduce(function(memo, layer) {
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        ctx.params = layer.params(path, ctx.captures, ctx.params);
        // console.log('captures2', ctx.captures)
        // ctx.captures是 :id 的捕获,正则婚配slice截取获得
        // ctx.params是对象 {id:1}
        ctx.routerName = layer.name;
        return next();
      });
      return memo.concat(layer.stack);
    }, []);
    // 中间件挪用layerChain
    return compose(layerChain)(ctx, next);
  };

  // routes挂载router对象
  dispatch.router = this;
  // 每次挪用routes返回一个dispatch函数(layer.stack和memo),函数另有一个属于这个途径下的router属性对象
  return dispatch;
};

这里运用compose-koa中间件的体式格局来处置惩罚通报多个函数和多种婚配的状况
captures和params 处置惩罚自定义途径传参

param

完成以下需求,接见/users/:1
在param中能拿到user

router
  .param('user', (user, ctx, next) => {
    ctx.user = user;
     if (!ctx.user) return ctx.status = 404;
    return next();
   })
  .get('/users/:user', ctx => {
    ctx.body = ctx.user;
  })
Router.prototype.param = function (param, middleware) {
  this.params[param] = middleware;
  this.stack.forEach(function (route) {
    route.param(param, middleware);
  });
  return this;
};
Layer.prototype.param = function (param, fn) {
  var stack = this.stack;
  var params = this.paramNames;
  var middleware = function (ctx, next) {
   // 第一个参数是 ctx.params[param], params拿到了user
    return fn.call(this, ctx.params[param], ctx, next);
  };
};

params

完成以下需求

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

例子

router.get('/string/:id',async (ctx, next) => {
  ctx.body = 'koa2 string'
})

接见 string/1

// 拿到{id:1}
ctx.params = layer.params(path, ctx.captures, ctx.params);

  
Layer.prototype.params = function (path, captures, existingParams) {
  var params = existingParams || {};
    
  for (var len = captures.length, i=0; i<len; i++) {
    if (this.paramNames[i]) {
      var c = captures[i];
      // 找到name赋值
      params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c;
    }
  }
  // 返回{id:1}
  return params;
};

有兴致的能够研讨一下allowedMethods,prefix,use,redirect等原型要领,这里已把最中心的展现了,至此,koa源码系列解读终了。

尾声

从vue源码读到webpack再到koa,深感源码架构的风趣,比做营业风趣太多,有意义太多。
今后源码浏览应当不会纪录blog了,如许学起来太慢了。固然也会继承研讨源码。
我以为程序员不做开源不去github孝敬源码的人生是没有意义的。
不想当将军的兵士不是好兵士。
所以今后大部份时候会去做开源,
感谢浏览。

    原文作者:织雪纱奈
    原文地址: https://segmentfault.com/a/1190000018606149
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞