本打算教一步步实现koa-router,由于要解释的太多了,因此先简化成mini版本,从实现部分功能到阅读源码,但愿能让你好理解一些。
但愿你以前有读过koa源码,没有的话,给你连接javascript
router最重要的就是路由匹配,咱们就从最核心的入手vue
router.get('/string',async (ctx, next) => { ctx.body = 'koa2 string' }) router.get('/json',async (ctx, next) => { ctx.body = 'koa2 json' })
咱们但愿java
1.咱们须要一个数组,数组里每一个都是一个对象,每一个对象包含路径,方法,函数,传参等信息
这个数组咱们起个名字叫stacknode
const stack = []
2.对于每个对象,咱们起名叫layer
咱们把它定义成一个函数webpack
function Layer() { }
咱们把页面比喻成一个箱子,箱子是对外的,箱子须要有入口,须要容纳。把每个router比做放在箱子里的物件,物件是内部的git
定义两个js页面,router.js作为入口,对于当前页面的访问的处理,layer.js包含开发者已经约定好的规则程序员
router.jsgithub
module.exports = Router; function Router(opts) { // 容纳layer层 this.stack = []; };
layer.jsweb
module.exports = Layer; function Layer() { };
咱们在Router要放上许多方法,咱们能够在Router内部挂载方法,也能够在原型上挂载函数json
可是要考虑多可能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 }; });
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());
var router = new Router({ prefix: '/users' }); router.get('/', ...); // responds to "/users" router.get('/:id', ...); // responds to "/users/:id"
router.get('/:category/:title', (ctx, next) => { console.log(ctx.params); // => { category: 'programming', title: 'how-to-node' } });
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 处理自定义路径传参
实现以下需求,访问/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); }; };
实现以下需求
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贡献源码的人生是没有意义的。不想当将军的士兵不是好士兵。因此之后大部分时间会去作开源,谢谢阅读。