我看到一篇关于如何在纯 Vanilla JavaScript 中创建路由器的帖子。由于它不是在讨论散列路由,因此我决定创建这篇文章来与您分享我的知识。
我为什么要这么做?
多亏了 History API,在 JavaScript 中构建路由器现在变得很简单。它在很大程度上得到支持,让您可以按照自己的方式构建路由器,而无需依赖第三方库。在 Vue.js 中,您甚至可以构建自己的自制路由器并将其插入,这要归功于 Vue.js 插件。事实上,Vue.js 的官方路由器库 Vue Router 依赖于 History API。但不仅如此。因为它可以选择让您构建所谓的散列路由器。
什么是散列路由器?
它是一个不依赖 History API 而是依赖于您网站的哈希 URL 的路由器。假设您的网页上有一个标题。
<h2>About me</h2>
<p>Lorem ipsum dolor sit amet...</p>
并且您希望您的用户在页面顶部时直接跳转到此部分。你会想id
为你的标题使用一个属性。
<h2 id="about">About me</h2>
并创建一个链接,例如在您的标题中,将您的用户重定向到此部分。
<header>
<a href="#about">About me</a>
</header>
现在,如果您单击链接,URL 应该来自
http://yoursite.com
到
http://yoursite.com#about
没什么花哨的,对吧?
为什么我要使用散列路由器?
基于历史 API 的路由器的问题在于它依赖于页面的来源来工作。如果您尝试打开实现 History API 的 HTML 页面,您应该会看到如下内容:
Failed to execute 'pushState' on 'History': A history state object with URL 'file:///path/to/index.html' cannot be created in a document with origin 'null'
这是因为默认情况下,作为文件打开的 HTML 文档的来源设置为null
. 这是个问题。
相反,基于 hased 的路由器不依赖于它,而是依赖于窗口对象触发的事件。当我们更改 url 的哈希时,将触发此事件。在我们之前的示例中,单击链接会触发此事件。不需要网络服务器。
如何实现散列路由器?
它就像只使用一个事件一样简单。事件onHashChange。
<!DOCTYPE html>
<html>
<body>
<a href="#home">Home</a>
<a href="#about">About</a>
<script src="script.js"></script>
</body>
</html>
function onRouteChanged() {
console.log("Hash changed!");
}
window.addEventListener("hashchange", onRouteChanged);
Hash changed!
Hash changed!
在线尝试。
实现路由
我们现在需要获取用户发出的路由。我们可以使用该window.location.hash属性来获取当前“路线”的值。
function onRouteChanged() {
console.log(window.location.hash);
}
#home
#about
在线尝试。
我们现在拥有我们需要的一切。我们可以开始为我们的路由器实现视图渲染器。
<a href="#about">About</a>
<a href="#contact">Contact</a>
<main id="router-view"></main>
<script src="script.js"></script>
我还添加了另一个链接。这将帮助我向您展示我们如何实现404 - page not found
处理程序。你会惊讶于它是多么容易。
接下来,我们需要向我们的处理程序添加更多逻辑,onRouteChange
以便它可以像路由器一样呈现我们的路由。
function onRouteChanged() {
const hash = window.location.hash;
const routerView = document.getElementById("router-view");
if (!(routerView instanceof HTMLElement)) {
throw new ReferenceError("No router view element available for rendering");
}
switch (hash) {
case "#home":
routerView.innerHTML = "<h1>Home page</h1>";
break;
case "#about":
routerView.innerHTML = "<h1>About page</h1>";
break;
default:
routerView.innerHTML = "<h1>404 - Page Not Found</h1>";
break;
}
}
在线尝试。
我将哈希 URL 存储在一个变量中,以便我可以使用该switch
语句根据用户发出的路由呈现不同的 HTML 内容。我还存储了路由器视图元素以检查该元素是否确实在文档中(我们永远不知道会发生什么,在这种情况下有一些雄辩的错误消息会很好)。我还需要它来更新语句innerHTML
中路由器的内容。switch
switch 的默认语句将通过我们的联系链接触发,因为我们没有在 switch 中为它指定任何处理程序。
而已!您有一个非常基本的路由器,可以在任何地方工作,无论是托管在 Web 服务器上,还是作为单个 HTML 页面共享。例如,当您需要向客户展示网站的快速原型时,我可以看到一些用例。他所要做的就是在他的浏览器中打开页面,然后tada!
限制
当然,这种路由有一个明显的限制,因为我们使用的是 URL 的哈希值,并破解了它的初衷,将其用作路由器。但是如果我们需要在我们的页面中使用常规的hrefs,它只会破坏路由,因为它会触发我们的哈希更改处理程序。
解决方案
我为这个问题找到的一个解决方案可能不是最好的,但如果你绝对需要使用基于散列的路由,那么它是值得的,它是使用data-*
属性和一点 JavaScript。
<button data-go-to-id="a-little-introduction">to the intro</button>
<!-- later in the document -->
<h2 id="a-little-introduction>A little introduction</h2>
"use strict";
document.querySelectorAll("[data-go-to-id]").forEach(function(link) {
link.addEventListener("click", function() {
const element = document.getElementById(link.dataset.goToId);
if (!(element instanceof HTMLElement)) {
throw new ReferenceError(`Unable to found element with id "${goToId}"`);
}
window.scroll({
top: element.getBoundingClientRect().top,
left: 0,
behavior: "smooth"
});
});
});
某些设备上的平滑滚动不起作用(我特别想到了一些 Apple 设备),但我敢肯定,这将是您可以找到解决此问题的许多解决方案之一。我的解决方案的缺点是它不能用于共享链接,如Hey, look what I found here: http://yoursite.com#home#title-of-article
. 我将此作为练习,供读者实施更好的解决方案。
结论
基于哈希的路由器是另一种无需重新加载页面即可路由用户的方式。这在创建 GitHub 页面时也很方便,因为我们不必重新考虑基于历史记录的路由器并在所有路由前加上/github-repo/about
.
如果您不需要使用大量的 href 重定向并且不想/不能使用 History API,那么在您的页面中使用路由器可能是一个很好的解决方案。
我向您展示的是基于 hased 的路由器的一个非常基本的实现。如果你想更进一步,你可以:
- 在一个对象中实现这个路由器,就像
new HashedRouter
使 API 更易于使用一样。addRoute
特别是使用和之类的方法start
。 - 找到比我用来在页面中实现链接更好的解决方案。
'use strict';
function onRouteChanged() {
const hash = window.location.hash,
routerView = document.querySelector('#router-view');
if (!(routerView instanceof(HTMLElement))) {
throw new ReferenceError('No router view element available for rendering');
}
switch (hash) {
case '#home':
routerView.innerHTML = '<object type="text/html" data="views/home.html"></object>';
break;
case '#about':
routerView.innerHTML = '<object type="text/html" data="views/about.html"></object>';
break;
default:
routerView.innerHTML = '<object type="text/html" data="views/404.html"></object>';
break;
}
//console.log(hash);
}
window.addEventListener('hashchange', onRouteChanged);