本打算教一步步實現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貢獻源碼的人生是沒有意義的。 不想當將軍的士兵不是好士兵。 因此之後大部分時間會去作開源,謝謝閱讀。