URLPattern 现在已经在所有浏览器中可用了!所以我想深入研究一下,看看如何使用原生 JavaScript 和浏览器 API 制作一个简单的 SPA 路由器。我们应该能够创建一个组件,它接受路由器配置,并根据浏览器 URL 渲染相应的组件。
URLPattern() 是做什么的?📎
对于路由器来说,条件渲染组件并不是困难的部分。困难的是准确测试浏览器 URL,以确定应该渲染哪个组件。而且不仅如此,我们还需要能够捕获路由的动态部分(比如像 /posts/{post_id} 这样的路径)。
不多说,让我们来看一些示例,展示如何测试路由 URL 是否匹配某个模式!然后你可以使用这个机制来创建一个具有易于配置路径的路由器。
javascript
const catUrlPattern = new URLPattern({ pathname: "/cat" });
catUrlPattern.test("http://www.jschof.dev/cat"); // True!
catUrlPattern.test("http://www.jschof.dev/dog"); // False!
catUrlPattern.test({ pathname: "/cat" }); // True!
catUrlPattern.test("http://www.jschof.dev/cat/"); // False!
catUrlPattern.test("http://www.jschof.dev/cat/other-things?yes"); // False!
你可能会对上面的第四个例子感到惊讶。/cat 和 /cat/ 是有区别的。所以为了处理这种情况,你可以在一个用大括号括起来的组中使模式可选地包含一个结尾斜杠,并使用 ?: 标记它为可选:
javascript
const catUrlPattern = new URLPattern({ pathname: "/cat{/}?" });
catUrlPattern.test("http://www.jschof.dev/cat"); // True!
catUrlPattern.test({ pathname: "/cat/" }); // True!
catUrlPattern.test("http://www.jschof.dev/cat/"); // True!
catUrlPattern.test("http://www.jschof.dev/cat/other-things?yes"); // False!
又一个惊喜!你可能期望可以接受 /cat/ 后面的更多内容。要做到这一点,需要包含一个通配符星号:
javascript
const catUrlPattern = new URLPattern({ pathname: "/cat{/}?*" });
catUrlPattern.test("http://www.jschof.dev/cat"); // True!
catUrlPattern.test({ pathname: "/cat/" }); // True!
catUrlPattern.test("http://www.jschof.dev/cat/"); // True!
catUrlPattern.test("http://www.jschof.dev/cat/other-things?yes"); // True!
我们从哪里开始?📎
我将使用一个配置对象数组,将 URL 路由与特定的 Web 组件关联起来。这与你使用 vue-router 创建路由器的方式非常相似。
javascript
const routerConfig = [
{ pathName: new URLPattern("/home{/}?"), component: "my-home" },
{ pathName: new URLPattern("/posts{/}?"), component: "my-posts" },
{ pathName: new URLPattern("/about{/}?"), component: "my-about" },
];
配置对象的顺序很重要。我们会逐个测试每个模式,如果找到匹配项,就渲染那个 Web 组件。
javascript
for (const config of routerConfig) {
if (config.pathName.test(window.location.href)) {
// 渲染 config.component!
return;
}
}
// TODO: 处理 404!
如何进行渲染呢?这将是我们放置所有这些逻辑的 Web 组件的工作。该组件会查看当前窗口 URL,将其与我们用 URLPattern 设置的所有路由器配置进行测试,然后创建并渲染适当的 Web 组件作为子元素。
有些框架称这个路由器为"出口"组件。
javascript
const routerConfig = [
{ pathName: new URLPattern("/home{/}?"), component: "my-home" },
{ pathName: new URLPattern("/posts{/}?"), component: "my-posts" },
{ pathName: new URLPattern("/about{/}?"), component: "my-about" },
];
class MyRouter extends HTMLElement {
constructor() {
super();
const matchedComponent = this.getRouteMatch();
this.renderComponent(matchedComponent);
}
getRouteMatch() {
for (const config of routerConfig) {
if (config.pathName.test(window.location.href)) {
return config.component;
}
}
// TODO: 处理 404!
}
renderComponent(component) {
this.innerHTML = "";
const viewElement = document.createElement(component);
this.appendChild(viewElement);
}
}
customElements.define("my-router", MyRouter);
当然,你还需要注册 my-home、my-posts 和 my-about 这些 Web 组件。
这样我们就有了一个路由器,它会在页面加载时渲染适当的 Web 组件。不过我们还有很多工作要做。如果有人点击链接怎么办?如果有人使用浏览器导航前进或后退怎么办?我们需要处理这些情况,幸运的是这并不太困难。
处理 SPA 导航和链接点击 📎
有一点需要意识到的是,如果你导航到 http://www.myblog.com/some/path,服务器通常会尝试在后端解析 /some/path。它实际上可能会寻找 "some" 和 "path" 文件夹。但在 SPA 中,我们没有文件夹------我们只有一个处理虚拟路径的 index HTML 文件。所有这些都在客户端用 JS 完成!无论我们要到哪个路径,服务器实际上只需要提供 index 页面。然后客户端会接管,使用我们的新 URLPattern 并处理渲染适当的组件。
对于配置 Vite 来说,这非常简单。你可以使用一个名为 spa 的配置。只需更新你的 vite 配置:
javascript
import { defineConfig } from "vite";
export default defineConfig({
appType: "spa",
});
这对于你的 Vite 本地服务器来说效果很好。但不幸的是,这将取决于你部署的位置以及你使用的其他框架/开发服务器。对于像 netlify 这样的地方,你需要在你的 netlify 配置中设置重定向规则。你可能需要查阅 Stack Overflow、Google 或你选择的 LLM 来了解如何针对你的特定情况执行此操作。
但一旦你设置好服务器配置和重定向,我们就可以开始处理点击事件了!
我们基本上想拦截任何链接点击,并阻止浏览器正常导航。这意味着我们要对所有点击事件调用 preventDefault(),并提取链接的目标来与我们的 URL 模式进行测试。这让我们知道应该渲染哪个组件,然后我们手动将 URL 设置为锚标签指向的内容。看起来我们正在更改页面,但实际上我们是在模拟页面过渡。
我们需要在路由器组件连接到 DOM 时设置一个点击处理器:
javascript
class MyRouter extends HTMLElement {
//...
connectedCallback() {
window.addEventListener("click", this.handleClicks);
}
handleClicks = (event) => {
if (event.target instanceof HTMLAnchorElement) {
// 不要像通常那样处理这个链接!
event.preventDefault();
// 手动设置 URL
const toUrl = event.target.getAttribute("href");
window.history.pushState({}, "", toUrl);
// 现在 URL 已经设置好了,执行通常的匹配和渲染!
const matchedComponent = this.getRouteMatch();
this.renderComponent(matchedComponent);
}
};
disconnectedCallback() {
window.removeEventListener("click", this.handleClicks);
}
}
当然,确保你在 disconnectedCallback 中清理这些处理器!
当用户点击链接时,他们会看到 URL 改变,页面过渡,甚至浏览器导航历史中会有一个新条目。现在,我们需要确保浏览器不会实际前进或后退,而是在点击前进/后退按钮时钩子到我们的路由器。
注意:在上面的例子中,我们拦截了所有锚标签上的点击,无论点击是在我们的路由器组件内部还是外部。你可能只想处理这个路由器组件内部的点击,这样你就不会承担整个页面上链接的责任。
另外,请注意,我们甚至还没有触及如何处理外部链接。这需要特别注意,但这超出了这篇博客文章的范围。
最后一个细节:浏览器导航 📎
当浏览器前进或后退时(无论是通过编程方式 window.back()/forward() 还是用户点击前进/后退按钮),都会触发一个 popstate 事件。
这个事件的好处是,当我们使用 window.pushState 时,浏览器已经在历史栈中前后移动到了我们推送的条目。换句话说,我们只需要监听这种后退/前进导航何时发生,并渲染我们的组件。在按下后退/前进按钮后,URL 已经更新了。
这是我们最小可行路由器的最后一部分:
javascript
class MyRouter extends HTMLElement {
//...
connectedCallback() {
window.addEventListener("click", this.handleClicks);
window.addEventListener("popstate", this.handlePopState);
}
handlePopState = (event) => {
const matchedComponent = this.getRouteMatch();
this.renderComponent(matchedComponent);
};
disconnectedCallback() {
window.removeEventListener("click", this.handleClicks);
window.removeEventListener("popstate", this.handlePopState);
}
}
一个工作示例 📎
如果你有兴趣看到它的工作原理,这里有一个在 StackBlitz 上构建的示例。
还有更多工作要做!以下是我建议研究的事情:
- 创建动态段,如
/posts/:id。这里有一些关于处理动态参数的文档 - 使用
search处理查询参数 - 处理嵌套或子路由器
需要记住的事情... 📎
我们正在处理相当底层的东西。像这样创建自己的路由器渲染器可能会让你暴露在 XSS 攻击中,如果你留下了让人们破坏你的路由器配置的方法。例如,如果你让人们直接在路由器组件上设置配置数组,有人可能会在控制台中注册他们自己的 Web 组件,并导航到他们的组件来渲染它。实际上,你将允许其他方在你的路由器中运行他们的代码!
这就是为什么,至少在我们讨论的例子中,路由器配置是在我们的路由器组件中的一个私有变量中定义的。如果我们公开这个属性,有人可以注册他们自己的 Web 组件并让我们的路由器渲染它。
我们如何解决这个问题?我想说,永远不要纯粹基于 URL 的查询参数或动态段进行渲染。始终将实际渲染的 Web 组件保存在静态列表中------可能是路由器上的私有变量中的列表。
另一件事------我们应该用 Web 组件构建路由器吗?嗯...也许不应该?Lit 似乎认为这会有帮助和有用。但你自己实现的路由器需要处理很多框架路由器已经解决的问题。Web 组件还增加了另一个你需要注意的安全级别。
我认为这值得研究和学习。而且了解不断推出的原生 API 也很好,这些 API 让我们在依赖平台时的生活更轻松。