作为一名后端程序员日常打交道最多是 web 应用开发,当然也有一部分从事的其他方面开发,但是最终服务还是基于 web2.0 的应用程序,万维网(World Wide Web)是互联网上的一个重要应用,是一种通过超文本链接将各种信息资源组织起来的系统,它是由一系列的网页组成的,这些网页通过超链接相互连接,万维网通过 HTTP 协议传输网页内容,用户可以使用 Web 浏览器访问和浏览网页上的内容。

但是目前 web2.0 也不仅仅限制于在浏览器交互数据上,有了 HTTP 协议和移动互联网,更多是指基于 web2.0 异步数据交换方式。Web1.0 是互联网的早期阶段人们上网的方式是查看一些固定死的静态网页内容,网站维护者每更新一次网站就需要重新发布一个新的网页版本,而且缺乏和用户互动性。Web2.0 是互联网的现代阶段,大约从 2004 年开始至今,在 Web2.0 时代,互联网变得更加互动和社交,用户可以更积极地参与和贡献内容,用户可以把自己的数据通过 HTTP 协议发送到远端服务器上存储,其他用户可。在 web2.0 不管是 web 应用还是应用程序本质上都是异步请求数据然后交互,没多大区别都有后端支撑着,只是前端视图部分换了不同方式实现。这篇文章讲介绍如何在不刷新页面的情况下更新浏览器地址栏和动态刷新局部页面的 PJAX 技术,PJAX 早期的动态 WEB 开发应用广泛,和目前的 MVVM 框架的路由器功能类似,只在前端完成 URL 和页面加载刷新工作。


网页基础结构

HTML 是整个网站和网页的组成部分,有了 HTML 才有浏览器显示的用户产品的界面,一个 HTML 文件中可能包含了 HTML 网页主体结构标签代码,还有一些 css 样式代码,有了 css 样式代码网页才有了各种好看页面,最重要还有 js 代码,有了 js 代码网页才有了灵魂,可以实现一些 html 组建的逻辑交互作用,下面是一个完整的网页加载流程:

其中为了提高代码的复用性,css 和 js 代码可以单独抽离为一个单独的文件,存放在网络第三方服务器上,再通过网络进行加载,很多的第三方广告系统实现就是基于此实现的。一个典型的例子就是网站 PV 和 UV 的统计,这方面 Google Analytics 功能的实现就是靠着外部引入的 js 脚步文件实现。

  • window 对象是浏览器顶层对象,表示整个浏览器窗口,提供浏览器级别的属性和方法。
  • document 对象表示当前加载的 HTML 文档,用于操作和访问文档结构和内容。
  • element 对象是 HTML 文档中具体的元素对象,用于操作和控制每个单独的元素。
<!DOCTYPE html>

<head>
    <title>View HTML</title>

    <style type="text/css">
        /* css 元素样式 ID 选择器 */
        #container {
            background-color: blueviolet;
            width: 200px;
            height: 200px;
        }
    </style>

    <!-- type 属性是最为常用的指定脚本语言类型,目前还是 js 使用的最多 -->
    <script type="application/javascript" defer src="./defer.js"></script>

    <script type="application/javascript">
        console.log("嵌入到 HTML 中的 JS 执行了。");
    </script>
</head>

<body>
    <div id="container"></div>
    <!-- type 属性才是最为常见的  language 目前不常用的-->
    <script language="javascript" async src="./async.js"></script>
</body>

上面的为一个完整的 HTML 结构代码,在 HTML 中嵌入了 js 代码文件,分别以 3 种不同的方式引入的,准确来说是 2 种,第一种是直接在 HTML 编写 js 代码,而其他两种则是通过网络 js 文件引入的方式执行,这里浏览器会自动到指定网址下载对应 js 文件到浏览器内存中,这个过程可能是同步或者异步的得看 <script> 标签中的 asyncdefer 属性。

在一个完整的 HTML 文档中的所有节点和元素都是一个 Document Object Model ,DOM 分为很多种类型,<body> 标签对应的 HTMLBodyElement<table> 标签对应的是 HTMLTableElementImage 标签对应的是 HTMLImageElement<Video> 标签是 HTMLVideoElement 这些类型对应,基于这几个标签就可以组成一个完整的 HTML 页面。

客户端 HTML 中的加载流程如上图,客户端的 HTML 解析器会从头部开始解析 HTML 中的每个标签元素对象,如果碰到了一个 <script> 标签没有任何属性修饰则会导致 HTML 解析器停止加载剩下的元素对象,与此同时会使用 document.weite() 向输入流中插入对于的脚本文本,内容会编程 HTML 文件的内容的一部分,并且只能看到自己 <script> 标签内容之前的 HTML 文档内容,能放到的内容取决于自己当前出现位置的前段时间线;在全局 document 对象中会维护一个状态为 document.readyState ,此状态对应的是整个 HTML 页面加载的步骤状态。

当一个完整页面需要加载时,通过使用的 URL 进行访问,在浏览器地址栏中输入 URL 按下回车键页面数据即可被加载的到浏览器内存中,这时我们主动操作达到加载新文档流程。在浏览器中还提供一套用于编程使用的 API ,可以通过 API 来操作文档加载荷文档会退过程,让加载文档成为可编程操作,这里主要是用的是 window.locationdocument.location ,此处的 location 属性都是为 Location 对象,它可以表示当前页面文档的 URL ,通过操作这个 URL 即可达到操作整个页面窗口功能,这块唯一区别于的是 window.location.replacewindow.location.assign 方法,replace 会直接跳转 URL 栏并且清理掉记录,使用浏览器的回退按钮就不能起作用;而 assign 会保存当前浏览器加载的 URL 记录,并且可以回退返回到上一页,二者差距如下图:

不管是 window.location 任何方法,最终操作的浏览器加载新 URL 和 Document 时,都会重新刷新一次页面,导致页面被重新加载,可能部分资源浏览器存在缓存可以临时降低资源加载耗时,但是不会解决整个页面重新载入的问题。


Pjax 技术加载页面

在实际项目开发中大多数 HTML 页面的结构都是类似的,例如导航栏、菜单栏,这些功能的页面结构都是不变,而经常变化的是一些独立于导航栏和菜单栏的页面,它们往往都要各自的功能界面,所以能不能像 Ajax 一样通过局部界面数据刷新技术那样来实现页面局部加载?当在菜单中点击来一个按钮导航,那么对应区域会主动切换到对应界面上?现在一些主流的框架 Vue 和 React 都要相关的前端路由器实现,可以在不刷新页面 URL 情况达到这种效果,但是今天我要推荐是 Pjax 技术,Pjax 技术可以实现在不刷新整个页面情况下,来动态加载部分局部页面,然后再 Pjax 提供的 URL 重写功能来实现浏览器地址栏刷新工作,区别于传统的 Ajax 功能是 URL 地址会被重写,而传统 Ajax 针对是异步请求数据和操作 DOM 重绘。

要实现 Pjax 就必须要依赖于 HTML5 引入了 History API ,History 是方便开发者通过 API 去操作页面跳转使用的,其中包括 PushState API ,它允许你在不刷新整个页面的情况下修改浏览器的历史记录 、URL 以及页面内容,PushState API 对于实现单页应用(SPA)以及无刷新页面更新非常有用,API 使用案例如下:

history.pushState(state, title, url)

history.pushState() 函数需要传入三个参数,分别为 statetitleurl,作用如下如:

参数作用
state一个与指定 URL 相关的状态对象,可以包含任何你希望与 URL 关联的信息,这个状态对象在 PopState 事件中可以被访问到。
title一个可选的标题,目前大多数浏览器不会显示这个标题。
url这个 URL 可以是相对路径或绝对路径。

要实现 Pjax 更新页面的流程如下:

  1. 通过调用 history.pushState() 方法添加新的历史记录条目,并且页面内容保持不变。
  2. 再在自定义逻辑里面使用 Ajax 向服务器端获取或其他方法获取新的内容,这里的内容更多是 HTML 结构代码。
  3. 将新内容插入到页面中,如果需要更新页面的标题。
  4. 再使用 PopState 事件中处理 URL 的变化,加载对应的内容,当然 PopState 事件在浏览器前进和后退按钮被点击时触发,你可以通过监听这个事件来更新页面内容。

在下面这个例子中,我使用了 history.pushState + Ajax 来完成页面的更新操作,能达到效果是浏览器地址栏中的 URL 会主动更新,根据 a 标签的 href 属性来处理对应页面内容加载工作,能达成的效果和前端 MVVC 框架的静态路由器效果:

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Single Page Application</title>
</head>

<body>
    <nav>
        <ul>
            <li><a href="page1">Page 1</a></li>
            <li><a href="page2">Page 2</a></li>
            <li><a href="page3">Page 3</a></li>
        </ul>
    </nav>
    <div id="content">
        <!-- Ajax-loaded content will be inserted here -->
    </div>
    <script type="text/javascript">
        // 获取内容容器
        const contentContainer = document.getElementById('content');

        // 处理导航菜单链接的点击事件
        document.querySelectorAll('nav a').forEach(link => {
            link.addEventListener('click', function (event) {
                event.preventDefault(); // 阻止默认链接行为

                // 获取目标链接的URL
                const targetUrl = link.getAttribute('href');

                // 使用Ajax请求获取新的内容
                fetch(targetUrl)
                    .then(response => response.text())
                    .then(html => {
                        // 将新内容插入到内容容器中
                        contentContainer.innerHTML = html;

                        // 使用PushState更新URL,以便用户可以使用浏览器的前进/后退按钮
                        history.pushState({ page: targetUrl }, null, targetUrl);
                    })
                    .catch(error => {
                        console.error('Error fetching content:', error);
                    });
            });
        });

        // 监听PopState事件,以便在用户点击浏览器的前进/后退按钮时更新内容
        window.addEventListener('popstate', event => {
            const targetUrl = event.state.page;

            // 使用Ajax请求获取对应的内容
            fetch(targetUrl)
                .then(response => response.text())
                .then(html => {
                    // 更新内容容器的内容
                    contentContainer.innerHTML = html;
                })
                .catch(error => {
                    console.error('Error fetching content:', error);
                });
        });

    </script>
</body>

</html>

当这个页面被浏览器正确加载执行时就会达到我们想要的结果,效果如下图:

在这个例子中通过 fetch 函数异步请求网络数据,请求地址为 targetUrl ,最后使用 pushState 函数在多个页面记录中传递 targetUrl 状态信息, 使用 pushState 更新 URL ,以便用户可以使用浏览器的前进和后退按钮时能达到类似于正常操作 History API 来实现页面跳转功能,但是区别于页面只是更新了 URL 并没有进行全局页面刷新操作。


开源方案

在上的例子中我通过原生的 JS 和 history.pushStatefetch API 来实现的 PJAX 案例,在这个例子中大量编写了 JS 代码和 DOM 操作的代码,有没有简单的方式来实现 PJAX 功能呢?答案是有的,目前有很多开源解决方案,例如:MoOx/pjaxdefunkt/jquery-pjax 后者需要 JQuery 的支持。

在这里我会推荐使用 MoOx/pjax 这个库,因为它不依赖于其他第三方库,引入它本身即可使用 Pjax 功能,使用第三方的 CDN 引入对应的 JS 文件即可。在下面的例子中我直接使用 JSDelivr 公共 CDN 友情提供的地址,在页面的 <body> 内设置了一个容器 <div id="pjax-container"> ,它的内容将会在局部加载时被替换,而加载过程由 pjax 完成,代码入下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pjax Example</title>
    <!-- 引入第三方资源包 -->
    <script src="https://cdn.jsdelivr.net/npm/pjax/pjax.js"></script>
</head>

<body>
    <div id="pjax-container">
        <p>This is the initial content.</p>
    </div>

    <a href="new-page.html" data-pjax>Load New Page</a>

    <script>
        var pjax = new Pjax({
            selectors: ["#pjax-container"],
            cacheBust: false,
        });
    </script>
</body>

</html>

要使用 Pjax 功能,需要手动通过 js 代码创建一个 pjax 对象,其中 new Pjax() 的构造函数,需要传入一个对象参数,中对象的 selectors 至关重要,这里的 selectors 属性为需要动态刷新的页面 DOM 节点,参数和普通的 DOM 选择器一样适用,这里的使用的 ID 选择器,将 ID 为 #pjax-container 的元素作为 PJAX 数据刷新的容器,其他页面部位不进行更改。其中的 <a> 标签作为触发页面刷新的链接,其中的 data-pjax 属性是在 MoOx/pjax 库中规定的,用于标记链接的自定义数据属性,当一个 <a> 具有 data-pjax 属性时,它会告诉 MoOx/pjax 库这个链接应该以局部加载的方式进行处理,而不是进行传统的页面刷新。

需要动态刷新的部分是 new-page.html 页面的内容,这里内容如下,同样有一个 ID 为 pjax-container 元素,这样 MoOx/pjax 会主动将请求到的页面数据加载到 pjax-container 中,使得我们看到效果是浏览器地址栏路由和页面都更新了的错觉,本质上只是发送一个异步请求数据:

<div id="pjax-container">
    <p>This is the content of the new page.</p>
</div>

最终实现出来的效果为下图:

第三方的 PJAX 库它会在局部加载时被更新,而不会影响页面的其他部分,从而达到不重复加载页面资源的效果,部分页面固定则无需被刷新,MoOx/pjax 库会自动封装一个 XMLHttpRequest 请求头,请求头中 Header 会包含两个字段,分别为:

请求头作用说明
X-Pjax当值为 true 时,表示请求是一个 PJAX 请求,即服务器端通过 PJAX 方式来处理请求。
X-Pjax-Selectors值为一个选择器字符串数组,表示当前页面哪些部分需要从服务器端获取,最后内容在页面部分区域中更新。

通过 Chrome 浏览器中的 DevTools 的网络抓包工具可以查看到对应的请求头,截图如下:

在 Web 开发前后端没有分离的年代,PJAX 技术是很流行的,现在也有一些动态网站也还在使用此类技术,历史上知名的 Facebook 和 Twitter 网站都曾经使用过 PJAX 来实现局部页面更新刷新操作,缺点很明显服务器端要配合前端做对应 PJAX 请求处理;现在很多网站已经实行了前后端分离的架构,更多的是使用 MVVM 这类的框架实现,网站静态资源被加载之后采用的是前端路由和异步请求后端 JSON 数据渲染的方式进行。


其他资料

便宜 VPS vultr
最后修改:2023 年 09 月 05 日
如果觉得我的文章对你有用,请随意赞赏 🌹 谢谢 !