远古时期,当时前后端还是不分离的,路由全部都是由服务端控制的,前端代码和服务端代码过度融合在一起。
客户端 --> 前端发起 http 请求 --> 服务端 --> url 路径去匹配不同的路由 --> 返回不同的数据。
这种方式的缺点和优点都非常明显:
后来 ...随之 ajax 的流行,异步数据请求可以在浏览器不刷新的情况下进行。
后来 ...出现了更高级的体验 —— 单页应用。
在单页应用中,不仅在页面中的交互是不刷新页面的,就连页面跳转也都是不刷新页面的。
单页应用的特点:
而支持起单页应用这种特性的,就是 前端路由。
前端路由的需求是什么?
也就是可以在改变 url 的前提下,保证页面不刷新。
Hash 路由和 History 路由的区别?
hash 的出现满足了这个需求,他有以下几种特征:
1 2 3 |
location.hash = '#aaa'; location.hash = '#bbb'; // 从 #aaa 到 #bbb,页面是不会刷新的 |
1 2 3 4 |
location.hash = '#aaa'; location.hash = '#bbb';
window.addEventLisenter('hashchange', () => {}); |
我们同样有两种方式来控制 hash 的变化:
1 2 |
location.hash = '#aaa'; location.hash = '#bbb'; |
1 2 3 4 |
<a href="#user" rel="external nofollow" > 点击跳转到 user </a>
<!-- 等同于下面的写法 --> location.hash = '#user'; |
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')); |
hash 有个 # 符号,不美观,服务端无法接受到 hash 路径和参数。
历史的车轮无情撵过 hash,到了 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 会直接替换当前历史记录。
他们的参数是?样的,三个参数分别是:
History API 有以下几个特性:
pushState 时,会触发 popstate 吗?
我们可以使用 popstate 来监听 url 的变化;
popstate 到底什么时候才能触发:
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.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'); |
我们经常需要把某种模式匹配到的所有路由,全部映射到同个组件,比如用户信息组件,不同用户使用同一个组件。
可以通过 $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>', }; |
复用组件时,想对 路由参数 的变化作出响应的话,可以使用 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() }, }; |
当时用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该在 最后。
举个例子:
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种方式植入路由导航过程中:
举个例子:
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 必须调用:
vue-router 里面,怎么记住前一个页面的滚动条的位置???
使用前端路由,当切换到新路由时,想要页面滚动到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。
Vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
【注意】:这个功能只在支持 history.pushState 的浏览器中可用。
scrollBehavior 生效的条件:
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; }, }); |
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
举个例子:
1 2 3 4 5 |
const Foo = () => import(/* webpackChunkName: "foo" */ './Foo.vue');
const router = new VueRouter({ routes: [{ path: '/foo', component: Foo }], }); |