Skip to content

Commit

Permalink
chore: add add pwa support for react docs (arco-design#1172)
Browse files Browse the repository at this point in the history
  • Loading branch information
headwindz committed Jul 22, 2022
1 parent 64310cb commit 86ddf67
Show file tree
Hide file tree
Showing 9 changed files with 2,205 additions and 15 deletions.
10 changes: 7 additions & 3 deletions .config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const { version } = require('../package.json');
const { getPWAConfig } = require('../site/config/pwa');

// 组件 dist 打包
exports.component = (config) => {
Expand All @@ -27,7 +28,8 @@ exports.icon = (config) => {

// 官网
exports.site = (config, env) => {
if (env === 'prod') {
const isProd = env === 'prod';
if (isProd) {
config.output.publicPath = '/';
}

Expand Down Expand Up @@ -65,7 +67,7 @@ exports.site = (config, env) => {

config.plugins.push(
new HtmlWebpackPlugin({
filename: 'index-en.html',
filename: 'react-en.html',
template: path.resolve(__dirname, '../site/public/index.ejs'),
templateParameters: {
title:
Expand All @@ -82,9 +84,11 @@ exports.site = (config, env) => {
if (env === 'dev') {
config.devServer.historyApiFallback = {
rewrites: [
{ from: /^(\/(react|docs|showcase)){0,1}\/en-US/, to: '/index-en.html' },
{ from: /^(\/(react|docs|showcase)){0,1}\/en-US/, to: '/react-en.html' },
{ from: /^\/$/, to: '/index.html' },
],
};
}

getPWAConfig(config, env);
};
6 changes: 4 additions & 2 deletions scripts/init.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
cd site
yarn

cd ..
yarn
yarn icon
yarn build

cd site
yarn
16 changes: 16 additions & 0 deletions site/config/pwa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const path = require('path');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');

exports.getPWAConfig = (config, env) => {
const isProd = env === 'prod';

if (isProd) {
config.plugins.push(
new WorkboxWebpackPlugin.InjectManifest({
swSrc: path.resolve(__dirname, '../src/serviceWorker.js'),
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
})
);
}
};
6 changes: 5 additions & 1 deletion site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@
"@types/aos": "^3.0.4",
"prism-themes": "^1.5.0",
"prismjs": "^1.23.0",
"webpack-bundle-analyzer": "^3.6.0"
"webpack-bundle-analyzer": "^3.6.0",
"workbox-core": "^6.5.3",
"workbox-precaching": "^6.5.3",
"workbox-routing": "^6.5.3",
"workbox-webpack-plugin": "^6.5.3"
},
"files": [
"dist"
Expand Down
9 changes: 9 additions & 0 deletions site/src/index-en.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { GlobalContext, GlobalNoticeContext } from './context';
import locale from './locale/en';
import tea from './utils/tea';
import './style/index.less';
import { registerServiceWorker } from './serviceWorkerRegistration';

import { isProduction } from './utils/env';

Expand Down Expand Up @@ -54,6 +55,14 @@ function Index() {
);
}

// register service worker on prod
if (isProduction) {
registerServiceWorker({
content: 'A new version is available, refresh page to get the latest version?',
okText: 'Ok',
cancelText: 'Cancel',
});
}
ReactDOM.render(<Index />, document.getElementById('root'));

tea({ name: 'site_components_en' });
9 changes: 9 additions & 0 deletions site/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import tea from './utils/tea';
import locale from './locale/zh';
import './style/index.less';
import { isProduction } from './utils/env';
import { registerServiceWorker } from './serviceWorkerRegistration';

const requestDomain = isProduction ? `//${location.hostname}/` : '//localhost:3000';

Expand Down Expand Up @@ -53,6 +54,14 @@ function Index() {
);
}

// register service worker on prod
if (isProduction) {
registerServiceWorker({
content: '检测到有新版本,是否刷新页面加载最新版本?',
okText: '确认',
cancelText: '取消',
});
}
ReactDOM.render(<Index />, document.getElementById('root'));

tea({ name: 'site_components_zh' });
59 changes: 59 additions & 0 deletions site/src/serviceWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { clientsClaim } from 'workbox-core';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';

clientsClaim();

const manifest = self.__WB_MANIFEST;

const _manifest = manifest.map((item) => {
const { revision, url } = item;
// html won't be served through cdn, stripe out publicPath
if (url && /.html$/.test(url)) {
const paths = url.split('/');
return {
revision,
url: `/${paths[paths.length - 1]}`,
};
}
return item;
});

precacheAndRoute(_manifest);

// fallback for history routes
const handlers = [
{
regexp: /^(\/(react|docs|showcase))\/en-US/,
dest: '/react-en.html',
},
{
regexp: /^(\/(react|docs|showcase))(?!\/en\-US)/,
dest: '/index.html',
},
];

const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');

handlers.forEach((handler) => {
const { regexp, dest } = handler;
registerRoute(({ request, url }) => {
if (request.mode !== 'navigate') {
return false;
}
const pathname = url.pathname;

if (pathname.match(fileExtensionRegexp)) {
return false;
}

return regexp.test(pathname);
}, createHandlerBoundToURL(dest));
});

// trigger skipWaiting via message
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
97 changes: 97 additions & 0 deletions site/src/serviceWorkerRegistration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React from 'react';
import { Notification, Button } from '@arco-design/web-react';

let refreshing;

function invokeServiceWorkerUpdateFlow(config, registration) {
const id = 'notification';
// there is a new service worker available, show the notification
Notification.info({
id,
closable: true,
duration: 0,
content: config.content,
btn: (
<span>
<Button
type="secondary"
size="small"
onClick={() => Notification.remove(id)}
style={{ marginRight: 12 }}
>
{config.cancelText}
</Button>
<Button
type="primary"
size="small"
onClick={() => {
if (registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
Notification.remove(id);
}
}}
>
{config.okText}
</Button>
</span>
),
position: 'bottomRight',
});
}

export function registerServiceWorker(config) {
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
const swUrl = '/serviceWorker.js';
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
if (registration.waiting) {
invokeServiceWorkerUpdateFlow(config, registration);
}

registration.addEventListener('updatefound', () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.addEventListener('statechange', () => {
if (registration.waiting) {
// if there's an existing controller (previous Service Worker), show the prompt
if (navigator.serviceWorker.controller) {
invokeServiceWorkerUpdateFlow(config, registration);
}
}
});
});
})
.catch((error) => {
console.error('Error during service worker registration:', error);
});

// avoid reload on first time
const oldSw = (await navigator.serviceWorker.getRegistration())?.active?.state;
navigator.serviceWorker.addEventListener('controllerchange', async () => {
if (refreshing) return;
const newSw = (await navigator.serviceWorker.getRegistration())?.active?.state;
if (oldSw === 'activated' && newSw === 'activating') {
// reload on controller change
window.location.reload();
refreshing = true;
}
});
});
}
}

export function unregisterServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
registration.unregister();
})
.catch((error) => {
console.error(error.message);
});
}
}
Loading

0 comments on commit 86ddf67

Please sign in to comment.