如何强制更新单页应用程序(SPA)页?

在完全基于服务器端的呈现(非 Web 2.0)中,部署服务器端代码将在页面重新加载时直接更新客户端页面。相比之下,在基于 React 的单页应用程序中,即使在 React 组件更新后,仍然会有一些客户端使用旧版本的组件(他们只有在浏览器重新加载时才会得到新版本,这种情况很少发生)-> 如果页面是完全 SPA 的,有可能一些客户端只是在几个小时后刷新页面。

应该采用什么技术来确保旧的组件版本不再被任何客户机使用?

更新: API 没有改变,只有 React Component 用更新的版本进行了更新。

58187 次浏览

You can have a React component make an ajax request to your server, when the application loads, to fetch "interface version". In the server API, you can maintain an incremental value for the client version. The React component can store this value on the client (cookie/local storage/etc). When it detects a change, it can invoke window.location.reload(true); which should force the browser to discard client cache and reload the SPA. Or better still, inform the end-user that a new version will be loaded and ask them if they wish to save the work and then reload etc. Depends on what you wanna do.

Yes in server side rendering if you need to update a small part of the page also you need to reload whole page. But in SPAs you update your stuffs using ajax, hence no need to reload the page. Seeing your problem I have some assumptions:

You see one of your component got updated but other components getting data from same API didn't update. Here comes Flux Architecture. where you have your data in store and your component listen to store's changes, whenever data in your store changes all your components listening to it's change will be updated (no scene of caching).

Or

You need to control your component to be updated automatically. For that

  1. You can request your server for data in specific intervals
  2. Websockets can help you updating component data from server.

You can send app’s version with every response from any endpoint of your API. So that when the app makes any API request you can easily check there’s a new version and you need a hard reload. If the version in the API response is newer than the one stored in localStorage, set window.updateRequired = true. And you can have the following react component that wraps react-router's Link:

import React from 'react';
import { Link, browserHistory } from 'react-router';


const CustomLink = ({ to, onClick, ...otherProps }) => (
<Link
to={to}
onClick={e => {
e.preventDefault();
if (window.updateRequired) return (window.location = to);
return browserHistory.push(to);
}}
{...otherProps}
/>
);


export default CustomLink;

And use it instead of react-router's Link throughout the app. So whenever there's an update and the user navigates to another page, there will be a hard reload and the user will get the latest version of the app.

Also you can show a popup saying: "There's an update, click [here] to enable it." if you have only one page or your users navigate very rarely. Or just reload the app without asking. It depends on you app and users.

Similar to Steve Taylor's answer but instead of versioning API endpoints I would version the client app, in the following way.

With each HTTP request send a custom header, such as:

X-Client-Version: 1.0.0

The server would then be able to intercept such header and respond accordingly.

If the server is aware that the client's version is stale, for example if the current version is 1.1.0, respond with an HTTP status code that will be appropriately handled by the client, such as:

418 - I'm a Teapot

The client can then be programmed to react to such a response by refreshing the app with:

window.location.reload(true)

The underlying premise is that the server is aware of the latest client version.

EDIT:

A similar answer is given here.

What techniques should be employed to make sure the old components versions are not used anymore by any clients?

today (2018), many front apps use service workers. With it, it's possible to manage your app lifecycle by several means.

Here is a first example, by using a ui notification, asking your visitors to refresh webpage in order to get latest application version.

import * as SnackBar from 'node-snackbar';


// ....


// Service Worker
// https://github.com/GoogleChrome/sw-precache/blob/master/demo/app/js/service-worker-registration.js


const offlineMsg = 'Vous êtes passé(e) en mode déconnecté.';
const onlineMsg = 'Vous êtes de nouveau connecté(e).';
const redundantMsg = 'SW : The installing service worker became redundant.';
const errorMsg = 'SW : Error during service worker registration : ';
const refreshMsg = 'Du nouveau contenu est disponible sur le site, vous pouvez y accéder en rafraichissant cette page.';
const availableMsg = 'SW : Content is now available offline.';
const close = 'Fermer';
const refresh = 'Rafraîchir';


if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
function updateOnlineStatus() {
SnackBar.show({
text: navigator.onLine ? onlineMsg : offlineMsg,
backgroundColor: '#000000',
actionText: close,
});
}
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
navigator.serviceWorker.register('sw.js').then((reg) => {
reg.onupdatefound = () => {
const installingWorker = reg.installing;
installingWorker.onstatechange = () => {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
SnackBar.show({
text: refreshMsg,
backgroundColor: '#000000',
actionText: refresh,
onActionClick: () => { location.reload(); },
});
} else {
console.info(availableMsg);
}
break;
case 'redundant':
console.info(redundantMsg);
break;
default:
break;
}
};
};
}).catch((e) => {
console.error(errorMsg, e);
});
});
}


// ....

There's also an elegant way to check for upgrades in background and then silently upgrade app when user clicks an internal link. This method is presented on zach.codes and discussed on this thread as well.

I know this is an old thread, and service workers are probably the best answer. But I have a simple approach that appears to work:

I added a meta tag to my "index.html" file :

<meta name="version" content="0.0.3"/>

I then have a very simple php scrip in the same folder as the index.html that responds to a simple REST request. The PHP script parses the server copy of the index.html file, extracts the version number and returns it. In my SPA code, every time a new page is rendered I make an ajax call to the PHP script, extract the version from the local meta tag and compare the two. If different I trigger an alert to the user.

PHP script:

<?php
include_once('simplehtmldom_1_9/simple_html_dom.php');
header("Content-Type:application/json");
/*
blantly stolen from: https://shareurcodes.com/blog/creating%20a%20simple%20rest%20api%20in%20php
*/


if(!empty($_GET['name']))
{
$name=$_GET['name'];
$price = get_meta($name);


if(empty($price))
{
response(200,"META Not Found",NULL);
}
else
{
response(200,"META Found",$price);
}
}
else
{
response(400,"Invalid Request",NULL);
}


function response($status,$status_message,$data)
{
header("HTTP/1.1 ".$status);


$response['status']=$status;
$response['status_message']=$status_message;
$response['content']=$data;


$json_response = json_encode($response);
echo $json_response;
}


function get_meta($name)
{
$html = file_get_html('index.html');
foreach($html->find('meta') as $e){
if ( $e->name == $name){
return $e->content ;
}
}
}