PWA offline solution research report | JD Cloud technical team

This article does not introduce how to configure a web page as an offline application and support installation and downloading. The purpose of studying PWA is only to ensure that users' resources can be loaded directly from the local area, ignoring the impact of national or global network quality on page loading speed. Of course, if the resources required on the page do not require any network requests except resource files, then it is considered an offline application except that it does not support installation on the desktop .

What is PWA

PWA (Progressive Web App) is a new application development method that combines the functions of web pages and native applications. PWA provides users with a native app-like experience by using modern web technologies such as Service Worker and Web App Manifest.

From a user perspective, PWA has the following characteristics:

1. Offline access: PWA can be loaded and used offline, allowing users to continue browsing the application without a network connection;
2. Installable: Users can add PWA to the home screen, just like installing native applications, for quick and easy access;
3. Push notification: PWA supports push notification function, which can send real-time updates and reminders to users;
4. Responsive layout: PWA can adapt to different devices and screen sizes, providing a consistent user experience.

From a developer's perspective, PWA has the following advantages:

1. Cross-platform development: PWA can run on multiple platforms without the need to develop different applications separately;
2. Convenient updates: PWA updates can be achieved by updating the Service Worker on the server side, and users do not need to manually update the application;
3. Discoverability: PWA can be indexed through search engines, increasing the discoverability of the application;
4. Security: PWA uses HTTPS protocol to transmit data, providing higher security.

In short, PWA is a new application development method with features such as offline access, installability, push notifications, and responsive layout. It provides users with a better experience and brings higher efficiency to developers.

From the various capabilities of PWA, we focus on its ability to be accessed offline .

Service Worker

Offline loading essentially means that all the information required by the page , as well as the page itself , can be cached locally instead of being requested from the network. This ability is achieved through.jscsshtmlService Worker

A service worker is a script that runs behind the browser to handle network requests and cache data. It can intercept and process web page requests so that web pages can be loaded and run offline. Service Workers can cache resources, including HTML, CSS, JavaScript, and images, providing faster loading and offline access. It can also implement functions such as push notifications and background synchronization, bringing more powerful functions and user experience to web applications.

In some cases, Service Worker and browser plug-in background are very similar, but there are some differences in functionality and usage:

  • Functional differences: Service Worker is mainly used to process network requests and cache data. It can intercept and process web page requests, and implement functions such as offline access and resource caching. The background of browser plug-ins is mainly used to extend browser functions, such as modifying pages, intercepting requests, operating DOM, etc.
  • Running environment: Service Worker runs in the background of the browser and runs independently of the web page. It can continue to run after the web page is closed, and state can be shared between multiple pages. The background of the browser plug-in also runs in the background, but its life cycle is related to the browser window. The plug-in will also be terminated after closing the browser window.
  • Permission restrictions: Due to security considerations, Service Worker is subject to certain restrictions and cannot directly access the DOM. It can only communicate with the web page through the postMessage() method. The background of the browser plug-in can directly operate the DOM and have higher control over the page.

In general, Service Worker is more suitable for processing network requests and caching data, providing offline access and push notifications, etc.; while the background of browser plug-ins is more suitable for extending browser functions, operating page DOM, intercepting requests, etc. .

register

Registering a Service Worker is actually very simple. Here is a simple example:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Service Worker 示例</title>
</head>
<body>
  <script>
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', function() {
        navigator.serviceWorker.register('/service-worker.js')
          .then(function(registration) {
            console.log('Service Worker 注册成功:', registration.scope);
          })
          .catch(function(error) {
            console.log('Service Worker 注册失败:', error);
          });
      });
    }
  </script>
</body>
</html>

// service-worker.js

// 定义需要预缓存的文件列表
const filesToCache = [
  '/',
  '/index.html',
  '/styles.css',
  '/script.js',
  '/image.jpg'
];

// 安装Service Worker时进行预缓存
self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('my-cache')
      .then(function(cache) {
        return cache.addAll(filesToCache);
      })
  );
});

// 激活Service Worker
self.addEventListener('activate', function(event) {
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.filter(function(cacheName) {
          return cacheName !== 'my-cache';
        }).map(function(cacheName) {
          return caches.delete(cacheName);
        })
      );
    })
  );
});

// 拦截fetch事件并从缓存中返回响应
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        return response || fetch(event.request);
      })
  );
});


In the above example, the logic for registering the Service Worker is contained in the tags of the HTML file. When the browser loads the page, it checks whether Service Worker is supported, and if so, the Service Worker file is registered .<script>/service-worker.js

In the Service Worker file, the list of files that need to be pre-cached is first defined . In the event, these files are added to the cache. In the event, delete the old cache. In the event, intercept the request and return the response from cache.filesToCacheinstallactivatefetch

Service Worker files need to be placed under the main domain name of the page. When calling , you can set it in the second parameter to determine the scope of influence of the Service Worker. The default is the scope of the path where the sw file is located.service-worker.jsnavigator.serviceWorker.register('/service-worker.js')scope

It should be noted that if the sw file is placed in a directory, the scope cannot be set . Because the level of the file itself is smaller than the root path./a/

use

When we follow the above example and configure the corresponding settings , start the service and refresh the page, you should be able to see the log printed by the console .htmlsw.jsService Worker 注册成功

If you are in the chrome browser, you can open the console and switch to the application tab to see the application we just registered.



At this time, in the cache space of the browser, you can also find the cache we opened , which stores the pre-cache file we specified . Since my project only has the root page, there is only one entry.my-cacheindex.html



At this time, if all the files required for the page are cached, even if the browser is set to offline mode, the page can be opened by refreshing the page. The purpose of this article is not to create offline applications. Let's talk about the problems faced by the above method.

How to determine precaching range

If our project has only one warehouse, we can use some plug-ins to directly generate files for us. New files will be generated every time you rebuild, so you don't have to worry about saving too many or too few files. At the same time, in the next chapter of deleting old cache, just update the version number every time.webpacksw

 Workbox is a JavaScript library for creating offline-first web applications. It provides a set of tools and features that help developers create reliable offline experiences and enable web applications to work despite unstable or dropped network connections. Workbox can be used to cache and provide offline resources, implement offline page navigation, handle background synchronization and push notifications and other functions. It simplifies the development process of offline applications and provides powerful cache management and resource loading capabilities.

For micro front-end projects with a unified configuration backend, this problem is a bit tricky.

1. Due to the background management, it is common to update the files of a certain module, but you do not want to update it every time .sw.js
2. Due to the uncertainty of resources, it is impossible to list all resources in . Even if it is listed, the user may never use a certain file, causing cache waste or overflow.precache
3. For reasons 1 and 2, after updating the file, it is impossible to determine how to delete the old cache.sw

For this problem, the first thing to determine is to list all the resource files of the basic base in and occupy one separately .precachecacheName

For the remaining uncertain business files, dynamic caching can be used. This will be explained in detail later and is also the focus of this article.

Resource updates

Because after refreshing the page, all resources are obtained from the cache. After modification , the page is not updated when the browser is refreshed.html

In fact, you don’t need to worry too much about this problem. Although our resources are cached, they themselves will not be cached. Even if we delete the code to register the Service Worker on the page in the next update, the registered Service Worker will remain active until we actively delete it.sw.js

For general SPA projects, the resources generally remain unchanged after going online. If we want to update the page, we only need to update it. When the registered Service Worker file changes, the browser will automatically download the new Service Worker file and activate the new Service Worker the next time you visit the page.sw.js

There are several issues you need to pay attention to when updating files:

1. Delete old cache:

In the sample code, in the stage, we execute the logic of deleting the cache. In a real environment, the version number is usually included, and the version number is updated every time it is updated. In this way, the old cache will be deleted every time and a new version of the cache will be reopened. Each browser handles cache exceedance differently, such as cache eviction policy. Clearing the cache in time can prevent some strange problems from occurring.activatecacheNameswchrome

const version = 'v1';

const preCacheName = 'pre-cache-'+ version;

// 将后文调用的 ’my-cache‘的位置替换为 preCacheName

2. Service Worker is not updated in time:

Under the same domain, only one Service Worker can be activated. Only when all pages under the domain are closed can the next registered Service Worker be activated and replace the previous one. For some users, this time is too long.

Therefore, we need to call in the event to immediately activate the new Service Worker after the waiting period is over , but it will not immediately take over control of all clients (ie, browser tabs). This means that the old Service Worker will still process currently open pages until those pages are closed or reloaded.installprecacheself.skipWaiting();

To ensure that the new Service Worker can take over all clients immediately, call the method in the event. This method will take over all open pages immediately after the new Service Worker is activated, without waiting for these pages to be reloaded. This ensures that the new Service Worker can take effect immediately and provide updated functions and services.activateclients.claim()

The changed code is as follows. After modification, the and method will be called after the asynchronous operation is completed, ensuring that the new Service Worker is activated and takes over all clients immediately after the installation is completed.skipWaiting()clients.claim()

// 安装Service Worker时进行预缓存
self.addEventListener('install', function (event) {
  event.waitUntil(
    (async function () {
      await caches
        .open(preCacheName)
        .then(function (cache) {
          return cache.addAll(filesToCache);
        })
        .then(() => {
          self.skipWaiting();
        });
    })()
  );
});

// 激活Service Worker
self.addEventListener('activate', function (event) {
  event.waitUntil((async function () {
    await clearOutdateResources();
    self.clients.claim();
  })());
});

Now, update , then save the above updates, and then refresh the page twice (don't worry, give some time to register and load resources, you can observe the active status of Service Worker and cache changes in the console).index.htmlsw.js



At a certain moment, we can find that there are two Service Workers at the same time, one is in the activated state and is what we are using, and the other is in the to-be-activated state because it is in progress . At this time, two versions of the cache will also exist in the cache space at the same time. After the new Service Worker is activated, the old cache will be deleted. Then there is only one latest Service Worker, and there is only one cache left.install

Now every time the user opens a new page,

  • Get resources from cache first
  • If a file is found to have been updated, install the new filesw
  • New resources will be downloaded in the file, old caches will be deleted, and all pages will be taken over.
  • The next time the user opens a new page or refreshes the current page, the latest content will be displayed

Capability expansion

The basic operations are done. But we still owe some technical debt above, that is, if we are not sure what resources there are, how to dynamically cache them. Don't worry, let's do some further reading now.

Several strategies for caching

When it comes to Service Worker caching strategies, there are several common strategies:

  • Cache First: First try to get the response from the cache. If the resource exists in the cache, it returns directly; if there is no cache or the cache has expired, a request is sent to the network.
  • Network First (Priority Network): First try to get the response from the network, if the network request is successful, then return the network response; if the network request fails, get the response from the cache, even if the cache expires, the cached response will be returned.
  • Cache Only: Only obtains responses from the cache and does not send requests to the network. For resources that are fully offline accessible.
  • Network Only: Gets responses from the network only and does not use caching. Suitable for scenarios that require real-time data.
  • Stale-While-Revalidate (update and use cache at the same time): First try to get the response from the cache, if the cache expires, send a request to the network to get the latest response, and update the cache. Also returns a cached response for fast display of content.

What we used above is the cache priority mode. For applications that are rarely updated or have only one repository, file updates are sufficient. After all, the more code you write, the more bugs you will have. Year-on-year, the more frequent the updates, the more unstable the system becomes.sw.js

Stale-While-Revalidate

If you are interested in other strategies, you can search for them yourself. Now let’s talk about how dynamic caching is implemented. After all, for microservices, it is best not to update, and it would be better if you can forget about it.sw

We introduced it above and re-attached the code.Cache First

// 拦截fetch事件并从缓存中返回响应
self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.match(event.request).then(function (response) {
      return response || fetch(event.request);
    })
  );
});

Add a new one , and the script will add a string to the body. How to load the file .mock.jsjsscript

// mock.js
const div = document.createElement('div');
div.innerText = 'Hello World';
document.body.appendChild(div);

// index.html
<script src="./mock.js" type="text/javascript"></script>

At the same time, adjust the interception logic.sw

// 新增runtime缓存
const runtimeCacheName = 'runtime-cache-' + version;

// 符合条件也是缓存优先,但是每次都重新发起网络请求更新缓存
const isStaleWhileRevalidate = (request) => {
  const url = request.url;
  const index = ['http://127.0.0.1:5500/mock.js'].indexOf(url);
  return index !== -1;
};

self.addEventListener('fetch', function (event) {
  event.respondWith(
    // 尝试从缓存中获取响应
    caches.match(event.request).then(function (response) {
      var fetchPromise = fetch(event.request).then(function (networkResponse) {

        // 符合匹配条件才克隆响应并将其添加到缓存中
        if (isStaleWhileRevalidate(event.request)) {
          var responseToCache = networkResponse.clone();
          caches.open(runtimeCacheName).then(function (cache) {
            cache.put(event.request, responseToCache.clone());
          });
        }
        return networkResponse;
      });

      // 返回缓存的响应,然后更新缓存中的响应
      return response || fetchPromise;
    })
  );
});

Now every time the user opens a new page,

  • Get resources from the cache first and initiate a network request at the same time
  • If there is a cache, return the cache directly; if not, return onefetchPromise
  • fetchPromiseInternally update cache-eligible requests
  • The next time the user opens a new page or refreshes the current page, the latest content will be displayed

By modifying the matching conditions of the URL, you can control whether to update the cache. In the example above, we could remove from the list, put into, or specifically handle the placement rules to update the cache. It is best not to have the same cache in multiple cache buckets, otherwise you will not know which cache is used.isStaleWhileRevalidateindex.htmlprecacheruntimeindex.htmlprecacherequest

Generally speaking, in micro-front-end applications, resource files have a fixed storage location, and the files themselves are distinguished by adding a version number to the file name. We match the path to the resource location in the function, so that when the user opens the page for the second time, they can directly use the cache. If it is an embedded page, you can communicate with the platform whether you can secretly access a resource page when the application is cold and preload it in advance, so that you can enjoy local caching when you open it for the first time.hashisStaleWhileRevalidate

Cache expiration

Even if we cache some resource files, such as Iconfont, font libraries, etc., they will only update their own content but not change their names. It is actually possible to just use it . Users will see the latest content the second time they open the page.Stale-While-Revalidate

But in order to improve some experience, for example, if the user has not opened the page for half a year, and suddenly opens it today, it is not appropriate to display historical content. At this time, a cache expiration policy can be added.

If we are using , it is achieved by using. is a caching plugin in Cache that allows setting expiration times for cache entries. An example is shown belowWorkboxExpirationPluginExpirationPluginWorkbox

import { registerRoute } from 'workbox-routing';
import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';

// 设置缓存的有效期为一小时
const cacheExpiration = {
  maxAgeSeconds: 60 * 60, // 一小时
};

// 使用CacheFirst策略,并应用ExpirationPlugin
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'image-cache',
    plugins: [
      new ExpirationPlugin(cacheExpiration),
    ],
  })
);

// 使用StaleWhileRevalidate策略,并应用ExpirationPlugin
registerRoute(
  ({ request }) => request.destination === 'script',
  new StaleWhileRevalidate({
    cacheName: 'script-cache',
    plugins: [
      new ExpirationPlugin(cacheExpiration),
    ],
  })
);

Or we can implement our own cache expiration strategy. The first is to increase the cache expiration time. Based on the original update cache, set your own and then put it in the cache. The original one is deleted directly in the example . In actual use, you need to make a judgment. For example , do not use the cache for the type of resource.cache-controlcache-controlno-cache

Each time the cache is hit, it will be judged whether it has expired. If it has expired, the latest request obtained from the network will be returned directly and the cache will be updated.

self.addEventListener('fetch', function (event) {
  event.respondWith(
    // 尝试从缓存中获取响应
    caches.match(event.request).then(function (response) {
      var fetchPromise = fetch(event.request).then(function (networkResponse) {
        if (isStaleWhileRevalidate(event.request)) {
          // 检查响应的状态码是否为成功
          if (networkResponse.status === 200) {
            // 克隆响应并将其添加到缓存中
            var clonedResponse = networkResponse.clone();
            // 在存储到缓存之前,设置正确的缓存头部
            var headers = new Headers(networkResponse.headers);
            headers.delete('cache-control');
            headers.append('cache-control', 'public, max-age=3600'); // 设置缓存有效期为1小时

            // 创建新的响应对象并存储到缓存中
            var cachedResponse = new Response(clonedResponse.body, {
              status: networkResponse.status,
              statusText: networkResponse.statusText,
              headers: headers,
            });

            caches.open(runtimeCacheName).then((cache) => {
              cache.put(event.request, cachedResponse);
            });
          }
        }
        return networkResponse;
      });

      // 检查缓存的响应是否存在且未过期
      if (response && !isExpired(response)) {
        return response; // 返回缓存的响应
      }
      return fetchPromise;
    })
  );
});

function isExpired(response) {
  // 从响应的headers中获取缓存的有效期信息
  var cacheControl = response.headers.get('cache-control');
  if (cacheControl) {
    var maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
    if (maxAgeMatch) {
      var maxAgeSeconds = parseInt(maxAgeMatch[1], 10);
      var requestTime = Date.parse(response.headers.get('date'));
      var expirationTime = requestTime + maxAgeSeconds * 1000;

      // 检查当前时间是否超过了缓存的有效期
      if (Date.now() < expirationTime) {
        return false; // 未过期
      }
    }
  }

  return true; // 已过期
}

Requests initiated from the Service Worker may be captured by the browser's own memory cache or hard disk cache, and then returned directly.

Clear cache accurately

The following content defaults to a micro front-end application.

As micro front-end applications are updated, invalid resource files will gradually appear in the cache, which may cause cache overflow over time.

Update regularly

For example, the version number of a file is updated regularly for half a year . Each update will wipe out the dynamic cache in the previous version . This operation will cause the next loading to be slower because it will be created by loading again through network requests. cache. But if the update frequency is properly controlled and the resources are split reasonably, user perception will not be great.sw

Handle infrequently used caches

The cache expiration policy above does not apply here. Because in resource files in microservices, as long as the file name remains unchanged, the content should remain unchanged. We just want to delete entries that have not been used for more than a certain period of time to prevent buffer overflow. The reason it is also used here is to help us identify files that have not been used for a long time and facilitate deletion.Stale-While-Revalidatejs

It could have been used to create a periodic task, but due to compatibility issues, it was abandoned. You can do your own research if needed, and the URL is attached .self.registration.periodicSync.register

Here we change the conditions. Whenever a network request is triggered, a function with a delay of 20s is started to handle the cache problem. First, rename the previous function to clear the old version cache . Then set the cache expiration time to 10s and refresh the page twice to trigger a network request. After 20s, the cache will be deleted. In real scenarios, the delay function and cache expiration will not be so short, and can be set to 5 minutes and 3 months.debounceclearOldResourcesruntimemock.js

function debounce(func, delay) {
  let timerId;

  return function (...args) {
    clearTimeout(timerId);

    timerId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

const clearOutdateResources = debounce(function () {
  cache
    .open(runtimeCacheName)
    .keys()
    .then(function (requests) {
      requests.forEach(function (request) {
        cache.match(request).then(function (response) {
          // response为匹配到的Response对象
          if (isExpiredWithTime(response, 10)) {
            cache.delete(request);
          }
        });
      });
    });
});

function isExpiredWithTime(response, time) {
  var requestTime = Date.parse(response.headers.get('date'));
  if (!requestTime) {
    return false;
  }
  var expirationTime = requestTime + time * 1000;

  // 检查当前时间是否超过了缓存的有效期
  if (Date.now() < expirationTime) {
    return false; // 未过期
  }
  return true; // 已过期
}

Let’s re-summarize the cache configuration under micro front-end applications:

1. Use the version number and initialize andpreCacheruntimeCache
2. Pre-cache the base data and use the strategy. If it is not updated, the base data will not be updated. preCacheCache Firstsw
3. Use policies to dynamically cache business resource data, and update the page dynamically every time you visit it. runtimeCacheStale-While-Revalidate
4. Use the function to delay clearing the expired cache every time you visit the page.debounce
5. If you need to update the base data, you need to upgrade the version number and reinstall the file. After the new service is activated, the data of the previous version will be deleted.preCachesw
6. You cannot store a resource at the same time, otherwise it may cause confusion. runtimeCachepreCache

Final example

The following is the final one . I deleted the cache expiration logic. If necessary, please get it from the code above. By the way, I added a little crazy error handling logic.sw.js

Theoretically, it should be put into the pre-cached list, but I am too lazy to write it in and update it separately . I believe that after reading the above content, you will be able to implement the corresponding logic by yourself.index.htmlStale-While-RevalidatepreCacheruntimeCache

If you use the following file, the runtime cache will be cleared 20 seconds after each page refresh, because we only set the expiration time to 10 seconds. The expiration judgment will be made 20 seconds after each request is initiated.

In the actual verification process, some

const version = 'v1';

const preCacheName = 'pre-cache-' + version;
const runtimeCacheName = 'runtime-cache'; // runtime不进行整体清除

const filesToCache = []; // 这里将index.html放到动态缓存里了,为了搭自动更新的便车。这个小项目也没别的需要预缓存的了

const maxAgeSeconds = 10; // 缓存过期时间,单位s

const debounceClearTime = 20; // 延迟清理缓存时间,单位s

// 符合条件也是缓存优先,但是每次都重新发起网络请求更新缓存
const isStaleWhileRevalidate = (request) => {
  const url = request.url;
  const index = [`${self.location.origin}/mock.js`, `${self.location.origin}/index.html`].indexOf(url);
  return index !== -1;
};

/*********************上面是配置代码***************************** */

const addResourcesToCache = async () => {
  return caches.open(preCacheName).then((cache) => {
    return cache.addAll(filesToCache);
  });
};

// 安装Service Worker时进行预缓存
self.addEventListener('install', function (event) {
  event.waitUntil(
    addResourcesToCache().then(() => {
      self.skipWaiting();
    })
  );
});

// 删除上个版本的数据
async function clearOldResources() {
  return caches.keys().then(function (cacheNames) {
    return Promise.all(
      cacheNames
        .filter(function (cacheName) {
          return ![preCacheName, runtimeCacheName].includes(cacheName);
        })
        .map(function (cacheName) {
          return caches.delete(cacheName);
        })
    );
  });
}

// 激活Service Worker
self.addEventListener('activate', function (event) {
  event.waitUntil(
    clearOldResources().finally(() => {
      self.clients.claim();
      clearOutdateResources();
    })
  );
});

// 缓存优先
const isCacheFirst = (request) => {
  const url = request.url;
  const index = filesToCache.findIndex((u) => url.includes(u));
  return index !== -1;
};

function addToCache(cacheName, request, response) {
  try {
    caches.open(cacheName).then((cache) => {
      cache.put(request, response);
    });
  } catch (error) {
    console.error('add to cache error =>', error);
  }
}

async function cacheFirst(request) {
  try {
    return caches
      .match(request)
      .then((response) => {
        if (response) {
          return response;
        }

        return fetch(request).then((response) => {
          // 检查是否成功获取到响应
          if (!response || response.status !== 200) {
            return response; // 返回原始响应
          }

          var clonedResponse = response.clone();
          addToCache(runtimeCacheName, request, clonedResponse);
          return response;
        });
      })
      .catch(() => {
        console.error('match in cacheFirst error', error);
        return fetch(request);
      });
  } catch (error) {
    console.error(error);
    return fetch(request);
  }
}

// 缓存优先,同步更新
async function handleFetch(request) {
  try {
    clearOutdateResources();
    // 尝试从缓存中获取响应
    return caches.match(request).then(function (response) {
      var fetchPromise = fetch(request).then(function (networkResponse) {
        // 检查响应的状态码是否为成功
        if (!networkResponse || networkResponse.status !== 200) {
          return networkResponse;
        }
        // 克隆响应并将其添加到缓存中
        var clonedResponse = networkResponse.clone();
        addToCache(runtimeCacheName, request, clonedResponse);

        return networkResponse;
      });

      // 返回缓存的响应,然后更新缓存中的响应
      return response || fetchPromise;
    });
  } catch (error) {
    console.error(error);
    return fetch(request);
  }
}

self.addEventListener('fetch', function (event) {
  const { request } = event;

  if (isCacheFirst(request)) {
    event.respondWith(cacheFirst(request));
    return;
  }
  if (isStaleWhileRevalidate(request)) {
    event.respondWith(handleFetch(request));
    return;
  }
});

function debounce(func, delay) {
  let timerId;

  return function (...args) {
    clearTimeout(timerId);

    timerId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

const clearOutdateResources = debounce(function () {
  try {
    caches.open(runtimeCacheName).then((cache) => {
      cache.keys().then(function (requests) {
        requests.forEach(function (request) {
          cache.match(request).then(function (response) {
            const isExpired = isExpiredWithTime(response, maxAgeSeconds);
            if (isExpired) {
              cache.delete(request);
            }
          });
        });
      });
    });
  } catch (error) {
    console.error('clearOutdateResources error => ', error);
  }
}, debounceClearTime * 1000);

function isExpiredWithTime(response, time) {
  var requestTime = Date.parse(response.headers.get('date'));
  if (!requestTime) {
    return false;
  }
  var expirationTime = requestTime + time * 1000;

  // 检查当前时间是否超过了缓存的有效期
  if (Date.now() < expirationTime) {
    return false; // 未过期
  }
  return true; // 已过期
}

Notice

During the actual verification process, some resources cannot obtain this data, so to be on the safe side, we still add a storage time ourselves when storing it in the cache.date

 // 克隆响应并将其添加到缓存中
var clonedResponse = networkResponse.clone();
// 在存储到缓存之前,设置正确的缓存头部
var headers = new Headers(networkResponse.headers);

headers.append('sw-save-date', Date.now()); 

// 创建新的响应对象并存储到缓存中
var cachedResponse = new Response(clonedResponse.body, {
  status: networkResponse.status,
  statusText: networkResponse.statusText,
  headers: headers,
});

When judging expiration, just take what we wrote ourselves.key

function isExpiredWithTime(response, time) {
  var requestTime = Number(response.headers.get('sw-save-date'));
  if (!requestTime) {
    return false;
  }
  var expirationTime = requestTime + time * 1000;
  // 检查当前时间是否超过了缓存的有效期
  if (Date.now() < expirationTime) {
    return false; // 未过期
  }
  return true; // 已过期
}

Invisible response

Remember that for security reasons above, when storing in the cache, the status of the response is judged, and anything other than 200 is not cached. Then another unusual scene was discovered.

 // 检查是否成功获取到响应
if (!response || response.status !== 200) {
  return response; // 返回原始响应
}

opaqueResponse usually refers to a situation in cross-origin requests (CORS) in which the browser does not allow access to the response content returned by the server for security reasons. Responses typically occur in cross-origin requests made by Service Workers without CORS headers.opaque

opaqueThe response is characterized by:

  • The content of the response is not accessible to JavaScript.
  • The size of the response cannot be determined, so it will appear as (opaque) in Chrome Developer Tools.
  • The response status code is usually 0, even though the server may actually return a different status code.

So we need to do some additional actions. Not only the supplementary mode, but also the settings must be synchronized .corscredentials

 const newRequest =
  request.url === 'index.html'
    ? request
    : new Request(request, { mode: 'cors', credentials: 'omit' });

When Service Workers initiate a network request, if the page itself requires authentication, then make a judgment on the page request just like the above code. This is an example I wrote. In a real request, the complete URL path needs to be spelled out. For resource files, just make a non-authenticated request. Change the requested one to our changed one , and the requested resource can be cached normally.request.url === 'index.html'corsrequestnewRequest

var fetchPromise = fetch(newRequest).then(function (networkResponse)

destroy

If you use the offline cache well, you will get a promotion and salary increase, but if you don't use it well, you will delete the database and run away. In addition to the above little bit of error-proofing logic, there must be an overall downgrade plan.

Seeing this, you may have forgotten how the Service Worker is registered. It's okay, let's look at a new script. On the basis of the original, we added a variable . If there is a problem with the offline cache, quickly go to the management background and change the corresponding value . Just let the user refresh twice more. As long as it's not a complete crash that prevents updates, this solution is fine.SW_FALLBACKtruehtml

// 如果有问题,将此值改成true
SW_FALLBACK = false;
 
if ('serviceWorker' in navigator) {
  if (!SW_FALLBACK) {
    navigator.serviceWorker
      .register('/eemf-service-worker.js')
      .then((registration) => {
        console.log('Service Worker 注册成功!');
      })
      .catch((error) => {
        console.log('Service Worker 注册失败:', error);
      });
  } else {
    navigator.serviceWorker.getRegistration('/').then((reg) => {
      reg && reg.unregister();
      if(reg){
        window.location.reload();
      }
    });
  }
}

For projects without management background configuration, you can move the above script into the script of , load the script in the form of , and set the file cache to , and do not cache the file in . If something goes wrong, just overwrite the file.htmlsw-register.jshtmlscriptno-cachesw

Summarize

All that needs to be said has been said above. The offline solution of PWA is a good solution, but it also has its limitations. The demo used in this project has been uploaded to github and can be viewed by yourself.

Reference documentation

Author: CHO Zhang Pengcheng

Source: JD Cloud Developer Community Please indicate the source when reprinting

IntelliJ IDEA 2023.3 & JetBrains Family Bucket annual major version update new concept "defensive programming": make yourself a stable job GitHub.com runs more than 1,200 MySQL hosts, how to seamlessly upgrade to 8.0? Stephen Chow's Web3 team will launch an independent App next month. Will Firefox be eliminated? Visual Studio Code 1.85 released, floating window Yu Chengdong: Huawei will launch disruptive products next year and rewrite the history of the industry. The US CISA recommends abandoning C/C++ to eliminate memory security vulnerabilities. TIOBE December: C# is expected to become the programming language of the year. A paper written by Lei Jun 30 years ago : "Principle and Design of Computer Virus Determination Expert System"
{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/u/4090830/blog/10320663