25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

3 yıl önce
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. /*
  2. Copyright 2018 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 {cacheNames} from 'workbox-core/_private/cacheNames.mjs';
  14. import {cacheWrapper} from 'workbox-core/_private/cacheWrapper.mjs';
  15. import {fetchWrapper} from 'workbox-core/_private/fetchWrapper.mjs';
  16. import {assert} from 'workbox-core/_private/assert.mjs';
  17. import {logger} from 'workbox-core/_private/logger.mjs';
  18. import {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.mjs';
  19. import messages from './utils/messages.mjs';
  20. import cacheOkAndOpaquePlugin from './plugins/cacheOkAndOpaquePlugin.mjs';
  21. import './_version.mjs';
  22. /**
  23. * An implementation of a
  24. * [network first]{@link https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#network-falling-back-to-cache}
  25. * request strategy.
  26. *
  27. * By default, this strategy will cache responses with a 200 status code as
  28. * well as [opaque responses]{@link https://developers.google.com/web/tools/workbox/guides/handle-third-party-requests}.
  29. * Opaque responses are are cross-origin requests where the response doesn't
  30. * support [CORS]{@link https://enable-cors.org/}.
  31. *
  32. * @memberof workbox.strategies
  33. */
  34. class NetworkFirst {
  35. /**
  36. * @param {Object} options
  37. * @param {string} options.cacheName Cache name to store and retrieve
  38. * requests. Defaults to cache names provided by
  39. * [workbox-core]{@link workbox.core.cacheNames}.
  40. * @param {Array<Object>} options.plugins [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}
  41. * to use in conjunction with this caching strategy.
  42. * @param {Object} options.fetchOptions Values passed along to the
  43. * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters)
  44. * of all fetch() requests made by this strategy.
  45. * @param {Object} options.matchOptions [`CacheQueryOptions`](https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions)
  46. * @param {number} options.networkTimeoutSeconds If set, any network requests
  47. * that fail to respond within the timeout will fallback to the cache.
  48. *
  49. * This option can be used to combat
  50. * "[lie-fi]{@link https://developers.google.com/web/fundamentals/performance/poor-connectivity/#lie-fi}"
  51. * scenarios.
  52. */
  53. constructor(options = {}) {
  54. this._cacheName = cacheNames.getRuntimeName(options.cacheName);
  55. if (options.plugins) {
  56. let isUsingCacheWillUpdate =
  57. options.plugins.some((plugin) => !!plugin.cacheWillUpdate);
  58. this._plugins = isUsingCacheWillUpdate ?
  59. options.plugins : [cacheOkAndOpaquePlugin, ...options.plugins];
  60. } else {
  61. // No plugins passed in, use the default plugin.
  62. this._plugins = [cacheOkAndOpaquePlugin];
  63. }
  64. this._networkTimeoutSeconds = options.networkTimeoutSeconds;
  65. if (process.env.NODE_ENV !== 'production') {
  66. if (this._networkTimeoutSeconds) {
  67. assert.isType(this._networkTimeoutSeconds, 'number', {
  68. moduleName: 'workbox-strategies',
  69. className: 'NetworkFirst',
  70. funcName: 'constructor',
  71. paramName: 'networkTimeoutSeconds',
  72. });
  73. }
  74. }
  75. this._fetchOptions = options.fetchOptions || null;
  76. this._matchOptions = options.matchOptions || null;
  77. }
  78. /**
  79. * This method will perform a request strategy and follows an API that
  80. * will work with the
  81. * [Workbox Router]{@link workbox.routing.Router}.
  82. *
  83. * @param {Object} options
  84. * @param {FetchEvent} options.event The fetch event to run this strategy
  85. * against.
  86. * @return {Promise<Response>}
  87. */
  88. async handle({event}) {
  89. if (process.env.NODE_ENV !== 'production') {
  90. assert.isInstance(event, FetchEvent, {
  91. moduleName: 'workbox-strategies',
  92. className: 'NetworkFirst',
  93. funcName: 'handle',
  94. paramName: 'event',
  95. });
  96. }
  97. return this.makeRequest({
  98. event,
  99. request: event.request,
  100. });
  101. }
  102. /**
  103. * This method can be used to perform a make a standalone request outside the
  104. * context of the [Workbox Router]{@link workbox.routing.Router}.
  105. *
  106. * See "[Advanced Recipes](https://developers.google.com/web/tools/workbox/guides/advanced-recipes#make-requests)"
  107. * for more usage information.
  108. *
  109. * @param {Object} options
  110. * @param {Request|string} options.request Either a
  111. * [`Request`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Request}
  112. * object, or a string URL, corresponding to the request to be made.
  113. * @param {FetchEvent} [options.event] If provided, `event.waitUntil()` will
  114. * be called automatically to extend the service worker's lifetime.
  115. * @return {Promise<Response>}
  116. */
  117. async makeRequest({event, request}) {
  118. const logs = [];
  119. if (typeof request === 'string') {
  120. request = new Request(request);
  121. }
  122. if (process.env.NODE_ENV !== 'production') {
  123. assert.isInstance(request, Request, {
  124. moduleName: 'workbox-strategies',
  125. className: 'NetworkFirst',
  126. funcName: 'handle',
  127. paramName: 'makeRequest',
  128. });
  129. }
  130. const promises = [];
  131. let timeoutId;
  132. if (this._networkTimeoutSeconds) {
  133. const {id, promise} = this._getTimeoutPromise({request, event, logs});
  134. timeoutId = id;
  135. promises.push(promise);
  136. }
  137. const networkPromise =
  138. this._getNetworkPromise({timeoutId, request, event, logs});
  139. promises.push(networkPromise);
  140. // Promise.race() will resolve as soon as the first promise resolves.
  141. let response = await Promise.race(promises);
  142. // If Promise.race() resolved with null, it might be due to a network
  143. // timeout + a cache miss. If that were to happen, we'd rather wait until
  144. // the networkPromise resolves instead of returning null.
  145. // Note that it's fine to await an already-resolved promise, so we don't
  146. // have to check to see if it's still "in flight".
  147. if (!response) {
  148. response = await networkPromise;
  149. }
  150. if (process.env.NODE_ENV !== 'production') {
  151. logger.groupCollapsed(
  152. messages.strategyStart('NetworkFirst', request));
  153. for (let log of logs) {
  154. logger.log(log);
  155. }
  156. messages.printFinalResponse(response);
  157. logger.groupEnd();
  158. }
  159. return response;
  160. }
  161. /**
  162. * @param {Object} options
  163. * @param {Request} options.request
  164. * @param {Array} options.logs A reference to the logs array
  165. * @param {Event} [options.event]
  166. * @return {Promise<Response>}
  167. *
  168. * @private
  169. */
  170. _getTimeoutPromise({request, logs, event}) {
  171. let timeoutId;
  172. const timeoutPromise = new Promise((resolve) => {
  173. const onNetworkTimeout = async () => {
  174. if (process.env.NODE_ENV !== 'production') {
  175. logs.push(`Timing out the network response at ` +
  176. `${this._networkTimeoutSeconds} seconds.`);
  177. }
  178. resolve(await this._respondFromCache({request, event}));
  179. };
  180. timeoutId = setTimeout(
  181. onNetworkTimeout,
  182. this._networkTimeoutSeconds * 1000,
  183. );
  184. });
  185. return {
  186. promise: timeoutPromise,
  187. id: timeoutId,
  188. };
  189. }
  190. /**
  191. * @param {Object} options
  192. * @param {number|undefined} options.timeoutId
  193. * @param {Request} options.request
  194. * @param {Array} options.logs A reference to the logs Array.
  195. * @param {Event} [options.event]
  196. * @return {Promise<Response>}
  197. *
  198. * @private
  199. */
  200. async _getNetworkPromise({timeoutId, request, logs, event}) {
  201. let error;
  202. let response;
  203. try {
  204. response = await fetchWrapper.fetch({
  205. request,
  206. event,
  207. fetchOptions: this._fetchOptions,
  208. plugins: this._plugins,
  209. });
  210. } catch (err) {
  211. error = err;
  212. }
  213. if (timeoutId) {
  214. clearTimeout(timeoutId);
  215. }
  216. if (process.env.NODE_ENV !== 'production') {
  217. if (response) {
  218. logs.push(`Got response from network.`);
  219. } else {
  220. logs.push(`Unable to get a response from the network. Will respond ` +
  221. `with a cached response.`);
  222. }
  223. }
  224. if (error || !response) {
  225. response = await this._respondFromCache({request, event});
  226. if (process.env.NODE_ENV !== 'production') {
  227. if (response) {
  228. logs.push(`Found a cached response in the '${this._cacheName}'` +
  229. ` cache.`);
  230. } else {
  231. logs.push(`No response found in the '${this._cacheName}' cache.`);
  232. }
  233. }
  234. } else {
  235. // Keep the service worker alive while we put the request in the cache
  236. const responseClone = response.clone();
  237. const cachePut = cacheWrapper.put({
  238. cacheName: this._cacheName,
  239. request,
  240. response: responseClone,
  241. event,
  242. plugins: this._plugins,
  243. });
  244. if (event) {
  245. try {
  246. // The event has been responded to so we can keep the SW alive to
  247. // respond to the request
  248. event.waitUntil(cachePut);
  249. } catch (err) {
  250. if (process.env.NODE_ENV !== 'production') {
  251. logger.warn(`Unable to ensure service worker stays alive when ` +
  252. `updating cache for '${getFriendlyURL(event.request.url)}'.`);
  253. }
  254. }
  255. }
  256. }
  257. return response;
  258. }
  259. /**
  260. * Used if the network timeouts or fails to make the request.
  261. *
  262. * @param {Object} options
  263. * @param {Request} request The request to match in the cache
  264. * @param {Event} [options.event]
  265. * @return {Promise<Object>}
  266. *
  267. * @private
  268. */
  269. _respondFromCache({event, request}) {
  270. return cacheWrapper.match({
  271. cacheName: this._cacheName,
  272. request,
  273. event,
  274. matchOptions: this._matchOptions,
  275. plugins: this._plugins,
  276. });
  277. }
  278. }
  279. export {NetworkFirst};