Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support PWA #8

Draft
wants to merge 11 commits into
base: asf-site
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2
uses: actions/checkout@v3

- name: Prepare workspace
run: mkdir tmp-workspace
Expand Down Expand Up @@ -48,7 +48,6 @@ jobs:
cd echarts-handbook
npm install


- name: Build 🔧
working-directory: tmp-workspace
run: |
Expand All @@ -59,6 +58,9 @@ jobs:
- name: Install Dep
run: npm install

- name: Build PWA
run: npm run build:pwa

- name: Deploy 🚀
uses: ./node_modules/@jamesives/github-pages-deploy-action
with:
Expand Down
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ _*.html
.DS_Store
en/_*.html
node_modules
package-lock.json
/examples/data/option
/examples/data-gl/option
/examples/data-gl/option
pwa.js
pwa-*.js
workbox-*.js
6 changes: 6 additions & 0 deletions .htaccess
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ RedirectMatch 404 /\.htaccess
RedirectMatch 404 /\.scripts
RedirectMatch 404 /\.github
RedirectMatch 404 /\.asf\.yaml
RedirectMatch 404 /package\.json
RedirectMatch 404 /package-lock\.json

<IfModule mod_headers.c>
Header set Access-Control-Allow-Origin: https://echarts.apache.org
Header set Access-Control-Allow-Methods "*"
Header set Access-Control-Allow-Headers "*"
</IfModule>

<IfModule mod_mime.c>
# Manifest file
AddType application/manifest+json webmanifest
</IfModule>

<IfModule mod_expires.c>
ExpiresActive on
Expand Down
11 changes: 0 additions & 11 deletions .scripts/package.json

This file was deleted.

14 changes: 14 additions & 0 deletions .scripts/pwa/i18n.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"zh": {
"Reload": "刷新",
"Close": "关闭",
"NewContent": "有新内容更新,点击”刷新“按钮以获取最新内容",
"Offline": "📴 您当前处于离线模式"
},
"en": {
"Reload": "Reload",
"Close": "Close",
"NewContent": "New content available, click on reload button to update.",
"Offline": "📴 You are in OFFLINE mode."
}
}
63 changes: 63 additions & 0 deletions .scripts/pwa/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { registerSW } from 'virtual:pwa-register'
import i18n from './i18n.json'
import styleCSS from './style.css?inline'

window.addEventListener('load', () => {
const lang = i18n[window.EC_WWW_LANG || 'en']

const toastStyleEl = document.createElement('style')
toastStyleEl.innerText = styleCSS
document.head.appendChild(toastStyleEl)

const pwaToast = document.createElement('div')
pwaToast.className = 'pwa-toast'
pwaToast.setAttribute('role', 'alert')
pwaToast.innerHTML = `<div class="pwa-msg"></div><button class="pwa-close">${lang['Close']}</button><button class="pwa-refresh">${lang['Reload']}</button>`
document.body.appendChild(pwaToast)

const pwaMsg = pwaToast.querySelector('.pwa-msg')
const pwaCloseBtn = pwaToast.querySelector('.pwa-close')
const pwaRefreshBtn = pwaToast.querySelector('.pwa-refresh')

let refreshSW
let needRefresh

const refreshCallback = () => {
refreshSW && refreshSW(true)
needRefresh = false
}

const hideToast = () => {
pwaToast.classList.remove('show')
}

const showToast = (msg, isRefresh) => {
pwaMsg.innerText = msg
pwaToast.classList[isRefresh ? 'add' : 'remove']('is-refresh')
pwaToast.classList.add('show')
}

pwaRefreshBtn.addEventListener('click', refreshCallback)
pwaCloseBtn.addEventListener('click', hideToast)

const onOffline = () => needRefresh || showToast(lang['Offline'])
navigator.onLine || onOffline()

window.addEventListener('online', () => needRefresh || hideToast())
window.addEventListener('offline', onOffline)

refreshSW = registerSW({
immediate: true,
onOfflineReady() {
console.log('Page is ready to work offline')
},
onNeedRefresh() {
console.log('New content available')
needRefresh = true
showToast(lang['NewContent'], true)
},
onRegisterError(e) {
console.error('failed to register service worker', e)
}
})
})
62 changes: 62 additions & 0 deletions .scripts/pwa/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
.pwa-toast {
visibility: hidden;
opacity: 0;
position: fixed;
right: 16px;
bottom: 16px;
padding: 12px;
max-width: 240px;
color: #fff;
background-color: rgba(0, 0, 0, .6);
border: 1px solid #dcdfe6;
border-radius: 4px;
-webkit-box-shadow: 0 5px 10px 0 rgba(0, 0, 0, .2);
box-shadow: 0 5px 10px 0 rgba(0, 0, 0, .2);
font-size: 14px;
box-sizing: border-box;
z-index: 9999;
text-align: left;
-webkit-transition: opacity ease .5s, visibility ease .5s;
transition: opacity ease .5s, visibility ease .5s;
}
.pwa-toast button {
float: right;
border: 1px solid #F72C5B;
margin-left: 5px;
margin-top: 8px;
border-radius: 2px;
padding: 2px 8px;
background-color: #F72C5B;
font-weight: 500;
font-size: 86%;
vertical-align: middle;
-webkit-appearance: none;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
cursor: pointer;
}
.pwa-toast button,
.pwa-toast button:active,
.pwa-toast button:hover {
color: #fff;
outline: none;
}
.pwa-toast button:hover {
border-color: #f96b8c;
background-color: #f96b8c;
}
.pwa-toast button:active {
border-color: #ca274d;
background-color: #ca274d;
}
.pwa-toast .pwa-refresh {
display: none;
}
.pwa-toast.is-refresh .pwa-refresh {
display: block;
}
.pwa-toast.show {
visibility: visible;
opacity: 1;
}
167 changes: 167 additions & 0 deletions .scripts/pwa/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
const path = require('path')
const { defineConfig } = require('vite')
const { VitePWA } = require('vite-plugin-pwa')

const ROOT = path.resolve(__dirname)

module.exports = defineConfig({
root: ROOT,
plugins: [
VitePWA({
base: '__HOST__',
scope: '__SCOPE__',
manifest: false,
filename: 'pwa-sw.js',
workbox: {
globDirectory: path.resolve(ROOT, '../../'),
globPatterns: [
// '**\/**\/*.{js,css,html}'
// avoid precaching too many files, use runtime caching as much as possible.
// PENDING: also precache css & js?
'**\/**\/{index,option,api,download}\.html',
'**\/**\/{handbook,examples}\/**\/index\.html'
],
globIgnores: [
'**\/\.*\/**\/*',
'**\/node_modules\/**\/*',
'**\/examples\/**\/view\.html',
'v4\/**\/*',
'pwa\.js',
'pwa-*\.js',
'workbox-*\.js'
],
// ignoreURLParametersMatching: true,
offlineGoogleAnalytics: false,
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts',
expiration: {
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60, // 365 days
},
fetchOptions: {
mode: 'cors',
credentials: 'omit'
}
},
},
{
urlPattern: /^https:\/\/.*\.?(?:hm\.baidu|google-analytics|googletagmanager).*/i,
handler: 'NetworkOnly'
},
{
urlPattern: /^https:\/\/.*\.(?:jsdelivr|unpkg)(?:.*\/gh\/apache\/).*/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'cdn-apache-assets',
expiration: {
// PENDING not limit our apache repositories?
// maxEntries: 128,
maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days
},
fetchOptions: {
mode: 'cors',
credentials: 'omit'
}
},
},
{
urlPattern: /^https:\/\/.*\.(?:jsdelivr|unpkg).*/i,
handler: 'CacheFirst',
options: {
cacheName: 'cdn-assets',
expiration: {
maxEntries: 128,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
},
fetchOptions: {
mode: 'cors',
credentials: 'omit'
}
},
},
{
urlPattern: /\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-font-assets',
expiration: {
maxEntries: 8,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
}
},
},
{
urlPattern: /\.(?:jpg|jpeg|gif|png|svg|ico|webp)/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-image-assets',
expiration: {
maxEntries: 128,
maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days
},
},
},
{
urlPattern: /\.(?:js)/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-js-assets',
expiration: {
maxEntries: 64,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
{
urlPattern: /\.(?:css|less)/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-style-assets',
expiration: {
maxEntries: 64,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
{
urlPattern: /.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'others',
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
networkTimeoutSeconds: 10,
},
},
]
}
}),
{
name: 'replace-host',
transform(code) {
return {
// FIXME a bit tricky
code: code
// inject by echarts-www
.replace(/\/?__HOST__/g, '" + window.EC_WWW_HOST + "/')
.replace(/__SCOPE__/g, '')
}
}
}
],
build: {
lib: {
entry: 'main.js',
formats: ['iife'],
name: 'PWA',
fileName: () => 'pwa.js'
},
outDir: path.resolve(ROOT, '../../')
}
})
Loading