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).
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.
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:
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.
To summarize the idea:
- Retrofit the back-end template to support the return of full pages and content fragments
- The server adds a routing rule for App Shell and returns the HTML page containing only App Shell
- Precache App Shell pages
- SW intercepts all HTML requests and returns the cached App Shell page
- 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.
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!