Pjax 下动态加载插件方案

在纯静态网站里,有时候会动态更新某个区域往会选择 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,用来判断哪些比是需要新增的哪些是需要删除的。

结尾

本文示例代码完整版本可以 参考这里

发表评论

相关文章