Design and Implementation of a Frontend Version Upgrade Prompt for B2B Platforms
This article describes a comprehensive solution for notifying B2B web users about outdated frontend versions, covering the problem background, version number generation, trigger conditions, micro‑frontend adaptation, detailed plugin and React component code, integration steps for main and sub‑applications, debugging guidance, and measured user impact.
Background
In B2B (TOB) platforms that rely mainly on PC pages, many users keep the page open for hours, so a new frontend version released in the afternoon may not reach more than 98% of users until the next day. Statistics from the BSCM platform show that about 6% of users (over 600 people per 10k UV) still use the old version after a 2‑3 pm release.
Solution
Popup Content
Trigger Conditions
The system distinguishes between a local version number (the version embedded in the HTML the user loads) and a cloud version number (the latest version generated when developers publish new code).
Timing of Condition Evaluation
Two possible moments to evaluate the trigger condition are:
When the frontend detects that a new version has been published (e.g., via WebSocket message).
When the frontend actively polls or checks during certain low‑frequency events (e.g., route changes, visibility changes).
These moments are compared on dimensions such as timeliness, number of checks, and implementation cost.
⭐️ Higher scores indicate better performance on the evaluated dimension.
The comparison shows that a combination of WebSocket push and frontend event listening is most suitable, but because daily releases are limited, event listening offers lower implementation cost and is preferred for practical deployment.
The visibilitychange event also satisfies the requirements for PC browsers in B‑end pages.
Version Number Generation
Local version number
The HTML file itself carries the local version; a webpack plugin can inject a version identifier into the HTML during the build.
Cloud version number
The cloud version can be stored in a JSON file, CDN, or database. Two practical approaches are used: generate version.json alongside the build and serve it via a route, or parse the version directly from the latest HTML file.
Micro‑frontend Adaptation
Both main and sub‑applications need independent version prompts without overlapping dialogs. Three problems must be solved:
Distinguish local version identifiers for each app within a single HTML file (handled by the plugin).
Request the correct cloud version for each app (main app fetches version.json , sub‑app parses its own HTML).
Prevent duplicate dialogs by checking whether a dialog is already displayed.
Specific Implementation
Writing and Reading Version Numbers
Listening Timing and Rate‑Limiting Logic
Version releases are low‑frequency events, but the check frequency should be limited to avoid excessive requests and user annoyance. A half‑hour cache and a daily expiration for the update prompt are applied.
Specific Code
Plugin (VersionPlugin)
/* eslint-disable */
import { CoraWebpackPlugin, WebpackCompiler } from '@ies/eden-web-build';
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');
interface IVersion {
name?: string; // build folder name
subName?: string; // sub‑app name, optional for main app
}
export class VersionPlugin implements CoraWebpackPlugin {
readonly name = 'versionPlugin';
private _version: number;
private _name: string;
private _subName: string;
constructor(params: IVersion) {
this._version = new Date().getTime();
this._name = params?.name || 'build';
this._subName = params?.subName || '';
}
apply(compiler: WebpackCompiler): void {
compiler.hooks.afterCompile.tap('versionPlugin', () => {
try {
const filePath = path.resolve(`./${this._name}/template/version.json`);
fs.writeFile(filePath, JSON.stringify({ version: this._version }), (err) => {
if (err) console.log('@@@err', err);
});
const htmlPath = path.resolve(`./${this._name}/template/index.html`);
const data = fs.readFileSync(htmlPath);
const $ = cheerio.load(data);
$('body').append(`<div id="${this._subName}versionTag" style="display: none">${this._version}</div>`);
fs.writeFile(htmlPath, $.html(), (err) => {
if (err) console.log('@@@htmlerr', err);
});
} catch (err) {
console.log(err);
}
});
}
}Popup Component (FeVersionWatcher)
import React, { useEffect } from 'react';
import { Modal } from '@ecom/auxo';
import axios from 'axios';
import moment from 'moment';
export interface IProps {
isSub?: boolean; // whether it is a sub‑app
subName?: string;
resourceUrl?: string; // sub‑app resource URL
}
export type IType = 'visibilitychange' | 'popstate' | 'init';
export default React.memo
(props => {
const { isSub = false, subName = '', resourceUrl = '' } = props || {};
const cb = (latestVersion, currentVersion, type) => {
try {
if (latestVersion && currentVersion && latestVersion > currentVersion) {
localStorage.setItem('versionUpdateExpireTime', moment().endOf('day').format('x'));
if (!document.getElementById('versionModalTitle')) {
Modal.confirm({
title:
版本更新提示
,
content: '您已经长时间未使用此页面,在此期间平台有过更新,如您此时在页面中没有填写相关信息等操作,请点击刷新页面使用最新版本!',
okText:
刷新页面
,
cancelText:
我知道了
,
onCancel: () => { console.log('fe-version-watcher INFO: 未更新~'); },
onOk: () => { location.reload(); },
});
}
}
localStorage.setItem('versionInfoExpireTime', String(Date.now() + 1000 * 60 * 30));
} catch {}
};
const formatVersion = (text) => (text ? Number(text) : undefined);
useEffect(() => {
try {
const fn = (type) => {
if (document.visibilityState === 'visible') {
if (Number(localStorage.getItem('versionUpdateExpireTime') || 0) >= Date.now()) return;
if (Number(localStorage.getItem('versionInfoExpireTime') || 0) > Date.now()) return;
if (!isSub) {
const dom = document.getElementById('versionTag');
const currentVersion = formatVersion(dom?.innerText);
axios.get(`/version?timestamp=${Date.now()}`).then(res => {
const latestVersion = res?.data?.version;
cb(latestVersion, currentVersion, type);
});
} else {
if (resourceUrl) {
const dom = document.getElementById(`${subName}versionTag`);
const currentVersion = dom?.innerText ? Number(dom.innerText) : undefined;
axios.get(resourceUrl).then(res => {
try {
const html = res.data;
const doc = new DOMParser().parseFromString(html, 'text/html');
const latestVersion = formatVersion(doc.getElementById(`${subName}versionTag`)?.innerText);
cb(latestVersion, currentVersion, type);
} catch {}
});
}
}
}
};
const visibleFn = () => fn('visibilitychange');
const routerFn = () => fn('popstate');
if (isSub) fn('init');
document.addEventListener('visibilitychange', visibleFn);
window.addEventListener('popstate', routerFn);
return () => {
document.removeEventListener('visibilitychange', visibleFn);
window.removeEventListener('popstate', routerFn);
};
} catch {}
}, []);
return
;
});How to Integrate
Main Application Version
Install dependencies: npm i @ecom/fe-version-watcher-plugin # plugin npm i @ecom/logistics-supply-chain-fe-version-watcher # popup component
Import VersionPlugin in the webpack config to generate version.json and inject the version tag into HTML. import { VersionPlugin } from '@ecom/fe-version-watcher-plugin'; // webpack config module.exports = { // ... plugins: [ // other plugins [VersionPlugin, {}], ], };
Import and render the watcher component: import { FeVersionWatcher } from '@ecom/logistics-supply-chain-fe-version-watcher';
Add a route /version that serves the generated version.json (or any equivalent endpoint).
Sub‑Application Version
Install the same dependencies as the main app.
Configure VersionPlugin with a subName so the HTML contains a distinct version tag. import { VersionPlugin } from '@ecom/fe-version-watcher-plugin'; module.exports = { // ... plugins: [ // other plugins [VersionPlugin, { subName: 'general-supplier', name: 'build_cn' }], ], };
Render the watcher with matching props: import { FeVersionWatcher } from '@ecom/logistics-supply-chain-fe-version-watcher';
Debugging / Demonstration
Clear related values from localStorage .
Modify the version number in the HTML to a smaller value.
Navigate to a different route or hide/show the page to trigger the popup.
Benefit Statistics
Four releases at 2‑3 pm show a clear decline in the UV share of old‑version users.
Since launch, the system has prompted over 100 k users and helped roughly 50 k users update to the latest frontend code.
Join Us
📬 The Douyin E‑commerce Frontend team for logistics and supply‑chain is responsible for platforms such as Logistics Management Center, Supply‑Chain Management Platform, and WMS.
We welcome interested engineers to read the original article or scan the QR code below to apply.
Beijing / Hangzhou
ByteFE
Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.