首页 » SEO优化 » phphistoryback技巧_从理解路由到实现一套Router路由

phphistoryback技巧_从理解路由到实现一套Router路由

访客 2024-12-13 0

扫一扫用手机浏览

文章目录 [+]

原文:https://juejin.cn/post/7150794643985137695

如有侵权,联系删除

phphistoryback技巧_从理解路由到实现一套Router路由

平时在Vue项目中常常用到路由,但是也仅仅处于会用的层面,很多根本知识并不是真正的理解。
于是就查阅了很多资料,总结下路由干系的知识,查缺不漏,加深自己对路由的理解。

phphistoryback技巧_从理解路由到实现一套Router路由
(图片来自网络侵删)

路由

在 Web 开拓过程中,常常碰着路由的观点。
那么到底什么是路由呢?大略来说,路由便是 URL 到函数的映射。

路由这个观点本来是由后端提出来的,在以前用模板引擎开拓页面的时候,是利用路由返回不同的页面,大致流程是这样的:

浏览器发出要求;做事器监听到 80 或者 443 端口有要求过来,并解析 UR L路径;做事端根据路由设置,查询相应的资源,可能是 html 文件,也可能是图片资源......,然后将这些资源处理并返回给浏览器;浏览器吸收到数据,通过content-type决定如何解析数据

大略来说,路由便是用来跟后端做事器交互的一种办法,通过不同的路径来要求不同的资源,要求HTML页面只是路由的个中一项功能。

做事端路由

当做事端吸收到客户端发来的 HTTP 要求时,会根据要求的 URL,找到相应的映射函数,然后实行该函数,并将函数的返回值发送给客户端。

对付最大略的静态资源做事器,可以认为,所有 URL 的映射函数便是一个文件读取操作。
对付动态资源,映射函数可能是一个数据库读取操作,也可能进行一些数据处理,等等。

客户端路由

做事端路由会造成做事器压力比较大,而且用户访问速率也比较慢。
在这种情形下,涌现了单页运用。

单页运用,便是只有一个页面,用户访问网址,做事器返回的页面始终只有一个,不管用户改变了浏览器地址栏的内容或者在页面发生了跳转,做事器不会重新返回新的页面,而是通过相应的js操作来实现页面的变动。

前端路由实在便是:通过地址栏内容的改变,显示不同的页面。

前端路由的优点:

前端路由可以让前端自己掩护路由与页面展示的逻辑,每次页面改动不须要关照做事端。
更好的交互体验:不用每次从做事端拉取资源。

前端路由的缺陷: 利用浏览器的提高、退却撤退键时会重新发送要求,来获取数据,没有合理利用缓存。

前端路由实现事理: 实质便是监测 URL 的变革,通过拦截 URL 然后解析匹配路由规则。

前端路由的实现办法

hash模式(location.hash + hashchange 事宜)

hash 模式的实现办法便是通过监听 URL 中的 hash 部分的变革,触发haschange事宜,页面做出不同的相应。
但是 hash 模式下,URL 中会带有 #,不太都雅。

history模式

history 路由模式的实现,基于 HTML5 供应的 History 全局工具,它的方法有:

history.go():在会话历史中向前或者向后移动指定页数history.forward():在会话历史中向前移动一页,跟浏览器的提高按钮功能相同history.back():在会话历史记录中向后移动一页,跟浏览器的后腿按钮功能相同history.pushState():向当前浏览器会话的历史堆栈中添加一个状态,会改变当前页面url,但是不会伴随这刷新history.replaceState():将当前的会话页面的url更换成指定的数据,replaceState 会改变当前页面的url,但也不会刷新页面window.onpopstate:当前活动历史记录条款变动时,将触发popstate事宜

history路由的实现,紧张是依赖pushState、replaceState和window.onpopstate实现的。
但是有几点要把稳:

当活动历史记录条款变动时,将触发 popstate 事宜;调用history.pushState()或history.replaceState()不会触发 popstate 事宜popstate 事宜只会在浏览器某些行为下触发,比如:点击退却撤退、提高按钮(或者在 JavaScript 中调用history.back()、history.forward()、history.go()方法)a 标签的锚点也会触发该事宜

对 pushState 和 replaceState 行为的监听

如果想监听 pushState 和 replaceState 行为,可以通过在方法里面主动去触发 popstate 事宜,另一种是重写history.pushState,通过创建自己的eventedPushState自定义事宜,并手动派发,实际利用过程中就可以监听了。
详细做法如下:

function eventedPushState(state, title, url) { var pushChangeEvent = new CustomEvent("onpushstate", { detail: { state, title, url } }); document.dispatchEvent(pushChangeEvent); return history.pushState(state, title, url);}document.addEventListener( "onpushstate", function(event) { console.log(event.detail); }, false);eventedPushState({}, "", "new-slug"); 复制代码

router 和 route 的差异

route 便是一条路由,它将一个 URL 路径和一个函数进行映射。
而 router 可以理解为一个容器,或者说一种机制,它管理了一组 route。

概括为:route 只是进行了 URL 和函数的映射,在当吸收到一个 URL 后,须要去路由映射表中查找相应的函数,这个过程是由 router 来处理的。

动态路由和静态路由

静态路由

静态路由只支持基于地址的全匹配。

动态路由

动态路由除了可以兼容全匹配外还支持多种”高等匹配模式“,它的路径地址中含有路径参数,使得它可以按照给定的匹配模式将符合条件的一个或多个地址映射到同一个组件上。

动态路由一样平常结合角色权限掌握利用。

动态路由的存储有两种办法:

将路由存储到前端将路由存储到数据库

动态路由的好处:

灵巧,无需手动掩护存储到数据库,增加安全性

实现一个路由

一个大略的Router该当具备哪些功能

以 Vue为例,须要有 <router-link>链接、<router-view>容器、component组件和path路由路径:

<div id="app"> <h1>Hello World</h1> <p> <!-- 利用 router-link 组件进行导航 --> <!-- 通过通报 to 来指定链接 --> <!-- <router-link> 将呈现一个带有精确 href属性的<a>标签 --> <router-link to="/">Go to Home</router-link> <router-link to="/about">Go to About</router-link> </p> <!-- 路由出口 --> <!-- 路由匹配到的组件将渲染在这里 --> <router-view></router-view></div>复制代码

const routes = [{ path: '/', component: Home},{ path: '/about', component: About}]复制代码

以React为例,须要有<BrowserRouter>容器、<Route>路由、组件和链接:

<BrowserRouter> <Routes> <Route path="/" element={<App />}> <Route index element={<Home />} /> <Route path="teams" element={<Teams />}> <Route path=":teamId" element={<Team />} /> <Route path="new" element={<NewTeamForm />} /> <Route index element={<LeagueStandings />} /> </Route> </Route> </Routes></BrowserRouter>复制代码

<div> <h1>Home</h1> <nav> <Link to="/">Home</Link> | {""} <Link to="about">About</Link> </nav></div>复制代码

综上,一个大略的 Router 该当具备以下功能:容器(组件)路由业务组件 & 链接组件

不借助第三方工具库,如何实现路由

不借助第三方工具库实现路由,我们须要思考以下几个问题:

如何实现自定义标签,如vue的<router-view>,React的<Router>如何实现业务组件如何动态切换路由

准备事情

根据对前端路由 history 模式的理解,将大致过程用如下流程图表示:

1.jpeg

如果不借助第三方库,我们选择利用 Web components 。
Web Components由三项紧张技能组成,它们可以一起利用来创建封装功能的定制元素。
Custom elements(自定义元素) :一组JavaScript API,许可我们定义 custom elements及其行为,然后可以在界面按照须要利用它们。
Shadow DOM(影子DOM) :一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档分开呈现)并掌握关联的功能。
通过这种办法,可以保持元素的功能私有。
HTML template(HTML模版) :<template>和<slot>可以编写不在页面显示的标记模板,然后它们可以作为自定义元素构造的根本被多次重用。

其余还须要把稳 Web Components 的生命周期:

connectedCallback:当 custom element 首次被插入文档DOM时,被调用

disconnectedCallback:当 custom element 从文档DOM中删除时,被调用

adoptedCallback:当custom element 被移动到新的文档时,被调用

attributeChangedCallback:当 custom element 增加、删除、修正自身属性时,被调用

Shadow DOMShadow DOM 特有的术语:- Shadow host:一个常规DOM节点,Shadow DOM 会被附加到这个节点上 - Shadow tree:Shadow DOM 内部的 DOM 树 - Shadow boundary:Shadow DOM 结束的地方,也是常规DOM开始的地方 - Shadow root:Shadow tree 的根节点Shadow DOM的主要参数mode:- open:shadow root 元素可以从 js 外部访问根节点 - close :谢绝从 js 外部访问关闭的 shadow root 节点 - 语法:`const shadow = this.attachShadow({mode:closed});`通过自定义标签创建容器组件、路由、业务组件和链接组件标签,利用

CustomElementRegistry.define()注册自定义元素。
个中,Custom elements 的大略写法举例:

<my-text></my-text><script> class MyText extends HTMLElement{ constructor(){ super(); this.append(“我的文本”); } } window.customElements.define("my-text",MyText);</script>复制代码

组件的实现可以利用 Web Components,但是这样有缺点,我们没有打包引擎处理 Web Components组件,将其全部加载过来。

为理解决以上问题,我们选择动态加载,远程去加载一个 html 文件。
html文件里面的构造如下:支持模版(template),脚本(template),脚本(script),样式(style),非常地像vue。
组件开拓模版如下:

<template> <div>商品详情</div> <div id="detail"> 商品ID:<span id="product-id" class="product-id"></span> </div></template><script> this.querySelector("#product-id").textContent = history.state.id;</script><style> .product-id{ color:red; }</style>复制代码

监听路由的变革:

popstate可以监听大部分路由变革的场景,除了pushState 和 replaceState。

pushState 和 replaceState可以改变路由,改变历史记录,但是不能触发popstate事宜,须要自定义事宜并手动触发自定义事宜,做出相应。

整体架构图如下:

8. 组件功能拆剖解析如下:

链接组件 — CustomLink(c-link)

当用户点击<c-link>标签后,通过event.preventDefault();阻挡页面默认跳转。
根据当前标签的to属性获取路由,通过history.pushState("","",to)进行路由切换。

// <c-link to="/" class="c-link">首页</c-link>class CustomLink extends HTMLElement { connectedCallback() { this.addEventListener("click", ev => { ev.preventDefault(); const to = this.getAttribute("to"); // 更新浏览器历史记录 history.pushState("", "", to) }) }}window.customElements.define("c-link", CustomLink);复制代码

容器组件 — CustomRouter(c-router)

紧张是网络路由信息,监听路由信息的变革,然后加载对应的组件

路由 — CustomRoute(c-route)

紧张是供应配置信息,对外供应getData 的方法

// 优先于c-router注册// <c-route path="/" component="home" default></c-route>class CustomRoute extends HTMLElement { #data = null; getData() { return { default: this.hasAttribute("default"), path: this.getAttribute("path"), component: this.getAttribute("component") } }}window.customElements.define("c-route", CustomRoute);复制代码

业务组件 — CustomComponent(c-component)

实现组件,动态加载远程的html,并解析

完全代码实现

index.html:

<div class="product-item">测试的产品</div><div class="flex"> <ul class="menu-x"> <c-link to="/" class="c-link">首页</c-link> <c-link to="/about" class="c-link">关于</c-link> </ul></div><div> <c-router> <c-route path="/" component="home" default></c-route> <c-route path="/detail/:id" component="detail"></c-route> <c-route path="/about" component="about"></c-route> </c-router></div><script src="./router.js"></script>复制代码

home.html:

<template> <div>商品清单</div> <div id="product-list"> <div> <a data-id="10" class="product-item c-link">喷鼻香蕉</a> </div> <div> <a data-id="11" class="product-item c-link">苹果</a> </div> <div> <a data-id="12" class="product-item c-link">葡萄</a> </div> </div></template><script> let container = this.querySelector("#product-list"); // 触发历史更新 // 事宜代理 container.addEventListener("click", function (ev) { console.log("item clicked"); if (ev.target.classList.contains("product-item")) { const id = +ev.target.dataset.id; history.pushState({ id }, "", `/detail/${id}`) } })</script><style> .product-item { cursor: pointer; color: blue; }</style>复制代码

detail.html:

<template> <div>商品详情</div> <div id="detail"> 商品ID:<span id="product-id" class="product-id"></span> </div></template><script> this.querySelector("#product-id").textContent=history.state.id;</script><style> .product-id{ color:red; }</style>复制代码

about.html:

<template> About Me!</template>复制代码

route.js:

const oriPushState = history.pushState;// 重写pushStatehistory.pushState = function (state, title, url) { // 触发原事宜 oriPushState.apply(history, [state, title, url]); // 自定义事宜 var event = new CustomEvent("c-popstate", { detail: { state, title, url } }); window.dispatchEvent(event);}// <c-link to="/" class="c-link">首页</c-link>class CustomLink extends HTMLElement { connectedCallback() { this.addEventListener("click", ev => { ev.preventDefault(); const to = this.getAttribute("to"); // 更新浏览历史记录 history.pushState("", "", to); }) }}window.customElements.define("c-link", CustomLink);// 优先于c-router注册// <c-toute path="/" component="home" default></c-toute>class CustomRoute extends HTMLElement { #data = null; getData() { return { default: this.hasAttribute("default"), path: this.getAttribute("path"), component: this.getAttribute("component") } }}window.customElements.define("c-route", CustomRoute);// 容器组件class CustomComponent extends HTMLElement { async connectedCallback() { console.log("c-component connected"); // 获取组件的path,即html的路径 const strPath = this.getAttribute("path"); // 加载html const cInfos = await loadComponent(strPath); const shadow = this.attachShadow({ mode: "closed" }); // 添加html对应的内容 this.#addElement(shadow, cInfos); } #addElement(shadow, info) { // 添加模板内容 if (info.template) { shadow.appendChild(info.template.content.cloneNode(true)); } // 添加脚本 if (info.script) { // 防止全局污染,并得到根节点 var fun = new Function(`${info.script.textContent}`); // 绑定脚本的this为当前的影子根节点 fun.bind(shadow)(); } // 添加样式 if (info.style) { shadow.appendChild(info.style); } }}window.customElements.define("c-component", CustomComponent);// <c-router></c-router>class CustomRouter extends HTMLElement { #routes connectedCallback() { const routeNodes = this.querySelectorAll("c-route"); console.log("routes:", routeNodes); // 获取子节点的路由信息 this.#routes = Array.from(routeNodes).map(node => node.getData()); // 查找默认的路由 const defaultRoute = this.#routes.find(r => r.default) || this.#routes[0]; // 渲染对应的路由 this.#onRenderRoute(defaultRoute); // 监听路由变革 this.#listenerHistory(); } // 渲染路由对应的内容 #onRenderRoute(route) { var el = document.createElement("c-component"); el.setAttribute("path", `/${route.component}.html`); el.id = "_route_"; this.append(el); } // 卸载路由清理事情 #onUploadRoute(route) { this.removeChild(this.querySelector("#_route_")); } // 监听路由变革 #listenerHistory() { // 导航的路由切换 window.addEventListener("popstate", ev => { console.log("onpopstate:", ev); const url = location.pathname.endsWith(".html") ? "/" : location.pathname; const route = this.#getRoute(this.#routes, url); this.#onUploadRoute(); this.#onRenderRoute(route); }); // pushStat或replaceSate window.addEventListener("c-popstate", ev => { console.log("c-popstate:", ev); const detail = ev.detail; const route = this.#getRoute(this.#routes, detail.url); this.#onUploadRoute(); this.#onRenderRoute(route); }) } // 路由查找 #getRoute(routes, url) { return routes.find(function (r) { const path = r.path; const strPaths = path.split('/'); const strUrlPaths = url.split("/"); let match = true; for (let i = 0; i < strPaths.length; i++) { if (strPaths[i].startsWith(":")) { continue; } match = strPaths[i] === strUrlPaths[i]; if (!match) { break; } } return match; }) }}window.customElements.define("c-router", CustomRouter);// 动态加载组件并解析async function loadComponent(path, name) { this.caches = this.caches || {}; // 缓存存在,直接返回 if (!!this.caches[path]) { return this.caches[path]; } const res = await fetch(path).then(res => res.text()); // 利用DOMParser校验 const parser = new DOMParser(); const doc = parser.parseFromString(res, "text/html"); // 解析模板,脚本,样式 const template = doc.querySelector("template"); const script = doc.querySelector("script"); const style = doc.querySelector("style"); // 缓存内容 this.caches[path] = { template, script, style } return this.caches[path];}复制代码

标签:

相关文章

php反射hasmethod技巧_php反射机制用法详解

面向工具编程中工具被授予了自省的能力,而这个自省的过程便是反射。反射,直不雅观理解便是根据到达地找到出发地和来源。比如,一个光秃秃...

SEO优化 2024-12-14 阅读0 评论0