在純靜態網站里,有時候會動態更新某個區域往會選擇 Pjax(swup、barba.js)去處理,他們都是使用 ajax 和 pushState 通過真正的永久鏈接,頁面標題和後退按鈕提供快速瀏覽體驗。 但是實際使用中可能會遇到不同頁面可能會需要載入不同插件處理,有些人可能會全量選擇載入,這樣會導致加 ...
在純靜態網站里,有時候會動態更新某個區域往會選擇 Pjax(swup、barba.js)去處理,他們都是使用 ajax 和 pushState 通過真正的永久鏈接,頁面標題和後退按鈕提供快速瀏覽體驗。
但是實際使用中可能會遇到不同頁面可能會需要載入不同插件處理,有些人可能會全量選擇載入,這樣會導致載入很多無用的腳本,有可能在用戶關閉頁面時都不一定會訪問到,會很浪費資源。
解決思路
首先想到的肯定是在請求到新的頁面後,我們手動去比較當前 DOM 和 新 DOM 之間 script
標簽的差異,手動給他插入到 body 里。
處理 Script
一般來說 JavaScript 腳本都是放在 body
後,避免阻塞頁面渲染,假設我們頁面腳本也都是在 body
後,併在 script 添加 [data-reload-script]
表明哪些是需要動態載入的。
首先我們直接獲取到帶有 [data-reload-script]
屬性的 script 標簽:
// NewHTML 為 新頁面 HTML
const pageContent = NewHTML.replace('<body', '<div id="DynamicPluginBody"').replace('</body>', '</div>');
let element = document.createElement('div');
element.innerHTML = pageContent;
const children = element.querySelector('#DynamicPluginBody').querySelectorAll('script[data-reload-script]');
然後通過創建 script 標簽插入到 body
:
children.forEach(item => {
const element = document.createElement('script');
for (const { name, value } of arrayify(item.attributes)) {
element.setAttribute(name, value);
}
element.textContent = item.textContent;
element.setAttribute('async', 'false');
document.body.insertBefore(element)
})
如果你的插件都是通過 script 引入,且不需要執行額外的 JavaScript 代碼,只需要在 Pjax 鉤子函數這樣處理就可以了。
執行代碼塊
實際很多插件不僅僅需要你引入,還需要你手動去初始化做一些操作的。我們可以通過 src
去判斷是引入的腳本,還是代碼塊。
let scripts = Array.from(document.scripts)
let scriptCDN = []
let scriptBlock = []
children.forEach(item => {
if (item.src)
scripts.findIndex(s => s.src === item.src) < 0 && scriptCDN.push(item);
else
scriptBlock.push(item.innerText)
})
scriptCDN 繼續通過上面方式插入到 body 里,然後通過 eval 或者 new Function 去執行 scriptBlock 。因為 scriptBlock 里的代碼可能是會依賴 scriptCDN 里的插件的,所以需要在 scriptCDN 載入完成後在執行 scriptBlock 。
const loadScript = (item) => {
return new Promise((resolve, reject) => {
const element = document.createElement('script');
for (const { name, value } of arrayify(item.attributes)) {
element.setAttribute(name, value);
}
element.textContent = item.textContent;
element.setAttribute('async', 'false');
element.onload = resolve
element.onerror = reject
document.body.insertBefore(element)
})
}
const runScriptBlock = (code) => {
try {
const func = new Function(code);
func()
} catch (error) {
try {
window.eval(code)
} catch (error) {
}
}
}
Promise.all(scriptCDN.map(item => loadScript(item))).then(_ => {
scriptBlock.forEach(code => {
runScriptBlock(code)
})
})
卸載插件
按照上面思去處理之後,會存在一個問題。 比如:我們添加了一個 全局的 'resize' 事件的監聽,在跳轉其他頁面時候我們需要移除這個監聽事件。
這個時候我們需要對代碼塊的格式進行一個約束,比如像下麵這樣,在初次載入時執行 mount 里代碼,頁面卸載時執行 unmount 里代碼。
<script data-reload-script>
DynamicPlugin.add({
// 頁面載入時執行
mount() {
this.timer = setInterval(() => {
document.getElementById('time').innerText = new Date().toString()
}, 1000)
},
// 頁面卸載時執行
unmount() {
window.clearInterval(this.timer)
this.timer = null
}
})
</script>
DynamicPlugin 大致結構:
let cacheMount = []
let cacheUnMount = []
let context = {}
class DynamicPlugin {
add(options) {
if (isFunction(options))
cacheMount.push(options)
if (isPlainObject(options)) {
let { mount, unmount } = options
if (isFunction(mount))
cacheMount.push(mount)
if (isFunction(unmount))
cacheUnMount.push(unmount)
}
// 執行當前頁面載入鉤子
this.runMount()
}
runMount() {
while (cacheMount.length) {
let item = cacheMount.shift();
item.call(context);
}
}
runUnMount() {
while (cacheUnMount.length) {
let item = cacheUnMount.shift();
item.call(context);
}
}
}
頁面卸載時調用 DynamicPlugin.runUnMount()。
處理 Head
Head 部分處理來說相對比較簡單,可以通過拿到新舊兩個 Head,然後迴圈對比每個標簽的 outerHTML
,用來判斷哪些比是需要新增的哪些是需要刪除的。
結尾
本文示例代碼完整版本可以 參考這裡