PWA Learning and Practice series articles have been compiled into gitbook-PWA Learning Handbook, and the text has been synchronized to learning-PWA-ebook. Please indicate the author and source of reprint.

This is the ninth article in the PWA Learning & Practice series.

PWA, as one of the hottest technology concepts at present, has great significance for improving the security, performance and experience of Web applications, and is worth our understanding and learning. If you are interested in PWA, please pay attention to the PWA Learning and Practice series.

The introduction

In the last eight articles, I’ve covered some common techniques and uses in PWA. Although we have learned a lot about this, there are still many problems that will be exposed in practice. This article is a TroubleShooting article, which summarizes some problems I encounter in recent PWA practice and solutions for these problems. I hope I can help some friends who have similar problems.

1. Service Worker Scope

Note the scope of Service Worker registration

1.1. Problems encountered

I registered the Service Worker under /home:

navigator.serviceWorker.register('/static/home/js/sw.js')
Copy the code

By calling console.log() in.then(), you can see that the Service Worker was registered successfully, but not in the page. Why is that?

1.2. Causes

In my previous articles on Service workers, I didn’t put much emphasis on the concept of Scope:

scope: A USVString representing a URL that defines a service worker’s registration scope; what range of URLs a service worker can control. This is usually a relative URL. The default value is the URL you’d get if you resolved ‘./’ using the service worker script’s location as the base.

Scope specifies the Scope of the Service Worker’s function (URL). For example, a register in the https://www.sample.com/list directory Service Worker, the scope of its role can only be itself and its subpaths:

  • https://www.sample.com/list
  • https://www.sample.com/list/book
  • https://www.sample.com/list/book/comic

In https://www.sample.com, https://www.sample.com/book, the path is invalid.

Also, the default value for scope is./ (note that all relative paths here are not relative to the page, but to the sw.js script). Therefore, the navigator. ServiceWorker. Register (‘/static/home/js/sw. Js’) in the code/static/home/js is actually the scope, The Service Worker is registered under /static/home/js.

This is very common: we would place a file like sw.js in the project’s static directory (e.g. /static/home/js in this article) instead of the page path. Obviously, to solve this problem, you need to set the corresponding scope.

However, another problem arose. If you set scope to /home directly:

navigator.serviceWorker.register('/static/home/js/sw.js', {scope: '/home'})
Copy the code

You will see the following error message on the Chrome console:

Uncaught (in promise) DOMException: Failed to register a ServiceWorker: The path of the provided scope ('/home') is not under the max scope allowed ('/static/home/js/'). 
Adjust the scope, move the Service Worker script, or use the Service-Worker-Allowed HTTP header to allow the scope.
Copy the code

StackOverflow explains this:

Service workers can only intercept requests originating in the scope of the current directory that the service worker script is located in and its subdirectories.

In simple terms, Service workers are only allowed to register under the path where the Service Worker script is located and its subpaths. Obviously, my code above touches this rule. So what to do?

1.3. Solutions

There are two main ways to solve this problem.

Method 1: Modify the route so that the sw.js access path is in the appropriate position

router.get('/sw.js'.function (req, res) {
    res.sendFile(path.join(__dirname, '.. /.. /static/kspay-home/static/js/sw/'.'sw.js'));
});
Copy the code

This is a simple route in Express. By routing, we place the Service Worker script path in the root directory so that we can set scope to /home without violating its rules:

navigator.serviceWorker
    .register('/sw.js', {
        scope: '/home'
    })
Copy the code

Method 2: AddService-Worker-AllowedResponse headers

Scope specifications are sometimes too strict. So browsers also provide a way to get around this limitation. Set the service-worker-allowed response header.

Take serve-static, a static service middleware in Express, as an example, and perform the following configurations:

options: {
    maxAge: 0.setHeaders: function (res, path, stat) {
        // Add service-worker-allowed to expand the scope of Service workers
        if (/\/sw\/.+\.js/.test(path)) {
            res.set({
                'Content-Type': 'application/javascript'.'Service-Worker-Allowed': '/home'}); }}}Copy the code

2. CORS

A cache error occurred for cross-domain resources

2.1. Problems encountered

In “[PWA Learning & Practice] (3) Making Your WebApp Available Offline”, I explained how to use the Service Worker for caching to achieve offline functionality. In order to improve the experience, we will cache static files during Service Worker installation. Part of the code to achieve this function is as follows:

// Listen for install events and cache files after installation
self.addEventListener('install', e => {
    var cacheOpenPromise = caches.open(cacheName).then(function (cache) {
        return cache.addAll(cacheFiles);
    });
    e.waitUntil(cacheOpenPromise);
});
Copy the code

CacheFiles is a list of static files that need to be cached. However, after the Service Worker runs, the static resources of the cacheFiles are not cached in the Application TAB.

2.2. Causes

Switch to Console and you will see an error message similar to the following:

The front-end students are very familiar with this problem: cross-domain problem.

In order to make our page can load CDN and other external resources smoothly, the browser on script, link, img and other tags relaxed cross-domain restrictions. This allows us to load javascript scripts in pages with script tags without causing cross-domain problems (the classic JSONP implementation is based on this).

However, using cache.addall () in a Service Worker will fetch resources in a way similar to fetch request (similar to using XHR to request outbound scripts in a page), which is restricted by cross-domain resource policies and cannot be cached locally.

In the actual production environment, in order to shorten the response time of requests and reduce the server pressure, we usually distribute static resources such as javascript, CSS and image through CDN or place them in some independent static service clusters. Therefore, static resources online are basically “cross-site resources”.

2.3. Solutions

This is not a specific problem in the Service Worker. The solution is similar to a common cross-domain problem. You can set the access-Control-Allow-Origin response header to solve the problem.

  • If the CDN is used, configure it in the CDN service. General CDN services will support the configuration of HTTP response headers;
  • If you use a static server cluster, you can configure servers accordingly. Here is a repository containing nGIx, Apache, IIS and other common server configurations for reference.

3. IOS Standalone mode

Special processing in iOS Standalone mode

3.1. Problems encountered

Earlier this year, Apple announced support for Service workers in iOS Safari 11.3, which played an important role in the promotion of PWA, allowing us to implement PWA technology “across platforms”.

Although iOS Safari does not support the MANIFEST configuration for adding to the desktop, I used manifest in PWA (2), Make Your WebApp More “Native” shows how to implement the standalone mode using Safari’s own meta tags.

The problem, however, is the standalone mode. Aside from some of the other minor bugs (including the status bar display, white screen, duplicate additions, etc.), there is a major problem with the iOS Safari standalone mode that cannot be avoided. This comes from one important difference between iOS and Android:

IOS doesn’t have a back button, as most Android phones do.

Apps added in standalone mode on iOS cannot be backward because they do not have a browser toolbar. For example, when I open the home page and click on a course in the list of courses on the home page, the browser jumps to the course page. IOS has no back button, so you can’t go back to the home page unless you kill the “app” and restart.

3.2. Causes

As mentioned above, since iOS doesn’t have a back button and standalone mode hides the browser toolbar and navigation bar, using a WebApp saved to the desktop in iOS can be a journey of no return…

3.3. Solutions

Clearly, the experience is unacceptable. The solution I’ve used so far is very simple, judging when you open the page, or in standalone mode on iOS, displaying a small “Back” icon in the top right corner of the page. Click the icon to return to the previous page.

There is a separate property in iOS to identify standalone mode:

if ('standalone' in window.navigator && window.navigator.standalone) {
    // Standalone mode for special processing, such as displaying the back button
    backBtn.show();
}
Copy the code

Use the History API to back up the button:

backBtn.addEventListener('click'.function () {
    window.history.back();
});
Copy the code

4. Picture strategy

Resolve display of PWA offline resources that are not cached image resources

4.1. Problems encountered

In practice, I cache some API data that changes very little, such as the list information in the personal center, in order to satisfy certain offline functions. The list contains many more images. In order to save storage space for users, I did not choose cache for image resources.

This leads to a problem: when offline, the user can see the list normally, but the image part of the list is “cracked”, which is not a good experience.

4.2. Causes

As explained above, images cannot be requested offline, so some browsers display this “hanging” state.

4.3. Solution

The general idea to solve this experience problem is as follows:

  1. First, you need to cache the placeholder resources locally
  2. Second, determine whether there is an error when obtaining the picture
  3. Finally, use a placeholder map to replace errors

Since they are only cached placeholders, which are usually fixed and have a limited number of size styles, they do not occupy much cache space. Caching of a placeholder map can be done in conjunction with caching static resources.

There are two simple ways to replace a placeholder map when an image acquisition error occurs (possibly due to network reasons or URL errors) :

Method 1: Listen for the image resource in the FETCH event and use the placeholder map if an error occurs

self.addEventListener('fetch', e => {
    if (/\.png|jpeg|jpg|gif/i.test(e.request.url)) {
        e.respondWith(
            fetch(e.request).then(response= > {
                return response;
            }).catch(err= > {
                // Use a placeholder map when requesting an error
                return caches.match(placeholderPic).then(cache= >cache); }));return;
    }
Copy the code

Method 2: Request a placeholder map via the ONError attribute of the IMG tag

Let’s change the img tag to

<img class="list-cover"
    src="//your.sample.com/1234.png"
    alt="{{ item.desc }}"
    onerror="javascript:this.src='https://your.sample.com/placeholder.png'"/>   
Copy the code

The method specified in the onError property replaces SRC in case of an image loading error; At the same time, we adjust the code in the Service Worker:

self.addEventListener('fetch', e => {
    if (/\.png|jpeg|jpg|gif/i.test(e.request.url)) {
        e.respondWith(
            fetch(e.request).then(response= > {
                return response;
            // After onError is triggered, img will request the image placeholder.png again
            // This fetch still fails because there is no network connection
            }).catch(err= > {
                // Since we cached placeholder. PNG beforehand, the cached result will be returned
                return caches.match(e.request).then(cache= >cache); }));return;
    }
Copy the code

5. Write at the end

This article summarizes some problems I encountered in the PWA upgrade practice, hoping to have some inspiration or help to friends who encounter similar problems.

In the next article, I’ll return to PWA-related technologies and introduce Resource Hints, and how you can use Resource Hints to improve page loading performance and user experience.

PWA Learning and Practice series

  • Start your PWA learning journey in 2018
  • Learn to Use Manifest to Make your WebApp More “Native”
  • Make your WebApp available offline from today
  • Article 4: TroubleShooting: TroubleShooting FireBase Login Authentication Failures
  • Keep in touch with Your Users: The Web Push feature
  • How to Debug? Debug your PWA in Chrome
  • Enhanced Interaction: Use the Notification API for reminders
  • Chapter 8: Background Data Synchronization using Service Worker
  • Chapter 9: Problems and Solutions in PWA Practice (Article)
  • Resource Hint – Improving page loading performance and Experience
  • Part 11: Learning offline Strategies from workbox

The resources

Service Worker Scope

  • Service Worker Scope (MDN)
  • Understanding Service Worker scope
  • How exactly add “Service-Worker-Allowed” to register service worker scope in upper folder

CORS

  • What limitations apply to opaque responses?
  • Handle Third Party Requests
  • CORS settings attributes
  • Cross-Origin Resource Sharing (CORS)
  • Git Repo: server configs

iOS standalone

  • Don’t Use iOS Meta Tags Irmics in your Progressive Web Apps