方案一
代码语言:javascript复制const code = `
!(function () {
"use strict"
const assert = $$assert
function createElement(tag) {
const ele = document.createElement(tag.tagName)
if (tag.attributes) {
Object
.keys(tag.attributes)
.forEach(key => ele[key] = tag.attributes[key])
}
if (tag.innerHTML) {
ele.innerHTML = tag.innerHTML
}
ele.async = false
ele.defer = false
return ele
}
function appendTag(tag) {
document.body.appendChild(createElement(tag))
}
function inlineTag(tag, innerHTML) {
let path
if (tag.tagName.toUpperCase() === 'LINK') {
tag.tagName = 'style'
path = tag.attributes.href
delete tag.attributes
} else {
path = tag.attributes.src
delete tag.attributes.src
tag.attributes.async = false
}
tag.innerHTML = `// From CacheStorage: ${path} \n${innerHTML}`
return tag
}
const cacheName = 'assert'
try {
window.caches
.open(cacheName)
.then((cache) => {
return assert.map((tag) => {
if (tag.innerHTML) {
return tag
}
if (!tag.attributes.src && !tag.attributes.href) {
throw new Error(`tag.innerHTML 与 tag.attributes.src、tag.attributes.href 必传一个, 当前为 ${JSON.stringify(tag)}`)
}
const assertURL = tag.tagName.toUpperCase() === 'SCRIPT' ? tag.attributes.src : tag.attributes.href
return cache.match(assertURL)
.then((response) => {
if (!response) {
return cache
.add(assertURL)
.then(
() => cache
.match(assertURL)
.then(response => response.text())
.then(innerHTML => inlineTag(tag, innerHTML))
)
} else {
console.info(`From CacheStorage: ${assertURL}`)
return response.text().then(innerHTML => inlineTag(tag, innerHTML))
}
})
})
})
.then((scriptList) => {
// Ensure the order of execution
scriptList
.reduce(
(a, b) =>
a
.then(appendTag)
.then(() => b)
)
.then(appendTag)
})
} catch (e) {
console.error(e)
setTimeout(() => {
if (window.fundebug) {
fundebug.notifyError(e)
}
}, 3000)
const fragment = document.createDocumentFragment()
assert.map(tag => {
const ele = createElement(tag)
fragment.appendChild(ele)
})
document.body.appendChild(fragment)
}
})();
`
const compiler = source =>
require('@babel/core').transformSync(
source,
{
presets: [ '@babel/preset-env' ]
}
)
.code
class CacheStaticPlugin {
constructor(htmlWebpackPlugin, test) {
this.htmlWebpackPlugin = htmlWebpackPlugin
this.test = test
}
isTargetTag(tag) {
let link
const result = (tag.tagName === 'script' || tag.tagName === 'link')
&& !tag.innerHTML
&& tag.attributes
&& (link = (tag.attributes.src || tag.attributes.href))
&& (this.test instanceof RegExp ? this.test.test(link) : this.test(link))
if (!result) {
}
return !!result
}
injectCacheStaticCode(scriptList) {
return compiler(code.replace('$$assert', JSON.stringify(scriptList)))
}
apply(compiler) {
compiler.hooks.compilation.tap('StaticCachePlugin', compilation => {
const hooks = this.htmlWebpackPlugin.getHooks(compilation)
hooks.alterAssetTagGroups.tap('StaticCachePlugin', assets => {
assets.bodyTags = [
...assets.bodyTags.filter(tag => !this.isTargetTag(tag)),
{
tagName: 'script',
innerHTML: this.injectCacheStaticCode(assets.bodyTags.filter(this.isTargetTag.bind(this))),
closeTag: true
}
]
})
})
}
}
module.exports = CacheStaticPlugin
这是一个 Webpack 插件。使用时,通过传入特定的 正则表达式,筛选出需要缓存的静态 JS、CSS 文件,在 HTML 页面中注入一段代码。
当浏览器运行到这段代码时,带有特定标识符的 js、css 文件通过 cache.add() API 下载,并储存到 CacheStorage 中,接着把下载到的代码通过 script 标签注入到 HTML 中即可(注入时需考虑顺序问题)
优化思路
方案二
代码语言:javascript复制// cache
const OFFLINE_SUFFIX = '?offline';
let CURRENT_CACHES = {
offline: 'offline'
};
const cacheFiles = [
'/?offline'
];
self.addEventListener('install', function(event) {
event.waitUntil(self.skipWaiting());
event.waitUntil(
caches.open(CURRENT_CACHES.offline).then(cache => {
return cache.addAll(cacheFiles);
})
);
});
const matchCache = async function(event, caches) {
const res = await Promise.all([
caches.match(event.request.url OFFLINE_SUFFIX),
caches.match(event.request.url)
]);
if (res.some(e => e)) {
return res.filter(e => {
if (e) return e;
})[0];
} else {
return new Response();
}
};
const matchOfflineUrl = function(origin, event) {
if (event.request.method !== 'GET') {
return false;
}
return (
cacheFiles.some(url => origin url === event.request.url) ||
cacheFiles.some(url => origin url === event.request.url OFFLINE_SUFFIX)
);
};
self.addEventListener('fetch', function(event) {
const origin = location.origin;
if (matchOfflineUrl(origin, event)) {
event.respondWith(
fetch(event.request.url)
.then(response => {
if (!response.ok) {
return caches.open(CURRENT_CACHES.offline).then(cache => {
event.request.url === origin '/' && cache.add(cacheFiles[0]);
cache.put(event.request, response.clone());
return response;
});
} else {
return matchCache(event, caches).then(r => r);
}
})
.catch(err => {
console.error(err);
return matchCache(event, caches).then(r => r);
})
);
}
});
此方案为使用 Service Worker 技术中对 fetch 方法的监听,当 fetch 请求失败时,自动使用 CacheStorage 中的缓存进行返回
当用户再次进行联网时,更新缓存中储存的信息。
该方案与 Google Workbox 中某种策略方案类似
如果想了解其他前端性能优化方案,可以参考我另一篇文章:https://mp.weixin.qq.com/s/uly9sDgcUnuHdkfuEiUSXg