Merge branch 'from/develop/tusooa/sw-cache-assets' into 'develop'
Service worker: cache assets See merge request pleroma/pleroma-fe!1437
This commit is contained in:
commit
ec747c0818
9 changed files with 218 additions and 3 deletions
|
|
@ -11,6 +11,11 @@ const getSWMessagesAsText = async () => {
|
|||
}
|
||||
const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||
|
||||
const swEnvName = 'virtual:pleroma-fe/service_worker_env'
|
||||
const swEnvNameResolved = '\0' + swEnvName
|
||||
const getDevSwEnv = () => `self.serviceWorkerOption = { assets: [] };`
|
||||
const getProdSwEnv = ({ assets }) => `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };`
|
||||
|
||||
export const devSwPlugin = ({
|
||||
swSrc,
|
||||
swDest,
|
||||
|
|
@ -32,12 +37,16 @@ export const devSwPlugin = ({
|
|||
const name = id.startsWith('/') ? id.slice(1) : id
|
||||
if (name === swDest) {
|
||||
return swFullSrc
|
||||
} else if (name === swEnvName) {
|
||||
return swEnvNameResolved
|
||||
}
|
||||
return null
|
||||
},
|
||||
async load (id) {
|
||||
if (id === swFullSrc) {
|
||||
return readFile(swFullSrc, 'utf-8')
|
||||
} else if (id === swEnvNameResolved) {
|
||||
return getDevSwEnv()
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
|
@ -79,6 +88,21 @@ export const devSwPlugin = ({
|
|||
contents: await getSWMessagesAsText()
|
||||
}))
|
||||
}
|
||||
}, {
|
||||
name: 'sw-env',
|
||||
setup (b) {
|
||||
b.onResolve(
|
||||
{ filter: new RegExp('^' + swEnvName + '$') },
|
||||
args => ({
|
||||
path: args.path,
|
||||
namespace: 'sw-env'
|
||||
}))
|
||||
b.onLoad(
|
||||
{ filter: /.*/, namespace: 'sw-env' },
|
||||
() => ({
|
||||
contents: getDevSwEnv()
|
||||
}))
|
||||
}
|
||||
}]
|
||||
})
|
||||
const text = res.outputFiles[0].text
|
||||
|
|
@ -126,6 +150,30 @@ export const buildSwPlugin = ({
|
|||
configFile: false
|
||||
}
|
||||
},
|
||||
generateBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler (_, bundle) {
|
||||
const assets = Object.keys(bundle)
|
||||
.filter(name => !/\.map$/.test(name))
|
||||
.map(name => '/' + name)
|
||||
config.plugins.push({
|
||||
name: 'build-sw-env-plugin',
|
||||
resolveId (id) {
|
||||
if (id === swEnvName) {
|
||||
return swEnvNameResolved
|
||||
}
|
||||
return null
|
||||
},
|
||||
load (id) {
|
||||
if (id === swEnvNameResolved) {
|
||||
return getProdSwEnv({ assets })
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
closeBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
|
|
|
|||
1
changelog.d/sw-cache-assets.add
Normal file
1
changelog.d/sw-cache-assets.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Cache assets and emojis with service worker
|
||||
|
|
@ -14,6 +14,7 @@ import EditStatusModal from './components/edit_status_modal/edit_status_modal.vu
|
|||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
import { getOrCreateServiceWorker } from './services/sw/sw'
|
||||
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
|
@ -77,6 +78,7 @@ export default {
|
|||
this.setThemeBodyClass()
|
||||
this.removeSplash()
|
||||
}
|
||||
getOrCreateServiceWorker()
|
||||
},
|
||||
unmounted () {
|
||||
window.removeEventListener('resize', this.updateMobileState)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import InterfaceLanguageSwitcher from 'src/components/interface_language_switche
|
|||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
||||
import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGlobe
|
||||
|
|
@ -98,6 +99,21 @@ const GeneralTab = {
|
|||
methods: {
|
||||
changeDefaultScope (value) {
|
||||
this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
|
||||
},
|
||||
clearCache (key) {
|
||||
clearCache(key)
|
||||
.then(() => {
|
||||
this.$store.dispatch('settingsSaved', { success: true })
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.dispatch('settingsSaved', { error })
|
||||
})
|
||||
},
|
||||
clearAssetCache () {
|
||||
this.clearCache(cacheKey)
|
||||
},
|
||||
clearEmojiCache () {
|
||||
this.clearCache(emojiCacheKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -509,6 +509,29 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
class="setting-item"
|
||||
>
|
||||
<h2>{{ $t('settings.cache') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="clearAssetCache"
|
||||
>
|
||||
{{ $t('settings.clear_asset_cache') }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="clearEmojiCache"
|
||||
>
|
||||
{{ $t('settings.clear_emoji_cache') }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1091,7 +1091,10 @@
|
|||
"reset_value": "Reset",
|
||||
"reset_value_tooltip": "Reset draft",
|
||||
"hard_reset_value": "Hard reset",
|
||||
"hard_reset_value_tooltip": "Remove setting from storage, forcing use of default value"
|
||||
"hard_reset_value_tooltip": "Remove setting from storage, forcing use of default value",
|
||||
"cache": "Cache",
|
||||
"clear_asset_cache": "Clear asset cache",
|
||||
"clear_emoji_cache": "Clear emoji cache"
|
||||
},
|
||||
"admin_dash": {
|
||||
"window_title": "Administration",
|
||||
|
|
|
|||
|
|
@ -146,3 +146,11 @@ export function unregisterPushNotifications (token) {
|
|||
]).catch((e) => console.warn(`Failed to disable Web Push Notifications: ${e.message}`))
|
||||
}
|
||||
}
|
||||
|
||||
export const shouldCache = process.env.NODE_ENV === 'production'
|
||||
export const cacheKey = 'pleroma-fe'
|
||||
export const emojiCacheKey = 'pleroma-fe-emoji'
|
||||
|
||||
export const clearCache = (key) => caches.delete(key)
|
||||
|
||||
export { getOrCreateServiceWorker }
|
||||
|
|
|
|||
109
src/sw.js
109
src/sw.js
|
|
@ -1,8 +1,10 @@
|
|||
/* eslint-env serviceworker */
|
||||
|
||||
import 'virtual:pleroma-fe/service_worker_env'
|
||||
import { storage } from 'src/lib/storage.js'
|
||||
import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js'
|
||||
import { prepareNotificationObject } from './services/notification_utils/notification_utils.js'
|
||||
import { shouldCache, cacheKey, emojiCacheKey } from './services/sw/sw.js'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
// Collects all messages for service workers
|
||||
// Needed because service workers cannot use dynamic imports
|
||||
|
|
@ -85,6 +87,80 @@ const showPushNotification = async (event) => {
|
|||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const cacheFiles = self.serviceWorkerOption.assets
|
||||
const isEmoji = req => {
|
||||
if (req.method !== 'GET') {
|
||||
return false
|
||||
}
|
||||
const url = new URL(req.url)
|
||||
|
||||
return url.pathname.startsWith('/emoji/')
|
||||
}
|
||||
const isNotMedia = req => {
|
||||
if (req.method !== 'GET') {
|
||||
return false
|
||||
}
|
||||
const url = new URL(req.url)
|
||||
return !url.pathname.startsWith('/media/')
|
||||
}
|
||||
const isAsset = req => {
|
||||
const url = new URL(req.url)
|
||||
return cacheFiles.includes(url.pathname)
|
||||
}
|
||||
|
||||
const isSuccessful = (resp) => {
|
||||
if (!resp.ok) {
|
||||
return false
|
||||
}
|
||||
if ((new URL(resp.url)).pathname === '/index.html') {
|
||||
// For index.html itself, there is no fallback possible.
|
||||
return true
|
||||
}
|
||||
const type = resp.headers.get('Content-Type')
|
||||
// Backend will revert to index.html if the file does not exist, so text/html for emojis and assets is a failure
|
||||
return type && !type.includes('text/html')
|
||||
}
|
||||
|
||||
self.addEventListener('install', async (event) => {
|
||||
if (shouldCache) {
|
||||
event.waitUntil((async () => {
|
||||
// Do not preload i18n and emoji annotations to speed up loading
|
||||
const shouldPreload = (route) => {
|
||||
return !route.startsWith('/static/js/i18n/') && !route.startsWith('/static/js/emoji-annotations/')
|
||||
}
|
||||
const cache = await caches.open(cacheKey)
|
||||
await Promise.allSettled(cacheFiles.filter(shouldPreload).map(async (route) => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Cache/add
|
||||
// originally we used addAll() but it will raise a problem in one edge case:
|
||||
// when the file for the route is not found, backend will return index.html with code 200
|
||||
// but it's wrong, and it's cached, so we end up with a bad cache.
|
||||
// this can happen when you refresh when you are in the process of upgrading
|
||||
// the frontend.
|
||||
const resp = await fetch(route)
|
||||
if (isSuccessful(resp)) {
|
||||
await cache.put(route, resp)
|
||||
}
|
||||
}))
|
||||
})())
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('activate', async (event) => {
|
||||
if (shouldCache) {
|
||||
event.waitUntil((async () => {
|
||||
const cache = await caches.open(cacheKey)
|
||||
const keys = await cache.keys()
|
||||
await Promise.all(
|
||||
keys.filter(request => {
|
||||
const url = new URL(request.url)
|
||||
const shouldKeep = cacheFiles.includes(url.pathname)
|
||||
return !shouldKeep
|
||||
}).map(k => cache.delete(k))
|
||||
)
|
||||
})())
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('push', async (event) => {
|
||||
if (event.data) {
|
||||
// Supposedly, we HAVE to return a promise inside waitUntil otherwise it will
|
||||
|
|
@ -143,4 +219,35 @@ self.addEventListener('notificationclick', (event) => {
|
|||
}))
|
||||
})
|
||||
|
||||
console.log('sw here')
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// Do not mess up with remote things
|
||||
const isSameOrigin = (new URL(event.request.url)).origin === self.location.origin
|
||||
if (shouldCache && event.request.method === 'GET' && isSameOrigin && isNotMedia(event.request)) {
|
||||
// this is a bit spammy
|
||||
// console.debug('[Service worker] fetch:', event.request.url)
|
||||
event.respondWith((async () => {
|
||||
const r = await caches.match(event.request)
|
||||
const isEmojiReq = isEmoji(event.request)
|
||||
|
||||
if (r && isSuccessful(r)) {
|
||||
console.debug('[Service worker] already cached:', event.request.url)
|
||||
return r
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(event.request)
|
||||
if (response.ok &&
|
||||
isSuccessful(response) &&
|
||||
(isEmojiReq || isAsset(event.request))) {
|
||||
console.debug(`[Service worker] caching ${isEmojiReq ? 'emoji' : 'asset'}:`, event.request.url)
|
||||
const cache = await caches.open(isEmojiReq ? emojiCacheKey : cacheKey)
|
||||
await cache.put(event.request.clone(), response.clone())
|
||||
}
|
||||
return response
|
||||
} catch (e) {
|
||||
console.error('[Service worker] error when caching emoji:', e)
|
||||
throw e
|
||||
}
|
||||
})())
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -172,7 +172,14 @@ export default defineConfig(async ({ mode, command }) => {
|
|||
return 'static/js/[name].[hash].js'
|
||||
}
|
||||
},
|
||||
chunkFileNames () {
|
||||
chunkFileNames (chunkInfo) {
|
||||
if (chunkInfo.facadeModuleId) {
|
||||
if (chunkInfo.facadeModuleId.includes('node_modules/@kazvmoe-infra/unicode-emoji-json/annotations/')) {
|
||||
return 'static/js/emoji-annotations/[name].[hash].js'
|
||||
} else if (chunkInfo.facadeModuleId.includes('src/i18n/')) {
|
||||
return 'static/js/i18n/[name].[hash].js'
|
||||
}
|
||||
}
|
||||
return 'static/js/[name].[hash].js'
|
||||
},
|
||||
assetFileNames (assetInfo) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue