This series of articles will take a real project as the research object to explore the application of offline availability, an important feature of PWA in SSR architecture. Finally, Vue SSR was used in practical application.

In the first part of this article, pWA-directory__ is used as an example. This is a site that showcases the PWA project, along with the project Lighthouse score and other page performance data.

In the next Part, we will follow this line of thinking and combine Vue SSR with practical application in the project (follow OpenWeb developers and get the article in time).


PWA-Directory

This article assumes that readers have a certain understanding of PWA-related technologies, especially the basic knowledge of Service workers.

App Shell model

The App Shell is the minimal HTML, CSS, and JavaScript required to support the user interface. Caching it offline ensures instant, reliable performance in case of repeated user access. This means that the App Shell doesn’t have to be loaded from the network every time the user accesses it. You only need to load the necessary content from the network.


App Shell model

The PWA-Directory, including our subsequent discussion, is based on the App Shell model. Now we need to look at the details of caching.

Pre cache

One of the most important functions of a Service Worker is to control caching. Here’s a brief introduction to the basic workings of the pre-cache, or SW-precache plug-in.

During the project construction phase, the static resource list (as an array) and the build number are injected into the Service Worker code. At the SW runtime (Install phase), send requests in turn for resources in the static resource list (JS, CSS, HTML, IMG, FONT…). If yes, the device is cached and the device proceeds to the next stage (Activated). This process, which is cached by the SW before the actual request is called precaching.

In SPA/MPA architecture applications, App Shell is usually contained in HTML pages, which are pre-cached to ensure offline access. However, in the SSR architecture scenario, the situation is different. The first screen of all pages is rendered on the server side, and the pre-cached pages are no longer limited and fixed. If all pages are pre-cached and the SW needs to send a large number of requests, the App Shell part contained in each page is repeatedly cached, which also causes a waste of cache space.

Since pre-caching for all pages doesn’t work, can we separate the App Shell and cache only the empty Shell of the page? To do this, you need to modify the back-end template to return the full page OR snippet containing the App Shell by passing in parameters. In this way, the first screen uses a full page, while the subsequent pages are switched over to the front-end routing to request code snippet to fill. This is also the basic idea of isomorphic projects based on React, Vue and other technologies.

Modifying back-end templates is not complicated. For example, in PWA-Directory, using Handlebars as a back-end template, the custom contentOnly parameter can accommodate both the first screen and subsequent HTML fragment requests. Other template languages such as WordPress use PHP along the same lines.

// list.hbs



{{#unless contentOnly}}

<! DOCTYPE html>

<html lang="en">

 <head>

   {{> head}}

 </head>

 <body>

   {{> header}}

   <div class="page-holder">

     <main class="page">

{{/unless}}

. Page Content

{{#unless contentOnly}}

     </main>

     <div class='page-loader'>

     </div>

   </div>

   {{> footer}}

 </body>

</html>

{{/unless}}Copy the code

Then in SW, we need to pre-cache the App Shell page, which uses SW-Toolbox. At the same time, the back end needs to add a routing rule that returns App Shell, in this case /.app/ Shell.

// service-worker.js



const SHELL_URL = '/.app/shell';

const ASSETS = [

   SHELL_URL,

   '/favicons/android-chrome-72x72.png',

   '/manifest.json',

.

];

// Use sw-Toolbox to cache static resources

toolbox.precache(ASSETS);Copy the code

We end up intercepting all HTML requests, asking for a snippet of the target page instead of the full code (getContentOnlyUrl does the contentOnly concatenation), and returning the cached App Shell page.

// service-worker.js



toolbox.router.default = (request, values, options) => {

// Intercepts HTML requests

   if (request.mode === 'navigate') {

// Request an HTML snippet

       toolbox.cacheFirst(new Request(getContentOnlyUrl(request.url)), values, options);

// Return to the App Shell page

       return getFromCache(SHELL_URL)

           .then(response => response || gulliverHandler(request, values, options));

   }

   return gulliverHandler(request, values, options);

};Copy the code

It is important to note that the request for the target page content fragment is usually done in the front-end route, but in this case in the SW, what is the benefit? The PWA-Directory developer has an article __ devoted to this point, which is illustrated directly using the images in the article.

Let’s take a look at the previous approach, in front-end routing:


Front-end routing request snippet flowchart

As you can see, app.js does not issue a request for the HTML snippet until it is loaded and executed, and then waits for the server to respond. The SW is idle the whole time, but in fact the first time an HTML request is intercepted, the SW can request the snippet of code first (with parameters), get the response and put it in the cache. This way, when the app.js front-end routing execution makes a request, the browser finds that the segment is already in the cache and can be used directly. Of course, to achieve this, you need to set the response header cache-control: max-age on the server to ensure the Cache time of the content fragment.


SW request snippet flowchart

To summarize the idea:

  1. Retrofit the back-end template to support the return of full pages and content fragments
  2. The server adds a routing rule for App Shell and returns the HTML page containing only App Shell
  3. Precache App Shell pages
  4. SW intercepts all HTML requests and returns the cached App Shell page
  5. Front-end routing is responsible for filling in the code snippet and completing front-end rendering

In practice, when the user visits the application site for the first time, the first screen will be rendered by the server, and after the SUCCESSFUL installation of the SW, the subsequent route switching, including page refresh, will be rendered by the front-end, and the server will be responsible for providing only the HTML snipped-response.

With the pre-caching issue resolved, we now need to focus on another key issue involved in offline availability goals.

Data statistics

When measuring the effect of PWA, at least the following indicators can be considered:

  • When the banner added to the desktop is displayed, the user chooses to agree or reject it
  • Whether the current operation is from after being added to the desktop
  • Whether the current operation takes place offline

Get the user’s response to the banner added to the desktop easily with the beforeInstallPROMPt__ event:

window.addEventListener('beforeinstallprompt', e => {

    console.log(e.platforms); // e.g., ["web", "android", "windows"] 

    e.userChoice.then(outcome => {

        console.log(outcome); // either "installed", "dismissed", etc. 

    }, handleError); 

});Copy the code

By adding parameters to start_URL in manifest.json, it is easy to identify the current user access from the added desktop shortcut. For example, using GA Custom campaigns__ :

// manifest.json



{

"start_url": "/? utm_source=homescreen"

}Copy the code

Finally, use navigator.online__ to determine if you are currently offline. Note, however, that returning true does not mean you can actually access the Internet.

Now that we have these metrics, the next question is how to ensure that the statistics generated offline are not lost. A natural idea would be to intercept all statistics requests in SW, store statistics offline in local LocalStorage or IndexedDB, and synchronize data online.

Google has previously developed sw-Offline-Google-Analytics for GA to implement this function, which has been moved to Workbox as a separate module workbox-Google-analytics__. Can be easily used:

// service-worker.js



importScripts('path/to/offline-google-analytics-import.js');

workbox.googleAnalytics.initialize();Copy the code

This solves the problem of off-line statistics. The above code uses GA as an example, but other statistical scripts follow the same logic.

Offline User Experience

Finally, the highlight of offline user experience of this project. The offline user experience in PWA is more than just displaying offline pages instead of browser “dinosaurs.” Offline, “What exactly can I use?” It’s often the user who cares the most. Let’s take a look at how the PWA-Directory does this.


Pwa-directory offline effect

When offline, a Toast (red at the bottom of the figure) pops up to remind the user. This is easy to do by listening for online/offline events. Here’s the highlight.

As mentioned earlier, users are very concerned about what they can access when offline, and it would be nice to be able to annotate it explicitly with styles. In the figure above, I visited the first item in the table below the first Tab “New”, so when offline, the rest of the page is grayed out and unclickable, only cached content is retained, and the user no longer has the frustration of clicking around and meeting the same offline page.

This can be done in two ways. First, from the global style, you can add a custom attribute to the body or specific page container when offline. Components that care about offline function can define their own offline style under this rule.

window.addEventListener('offline', () => {

// Add custom attributes to the container

    document.body.setAttribute('offline', 'true');

});Copy the code

In addition, for specific components, such as list items in this project, clicking on the link for each PWA item will take you to the corresponding detail page, and the first visit will be added to the runtimeCache, so you just need to query the cache by link address to know whether the list item should be grayed.

// Determine if the link was visited

isAvailable(href) {

if (! href || this.window.navigator.onLine) return Promise.resolve(true);

    return caches.match(href)

        .then(response => response.status === 200)

        .catch(() => false);

}Copy the code

In short, the offline user experience needs to be carefully designed according to the actual project situation.

conclusion

From the perspective of PWA features, especially offline cache, it is necessary to separate App Shell for SSR architecture projects. Compared with the SPA/MPA pre-cache scheme, SSR requires some transformation of back-end template and front-end routing. In addition, for pWA-related data statistics and offline synchronization, Google Workbox can be used for reference. Finally, the offline user experience also needs careful consideration.

If interested, take a closer look at the PWA-Directory code __, along with several technical articles from the developer:

  • Optimize content loading speed __ with App Shell
  • Design of pWA-directory __

In the next Part, we will use Vue SSR in conjunction with Workbox to practice this idea in a project :).


The resources

  • App | Web Google Developers__ Shell model
  • Offline Cookbook | Web Google Developers__
  • Pwa-directory optimization of requested content fragments __
  • Design of pWA-directory __
  • PWA indicator statistics based on GA __
  • GA offline statistics __
  • Workbox Codelab

Brilliant Open Web

The BOW (Brillant Open Web) team is a dedicated Web technology building group dedicated to promoting the development of Open Web technology and making the Web the first choice for developers again.

BOW focuses on the front end, on the Web; Analyze technology and share practice; Talk about learning. Talk about management.

Follow OpenWeb developers, click “Add Group” and let’s push OpenWeb technology forward together!