前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >什么年代还在用传统 Pjax? —— 自定义 Pjax 提升页面加载速度

什么年代还在用传统 Pjax? —— 自定义 Pjax 提升页面加载速度

作者头像
OhhhCKY
发布于 2022-12-28 08:50:28
发布于 2022-12-28 08:50:28
3K00
代码可运行
举报
文章被收录于专栏:YFun's BlogYFun's Blog
运行总次数:0
代码可运行

前言

Hexo 属于静态博客,很多同学给自己的博客加上 Pjax 是为了音乐播放器等功能不中断。

之前我也想过对博客和主题加入 Pjax 支持,但经过一番分析后觉得,这不仅引入了一个巨大的 jquery.pjax.js,反而优化效果不明显。

原理

其实,Pjax 的原理并不复杂。或许说,README 一开始就告诉你了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pjax = pushState + ajax

其中 ajax 用于页面的新内容,pushState 改变浏览器状态。

很简单吧。

事实上,pjax 并不应该应用于整个页面当中。而应该只是局部更改。

这样,Blog 当中的导航栏、样式文件等就不需要重复下载与预览。

分析

以我使用 Miracle 为主题的博客为例,进入首页,按 F12 查看页面 Elements.

可以发现,页面主要更改的也就是 #page-main 部分,只需要实现动态刷新这部分的内容就可以了。

那怎么实现呢?

最小化的数据接口

现在生成的页面当中,有 <head> 部分声明大量样式与元信息,<body> 之下重复的页脚、导航栏,还有每个页面下方都有的一些 <script>

很明显,我们不需要这些。我们只要 #page-main 中的主要内容。

最重要的是,Hexo 是静态博客,这一点只能在生成文件时进行。

载入 HTML

我是用 Cheerio 模块帮我完成这一工作。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const cheerio = require('cheerio');
const fs = require("fs");
const path = require("path");
const filePath = path.resolve('public/');

定义一个 parse function,打开文件并解析相关信息,顺便把不是 HTML 的文件排除掉。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const parse = (filename, fullpath) => {
    // 不是 .html 我不要
    if (!filename.endsWith(".html")) {
        return false;
    }
}

然后通过 Cheerio 解析 HTML:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{...
  // 组合新文件名
  let filepath = fullpath+".page.json";
  // 读取文件内容
  let pageContent = fs.readFileSync(fullpath).toString();
  // 解析页面内容
  let $pg = cheerio.load(pageContent);
  let rtData = {};
...}

然后获取页面的标题和 #page-main 下的 HTML.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{...
	// 页面标题
    rtData.title = $pg("title").text();
    // OR $pg("#page-main").html()
    // 我这么写是因为主题 #page-main 下还有 script 无法执行
    rtData.page = `
    <div class="mg-top">
        ${$pg(".mg-top").html() || ""}
    </div>
    <footer class="text-center">
        ${$pg("footer").html() || ""}
    </footer>
    <div class="p-btn">
        ${$pg(".p-btn").html() || ""}
    </div>
    `;
    rtData.path = filename;
...}

页面中还有一些 script,比如阅读进度、懒加载等。所以需要一个 extraJS 放置额外的 Script.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{...
    rtData.extraJS = []
    // 只解析 #page-main 下的 script
    let $pageMain = cheerio.load($pg("#page-main").html());
    $pageMain('script').map(function(i, el) {
        // 尝试往 extraJS 中 push 相关代码
        try {rtData.extraJS.push($pageMain(this)[0].children[0].data);} catch(e) {}
        $pageMain(this).remove();
    });
...}

最后,将 JSON 写入文件中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{...
    fs.writeFileSync(filepath, JSON.stringify(rtData));
}

文件递归

我们还需要一个函数递归 public 目录下的所有文件,这个不用多说。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function fileDisplay(filePath) {
    // 根据文件路径读取文件,返回文件列表
    fs.readdir(filePath, function(err, files) {
        if (err) {
            console.warn(err, "读取文件夹错误!")
        } else {
            // 遍历读取到的文件列表
            files.forEach(function(filename) {
                // 获取当前文件的绝对路径
                var filedir = path.join(filePath, filename);
                var fullname = filedir.split("public")[1];
                fs.stat(filedir, function(eror, stats) {
                    if (eror) {
                        console.warn('获取文件 Stats 失败!');
                    } else {
                        var isFile = stats.isFile(); // 是文件
                        var isDir = stats.isDirectory(); // 是文件夹
                        if (isFile) {
                            parse(fullname, filedir);
                        }
                        if (isDir) {
                            fileDisplay(filedir); // 递归,如果是文件夹,就继续遍历该文件夹下面的文件
                        }
                    }
                });
            });
        }
    });
}
fileDisplay(filePath);

最后运行这个 Node.js 文件,就可以看到 public/ 目录下多出很多 ***.page.json 文件。

基本结构

这些文件内容也很简单,基本如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
    // 页面的标题
    "title": "Hello World",
    // 内容
    "page": "...",
    // 路径
    "path": "/foo/bar",
    // JS
    "extraJS": ['alert("Hello World");']
}

前端 pjax.js

新建一个 pjax.js

替换链接

我们需要先将页面当中所有本站链接转为 Pjax 的 Jump 函数。

判断条件是:有链接,不带 hash,且为本站链接

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 转换页面中的链接为 Pjax 链接
const $pjax_convertAllLinks = () => {
	// 所有的 a 标签
    const linkElements = document.querySelectorAll("a");
    for (let i of linkElements) {
        // 有链接,不带 hash,且为本站链接
        if (i.href && !i.href.includes("/#") && (i.href.startsWith("/") || i.href.match(new RegExp(window.location.hostname)))) {
            let thisLink = new URL(i.href).pathname+new URL(i.href).hash;
            i.href = `javascript:$pjax_jump('${thisLink}');`;
        }
    }
}

另外,要转化页面链接为全路径。

这里参考了下 ChenYFan 的 Service Worker 函数,需要根据实际情况做出调整。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 转换路径为全路径
const $pjax_fullpath = (path) => {
    path = path.split('?')[0].split('#')[0]
    if (path.match(/\/$/)) {
        path += 'index.html';
    }
    if (!path.match(/\.[a-zA-Z]+$/)) {
        path += '/index.html';
    }
    return path;
}

// $pjax_fullpath('/') => /index.html

跳转

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 跳转页面
const $pjax_jump = async (path) => {
    try {
        // 是 # 就别跳转了
        if (path.startsWith("#")) {
            window.hash = path;
            return false;
        }
        // 加载动画
        let loading = document.createElement('div');
        loading.innerHTML = `<div style="position: fixed;top:0;left:0;z-index:99999;display: block;width: 100%;height: 4px;overflow: hidden;background-color: rgba(63,81,181,.2);border-radius: 2px;"><div class="progress-indeterminate" style="background-color: #3f51b5;"></div><style>#page-main{transition:0.2s;}.progress-indeterminate::before{position:absolute;top:0;bottom:0;left:0;background-color:inherit;-webkit-animation:mdui-progress-indeterminate 2s linear infinite;animation:mdui-progress-indeterminate 2s linear infinite;content:' ';will-change:left,width;}.progress-indeterminate::after{position:absolute;top:0;bottom:0;left:0;background-color:inherit;-webkit-animation:mdui-progress-indeterminate-short 2s linear infinite;animation:mdui-progress-indeterminate-short 2s linear infinite;content:' ';will-change:left,width;}@keyframes mdui-progress-indeterminate{0%{left:0;width:0;}50%{left:30%;width:70%;}75%{left:100%;width:0;}}@keyframes mdui-progress-indeterminate-short{0%{left:0;width:0;}50%{left:0;width:0;}75%{left:0;width:25%;}100%{left:100%;width:0;}}</style></div>`;
        // 在 body 后加入 <div>
        document.body.appendChild(loading);
        // 如果页面中没有 page.css 或 search.css,为防止样式错乱,需要在加载过程中隐藏页面内容
        if (!document.getElementById("page_css") || !document.getElementById("search_css")) document.getElementById("page-main").style.opacity = 0;
        // 获取页面数据
        let pageData;
        // 看看 SessionStorage 里有没有缓存
        // 依赖后文的 prefetch
        if (sessionStorage.getItem(`${location.protocol}//${location.hostname}${location.port ? ":"+location.port:location.port}${$pjax_fullpath(path)}`)) {
            console.log("FROM SESSIONSTORAGE");
            try {
                pageData = JSON.parse(sessionStorage.getItem(`${location.protocol}//${location.hostname}${location.port ? ":"+location.port:location.port}${$pjax_fullpath(path)}`));
            } catch(e) {
                // 还是出错就从服务器获取
                console.log("FROM SERVER");
                pageData = await fetch($pjax_fullpath(path) + ".page.json").then(res => res.json());
                // 写到 SessionStorage 中
                sessionStorage.setItem(`${location.protocol}//${location.hostname}${location.port ? ":"+location.port:location.port}${$pjax_fullpath(path)}`, JSON.stringify(pageData));
            }
        } else {
            console.log("FROM SERVER");
            // fetch JSON
            pageData = await fetch($pjax_fullpath(path) + ".page.json").then(res => res.json());
            sessionStorage.setItem(`${location.protocol}//${location.hostname}${location.port ? ":"+location.port:location.port}${$pjax_fullpath(path)}`, JSON.stringify(pageData));
        }
        // 补齐页面 CSS
        if (!document.getElementById("search_css")) {
            fetch("/css/search.css").then(res => res.text()).then(res => {
                let ele = document.createElement("style");
                ele.innerHTML = res;
                ele.id = "search_css";
                document.body.appendChild(ele);
            });
        }
        if (!document.getElementById("page_css")) {
            fetch("/css/page.css").then(res => res.text()).then(res => {
                let ele = document.createElement("style");
                ele.innerHTML = res;
                ele.id = "page_css";
                document.body.appendChild(ele);
            });
        }
        if (!pageData) return false;
        // 组合 state
        var state = { title: '', url: window.location.href.split("?")[0] };
        // 利用 history.pushState() 修改地址栏而不跳转
        history.pushState(state, '', path);
        // 修改页面标题
        document.title = pageData.title;
        setTimeout(() => {
            // 滚动到页面顶部
            window.scrollTo({top: 0, behavior: "smooth"});
            // 写入 HTML
            document.getElementById("page-main").innerHTML = pageData.page;
            window.onscroll = null;
            for (let i in pageData.extraJS) {
                try {
                    // eval() 执行 JS
                    eval(pageData.extraJS[i]);
                } catch(e) {}
            }
            try{$pjax_prefetch();}catch(e){}
            // 再次转换所有链接
            $pjax_convertAllLinks();
        }, 200);
        setTimeout(() => {
            // 重新显示页面
            document.getElementById("page-main").style.opacity = 1;
            loading.remove();
        }, 1000);
    } catch(e) {
        // 有报错 直接跳转
        console.warn(e);
        window.location.href = path;
    }
}

如果使用 window.location.href 修改,那么页面就会刷新。 为了实现无刷新跳转,必须要使用 pushState() 更改。

执行 JavaScript 方面使用 eval() 函数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 组合 state
var state = { title: '', url: window.location.href.split("?")[0] };
// 利用 history.pushState() 修改地址栏而不跳转
history.pushState(state, '', path);
// 修改页面标题
document.title = pageData.title;
// 滚动到页面顶部
window.scrollTo({top: 0, behavior: "smooth"});
// 写入 HTML
document.getElementById("page-main").innerHTML = pageData.page;
window.onscroll = null;
for (let i in pageData.extraJS) {
  try {
    // eval() 执行 JS
    eval(pageData.extraJS[i]);
  } catch(e) {}
}

Prefetch & Refetch

此处借鉴乐特关于 Prefetch Page 的源码,当用户打开节流模式或为低速网络时就不要 Prefetch.

Prefetch 可以提前缓存部分数据。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const $pjax_prefetch = () => {
    // 节流和低速网络不要 Prefetch
    const nav = navigator;
    const { saveData, effectiveType } = nav.connection || nav.mozConnection || nav.webkitConnection || {};
    if (saveData || /2g/.test(effectiveType)) return false;
  
    // 此处是 Blog 的一些常见链接
    let posts_list = document.querySelectorAll(".index-header a");
    for (let i in posts_list) {
        // 全路径
        let thisLink = $pjax_fullpath(posts_list[i].href);
        // Session Storage 没有才 Fetch
        if (!sessionStorage.getItem(thisLink)) {
            fetch(thisLink + ".page.json").then(res => res.text()).then(res => {
                sessionStorage.setItem(thisLink,res);
            });
        }
    }
}

Refetch 用于刷新已有的缓存(虽然 SessionStorage 关闭页面就没了)

其原理也很简单,SessionStorage 中所有的 Pjax 缓存重新获取就完事了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const $pjax_refetch = () => {
    let sst = sessionStorage;
    for (let i in sst) {
        if (i.startsWith("http://") || i.startsWith("https://")) {
            fetch(i + ".page.json").then(res => res.text()).then(res => {
                sessionStorage.setItem(i, res);
            });
        }
    }
}

一些优化

Prefetch CSS 文件

既然 CSS 文件需要补齐,那么打开页面 5s 后自动 Prefetch 可以提升速度。

5s 后再获取是为了防止阻塞页面。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
setTimeout(() => {
    // Prefetch CSS 文件
    if (!document.getElementById("search_css")) {
        fetch("/css/search.css").then(res => res.text()).then(res => {
            let ele = document.createElement("style")
            ele.innerHTML = res;
            ele.id = "search_css";
            document.body.appendChild(ele);
        });
    }
    if (!document.getElementById("page_css")) {
        fetch("/css/page.css").then(res => res.text()).then(res => {
            let ele = document.createElement("style")
            ele.innerHTML = res;
            ele.id = "page_css";
            document.body.appendChild(ele);
        });
    }    
}, 5000);
关于 Robots

当你运行 pjax_convertAllLinks(); 后,你肯定会发现所有的链接都变成了 javascript:pjax_jump('/xxx');。这对机器人来说很不友好。

所以,我们需要排除这些机器人。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var runningOnBrowser = typeof window !== "undefined";
var isBot = runningOnBrowser && !("onscroll" in window) || typeof navigator !== "undefined" && /(gle|ing|ro|msn)bot|crawl|spider|yand|duckgo/i.test(navigator.userAgent);

if (runningOnBrowser && !isBot) {
    setTimeout(() => {
        try{$pjax_prefetch();}catch(e){}
        $pjax_convertAllLinks();
    }, 100);
}

最后

在启用 Pjax 后,YFun's Blog 传输大小理论上最高缩小 3/4,性能速度均有提升。

如果你也在使用 Pjax,不妨试试看。

还有一些错误

如果你定义了 onload 等事件,页面没有刷新即代表没有变化,你需要在 $pjax_jump() 中简单清除一下这些信息。

广告时间

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=16qkaef2qdvzm

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-12-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
pjax使用小结
上周看到一篇文章在分析简书 我的主页 页面 3 个 tab 页切换的 bug,起先以为是寻常的样式 bug 而已没怎么在意,后来在文章中看到 pjax 这个术语,长得和 ajax 有点像,遂去了解了下。
Lansonli
2021/10/09
3K0
pjax 历史管理 jQuery.History.js
更新 http://www.bootcdn.cn/jquery.pjax/ 简介 pjax是一个jQuery插件,使用ajax和pushState技术提供快速的浏览体验与真正的永久链接、网页标题、以及浏览器的后退前进按钮操作。 pjax通过抓取HTML从您的服务器通过Ajax和更换容器页面上的HTML内容会与Ajax。然后更新无需重新加载你的网页的布局或任何资源使用pushstate浏览器的当前URL(JS,CSS),提供了一个快速的外观,全页面加载。但它确实就是Ajax和pushstate。 点击这里查看
deepcc
2018/05/16
2.6K0
Matery主题添加Pjax
Pjax的使用可以在保证Nav Header Footer 不变的基础上改变 Main 的内容(适用于页面结构相对简单的主体)
闲花手札
2022/01/24
1.3K0
Matery主题添加Pjax
滴滴前端二面常考react面试题(持续更新中)_2023-03-01
refs允许你直接访问DOM元素或组件实例。为了使用它们,可以向组件添加个ref属性。
用户10376779
2023/03/01
4.6K0
基于Node.js实现一个小小的爬虫
从拉钩招聘网站中找出“前端开发”这一类岗位的信息,并作相应页面分析,提取出特定的几个部分如岗位名称、岗位薪资、岗位所属公司、岗位发布日期等。并将抓取到的这些信息,展现出来。
书童小二
2018/09/03
1.2K0
基于Node.js实现一个小小的爬虫
JS:Web Storage API(localStorage、sessionStorage)
Web Storage API 提供了存储机制,通过该机制,浏览器可以安全地存储键值对,比使用 cookie 更加直观。Web Storage 包含如下两种机制:
WEBJ2EE
2020/05/09
1.5K0
JS:Web Storage API(localStorage、sessionStorage)
经常被问到的react-router实现原理详解
而且还经常会被xxx面试官问到,什么是前端路由,它的原理的是什么,它是怎么实现,跳转不刷新页面的...
夏天的味道123
2022/10/17
5690
安全跳转页面·重制版
原本的安全跳转页面糟糕的一塌糊涂,因为当时水平有限,所以只能在别人的基础上修改,导致很多地方都不兼容,比如最简单的fancybox我都没有办法排除,会导致无法点击图片进行放大查看,除此之外无法排除友链页面,也无法排除友情链接的跳转卡片,兼容性也很差,群友想要使用我也没法提供解决方案,很是头疼,所以经过整整一个月的酝酿,我胡汉三又回来啦!此次重构大大简化了代码结构,并解决了前面的问题,为了测试稳定性,我还特意悄悄地上线了六天,好像也没人提出什么bug(也有可能是我的人气太少了呜呜呜),这才正式写出该重制版教程,给予需要的朋友以启发。
柳神
2024/05/30
1K0
安全跳转页面·重制版
JavaScript 允许自定义对象分析
JavaScript 中的所有事物都是对象:字符串、数值、数组、函数... 此外,JavaScript 允许自定义对象。 JavaScript 提供多个内建对象,比如 String、Date、Array 等等。 对象只是带有属性和方法的特殊数据类型。
用户7718188
2021/10/07
4140
hash和history的原理和区别
目前单页应用(SPA)越来越成为前端主流,单页应用一大特点就是使用前端路由,由前端来直接控制路由跳转逻辑,而不再由后端人员控制,这给了前端更多的自由。
前端小tips
2021/11/23
2K0
hash和history的原理和区别
记一次跳不出思维解决 admin pjax 自定义刷新页面问题
seth-shi
2023/12/18
3960
记一次跳不出思维解决 admin pjax 自定义刷新页面问题
【Web技术】913- 谈谈你对前端路由的理解
好了不装了,今天我就化身性感面试官在线问大家一个问题,“谈谈你对前端路由的理解”。看到这个问题,那回答可多了去了。但是换位思考一下,你问候选人这个问题的时候,你想要得到什么答案?以我个人拙见,我希望候选人能从全局解读这个问题,大致以下三点。
pingan8787
2021/04/07
6750
【Web技术】913- 谈谈你对前端路由的理解
301跳转
嗯对,爷不是换域名了吗 xiaolfeng.cn 。告别了 .xyz 的国际域名,转向 .cn 国内域名。 至于我为什么换域名呢,可能是因为 .cn 比 .xyz 高级(可能只是在国内是这样,国际上不一定) 反正我认为就行了,这是我的Blog~
筱锋xiao_lfeng
2022/03/16
1.9K0
301跳转
前端路由简介以及vue-router实现原理
简单来说路由就是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能。
muwoo
2018/06/06
1.6K1
前端路由简介以及vue-router实现原理
React.js实战之Router原理及 React-router页面路由Hash 路由H5路由
官网文档 https://reacttraining.com/react-router/core/guides/philosophy 页面路由 Hash 路由 H5路由 只对后退记录有效 // 页面
JavaEdge
2018/06/06
3.4K0
你好,谈谈你对前端路由的理解
作者:尼克陈 https://juejin.cn/post/6917523941435113486
用户4456933
2021/06/01
1K0
你好,谈谈你对前端路由的理解
通过session实现用户的登录与登出功能
本文讲解,就是在常见的登录注册页面中,我们是如何在登录之后,把用户的信息传送到后面的网页。
GeekLiHua
2025/01/21
1340
通过session实现用户的登录与登出功能
SAO UI Plan -- SAO Utils Web 1.0
由于本教程涉及的所有修改对缩进格式等有严格要求,担心自己控制不好的可以直接下载静态资源。参照教程进行修改。
Akilar
2021/06/11
1.8K1
Vue路由实现原理
其中pushState方法和replaceState方法可以分别增加和替换掉一条记录(必须同源),而不会重新加载页面。
愤怒的小鸟
2021/01/11
1.3K0
如何进行渗透测试XSS跨站攻击检测
国庆假期结束,这一节准备XSS跨站攻击渗透测试中的利用点,上一节讲了SQL注入攻击的详细流程,很多朋友想要咨询具体在跨站攻击上是如何实现和利用的,那么我们Sinesafe渗透测试工程师为大家详细的讲讲这个XSS是如何实现以及原理。
技术分享达人
2019/10/08
2.8K0
如何进行渗透测试XSS跨站攻击检测
推荐阅读
相关推荐
pjax使用小结
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验