| 
                            
                                  文章目标P6
	针对 react/vue 能够根据业务需求口喷 router 的关键配置,包括但不限于:路由的匹配规则、路由守卫、路由分层等;能够描述清楚 history 的主要模式,知道 history 和 router 的边界; P6+ ~ P7
	在没有路由的情况下,也可以根据也无需求,实现一个简单的路由;读过 router 底层的源码,不要求每行都读,可以口喷关键代码即可; 一、背景远古时期,当时前后端还是不分离的,路由全部都是由服务端控制的,前端代码和服务端代码过度融合在一起。 客户端 --> 前端发起 http 请求 --> 服务端 --> url 路径去匹配不同的路由 --> 返回不同的数据。 这种方式的缺点和优点都非常明显: 
	优点:因为直接返回一个 html,渲染了页面结构。SEO 的效果非常好,首屏时间特别快;
	
		在浏览器输入一个 url 开始到页面任意元素加载出来/渲染出来 --> 首屏时间;缺点:前端代码和服务端代码过度融合在一起,开发协同非常的乱。服务器压力大,因为把构建 html 的工作放在的服务端; 后来 ...随之 ajax 的流行,异步数据请求可以在浏览器不刷新的情况下进行。 后来 ...出现了更高级的体验 —— 单页应用。 
	页 --> HTML 文件单页 --> 单个 HTML 文件 在单页应用中,不仅在页面中的交互是不刷新页面的,就连页面跳转也都是不刷新页面的。 单页应用的特点: 
	页面中的交互是不刷新的页面的,比如点击按钮,比如点击出现一个弹窗;多个页面间的交互,不需要刷新页面(a/b/c,a-> b -> c);加载过的公共资源,无需再重复加载; 而支持起单页应用这种特性的,就是 前端路由。 二、前端路由特性前端路由的需求是什么? 也就是可以在改变 url 的前提下,保证页面不刷新。 三、面试!!!Hash 路由和 History 路由的区别? 
	hash 有 #,history 没有 #;hash 的 # 部分内容不会给服务端,主要一般是用于锚点, history 的所有内容都会给服务端;hash 路由是不支持 SSR 的,history 路由是可以的;hash 通过 hashchange 监听变化,history 通过 popstate 监听变化; 四、Hash 原理及实现1、特性hash 的出现满足了这个需求,他有以下几种特征: 
	url 中带有一个 # 符号,但是 # 只是浏览器端/客户端的状态,不会传递给服务端;
	
		客户端路由地址 www.baidu.com/#/user --> 通过 http 请求 --> 服务端接收到的 www.baidu.com/客户端路由地址 www.baidu.com/#/list/detail/1 --> 通过 http 请求 --> 服务端接收到的 www.baidu.com/hash 值的更改,不会导致页面的刷新; 
	
		
			| 1 2 3 | location.hash = '#aaa'; location.hash = '#bbb'; // 从 #aaa 到 #bbb,页面是不会刷新的 |  
	不同 url 会渲染不同的页面;hash 值的更改,会在浏览器的访问历史中添加一条记录,所以我们才可以通过浏览器的返回、前进按钮来控制 hash 的切换;hash 值的更改,会触发 hashchange 事件; 
	
		
			| 1 2 3 4 | location.hash = '#aaa'; location.hash = '#bbb';   window.addEventLisenter('hashchange', () => {}); |  2、如何更改 hash我们同样有两种方式来控制 hash 的变化: 
	
		
			| 1 2 | location.hash = '#aaa'; location.hash = '#bbb'; |  
	
		
			| 1 2 3 4 | <a href="#user" rel="external nofollow" > 点击跳转到 user </a>   <!-- 等同于下面的写法 --> location.hash = '#user'; |  3、手动实现一个基于 hash 的路由
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <!DOCTYPE html> <html lang="en">   <head>     <meta charset="UTF-8" />     <meta http-equiv="X-UA-Compatible" content="IE=edge" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>Document</title>     <link rel="stylesheet" href="./index.css" rel="external nofollow"  />   </head>   <body>     <div class="container">       <a href="#gray" rel="external nofollow" >灰色</a>       <a href="#green" rel="external nofollow" >绿色</a>       <a href="#" rel="external nofollow" >白色</a>       <button onclick="window.history.go(-1)">返回</button>     </div>       <script type="text/javascript" src="index.js"></script>   </body> </html> |  
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | .container {   width: 100%;   height: 60px;   display: flex;   justify-content: space-around;   align-items: center;     font-size: 18px;   font-weight: bold;     background: black;   color: white; }   a:link, a:hover, a:active, a:visited {   text-decoration: none;   color: white; } |  
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | /*   期望看到的效果:点击三个不同的 a 标签,页面的背景颜色会随之变化 */ class BaseRouter {   constructor() {     this.routes = {}; // 存储 path 以及 callback 的对应关系     this.refresh = this.refresh.bind(this); // 如果不 bind 的话,refresh 方法中的 this 指向 window     // 处理页面 hash 变化,可能存在问题:页面首次进来可能是 index.html,并不会触发 hashchange 方法     window.addEventListener('hashchange', this.refresh);       // 处理页面首次加载     window.addEventListener('load', this.refresh);   }     /**    * route    * @param {*} path 路由路径    * @param {*} callback 回调函数    */   route(path, callback) {     console.log('========= route 方法 ========== ', path);     // 向 this.routes 存储 path 以及 callback 的对应关系     this.routes[path] = callback || function () {};   }     refresh() {     // 刷新页面     const path = `/${location.hash.slice(1) || ''}`;     console.log('========= refresh 方法 ========== ', path);     this.routes[path]();   } }   const body = document.querySelector('body'); function changeBgColor(color) {   body.style.backgroundColor = color; }   const Router = new BaseRouter();   Router.route('/', () => changeBgColor('white')); Router.route('/green', () => changeBgColor('green')); Router.route('/gray', () => changeBgColor('gray')); |  五、History 原理及实现hash 有个 # 符号,不美观,服务端无法接受到 hash 路径和参数。 历史的车轮无情撵过 hash,到了 HTML5 时代,推出了 History API。 1、HTML5 History 常用的 API
	
		
			| 1 2 3 4 5 6 7 8 9 | window.history.back(); // 后退   window.history.forward(); // 前进   window.history.go(-3); // 接收 number 参数,后退 N 个页面   window.history.pushState(null, null, path);   window.history.replaceState(null, null, path); |  其中最主要的两个 API 是 pushState 和 replaceState,这两个 API 都可以在不刷新页面的情况下,操作浏览器历史记录。 不同的是,pushState 会增加历史记录,replaceState 会直接替换当前历史记录。 2、pushState/replaceState 的参数
	pushState:页面的浏览记录里添加一个历史记录;replaceState:替换当前历史记录; 他们的参数是?样的,三个参数分别是: 
	state:是一个对象,是一个与指定网址相关的对象,当 popstate 事件触发的时候,该对象会传入回调函数;title:新页面的标题,浏览器支持不一,建议直接使用 null;url:页面的新地址; 3、History 的特性History API 有以下几个特性: 
	没有 #;history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面渲染;可以使用 history.popstate 事件来监听 url 的变化;只有用户点击浏览器 倒退按钮 和 前进按钮,或者使用 JavaScript 调用 back、forward、go 方法时才会触发 popstate; 4、面试!!!5、手动实现一个基于 History 的路由
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | .container {   width: 100%;   height: 60px;   display: flex;   justify-content: space-around;   align-items: center;     font-size: 18px;   font-weight: bold;     background: black;   color: white; }   a:link, a:hover, a:active, a:visited {   text-decoration: none;   color: white; } |  
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | class BaseRouter {   constructor() {     this.routes = {};       // location.href; => hash 的方式     console.log('location.pathname ======== ', location.pathname); // http://127.0.0.1:8080/green ==> /green     this.init(location.pathname);     this._bindPopState();   }     init(path) {     // pushState/replaceState 不会触发页面的渲染,需要我们手动触发     window.history.replaceState({ path }, null, path);     const cb = this.routes[path];     if (cb) {       cb();     }   }     route(path, callback) {     this.routes[path] = callback || function () {};   }     // ! 跳转并执行对应的 callback   go(path) {     // pushState/replaceState 不会触发页面的渲染,需要我们手动触发     window.history.pushState({ path }, null, path);     const cb = this.routes[path];     if (cb) {       cb();     }   }   // ! 演示一下 popstate 事件触发后,会发生什么   _bindPopState() {     window.addEventListener('popstate', e => {       /*         触发条件:           1、点击浏览器前进按钮           2、点击浏览器后退按钮           3、js 调用 forward 方法           4、js 调用 back 方法           5、js 调用 go 方法       */       console.log('popstate 触发了');       const path = e.state && e.state.path;       console.log('path >>> ', path);       this.routes[path] && this.routes[path]();     });   } } const Router = new BaseRouter(); const body = document.querySelector('body'); const container = document.querySelector('.container');   function changeBgColor(color) {   body.style.backgroundColor = color; } Router.route('/', () => changeBgColor('white')); Router.route('/gray', () => changeBgColor('gray')); Router.route('/green', () => changeBgColor('green'));   container.addEventListener('click', e => {   if (e.target.tagName === 'A') {     e.preventDefault();     console.log(e.target.getAttribute('href')); // /gray  /green 等等     Router.go(e.target.getAttribute('href'));   } }); |  六、Vue-Router1、router 使用使用 Vue.js,我们已经可以通过组合组件来组成应用程序,当你要把 Vue Router 添加进来,我们需要做的是,将组件(components)映射到路由(routes),然后告诉 Vue Router 在哪里渲染它们。 举个例子: 
	
		
			| 1 2 3 4 | <!-- 路由匹配到的组件将渲染在这里 --> <div id="app">   <router-view></router-view> </div> |  
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // 如果使用模块化机制编程,导入 Vue 和 VueRouter,要调用 Vue.use(VueRouter)   // 1、定义(路由)组件 // 可以从其他文件 import 进来 const Foo = { template: '<div>foo</div>' }; const Bar = { template: '<div>bar</div>' };   // 2、定义路由 //每个路由应该映射一个组件,其中 component 可以是通过 Vue.extend() 创建的组件构造器,或者只是一个组件配置对象 const routes = [   { path: '/foo', component: Foo },   { path: '/bar', component: Bar }, ];   // 3、创建 router 实例,然后传 routes 配置 const router = new VueRouter({   routes, });   // 4、创建和挂载根实例 // 记得要通过 router 配置参数注入路由,从而让整个应用都有路由功能 const app = new Vue({   router, }).$mount('#app'); |  2、动态路由匹配我们经常需要把某种模式匹配到的所有路由,全部映射到同个组件,比如用户信息组件,不同用户使用同一个组件。 可以通过 $route.params.id 或者参数。 
	
		
			| 1 2 3 4 5 6 7 8 9 10 | const router = new VueRouter({   routes: [     // 动态路径参数,以冒号开头     { path: '/user/:id', component: User },   ], });   const User = {   template: '<div>User: {{ $route.params.id }}</div>', }; |  3、响应路由参数的变化复用组件时,想对 路由参数 的变化作出响应的话,可以使用 watch 或者 beforeRouteUpdate: 举个例子: 
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | const User = {   template: '...',   watch: {     $route(to, from) {       // 对路由变化作出响应...     },   }, };   const User = {   template: '...',   beforeRouteUpdate(to, from, next) {     // 对路由变化作出响应...     // don't forget to call next()   }, }; |  4、捕获所有路由或 404 Not found 路由当时用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该在 最后。 举个例子: 5、导航守卫vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种方式植入路由导航过程中: 
	全局的
	
		全局前置守卫:router.beforeEach全局解析守卫:router.beforeResolve全局后置钩子:router.afterEach单个路由独享的
	
	组件级的
	
		beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave 6、完整的导航解析流程
	导航被触发;在失活的组件里调用离开守卫(前一个组件的 beforeRouteLeave);调用全局的 beforeEach 守卫;在重用的组件里调用 beforeRouteUpdate 守卫;在路由配置里调用 beforeEnter;解析异步路由组件;在被激活的组件里调用 beforeRouterEnter;调用全局的 beforeResolve 守卫;导航被确认;调用全局的 afterEach 钩子;触发 DOM 更新;用创建好的实例调用 beforeRouterEnter 守卫中传给 next 的回调函数; 举个例子: 
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // 全局 const router = new VueRouter({   mode: 'history',   base: process.env.BASE_URL,   routes, });   // 全局的导航守卫 router.beforeEach((to, from, next) => {   console.log(`Router.beforeEach => from=${from.path}, to=${to.path}`);   // 可以设置页面的 title   document.title = to.meta.title || '默认标题';   // 执行下一个路由导航   next(); });   router.afterEach((to, from) => {   console.log(`Router.afterEach => from=${from.path}, to=${to.path}`); });   // 路由独享 const router = new VueRouter({   routes: [     {       path: '/foo',       component: Foo,       beforeEnter: (to, from, next) => {         // 配置数组里针对单个路由的导航守卫         console.log(`TestComponent route config beforeEnter => from=${from.path}, to=${to.path}`);         next();       },     },   ], });   // 组件 const Foo = {   template: `...`,   beforeRouteEnter(to, from, next) {     // 在渲染该组件的对应路由被 comfirm 前调用     // 不!能!获取组件实例 this,因为当守卫执行前,组件实例还没被调用   },   beforeRouteUpdate(to, from, next) {     // 在当前路由改变,但是该组件被复用时调用     // 举个例子来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候     // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用     // 可以访问组件实例 this   },   beforeRouteLeave(to, from, next) {     // 导航离开该组件的对应路由时调用     // 可以访问组件实例 this   }, }; |  next 必须调用: 
	next():进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed(确认的)。next(false):中断当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。next("/") 或者 next({ path: "/" }):跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: "home" 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。next(error):如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。 7、导航守卫执行顺序(面试!!!)
	【组件】前一个组件的 beforeRouteLeave【全局】的 router.beforeEach
	
		【组件】如果是路由参数变化,触发 beforeRouteUpdate【配置文件】里,下一个的 beforeEnter【组件】内部声明的 beforeRouteEnter【全局】的 router.afterEach 8、滚动行为(面试!!!)vue-router 里面,怎么记住前一个页面的滚动条的位置??? 使用前端路由,当切换到新路由时,想要页面滚动到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 Vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。 【注意】:这个功能只在支持 history.pushState 的浏览器中可用。 scrollBehavior 生效的条件: 
	浏览器支持 history API;页面间的交互是通过 go,forward,back 或者 浏览器的前进/返回按钮; 
	
		
			| 1 2 3 | window.history.back(); // 后退 window.history.forward(); // 前进 window.history.go(-3); // 接收 number 参数,后退 N 个页面 |  举个例子: 
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 | // 1. 记住:手动点击浏览器返回或者前进按钮,记住滚动条的位置,基于 history API 的,其中包括:go、back、forward、手动点击浏览器返回或者前进按钮 // 2. 没记住:router-link,并没有记住滚动条的位置   const router = new VueRouter({   mode: 'history',   base: process.env.BASE_URL,   routes,   scrollBehavior: (to, from, savedPosition) => {     console.log(savedPosition); // 已保存的位置信息     return savedPosition;   }, }); |  9、路由懒加载当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。 举个例子: 
	
		
			| 1 2 3 4 5 | const Foo = () => import(/* webpackChunkName: "foo" */ './Foo.vue');   const router = new VueRouter({   routes: [{ path: '/foo', component: Foo }], }); |  
 |