const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS']; module.exports = () => { const routes = []; const router = (req, res) => { }; router.use = (fn) => { routes.push({ method: null, path: null, handler: fn }); }; METHODS.forEach(item => { const method = item.toLowerCase(); router[method] = (path, fn) => { routes.push({ method, path, handler: fn }); }; }); };
以上主要是给 router 添加了 use、get、post 等方法,每当调用这些方法时,给 routes 添加一条 route 规则。
Note:
Javascript 中函数是一种特殊的对象,能被调用的同时,还可以拥有属性、方法。
接下来的重点在 router 函数,它需要做的是:
从req对象中取得 method、pathname
依据 method、pathname 将请求与routes数组内各个 route 按它们被添加的顺序依次匹配
如果与某个route匹配成功,执行 route.handler,执行完后与下一个 route 匹配或结束流程 (后面详述)
如果匹配不成功,继续与下一个 route 匹配,重复3、4步骤
const router = (req, res) => { const pathname = decodeURI(url.parse(req.url).pathname); const method = req.method.toLowerCase(); let i = 0; const next = () => { route = routes[i++]; if (!route) return; const routeForAllRequest = !route.method && !route.path; if (routeForAllRequest || (route.method === method && pathname === route.path)) { route.handler(req, res, next); } else { next(); } } next(); };
对于非路由中间件,直接调用其 handler。对于路由中间件,只有请求方法和路径都匹配成功时,才调用其 handler。当没有匹配上的 route 时,直接与下一个route继续匹配。
需要注意的是,在某条 route 匹配成功的情况下,执行完其 handler 之后,还会不会再接着与下个 route 匹配,就要看开发者在其 handler 内有没有主动调用 next() 交出控制权了。
在__server.js__中添加一些route:
router.use((req, res, next) => { console.info('New request arrived'); next() }); router.get('/actors', (req, res) => { res.end('Leonardo DiCaprio, Brad Pitt, Johnny Depp'); }); router.get('/actresses', (req, res) => { res.end('Jennifer Aniston, Scarlett Johansson, Kate Winslet'); }); router.use((req, res, next) => { res.statusCode = 404; res.end(); });
每个请求抵达时,首先打印出一条 log,接着匹配其他route。当匹配上 actors 或 actresses 的 get 请求时,直接发回演员名字,并不需要继续匹配其他 route。如果都没匹配上,返回 404。
在浏览器中依次访问 :9527/erwe、:9527/actors、:9527/actresses 测试一下:
network中观察到的结果符合预期,同时后台命令行中也打印出了三条New request arrived语句。
接下来继续改进 router 模块。
首先添加一个 router.all 方法,调用它即意味着为所有请求方法都添加了一条 route:
router.all = (path, fn) => { METHODS.forEach(item => { const method = item.toLowerCase(); router[method](path, fn); }) };
接着,添加错误处理。
/lib/router.js
const defaultErrorHander = (err, req, res) => { res.statusCode = 500; res.end(); }; module.exports = (errorHander) => { const routes = []; const router = (req, res) => { ... errorHander = errorHander || defaultErrorHander; const next = (err) => { if (err) return errorHander(err, req, res); ... } next(); };
server.js
... const router = require('./lib/router')((err, req, res) => { console.error(err); res.statusCode = 500; res.end(err.stack); }); ...
默认情况下,遇到错误时会返回 500,但开发者使用 router 模块时可以传入自己的错误处理函数将其替代。
修改一下代码,测试是否能正确执行错误处理:
router.use((req, res, next) => { console.info('New request arrived'); next(new Error('an error')); });
这样任何请求都应该返回 500:
继续,修改 route.path 与 pathname 的匹配规则。现在我们认为只有当两字符串相等时才让匹配通过,这没有考虑到 url 中包含路径参数的情况,比如:
localhost:9527/actors/Leonardo与
router.get('/actors/:name', someRouteHandler);这条route应该匹配成功才是。
新增一个函数用来将字符串类型的 route.path 转换成正则对象,并存入 route.pattern:
const getRoutePattern = pathname => { pathname = '^' + pathname.replace(/(\:\w+)/g, '\(\[a-zA-Z0-9-\]\+\\s\)') + '$'; return new RegExp(pathname); };
这样就可以匹配上带有路径参数的url了,并将这些路径参数存入 req.params 对象: