PrecacheStrategy.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. /*
  2. Copyright 2020 Google LLC
  3. Use of this source code is governed by an MIT-style
  4. license that can be found in the LICENSE file or at
  5. https://opensource.org/licenses/MIT.
  6. */
  7. import { copyResponse } from 'workbox-core/copyResponse.js';
  8. import { cacheNames } from 'workbox-core/_private/cacheNames.js';
  9. import { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';
  10. import { logger } from 'workbox-core/_private/logger.js';
  11. import { WorkboxError } from 'workbox-core/_private/WorkboxError.js';
  12. import { Strategy } from 'workbox-strategies/Strategy.js';
  13. import './_version.js';
  14. /**
  15. * A {@link workbox-strategies.Strategy} implementation
  16. * specifically designed to work with
  17. * {@link workbox-precaching.PrecacheController}
  18. * to both cache and fetch precached assets.
  19. *
  20. * Note: an instance of this class is created automatically when creating a
  21. * `PrecacheController`; it's generally not necessary to create this yourself.
  22. *
  23. * @extends workbox-strategies.Strategy
  24. * @memberof workbox-precaching
  25. */
  26. class PrecacheStrategy extends Strategy {
  27. /**
  28. *
  29. * @param {Object} [options]
  30. * @param {string} [options.cacheName] Cache name to store and retrieve
  31. * requests. Defaults to the cache names provided by
  32. * {@link workbox-core.cacheNames}.
  33. * @param {Array<Object>} [options.plugins] {@link https://developers.google.com/web/tools/workbox/guides/using-plugins|Plugins}
  34. * to use in conjunction with this caching strategy.
  35. * @param {Object} [options.fetchOptions] Values passed along to the
  36. * {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters|init}
  37. * of all fetch() requests made by this strategy.
  38. * @param {Object} [options.matchOptions] The
  39. * {@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions|CacheQueryOptions}
  40. * for any `cache.match()` or `cache.put()` calls made by this strategy.
  41. * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to
  42. * get the response from the network if there's a precache miss.
  43. */
  44. constructor(options = {}) {
  45. options.cacheName = cacheNames.getPrecacheName(options.cacheName);
  46. super(options);
  47. this._fallbackToNetwork =
  48. options.fallbackToNetwork === false ? false : true;
  49. // Redirected responses cannot be used to satisfy a navigation request, so
  50. // any redirected response must be "copied" rather than cloned, so the new
  51. // response doesn't contain the `redirected` flag. See:
  52. // https://bugs.chromium.org/p/chromium/issues/detail?id=669363&desc=2#c1
  53. this.plugins.push(PrecacheStrategy.copyRedirectedCacheableResponsesPlugin);
  54. }
  55. /**
  56. * @private
  57. * @param {Request|string} request A request to run this strategy for.
  58. * @param {workbox-strategies.StrategyHandler} handler The event that
  59. * triggered the request.
  60. * @return {Promise<Response>}
  61. */
  62. async _handle(request, handler) {
  63. const response = await handler.cacheMatch(request);
  64. if (response) {
  65. return response;
  66. }
  67. // If this is an `install` event for an entry that isn't already cached,
  68. // then populate the cache.
  69. if (handler.event && handler.event.type === 'install') {
  70. return await this._handleInstall(request, handler);
  71. }
  72. // Getting here means something went wrong. An entry that should have been
  73. // precached wasn't found in the cache.
  74. return await this._handleFetch(request, handler);
  75. }
  76. async _handleFetch(request, handler) {
  77. let response;
  78. const params = (handler.params || {});
  79. // Fall back to the network if we're configured to do so.
  80. if (this._fallbackToNetwork) {
  81. if (process.env.NODE_ENV !== 'production') {
  82. logger.warn(`The precached response for ` +
  83. `${getFriendlyURL(request.url)} in ${this.cacheName} was not ` +
  84. `found. Falling back to the network.`);
  85. }
  86. const integrityInManifest = params.integrity;
  87. const integrityInRequest = request.integrity;
  88. const noIntegrityConflict = !integrityInRequest || integrityInRequest === integrityInManifest;
  89. // Do not add integrity if the original request is no-cors
  90. // See https://github.com/GoogleChrome/workbox/issues/3096
  91. response = await handler.fetch(new Request(request, {
  92. integrity: request.mode !== 'no-cors'
  93. ? integrityInRequest || integrityInManifest
  94. : undefined,
  95. }));
  96. // It's only "safe" to repair the cache if we're using SRI to guarantee
  97. // that the response matches the precache manifest's expectations,
  98. // and there's either a) no integrity property in the incoming request
  99. // or b) there is an integrity, and it matches the precache manifest.
  100. // See https://github.com/GoogleChrome/workbox/issues/2858
  101. // Also if the original request users no-cors we don't use integrity.
  102. // See https://github.com/GoogleChrome/workbox/issues/3096
  103. if (integrityInManifest &&
  104. noIntegrityConflict &&
  105. request.mode !== 'no-cors') {
  106. this._useDefaultCacheabilityPluginIfNeeded();
  107. const wasCached = await handler.cachePut(request, response.clone());
  108. if (process.env.NODE_ENV !== 'production') {
  109. if (wasCached) {
  110. logger.log(`A response for ${getFriendlyURL(request.url)} ` +
  111. `was used to "repair" the precache.`);
  112. }
  113. }
  114. }
  115. }
  116. else {
  117. // This shouldn't normally happen, but there are edge cases:
  118. // https://github.com/GoogleChrome/workbox/issues/1441
  119. throw new WorkboxError('missing-precache-entry', {
  120. cacheName: this.cacheName,
  121. url: request.url,
  122. });
  123. }
  124. if (process.env.NODE_ENV !== 'production') {
  125. const cacheKey = params.cacheKey || (await handler.getCacheKey(request, 'read'));
  126. // Workbox is going to handle the route.
  127. // print the routing details to the console.
  128. logger.groupCollapsed(`Precaching is responding to: ` + getFriendlyURL(request.url));
  129. logger.log(`Serving the precached url: ${getFriendlyURL(cacheKey instanceof Request ? cacheKey.url : cacheKey)}`);
  130. logger.groupCollapsed(`View request details here.`);
  131. logger.log(request);
  132. logger.groupEnd();
  133. logger.groupCollapsed(`View response details here.`);
  134. logger.log(response);
  135. logger.groupEnd();
  136. logger.groupEnd();
  137. }
  138. return response;
  139. }
  140. async _handleInstall(request, handler) {
  141. this._useDefaultCacheabilityPluginIfNeeded();
  142. const response = await handler.fetch(request);
  143. // Make sure we defer cachePut() until after we know the response
  144. // should be cached; see https://github.com/GoogleChrome/workbox/issues/2737
  145. const wasCached = await handler.cachePut(request, response.clone());
  146. if (!wasCached) {
  147. // Throwing here will lead to the `install` handler failing, which
  148. // we want to do if *any* of the responses aren't safe to cache.
  149. throw new WorkboxError('bad-precaching-response', {
  150. url: request.url,
  151. status: response.status,
  152. });
  153. }
  154. return response;
  155. }
  156. /**
  157. * This method is complex, as there a number of things to account for:
  158. *
  159. * The `plugins` array can be set at construction, and/or it might be added to
  160. * to at any time before the strategy is used.
  161. *
  162. * At the time the strategy is used (i.e. during an `install` event), there
  163. * needs to be at least one plugin that implements `cacheWillUpdate` in the
  164. * array, other than `copyRedirectedCacheableResponsesPlugin`.
  165. *
  166. * - If this method is called and there are no suitable `cacheWillUpdate`
  167. * plugins, we need to add `defaultPrecacheCacheabilityPlugin`.
  168. *
  169. * - If this method is called and there is exactly one `cacheWillUpdate`, then
  170. * we don't have to do anything (this might be a previously added
  171. * `defaultPrecacheCacheabilityPlugin`, or it might be a custom plugin).
  172. *
  173. * - If this method is called and there is more than one `cacheWillUpdate`,
  174. * then we need to check if one is `defaultPrecacheCacheabilityPlugin`. If so,
  175. * we need to remove it. (This situation is unlikely, but it could happen if
  176. * the strategy is used multiple times, the first without a `cacheWillUpdate`,
  177. * and then later on after manually adding a custom `cacheWillUpdate`.)
  178. *
  179. * See https://github.com/GoogleChrome/workbox/issues/2737 for more context.
  180. *
  181. * @private
  182. */
  183. _useDefaultCacheabilityPluginIfNeeded() {
  184. let defaultPluginIndex = null;
  185. let cacheWillUpdatePluginCount = 0;
  186. for (const [index, plugin] of this.plugins.entries()) {
  187. // Ignore the copy redirected plugin when determining what to do.
  188. if (plugin === PrecacheStrategy.copyRedirectedCacheableResponsesPlugin) {
  189. continue;
  190. }
  191. // Save the default plugin's index, in case it needs to be removed.
  192. if (plugin === PrecacheStrategy.defaultPrecacheCacheabilityPlugin) {
  193. defaultPluginIndex = index;
  194. }
  195. if (plugin.cacheWillUpdate) {
  196. cacheWillUpdatePluginCount++;
  197. }
  198. }
  199. if (cacheWillUpdatePluginCount === 0) {
  200. this.plugins.push(PrecacheStrategy.defaultPrecacheCacheabilityPlugin);
  201. }
  202. else if (cacheWillUpdatePluginCount > 1 && defaultPluginIndex !== null) {
  203. // Only remove the default plugin; multiple custom plugins are allowed.
  204. this.plugins.splice(defaultPluginIndex, 1);
  205. }
  206. // Nothing needs to be done if cacheWillUpdatePluginCount is 1
  207. }
  208. }
  209. PrecacheStrategy.defaultPrecacheCacheabilityPlugin = {
  210. async cacheWillUpdate({ response }) {
  211. if (!response || response.status >= 400) {
  212. return null;
  213. }
  214. return response;
  215. },
  216. };
  217. PrecacheStrategy.copyRedirectedCacheableResponsesPlugin = {
  218. async cacheWillUpdate({ response }) {
  219. return response.redirected ? await copyResponse(response) : response;
  220. },
  221. };
  222. export { PrecacheStrategy };