方案一
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' ]
}
)
.codeclass 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 中即可(注入时需考虑顺序问题)
优化思路
方案二
// 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