使用Vue必然会用到VueRouter,也会遇到VueRouter的Hash和History两种模式。可是如果当问到VueRouter这两种模式分别是什么原理,不一定所有人都回答的上来。 这篇文章我们分别用原生JS实现两种模式的简易Router,揭开VueRouter的面纱。
先看代码,下面再对代码做个讲解
<nav> <a class="link" href="/">home</a> <a class="link" href="/a">a</a> <a class="link" href="/c">c</a> </nav> <section id="home"> <h1>home</h1> <p>This is home page</p> </section> <section id="a"> <h1>a</h1> <p>This is a page</p> </section> <section id="default"> <h1>404</h1> <p>404</p> </section> <script> class Router { constructor({ routes }) { this.routes = routes window.onhashchange = () => this.setPage() document.querySelectorAll('.link').forEach(node => { node.addEventListener('click', (e) => { location.hash = node.pathname // 当点击链接时,修改当前URl上的Hash e.preventDefault() }) }) this.setPage() // 初始时设置要展示的页面 } setPage() { this.routes.forEach(route => route.component.style.display = "none") //先隐藏所有部分 //从当前URL的hash从配置表中查到要展示的部分,如果不存在,使用默认部分 let route = this.routes.find(route => '#' + route.path === location.hash) || this.routes[this.routes.length - 1] route.component.style.display = "block" } } new Router({ routes: [ { path: '/', component: document.querySelector('#home') }, { path: '/a', component: document.querySelector('#a') }, { component: document.querySelector('#default') }, ] }) </Script>
以上代码的原理是:1. 先隐藏应用里的所有页面; 2. 根据URL的Hash,和之前配置的路由表,来确定要展示的特定区域; 3. 监听URL hash的变化,当用户通过点击链接触发hash变化时,重新设置页面上要展示的区域。
本质上是使用onhashchange监听hash的变化,用户通过点击触发hash的变化,hash发生改变时重新根据新hash和路由配置表展示特定区域。
先看history模式的案例,和hash模式相比,只有注释1、2、3部分有差别。
<nav> <a class="link" href="/">home</a> <a class="link" href="/a">a</a> <a class="link" href="/c">c</a> </nav> <section id="home"> <h1>home</h1> <p>This is home page</p> </section> <section id="a"> <h1>a</h1> <p>This is a page</p> </section> <section id="default"> <h1>404</h1> <p>404</p> </section> <script> class Router { constructor({ mode, routes }) { this.routes = routes window.onpopstate = (e) => this.setPage(location.pathname) //1 document.querySelectorAll('.link').forEach(node => { node.addEventListener('click', (e) => { this.setPage(node.pathname) //2 e.preventDefault() }) }) this.setPage(location.pathname) } setPage(path) { history.pushState({}, "", path) //3 this.routes.forEach(route => route.component.style.display = "none") let route = this.routes.find(route => route.path === path) || this.routes[this.routes.length - 1] route.component.style.display = "block" } } new Router({ routes: [ { path: '/', component: document.querySelector('#home') }, { path: '/a', component: document.querySelector('#a') }, { component: document.querySelector('#default') }, ] }) </Script>
以上代码中,history.pushState用来修改浏览器地址栏展示的路径,onpopstate用来监听用户浏览器的前进、后退事件。
当用户点击链接时,获取点击链接的路径,调用setPage渲染路由配置表中和路径匹配的部分,同时通过使用pushState修改浏览器的展示的路径。当监听到浏览器前进、回退事件时,根据新的pathname 重新调用setPage设置渲染区域。
关注点 使用history模式时,用户点击页面链接确实能“跳转”到不同页面,也会发现浏览器地址栏路径发生了视觉上的变化。但在子页面刷新时如果后端未做配置会出现问题。
比如 初始url是 http://localhost:8080 ,当用户点击链接时,url变成 http://localhost:8080/a 。 用户确实能看到/a 路径相对应的页面(区域/组件),但当在该链接下刷新时,向服务端发送请求的的路径是 http://localhost:8080/a ,而服务端并未做/a的配置,会导致请求失败。所以使用history模式需要服务端做响应的配置。
使用hash模式不存在以上问题,比如初始url是 http://localhost:8080,当用户点击链接时 url变成 http://localhost:8080#/a 。用户能看到配置的/a对应的新页面。当刷新时,向服务端发送的请求路径还是 / ,服务端会正常返回当前html的所有内容。