【Pjax 下动态加载插件方案】在纯静态网站里,有时候会动态更新某个区域往会选择 Pjax(swup、barba.js)去处理 , 他们都是使用 ajax 和 pushState 通过真正的永久链接,页面标题和后退按钮提供快速浏览体验 。
但是实际使用中可能会遇到不同页面可能会需要加载不同插件处理,有些人可能会全量选择加载,这样会导致加载很多无用的脚本,有可能在用户关闭页面时都不一定会访问到,会很浪费资源 。
解决思路首先想到的肯定是在请求到新的页面后 , 我们手动去比较当前 DOM 和 新 DOM 之间 script
标签的差异 , 手动给他插入到 body 里 。
处理 Script一般来说 JavaScript 脚本都是放在 body
后 , 避免阻塞页面渲染,假设我们页面脚本也都是在 body
后,并在 script 添加 [data-reload-script]
表明哪些是需要动态加载的 。
首先我们直接获取到带有 [data-reload-script]
属性的 script 标签:
// NewHTML 为 新页面 HTMLconst 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 =https://www.huyubaike.com/biancheng/== item.src) < 0 && scriptCDN.push(item);elsescriptBlock.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 = resolveelement.onerror = rejectdocument.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 } = optionsif (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() 。
处理 HeadHead 部分处理来说相对比较简单,可以通过拿到新旧两个 Head , 然后循环对比每个标签的 outerHTML
,用来判断哪些比是需要新增的哪些是需要删除的 。
结尾本文示例代码完整版本可以 参考这里
推荐阅读
- 快手直播放电视剧是怎么做到的(快手无需滑动播放下一个视频)
- 面试突击87:说一下 Spring 事务传播机制?
- 抖音怎么上下合拍(抖音合拍怎么放图片)
- 抖音下方与他合拍怎么设置的(抖音视频下方红色关注怎样设置)
- 苹果手机怎么截屏(苹果手机截屏敲两下怎么设置)
- 25 《吐血整理》高级系列教程-吃透Fiddler抓包教程-Fiddler如何优雅地在正式和测试环境之间来回切换-下篇
- 不负每一份热爱下一句 不负每一份热爱
- 苹果手机怎么选择截屏(苹果手机怎么把截屏放在下拉菜单)
- 如何化解九丑命,好方法请你收下
- .Net下的分布式唯一ID