目录
-
文章目标
-
P6
-
P6+ ~ P7
-
一、背景
-
二、前端路由特性
-
三、面试!!!
-
四、Hash 原理及实现
-
1、特性
-
2、如何更改 hash
-
3、手动实现一个基于 hash 的路由
-
五、History 原理及实现
-
1、HTML5 History 常用的 API
-
2、pushState/replaceState 的参数
-
3、History 的特性
-
4、面试!!!
-
5、手动实现一个基于 History 的路由
-
六、Vue-Router
-
1、router 使用
-
2、动态路由匹配
-
3、响应路由参数的变化
-
4、捕获所有路由或 404 Not found 路由
-
5、导航守卫
-
6、完整的导航解析流程
-
7、导航守卫执行顺序(面试!!!)
-
8、滚动行为(面试!!!)
-
9、路由懒加载
文章目标
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
渲染不同内容; -
不刷新页面;
也就是可以在改变 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
值的更改,不会导致页面的刷新;
location.hash = '#aaa';location.hash = '#bbb';// 从 #aaa 到 #bbb,页面是不会刷新的
-
不同
url
会渲染不同的页面; -
hash
值的更改,会在浏览器的访问历史中添加一条记录,所以我们才可以通过浏览器的返回、前进按钮来控制hash
的切换; -
hash
值的更改,会触发hashchange
事件;
location.hash = '#aaa';location.hash = '#bbb';window.addEventLisenter('hashchange', () => {});
2、如何更改 hash
我们同样有两种方式来控制 hash
的变化:
-
location.hash
的方式:
location.hash = '#aaa';location.hash = '#bbb';
-
html
标签的方式:
<a href="#user" rel="external nofollow" > 点击跳转到 user </a><!-- 等同于下面的写法 -->location.hash = '#user';
3、手动实现一个基于 hash 的路由
-
./index.html
<!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>
-
./index.css
.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;}
-
./index.js
/* 期望看到的效果:点击三个不同的 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
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、面试!!!
-
pushState
时,会触发popstate
吗? -
pushState/replaceState
并不会触发popstate
事件,这时我们需要手动触发页面的重新渲染; -
我们可以使用
popstate
来监听url
的变化; -
popstate
到底什么时候才能触发: -
点击浏览器后退按钮;
-
点击浏览器前进按钮;
-
js
调用back
方法; -
js
调用forward
方法; -
js
调用go
方法;
5、手动实现一个基于 History 的路由
-
./index.html
-
./index.css
.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;}
-
./index.js
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-Router
1、router 使用
使用 Vue.js
,我们已经可以通过组合组件来组成应用程序,当你要把 Vue Router
添加进来,我们需要做的是,将组件(components
)映射到路由(routes
),然后告诉 Vue Router
在哪里渲染它们。
举个例子:
<!-- 路由匹配到的组件将渲染在这里 --><div id="app"> <router-view></router-view></div>
// 如果使用模块化机制编程,导入 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
或者参数。
const router = new VueRouter({ routes: [ // 动态路径参数,以冒号开头 { path: '/user/:id', component: User }, ],});const User = { template: '<div>User: {{ $route.params.id }}</div>',};
3、响应路由参数的变化
复用组件时,想对 路由参数 的变化作出响应的话,可以使用 watch
或者 beforeRouteUpdate
:
举个例子:
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
-
单个路由独享的
-
路由独享守卫:
beforeEnter
-
组件级的
-
beforeRouteEnter
-
beforeRouteUpdate
-
beforeRouteLeave
6、完整的导航解析流程
-
导航被触发;
-
在失活的组件里调用离开守卫(前一个组件的
beforeRouteLeave
); -
调用全局的
beforeEach
守卫; -
在重用的组件里调用
beforeRouteUpdate
守卫; -
在路由配置里调用
beforeEnter
; -
解析异步路由组件;
-
在被激活的组件里调用
beforeRouterEnter
; -
调用全局的
beforeResolve
守卫; -
导航被确认;
-
调用全局的
afterEach
钩子; -
触发
DOM
更新; -
用创建好的实例调用
beforeRouterEnter
守卫中传给next
的回调函数;
举个例子:
// 全局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
或者 浏览器的前进/返回按钮;
window.history.back(); // 后退window.history.forward(); // 前进window.history.go(-3); // 接收 number 参数,后退 N 个页面
举个例子:
// 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
包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
举个例子:
const Foo = () => import(/* webpackChunkName: "foo" */ './Foo.vue');const router = new VueRouter({ routes: [{ path: '/foo', component: Foo }],});
关于Vue.js 前端路由和异步组件是什么就分享到这里了,希望以上内容可以对大家有一定的参考价值,可以学以致用。如果喜欢本篇文章,不妨把它分享出去让更多的人看到。
文章标题:Vue.js 前端路由和异步组件是什么,发布者:亿速云,转载请注明出处:https://worktile.com/kb/p/24495