egg_router.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. 'use strict';
  2. const is = require('is-type-of');
  3. const Router = require('./router');
  4. const utility = require('utility');
  5. const inflection = require('inflection');
  6. const assert = require('assert');
  7. const utils = require('./utils');
  8. const METHODS = [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete' ];
  9. const REST_MAP = {
  10. index: {
  11. suffix: '',
  12. method: 'GET',
  13. },
  14. new: {
  15. namePrefix: 'new_',
  16. member: true,
  17. suffix: 'new',
  18. method: 'GET',
  19. },
  20. create: {
  21. suffix: '',
  22. method: 'POST',
  23. },
  24. show: {
  25. member: true,
  26. suffix: ':id',
  27. method: 'GET',
  28. },
  29. edit: {
  30. member: true,
  31. namePrefix: 'edit_',
  32. suffix: ':id/edit',
  33. method: 'GET',
  34. },
  35. update: {
  36. member: true,
  37. namePrefix: '',
  38. suffix: ':id',
  39. method: [ 'PATCH', 'PUT' ],
  40. },
  41. destroy: {
  42. member: true,
  43. namePrefix: 'destroy_',
  44. suffix: ':id',
  45. method: 'DELETE',
  46. },
  47. };
  48. /**
  49. * FIXME: move these patch into @eggjs/router
  50. */
  51. class EggRouter extends Router {
  52. /**
  53. * @class
  54. * @param {Object} opts - Router options.
  55. * @param {Application} app - Application object.
  56. */
  57. constructor(opts, app) {
  58. super(opts);
  59. this.app = app;
  60. this.patchRouterMethod();
  61. }
  62. patchRouterMethod() {
  63. // patch router methods to support generator function middleware and string controller
  64. METHODS.concat([ 'all' ]).forEach(method => {
  65. this[method] = (...args) => {
  66. const splited = spliteAndResolveRouterParams({ args, app: this.app });
  67. // format and rebuild params
  68. args = splited.prefix.concat(splited.middlewares);
  69. return super[method](...args);
  70. };
  71. });
  72. }
  73. /**
  74. * Create and register a route.
  75. * @param {String} path - url path
  76. * @param {Array} methods - Array of HTTP verbs
  77. * @param {Array} middlewares -
  78. * @param {Object} opts -
  79. * @return {Route} this
  80. */
  81. register(path, methods, middlewares, opts) {
  82. // patch register to support generator function middleware and string controller
  83. middlewares = Array.isArray(middlewares) ? middlewares : [ middlewares ];
  84. middlewares = convertMiddlewares(middlewares, this.app);
  85. path = Array.isArray(path) ? path : [ path ];
  86. path.forEach(p => super.register(p, methods, middlewares, opts));
  87. return this;
  88. }
  89. /**
  90. * restful router api
  91. * @param {String} name - Router name
  92. * @param {String} prefix - url prefix
  93. * @param {Function} middleware - middleware or controller
  94. * @example
  95. * ```js
  96. * app.resources('/posts', 'posts')
  97. * app.resources('posts', '/posts', 'posts')
  98. * app.resources('posts', '/posts', app.role.can('user'), app.controller.posts)
  99. * ```
  100. *
  101. * Examples:
  102. *
  103. * ```js
  104. * app.resources('/posts', 'posts')
  105. * ```
  106. *
  107. * yield router mapping
  108. *
  109. * Method | Path | Route Name | Controller.Action
  110. * -------|-----------------|----------------|-----------------------------
  111. * GET | /posts | posts | app.controller.posts.index
  112. * GET | /posts/new | new_post | app.controller.posts.new
  113. * GET | /posts/:id | post | app.controller.posts.show
  114. * GET | /posts/:id/edit | edit_post | app.controller.posts.edit
  115. * POST | /posts | posts | app.controller.posts.create
  116. * PATCH | /posts/:id | post | app.controller.posts.update
  117. * DELETE | /posts/:id | post | app.controller.posts.destroy
  118. *
  119. * app.router.url can generate url based on arguments
  120. * ```js
  121. * app.router.url('posts')
  122. * => /posts
  123. * app.router.url('post', { id: 1 })
  124. * => /posts/1
  125. * app.router.url('new_post')
  126. * => /posts/new
  127. * app.router.url('edit_post', { id: 1 })
  128. * => /posts/1/edit
  129. * ```
  130. * @return {Router} return route object.
  131. * @since 1.0.0
  132. */
  133. resources(...args) {
  134. const splited = spliteAndResolveRouterParams({ args, app: this.app });
  135. const middlewares = splited.middlewares;
  136. // last argument is Controller object
  137. const controller = splited.middlewares.pop();
  138. let name = '';
  139. let prefix = '';
  140. if (splited.prefix.length === 2) {
  141. // router.get('users', '/users')
  142. name = splited.prefix[0];
  143. prefix = splited.prefix[1];
  144. } else {
  145. // router.get('/users')
  146. prefix = splited.prefix[0];
  147. }
  148. for (const key in REST_MAP) {
  149. const action = controller[key];
  150. if (!action) continue;
  151. const opts = REST_MAP[key];
  152. let formatedName;
  153. if (opts.member) {
  154. formatedName = inflection.singularize(name);
  155. } else {
  156. formatedName = inflection.pluralize(name);
  157. }
  158. if (opts.namePrefix) {
  159. formatedName = opts.namePrefix + formatedName;
  160. }
  161. prefix = prefix.replace(/\/$/, '');
  162. const path = opts.suffix ? `${prefix}/${opts.suffix}` : prefix;
  163. const method = Array.isArray(opts.method) ? opts.method : [ opts.method ];
  164. this.register(path, method, middlewares.concat(action), { name: formatedName });
  165. }
  166. return this;
  167. }
  168. /**
  169. * @param {String} name - Router name
  170. * @param {Object} params - more parameters
  171. * @example
  172. * ```js
  173. * router.url('edit_post', { id: 1, name: 'foo', page: 2 })
  174. * => /posts/1/edit?name=foo&page=2
  175. * router.url('posts', { name: 'foo&1', page: 2 })
  176. * => /posts?name=foo%261&page=2
  177. * ```
  178. * @return {String} url by path name and query params.
  179. * @since 1.0.0
  180. */
  181. url(name, params) {
  182. const route = this.route(name);
  183. if (!route) return '';
  184. const args = params;
  185. let url = route.path;
  186. assert(!is.regExp(url), `Can't get the url for regExp ${url} for by name '${name}'`);
  187. const queries = [];
  188. if (typeof args === 'object' && args !== null) {
  189. const replacedParams = [];
  190. url = url.replace(/:([a-zA-Z_]\w*)/g, function($0, key) {
  191. if (utility.has(args, key)) {
  192. const values = args[key];
  193. replacedParams.push(key);
  194. return utility.encodeURIComponent(Array.isArray(values) ? values[0] : values);
  195. }
  196. return $0;
  197. });
  198. for (const key in args) {
  199. if (replacedParams.includes(key)) {
  200. continue;
  201. }
  202. const values = args[key];
  203. const encodedKey = utility.encodeURIComponent(key);
  204. if (Array.isArray(values)) {
  205. for (const val of values) {
  206. queries.push(`${encodedKey}=${utility.encodeURIComponent(val)}`);
  207. }
  208. } else {
  209. queries.push(`${encodedKey}=${utility.encodeURIComponent(values)}`);
  210. }
  211. }
  212. }
  213. if (queries.length > 0) {
  214. const queryStr = queries.join('&');
  215. if (!url.includes('?')) {
  216. url = `${url}?${queryStr}`;
  217. } else {
  218. url = `${url}&${queryStr}`;
  219. }
  220. }
  221. return url;
  222. }
  223. pathFor(name, params) {
  224. return this.url(name, params);
  225. }
  226. }
  227. /**
  228. * 1. split (name, url, ...middleware, controller) to
  229. * {
  230. * prefix: [name, url]
  231. * middlewares [...middleware, controller]
  232. * }
  233. *
  234. * 2. resolve controller from string to function
  235. *
  236. * @param {Object} options inputs
  237. * @param {Object} options.args router params
  238. * @param {Object} options.app egg application instance
  239. * @return {Object} prefix and middlewares
  240. */
  241. function spliteAndResolveRouterParams({ args, app }) {
  242. let prefix;
  243. let middlewares;
  244. if (args.length >= 3 && (is.string(args[1]) || is.regExp(args[1]))) {
  245. // app.get(name, url, [...middleware], controller)
  246. prefix = args.slice(0, 2);
  247. middlewares = args.slice(2);
  248. } else {
  249. // app.get(url, [...middleware], controller)
  250. prefix = args.slice(0, 1);
  251. middlewares = args.slice(1);
  252. }
  253. // resolve controller
  254. const controller = middlewares.pop();
  255. middlewares.push(resolveController(controller, app));
  256. return { prefix, middlewares };
  257. }
  258. /**
  259. * resolve controller from string to function
  260. * @param {String|Function} controller input controller
  261. * @param {Application} app egg application instance
  262. * @return {Function} controller function
  263. */
  264. function resolveController(controller, app) {
  265. if (is.string(controller)) {
  266. const actions = controller.split('.');
  267. let obj = app.controller;
  268. actions.forEach(key => {
  269. obj = obj[key];
  270. if (!obj) throw new Error(`controller '${controller}' not exists`);
  271. });
  272. controller = obj;
  273. }
  274. // ensure controller is exists
  275. if (!controller) throw new Error('controller not exists');
  276. return controller;
  277. }
  278. /**
  279. * 1. ensure controller(last argument) support string
  280. * - [url, controller]: app.get('/home', 'home');
  281. * - [name, url, controller(string)]: app.get('posts', '/posts', 'posts.list');
  282. * - [name, url, controller]: app.get('posts', '/posts', app.controller.posts.list);
  283. * - [name, url(regexp), controller]: app.get('regRouter', /\/home\/index/, 'home.index');
  284. * - [name, url, middleware, [...], controller]: `app.get(/user/:id', hasLogin, canGetUser, 'user.show');`
  285. *
  286. * 2. make middleware support generator function
  287. *
  288. * @param {Array} middlewares middlewares and controller(last middleware)
  289. * @param {Application} app egg application instance
  290. * @return {Array} middlewares
  291. */
  292. function convertMiddlewares(middlewares, app) {
  293. // ensure controller is resolved
  294. const controller = resolveController(middlewares.pop(), app);
  295. // make middleware support generator function
  296. middlewares = middlewares.map(utils.middleware);
  297. const wrappedController = (ctx, next) => {
  298. return utils.callFn(controller, [ ctx, next ], ctx);
  299. };
  300. return middlewares.concat([ wrappedController ]);
  301. }
  302. module.exports = EggRouter;