Achieve a SPA-like architecture in multi-page applications by combining partials, service workers, and streams.
Updated
Network reliability
Single page application (SPA) it is an architectural pattern in which the browser executes JavaScript code to refresh the existing page when the user visits a different section of the site, instead of loading an entire new page.
This means that the web application does not perform an actual page reload. the History API it is used instead to navigate back and forth through the user's history and manipulate the contents of the history stack.
The use of this type of architecture can provide a shell application UX that's fast, reliable, and generally consumes less data when browsing.
In multi-page applications (MPAs), each time a user navigates to a new URL, the browser progressively generates HTML specific to that page. This means a full page refresh every time you visit a new page.
While both are equally valid models to use, you may want to bring some of the benefits of the SPA UX application shell to your existing MPA site. In this article we will discuss how you can achieve a SPA-like architecture in multi-page applications by combining partials, service workers, and streams.
Production case
DEV is a community where software developers write articles, participate in discussions, and create their professional profiles.
Its architecture is a multi-page application based on traditional backend templates through Ruby on Rails. His team was interested in some of the benefits of an application shell model, but did not want to undertake a major architectural change or move away from its original technology stack.
This is how your solution works:
- First, they create partials of your home page for the header and footer (
shell_top.html
andshell_bottom.html
) and publish them as separate HTML snippets with a period. These assets are added to the cache on the service worker.install
event (commonly called caching). - When the service worker intercepts a navigation request, he creates a response transmitted combining the cached header and footer with the main page content just arrived from the server. The body is the only real part of the page that requires getting data from the network.
The key element of this solution is the use of streams, which enables incremental creations and updates from data sources. The Streams API also provides an interface for reading or writing asynchronous chunks of data, of which only a subset can be available in memory at any one time. In this way, the page header can be rendered as soon as it is selected from the cache, while the rest of the content is fetched from the net. As a result, the browsing experience is so fast that users do not perceive an actual page update, only the new content (the body) is updated.
The resulting user experience is similar to the SPA application shell user experience pattern, implemented in an MPA site.
The previous section contains a quick summary of the DEV solution. For a more detailed explanation, see your blog post about this theme.
Implement an application shell UX architecture in MPA with Workbox
In this section, we will cover an overview of the different parts involved in implementing an application shell UX architecture in MPA. For a more detailed post on how to implement this on a real site, see Beyond SPAs: Alternative Architectures for Your PWA.
The server
Partial
The first step is to adopt a partial HTML-based site structure. These are just modular pieces of your pages that can be reused on your site and can also be delivered as separate HTML snippets.
the partial head it can contain all the logic needed to design and render the page header. the partial navigation bar can contain the logic of the navigation bar, the partial footer the code that needs to run there, and so on.
The first time the user visits the site, your server generates a response by assembling the different parts of the page:
app.get(routes.get('index'), async (req, beef) => {
beef.write(headPartial + navbarPartial);
const tag = req.query.tag || DEFAULT_TAG;
const data = await requestData(…);
beef.write(templates.index(tag, data.items));
beef.write(footPartial);
beef.end();
});
Using the beef
(response) of the object writing methodand referencing locally stored partial templates, the answer can be transmitted immediately, without being blocked by any external data source. The browser takes this initial HTML and displays a meaningful interface and loading message right away.
The next part of the page uses API data, which involves a network request. The web app can't process anything else until it receives a response and processes it, but at least the users aren't staring at a blank screen while waiting.
The service worker
The first time a user visits a site, the page header will render faster, without having to wait for the page body. The browser still needs to go to the net to find the rest of the page.
After the first page is loaded, the service worker logs in, allowing him to get the partials for the different static parts of the page (header, navbar, footer, etc.) from the cache.
Static active precaching
The first step is to cache the partial HTML templates, so they are available immediately. With Preloaded work box you can store these files in the install
service worker event and keep them updated when changes are made to the web application.
Depending on the build process, Workbox has different solutions to generate a service worker and indicate the list of files to preload, including web package and drink accessories, a generic nodejs module and a command line interface.
For a partial configuration like the one described above, the resulting service worker file should contain something similar to the following:
workbox.precaching.precacheAndRoute([
{
url: 'partials/about.html',
revision: '518747aad9d7e',
},
{
url: 'partials/foot.html',
revision: '69bf746a9ecc6',
},
]);
Transmission
Then add the service worker logic so that the cached partial HTML can be sent back to the web application immediately. This is a crucial part of being fast and reliable. Using the Streams API within our service worker makes it possible.
Workbox flows abstracts the details of how the transmission works. The package allows you to pass to the library a combination of streaming sources, both cache and runtime data that can come from the network. Workbox takes care of coordinating the individual sources and bringing them together into a single streaming response.
First, configure the strategies in Workbox to handle the different sources that will make up the broadcast response.
const cacheStrategy = workbox.strategies.cacheFirst({
cacheName: workbox.core.cacheNames.precache,
});const apiStrategy = workbox.strategies.staleWhileRevalidate({
cacheName: API_CACHE_NAME,
plugins: [
new workbox.expiration.Plugin({maxEntries: 50}),
],
});
Next, tell Workbox how to use the strategies to build a full stream response, passing a number of sources as functions to run right away:
workbox.streams.strategy([
() => cacheStrategy.makeRequest({request: '/head.html'}),
() => cacheStrategy.makeRequest({request: '/navbar.html'}),
async ({event, url}) => {
const tag = url.searchParams.get('tag') || DEFAULT_TAG;
const listResponse = await apiStrategy.makeRequest(…);
const data = await listResponse.json();
return templates.index(tag, data.items);
},
() => cacheStrategy.makeRequest({request: '/foot.html'}),
]);
- The first two sources are cached partial templates read directly from the service worker cache, so they will always be immediately available.
- The following source function fetches data from the network and processes the response in the HTML expected by the web application.
- Finally, a cached copy of the footer and closing HTML tags are passed to complete the response.
Workbox takes the result from each source and transmits it to the web application, in sequence, only lagging if the next function in the array has not yet completed. As a result, the user immediately sees the page being painted. The experience is so fast that when browsing the header stays in position without the user noticing the update of the entire page. This is very similar to the UX provided by the application shell SPA model.