在纯静态网站里,有时候会动态更新某个区域往会选择 Pjax(swup、barba.js)去处理,他们都是使用 ajax 和 pushState 通过真正的永久链接,页面标题和后退按钮提供快速浏览体验。
但是实际使用中可能会遇到不同页面可能会需要加载不同插件处理,有些人可能会全量选择加载,这样会导致加载很多无用的脚本,有可能在用户关闭页面时都不一定会访问到,会很浪费资源。
解决思路 首先想到的肯定是在请求到新的页面后,我们手动去比较当前 DOM 和 新 DOM 之间 script
标签的差异,手动给他插入到 body 里。
处理 Script 一般来说 JavaScript 脚本都是放在 body
后,避免阻塞页面渲染,假设我们页面脚本也都是在 body
后,并在 script 添加 [data-reload-script]
表明哪些是需要动态加载的。
首先我们直接获取到带有 [data-reload-script]
属性的 script 标签:
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
,用来判断哪些比是需要新增的哪些是需要删除的。
结尾 本文示例代码完整版本可以 参考这里