router.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. 'use strict';
  2. /**
  3. * RESTful resource routing middleware for eggjs.
  4. */
  5. const debug = require('debug')('egg-router');
  6. const compose = require('koa-compose');
  7. const HttpError = require('http-errors');
  8. const methods = require('methods');
  9. const Layer = require('./layer');
  10. /**
  11. * @module koa-router
  12. */
  13. class Router {
  14. /**
  15. * Create a new router.
  16. *
  17. * @example
  18. *
  19. * Basic usage:
  20. *
  21. * ```javascript
  22. * var Koa = require('koa');
  23. * var Router = require('koa-router');
  24. *
  25. * var app = new Koa();
  26. * var router = new Router();
  27. *
  28. * router.get('/', (ctx, next) => {
  29. * // ctx.router available
  30. * });
  31. *
  32. * app
  33. * .use(router.routes())
  34. * .use(router.allowedMethods());
  35. * ```
  36. *
  37. * @alias module:koa-router
  38. * @param {Object=} opts optional
  39. * @param {String=} opts.prefix prefix router paths
  40. * @class
  41. */
  42. constructor(opts) {
  43. this.opts = opts || {};
  44. this.methods = this.opts.methods || [
  45. 'HEAD',
  46. 'OPTIONS',
  47. 'GET',
  48. 'PUT',
  49. 'PATCH',
  50. 'POST',
  51. 'DELETE',
  52. ];
  53. this.params = {};
  54. this.stack = [];
  55. }
  56. /**
  57. * Use given middleware.
  58. *
  59. * Middleware run in the order they are defined by `.use()`. They are invoked
  60. * sequentially, requests start at the first middleware and work their way
  61. * "down" the middleware stack.
  62. *
  63. * @example
  64. *
  65. * ```javascript
  66. * // session middleware will run before authorize
  67. * router
  68. * .use(session())
  69. * .use(authorize());
  70. *
  71. * // use middleware only with given path
  72. * router.use('/users', userAuth());
  73. *
  74. * // or with an array of paths
  75. * router.use(['/users', '/admin'], userAuth());
  76. *
  77. * app.use(router.routes());
  78. * ```
  79. *
  80. * @param {String=} path path string
  81. * @param {Function} middleware middleware function
  82. * @return {Router} router instance
  83. */
  84. use(/* path, middleware */) {
  85. const router = this;
  86. const middleware = Array.prototype.slice.call(arguments);
  87. let path;
  88. // support array of paths
  89. if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
  90. middleware[0].forEach(function(p) {
  91. router.use.apply(router, [ p ].concat(middleware.slice(1)));
  92. });
  93. return this;
  94. }
  95. const hasPath = typeof middleware[0] === 'string';
  96. if (hasPath) {
  97. path = middleware.shift();
  98. }
  99. middleware.forEach(function(m) {
  100. if (m.router) {
  101. m.router.stack.forEach(function(nestedLayer) {
  102. if (path) nestedLayer.setPrefix(path);
  103. if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix);
  104. router.stack.push(nestedLayer);
  105. });
  106. if (router.params) {
  107. Object.keys(router.params).forEach(function(key) {
  108. m.router.param(key, router.params[key]);
  109. });
  110. }
  111. } else {
  112. router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
  113. }
  114. });
  115. return this;
  116. }
  117. /**
  118. * Set the path prefix for a Router instance that was already initialized.
  119. *
  120. * @example
  121. *
  122. * ```javascript
  123. * router.prefix('/things/:thing_id')
  124. * ```
  125. *
  126. * @param {String} prefix prefix string
  127. * @return {Router} router instance
  128. */
  129. prefix(prefix) {
  130. prefix = prefix.replace(/\/$/, '');
  131. this.opts.prefix = prefix;
  132. this.stack.forEach(function(route) {
  133. route.setPrefix(prefix);
  134. });
  135. return this;
  136. }
  137. /**
  138. * Returns router middleware which dispatches a route matching the request.
  139. *
  140. * @return {Function} middleware function
  141. */
  142. routes() {
  143. const router = this;
  144. const dispatch = function dispatch(ctx, next) {
  145. debug('%s %s', ctx.method, ctx.path);
  146. const path = router.opts.routerPath || ctx.routerPath || ctx.path;
  147. const matched = router.match(path, ctx.method);
  148. if (ctx.matched) {
  149. ctx.matched.push.apply(ctx.matched, matched.path);
  150. } else {
  151. ctx.matched = matched.path;
  152. }
  153. ctx.router = router;
  154. if (!matched.route) return next();
  155. const matchedLayers = matched.pathAndMethod;
  156. const layerChain = matchedLayers.reduce(function(memo, layer) {
  157. memo.push(function(ctx, next) {
  158. ctx.captures = layer.captures(path, ctx.captures);
  159. ctx.params = layer.params(path, ctx.captures, ctx.params);
  160. // ctx._matchedRouteName & ctx._matchedRoute for compatibility
  161. ctx._matchedRouteName = ctx.routerName = layer.name;
  162. if (!layer.name) ctx._matchedRouteName = undefined;
  163. ctx._matchedRoute = ctx.routerPath = layer.path;
  164. return next();
  165. });
  166. return memo.concat(layer.stack);
  167. }, []);
  168. return compose(layerChain)(ctx, next);
  169. };
  170. dispatch.router = this;
  171. return dispatch;
  172. }
  173. /**
  174. * Returns separate middleware for responding to `OPTIONS` requests with
  175. * an `Allow` header containing the allowed methods, as well as responding
  176. * with `405 Method Not Allowed` and `501 Not Implemented` as appropriate.
  177. *
  178. * @example
  179. *
  180. * ```javascript
  181. * var Koa = require('koa');
  182. * var Router = require('koa-router');
  183. *
  184. * var app = new Koa();
  185. * var router = new Router();
  186. *
  187. * app.use(router.routes());
  188. * app.use(router.allowedMethods());
  189. * ```
  190. *
  191. * **Example with [Boom](https://github.com/hapijs/boom)**
  192. *
  193. * ```javascript
  194. * var Koa = require('koa');
  195. * var Router = require('koa-router');
  196. * var Boom = require('boom');
  197. *
  198. * var app = new Koa();
  199. * var router = new Router();
  200. *
  201. * app.use(router.routes());
  202. * app.use(router.allowedMethods({
  203. * throw: true,
  204. * notImplemented: () => new Boom.notImplemented(),
  205. * methodNotAllowed: () => new Boom.methodNotAllowed()
  206. * }));
  207. * ```
  208. *
  209. * @param {Object=} options optional params
  210. * @param {Boolean=} options.throw throw error instead of setting status and header
  211. * @param {Function=} options.notImplemented throw the returned value in place of the default NotImplemented error
  212. * @param {Function=} options.methodNotAllowed throw the returned value in place of the default MethodNotAllowed error
  213. * @return {Function} middleware function
  214. */
  215. allowedMethods(options) {
  216. options = options || {};
  217. const implemented = this.methods;
  218. return function allowedMethods(ctx, next) {
  219. return next().then(function() {
  220. const allowed = {};
  221. if (!ctx.status || ctx.status === 404) {
  222. ctx.matched.forEach(function(route) {
  223. route.methods.forEach(function(method) {
  224. allowed[method] = method;
  225. });
  226. });
  227. const allowedArr = Object.keys(allowed);
  228. if (!implemented.includes(ctx.method)) {
  229. if (options.throw) {
  230. let notImplementedThrowable;
  231. if (typeof options.notImplemented === 'function') {
  232. notImplementedThrowable = options.notImplemented(); // set whatever the user returns from their function
  233. } else {
  234. notImplementedThrowable = new HttpError.NotImplemented();
  235. }
  236. throw notImplementedThrowable;
  237. } else {
  238. ctx.status = 501;
  239. ctx.set('Allow', allowedArr.join(', '));
  240. }
  241. } else if (allowedArr.length) {
  242. if (ctx.method === 'OPTIONS') {
  243. ctx.status = 200;
  244. ctx.body = '';
  245. ctx.set('Allow', allowedArr.join(', '));
  246. } else if (!allowed[ctx.method]) {
  247. if (options.throw) {
  248. let notAllowedThrowable;
  249. if (typeof options.methodNotAllowed === 'function') {
  250. notAllowedThrowable = options.methodNotAllowed(); // set whatever the user returns from their function
  251. } else {
  252. notAllowedThrowable = new HttpError.MethodNotAllowed();
  253. }
  254. throw notAllowedThrowable;
  255. } else {
  256. ctx.status = 405;
  257. ctx.set('Allow', allowedArr.join(', '));
  258. }
  259. }
  260. }
  261. }
  262. });
  263. };
  264. }
  265. /**
  266. * Register route with all methods.
  267. *
  268. * @param {String} name Optional.
  269. * @param {String} path path string
  270. * @param {Function=} middleware You may also pass multiple middleware.
  271. * @param {Function} callback callback function
  272. * @return {Router} router instance
  273. * @private
  274. */
  275. all(name, path/* , middleware */) {
  276. let middleware;
  277. if (typeof path === 'string') {
  278. middleware = Array.prototype.slice.call(arguments, 2);
  279. } else {
  280. middleware = Array.prototype.slice.call(arguments, 1);
  281. path = name;
  282. name = null;
  283. }
  284. this.register(path, methods, middleware, {
  285. name,
  286. });
  287. return this;
  288. }
  289. /**
  290. * Redirect `source` to `destination` URL with optional 30x status `code`.
  291. *
  292. * Both `source` and `destination` can be route names.
  293. *
  294. * ```javascript
  295. * router.redirect('/login', 'sign-in');
  296. * ```
  297. *
  298. * This is equivalent to:
  299. *
  300. * ```javascript
  301. * router.all('/login', ctx => {
  302. * ctx.redirect('/sign-in');
  303. * ctx.status = 301;
  304. * });
  305. * ```
  306. *
  307. * @param {String} source URL or route name.
  308. * @param {String} destination URL or route name.
  309. * @param {Number=} code HTTP status code (default: 301).
  310. * @return {Router} router instance
  311. */
  312. redirect(source, destination, code) {
  313. // lookup source route by name
  314. if (source[0] !== '/') {
  315. source = this.url(source);
  316. }
  317. // lookup destination route by name
  318. if (destination[0] !== '/') {
  319. destination = this.url(destination);
  320. }
  321. return this.all(source, ctx => {
  322. ctx.redirect(destination);
  323. ctx.status = code || 301;
  324. });
  325. }
  326. /**
  327. * Create and register a route.
  328. *
  329. * @param {String} path Path string.
  330. * @param {Array.<String>} methods Array of HTTP verbs.
  331. * @param {Function} middleware Multiple middleware also accepted.
  332. * @param {Object} [opts] optional params
  333. * @return {Layer} layer instance
  334. * @private
  335. */
  336. register(path, methods, middleware, opts) {
  337. opts = opts || {};
  338. const router = this;
  339. const stack = this.stack;
  340. // support array of paths
  341. if (Array.isArray(path)) {
  342. path.forEach(function(p) {
  343. router.register.call(router, p, methods, middleware, opts);
  344. });
  345. return this;
  346. }
  347. // create route
  348. const route = new Layer(path, methods, middleware, {
  349. end: opts.end === false ? opts.end : true,
  350. name: opts.name,
  351. sensitive: opts.sensitive || this.opts.sensitive || false,
  352. strict: opts.strict || this.opts.strict || false,
  353. prefix: opts.prefix || this.opts.prefix || '',
  354. ignoreCaptures: opts.ignoreCaptures,
  355. });
  356. if (this.opts.prefix) {
  357. route.setPrefix(this.opts.prefix);
  358. }
  359. // add parameter middleware
  360. Object.keys(this.params).forEach(function(param) {
  361. route.param(param, this.params[param]);
  362. }, this);
  363. stack.push(route);
  364. return route;
  365. }
  366. /**
  367. * Lookup route with given `name`.
  368. *
  369. * @param {String} name route name
  370. * @return {Layer|false} layer instance of false
  371. */
  372. route(name) {
  373. const routes = this.stack;
  374. for (let len = routes.length, i = 0; i < len; i++) {
  375. if (routes[i].name && routes[i].name === name) {
  376. return routes[i];
  377. }
  378. }
  379. return false;
  380. }
  381. /**
  382. * Generate URL for route. Takes a route name and map of named `params`.
  383. *
  384. * @example
  385. *
  386. * ```javascript
  387. * router.get('user', '/users/:id', (ctx, next) => {
  388. * // ...
  389. * });
  390. *
  391. * router.url('user', 3);
  392. * // => "/users/3"
  393. *
  394. * router.url('user', { id: 3 });
  395. * // => "/users/3"
  396. *
  397. * router.use((ctx, next) => {
  398. * // redirect to named route
  399. * ctx.redirect(ctx.router.url('sign-in'));
  400. * })
  401. *
  402. * router.url('user', { id: 3 }, { query: { limit: 1 } });
  403. * // => "/users/3?limit=1"
  404. *
  405. * router.url('user', { id: 3 }, { query: "limit=1" });
  406. * // => "/users/3?limit=1"
  407. * ```
  408. *
  409. * @param {String} name route name
  410. * @param {Object} params url parameters
  411. * @param {Object} [options] options parameter
  412. * @param {Object|String} [options.query] query options
  413. * @return {String|Error} string or error instance
  414. */
  415. url(name/* , params */) {
  416. const route = this.route(name);
  417. if (route) {
  418. const args = Array.prototype.slice.call(arguments, 1);
  419. return route.url.apply(route, args);
  420. }
  421. return new Error('No route found for name: ' + name);
  422. }
  423. /**
  424. * Match given `path` and return corresponding routes.
  425. *
  426. * @param {String} path path string
  427. * @param {String} method method name
  428. * @return {Object.<path, pathAndMethod>} returns layers that matched path and
  429. * path and method.
  430. * @private
  431. */
  432. match(path, method) {
  433. const layers = this.stack;
  434. let layer;
  435. const matched = {
  436. // matched path
  437. path: [],
  438. // matched path and method(including none method)
  439. pathAndMethod: [],
  440. // method matched or not
  441. route: false,
  442. };
  443. for (let len = layers.length, i = 0; i < len; i++) {
  444. layer = layers[i];
  445. debug('test %s %s', layer.path, layer.regexp);
  446. if (layer.match(path)) {
  447. matched.path.push(layer);
  448. if (layer.methods.length === 0 || layer.methods.includes(method)) {
  449. matched.pathAndMethod.push(layer);
  450. if (layer.methods.length) matched.route = true;
  451. }
  452. // if (layer.methods.length === 0) {
  453. // matched.pathAndMethod.push(layer);
  454. // } else if (layer.methods.includes(method)) {
  455. // matched.pathAndMethod.push(layer);
  456. // matched.route = true;
  457. // }
  458. }
  459. }
  460. return matched;
  461. }
  462. /**
  463. * Run middleware for named route parameters. Useful for auto-loading or
  464. * validation.
  465. *
  466. * @example
  467. *
  468. * ```javascript
  469. * router
  470. * .param('user', (id, ctx, next) => {
  471. * ctx.user = users[id];
  472. * if (!ctx.user) return ctx.status = 404;
  473. * return next();
  474. * })
  475. * .get('/users/:user', ctx => {
  476. * ctx.body = ctx.user;
  477. * })
  478. * .get('/users/:user/friends', ctx => {
  479. * return ctx.user.getFriends().then(function(friends) {
  480. * ctx.body = friends;
  481. * });
  482. * })
  483. * // /users/3 => {"id": 3, "name": "Alex"}
  484. * // /users/3/friends => [{"id": 4, "name": "TJ"}]
  485. * ```
  486. *
  487. * @param {String} param param
  488. * @param {Function} middleware route middleware
  489. * @return {Router} instance
  490. */
  491. param(param, middleware) {
  492. this.params[param] = middleware;
  493. this.stack.forEach(function(route) {
  494. route.param(param, middleware);
  495. });
  496. return this;
  497. }
  498. }
  499. /**
  500. * Create `router.verb()` methods, where *verb* is one of the HTTP verbs such
  501. * as `router.get()` or `router.post()`.
  502. *
  503. * Match URL patterns to callback functions or controller actions using `router.verb()`,
  504. * where **verb** is one of the HTTP verbs such as `router.get()` or `router.post()`.
  505. *
  506. * Additionaly, `router.all()` can be used to match against all methods.
  507. *
  508. * ```javascript
  509. * router
  510. * .get('/', (ctx, next) => {
  511. * ctx.body = 'Hello World!';
  512. * })
  513. * .post('/users', (ctx, next) => {
  514. * // ...
  515. * })
  516. * .put('/users/:id', (ctx, next) => {
  517. * // ...
  518. * })
  519. * .del('/users/:id', (ctx, next) => {
  520. * // ...
  521. * })
  522. * .all('/users/:id', (ctx, next) => {
  523. * // ...
  524. * });
  525. * ```
  526. *
  527. * When a route is matched, its path is available at `ctx._matchedRoute` and if named,
  528. * the name is available at `ctx._matchedRouteName`
  529. *
  530. * Route paths will be translated to regular expressions using
  531. * [path-to-regexp](https://github.com/pillarjs/path-to-regexp).
  532. *
  533. * Query strings will not be considered when matching requests.
  534. *
  535. * #### Named routes
  536. *
  537. * Routes can optionally have names. This allows generation of URLs and easy
  538. * renaming of URLs during development.
  539. *
  540. * ```javascript
  541. * router.get('user', '/users/:id', (ctx, next) => {
  542. * // ...
  543. * });
  544. *
  545. * router.url('user', 3);
  546. * // => "/users/3"
  547. * ```
  548. *
  549. * #### Multiple middleware
  550. *
  551. * Multiple middleware may be given:
  552. *
  553. * ```javascript
  554. * router.get(
  555. * '/users/:id',
  556. * (ctx, next) => {
  557. * return User.findOne(ctx.params.id).then(function(user) {
  558. * ctx.user = user;
  559. * next();
  560. * });
  561. * },
  562. * ctx => {
  563. * console.log(ctx.user);
  564. * // => { id: 17, name: "Alex" }
  565. * }
  566. * );
  567. * ```
  568. *
  569. * ### Nested routers
  570. *
  571. * Nesting routers is supported:
  572. *
  573. * ```javascript
  574. * var forums = new Router();
  575. * var posts = new Router();
  576. *
  577. * posts.get('/', (ctx, next) => {...});
  578. * posts.get('/:pid', (ctx, next) => {...});
  579. * forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());
  580. *
  581. * // responds to "/forums/123/posts" and "/forums/123/posts/123"
  582. * app.use(forums.routes());
  583. * ```
  584. *
  585. * #### Router prefixes
  586. *
  587. * Route paths can be prefixed at the router level:
  588. *
  589. * ```javascript
  590. * var router = new Router({
  591. * prefix: '/users'
  592. * });
  593. *
  594. * router.get('/', ...); // responds to "/users"
  595. * router.get('/:id', ...); // responds to "/users/:id"
  596. * ```
  597. *
  598. * #### URL parameters
  599. *
  600. * Named route parameters are captured and added to `ctx.params`.
  601. *
  602. * ```javascript
  603. * router.get('/:category/:title', (ctx, next) => {
  604. * console.log(ctx.params);
  605. * // => { category: 'programming', title: 'how-to-node' }
  606. * });
  607. * ```
  608. *
  609. * The [path-to-regexp](https://github.com/pillarjs/path-to-regexp) module is
  610. * used to convert paths to regular expressions.
  611. *
  612. * @name get|put|post|patch|delete|del
  613. * @memberof module:koa-router.prototype
  614. * @param {String} path
  615. * @param {Function=} middleware route middleware(s)
  616. * @param {Function} callback route callback
  617. * @returns {Router}
  618. */
  619. methods.forEach(function(method) {
  620. Router.prototype[method] = function(name, path /* , middleware */) {
  621. let middleware;
  622. if (typeof path === 'string' || path instanceof RegExp) {
  623. middleware = Array.prototype.slice.call(arguments, 2);
  624. } else {
  625. middleware = Array.prototype.slice.call(arguments, 1);
  626. path = name;
  627. name = null;
  628. }
  629. this.register(path, [ method ], middleware, {
  630. name,
  631. });
  632. return this;
  633. };
  634. });
  635. // Alias for `router.delete()` because delete is a reserved word
  636. Router.prototype.del = Router.prototype.delete;
  637. /**
  638. * Generate URL from url pattern and given `params`.
  639. *
  640. * @example
  641. *
  642. * ```javascript
  643. * var url = Router.url('/users/:id', {id: 1});
  644. * // => "/users/1"
  645. * ```
  646. *
  647. * @param {String} path url pattern
  648. * @param {Object} params url parameters
  649. * @return {String} url string
  650. */
  651. Router.url = function(path/* , params */) {
  652. const args = Array.prototype.slice.call(arguments, 1);
  653. return Layer.prototype.url.apply({ path }, args);
  654. };
  655. Router.prototype.middleware = Router.prototype.routes;
  656. module.exports = Router;