Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

Router.mjs 10 KiB

3 lat temu
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. /*
  2. Copyright 2017 Google Inc. All Rights Reserved.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. import {assert} from 'workbox-core/_private/assert.mjs';
  14. import {logger} from 'workbox-core/_private/logger.mjs';
  15. import {WorkboxError} from 'workbox-core/_private/WorkboxError.mjs';
  16. import {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.mjs';
  17. import normalizeHandler from './utils/normalizeHandler.mjs';
  18. import './_version.mjs';
  19. /**
  20. * The Router can be used to process a FetchEvent through one or more
  21. * [Routes]{@link workbox.routing.Route} responding with a Request if
  22. * a matching route exists.
  23. *
  24. * If no route matches a given a request, the Router will use a "default"
  25. * handler if one is defined.
  26. *
  27. * Should the matching Route throw an error, the Router will use a "catch"
  28. * handler if one is defined to gracefully deal with issues and respond with a
  29. * Request.
  30. *
  31. * If a request matches multiple routes, the **earliest** registered route will
  32. * be used to respond to the request.
  33. *
  34. * @memberof workbox.routing
  35. */
  36. class Router {
  37. /**
  38. * Initializes a new Router.
  39. */
  40. constructor() {
  41. // _routes will contain a mapping of HTTP method name ('GET', etc.) to an
  42. // array of all the corresponding Route instances that are registered.
  43. this._routes = new Map();
  44. }
  45. /**
  46. * Apply the routing rules to a FetchEvent object to get a Response from an
  47. * appropriate Route's handler.
  48. *
  49. * @param {FetchEvent} event The event from a service worker's 'fetch' event
  50. * listener.
  51. * @return {Promise<Response>|undefined} A promise is returned if a
  52. * registered route can handle the FetchEvent's request. If there is no
  53. * matching route and there's no `defaultHandler`, `undefined` is returned.
  54. */
  55. handleRequest(event) {
  56. if (process.env.NODE_ENV !== 'production') {
  57. assert.isInstance(event, FetchEvent, {
  58. moduleName: 'workbox-routing',
  59. className: 'Router',
  60. funcName: 'handleRequest',
  61. paramName: 'event',
  62. });
  63. }
  64. const url = new URL(event.request.url);
  65. if (!url.protocol.startsWith('http')) {
  66. if (process.env.NODE_ENV !== 'production') {
  67. logger.debug(
  68. `Workbox Router only supports URLs that start with 'http'.`);
  69. }
  70. return;
  71. }
  72. let route = null;
  73. let handler = null;
  74. let params = null;
  75. let debugMessages = [];
  76. const result = this._findHandlerAndParams(event, url);
  77. handler = result.handler;
  78. params = result.params;
  79. route = result.route;
  80. if (process.env.NODE_ENV !== 'production') {
  81. if (handler) {
  82. debugMessages.push([
  83. `Found a route to handle this request:`, route,
  84. ]);
  85. if (params) {
  86. debugMessages.push([
  87. `Passing the following params to the route's handler:`, params,
  88. ]);
  89. }
  90. }
  91. }
  92. // If we don't have a handler because there was no matching route, then
  93. // fall back to defaultHandler if that's defined.
  94. if (!handler && this._defaultHandler) {
  95. if (process.env.NODE_ENV !== 'production') {
  96. debugMessages.push(`Failed to find a matching route. Falling ` +
  97. `back to the default handler.`);
  98. // This is used for debugging in logs in the case of an error.
  99. route = '[Default Handler]';
  100. }
  101. handler = this._defaultHandler;
  102. }
  103. if (!handler) {
  104. if (process.env.NODE_ENV !== 'production') {
  105. // No handler so Workbox will do nothing. If logs is set of debug
  106. // i.e. verbose, we should print out this information.
  107. logger.debug(`No route found for: ${getFriendlyURL(url)}`);
  108. }
  109. return;
  110. }
  111. if (process.env.NODE_ENV !== 'production') {
  112. // We have a handler, meaning Workbox is going to handle the route.
  113. // print the routing details to the console.
  114. logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`);
  115. debugMessages.forEach((msg) => {
  116. if (Array.isArray(msg)) {
  117. logger.log(...msg);
  118. } else {
  119. logger.log(msg);
  120. }
  121. });
  122. // The Request and Response objects contains a great deal of information,
  123. // hide it under a group in case developers want to see it.
  124. logger.groupCollapsed(`View request details here.`);
  125. logger.unprefixed.log(event.request);
  126. logger.groupEnd();
  127. logger.groupEnd();
  128. }
  129. // Wrap in try and catch in case the handle method throws a synchronous
  130. // error. It should still callback to the catch handler.
  131. let responsePromise;
  132. try {
  133. responsePromise = handler.handle({url, event, params});
  134. } catch (err) {
  135. responsePromise = Promise.reject(err);
  136. }
  137. if (responsePromise && this._catchHandler) {
  138. responsePromise = responsePromise.catch((err) => {
  139. if (process.env.NODE_ENV !== 'production') {
  140. // Still include URL here as it will be async from the console group
  141. // and may not make sense without the URL
  142. logger.groupCollapsed(`Error thrown when responding to: ` +
  143. ` ${getFriendlyURL(url)}. Falling back to Catch Handler.`);
  144. logger.unprefixed.error(`Error thrown by:`, route);
  145. logger.unprefixed.error(err);
  146. logger.groupEnd();
  147. }
  148. return this._catchHandler.handle({url, event, err});
  149. });
  150. }
  151. return responsePromise;
  152. }
  153. /**
  154. * Checks the incoming `event.request` against the registered routes, and if
  155. * there's a match, returns the corresponding handler along with any params
  156. * generated by the match.
  157. *
  158. * @param {FetchEvent} event
  159. * @param {URL} url
  160. * @return {Object} Returns an object with `handler` and `params` properties.
  161. * They are populated if a matching route was found or `undefined` otherwise.
  162. *
  163. * @private
  164. */
  165. _findHandlerAndParams(event, url) {
  166. const routes = this._routes.get(event.request.method) || [];
  167. for (const route of routes) {
  168. let matchResult = route.match({url, event});
  169. if (matchResult) {
  170. if (Array.isArray(matchResult) && matchResult.length === 0) {
  171. // Instead of passing an empty array in as params, use undefined.
  172. matchResult = undefined;
  173. } else if ((matchResult.constructor === Object &&
  174. Object.keys(matchResult).length === 0) || matchResult === true) {
  175. // Instead of passing an empty object in as params, use undefined.
  176. matchResult = undefined;
  177. }
  178. // Break out of the loop and return the appropriate values as soon as
  179. // we have a match.
  180. return {
  181. route,
  182. params: matchResult,
  183. handler: route.handler,
  184. };
  185. }
  186. }
  187. // If we didn't have a match, then return undefined values.
  188. return {handler: undefined, params: undefined};
  189. }
  190. /**
  191. * Define a default `handler` that's called when no routes explicitly
  192. * match the incoming request.
  193. *
  194. * Without a default handler, unmatched requests will go against the
  195. * network as if there were no service worker present.
  196. *
  197. * @param {workbox.routing.Route~handlerCallback} handler A callback
  198. * function that returns a Promise resulting in a Response.
  199. */
  200. setDefaultHandler(handler) {
  201. this._defaultHandler = normalizeHandler(handler);
  202. }
  203. /**
  204. * If a Route throws an error while handling a request, this `handler`
  205. * will be called and given a chance to provide a response.
  206. *
  207. * @param {workbox.routing.Route~handlerCallback} handler A callback
  208. * function that returns a Promise resulting in a Response.
  209. */
  210. setCatchHandler(handler) {
  211. this._catchHandler = normalizeHandler(handler);
  212. }
  213. /**
  214. * Registers a route with the router.
  215. *
  216. * @param {workbox.routing.Route} route The route to register.
  217. */
  218. registerRoute(route) {
  219. if (process.env.NODE_ENV !== 'production') {
  220. assert.isType(route, 'object', {
  221. moduleName: 'workbox-routing',
  222. className: 'Router',
  223. funcName: 'registerRoute',
  224. paramName: 'route',
  225. });
  226. assert.hasMethod(route, 'match', {
  227. moduleName: 'workbox-routing',
  228. className: 'Router',
  229. funcName: 'registerRoute',
  230. paramName: 'route',
  231. });
  232. assert.isType(route.handler, 'object', {
  233. moduleName: 'workbox-routing',
  234. className: 'Router',
  235. funcName: 'registerRoute',
  236. paramName: 'route',
  237. });
  238. assert.hasMethod(route.handler, 'handle', {
  239. moduleName: 'workbox-routing',
  240. className: 'Router',
  241. funcName: 'registerRoute',
  242. paramName: 'route.handler',
  243. });
  244. assert.isType(route.method, 'string', {
  245. moduleName: 'workbox-routing',
  246. className: 'Router',
  247. funcName: 'registerRoute',
  248. paramName: 'route.method',
  249. });
  250. }
  251. if (!this._routes.has(route.method)) {
  252. this._routes.set(route.method, []);
  253. }
  254. // Give precedence to all of the earlier routes by adding this additional
  255. // route to the end of the array.
  256. this._routes.get(route.method).push(route);
  257. }
  258. /**
  259. * Unregisters a route with the router.
  260. *
  261. * @param {workbox.routing.Route} route The route to unregister.
  262. */
  263. unregisterRoute(route) {
  264. if (!this._routes.has(route.method)) {
  265. throw new WorkboxError(
  266. 'unregister-route-but-not-found-with-method', {
  267. method: route.method,
  268. }
  269. );
  270. }
  271. const routeIndex = this._routes.get(route.method).indexOf(route);
  272. if (routeIndex > -1) {
  273. this._routes.get(route.method).splice(routeIndex, 1);
  274. } else {
  275. throw new WorkboxError('unregister-route-route-not-registered');
  276. }
  277. }
  278. }
  279. export {Router};