使用原生 JavaScript 进行虚拟 URL 导航
我在一个 GitHub 项目中遇到了一个问题,维护者希望动态地更改页面内容,使其感觉像是一个“原生”页面,而无需实际创建另一个 HTML 文件。
他还希望他的网站只使用原生 JS 和 HTML 文件,因此向他介绍 Angular、Svelte 或任何其他框架都不现实。
幸运的是,我发现 HTML5 中引入了一个有趣的 API,可以完美地解决这类问题。
让我们一步一步地来探讨这个问题。
设置
为了举例说明,我们将构建一个非常简单的页面,以模拟使用某种路由功能。
首先,我们将创建一个简单的HTML页面:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Fake URL navigation</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<h1>Fake routing</h1>
<button id="details">Show details</button>
</body>
</html>
目前来看,这并没有造成太大影响。
更改网址
我们现在希望每当用户点击按钮时都更改 URL。
为此,我们可以使用pushState函数以编程方式向历史记录添加新条目,这也会导致 URL 被修改。
它的使用方法相当简单,由三个参数组成:
- 将通过 URL 推送的状态,可以是轻量级的 JS 对象,也可以是某种结构化数据。
- 由于向后兼容性,这是一个未使用的必填参数,我们可以将其替换为空字符串。
- 我们想要推送的实际 URL
我们来添加一小段脚本来修改它:
<script>
document.getElementById("details").onclick = (_event) => history.pushState({}, "", "/details");
</script>
然后测试一下:
太好了!我们现在可以专注于内容了。
更新页面
为了更新我们的内容,我们需要某种模板,并将当前的 HTML 正文替换为该模板。
使用原生 API,这非常简单:
<script>
+ const template = "<h1>Details</h1> <p>Some highly useful information</p>";
document.getElementById("details").onclick = (_event) => {
history.pushState({}, "", "/details");
+ document.body.innerHTML = template;
};
</script>
我们现在有了自己的网页!
修复返回按钮
按照我们目前的解决方案,一旦显示详情页,就无法在不重新加载整个页面的情况下再次显示索引页。
为了解决这个问题,我们可以先创建一个本地历史记录,方法是在删除 body 元素之前,将其内容推入堆栈:
<script>
const template = "<h1>Details</h1> <p>Some highly useful information</p>";
+ const bodyHistory = [];
document.getElementById("details").onclick = (_event) => {
history.pushState({}, "", "/details");
+ bodyHistory.push(document.body.innerHTML);
document.body.innerHTML = template;
};
</script>
然后,我们可以使用与以下事件互为镜像的事件pushState:popstate。
通过监听,我们就能知道何时按下了后退按钮,从而能够恢复页面之前的显示内容:
<script>
const template = "<h1>Details</h1> <p>Some highly useful information</p>";
const bodyHistory = [];
document.getElementById("details").onclick = (_event) => {
history.pushState({}, "", "/details");
bodyHistory.push(document.body.innerHTML);
document.body.innerHTML = template;
};
+ onpopstate = (_event) => {
+ const previousContent = bodyHistory.pop();
+
+ if (previousContent) {
+ document.body.innerHTML = previousContent;
+ }
+ };
</script>
此时,虽然可以回溯历史记录,但我们将无法再次导航,因为我们恢复了整个页面,但事件监听器尚未重新设置。
onpopstate一个快速的解决方法是,按照我们最初添加此逻辑时的方式,在我们的事件处理程序中重新注册它。
让我们把这个逻辑提取到一个专门的函数中,并称之为:
<script>
const template = "<h1>Details</h1> <p>Some highly useful information</p>";
const bodyHistory = [];
+ registerHandler();
+ function registerHandler() {
+ document.getElementById("details").onclick = (_event) => {
+ history.pushState({}, "", "/details");
+
+ bodyHistory.push(document.body.innerHTML);
+ document.body.innerHTML = template;
+ };
+ };
onpopstate = (_event) => {
const previousContent = bodyHistory.pop();
if (previousContent) {
document.body.innerHTML = previousContent;
+ registerHandler();
}
};
</script>
我们完成了!
在这篇短文中,我们看到了如何利用这对组合pushState/popState给用户一种浏览不同页面的感觉。
这是对某种虚拟导航的简单介绍,但就我而言,它帮助我成功地将其应用到这位维护者的项目中。
但是请注意,当用户访问的 URL 与实际路由不匹配时,重新加载页面将导致 HTTP 404 错误。
...#details一种解决方案是用哈希值(例如 `<h1> ` 而不是 `<h2> `)替换指向虚拟页面的导航.../details。这样,浏览器在重新加载时仍然会渲染根页面,而我们可以拦截哈希值后面的内容,并将其用于渲染详情页面。
希望你在那里学到了有用的东西!
演示中使用的所有源代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Fake URL navigation</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<h1>Fake routing</h1>
<button id="details">Show details</button>
</body>
<script>
const template = "<h1>Details</h1> <p>Some highly useful information</p>";
const bodyHistory = [];
registerHandler();
function registerHandler() {
document.getElementById("details").onclick = (_event) => {
history.pushState({}, "", "/details");
bodyHistory.push(document.body.innerHTML);
document.body.innerHTML = template;
};
}
onpopstate = (_event) => {
const previousContent = bodyHistory.pop();
if (previousContent) {
document.body.innerHTML = previousContent;
registerHandler();
}
};
</script>
</html>