路由分为前端路由和后端路由,最初呢,是有后端路由,但是每次路由切换的时候都需要去刷新页面,发送请求,服务端返回数据,用户体验不好,后来就出现了前端路由,特点是浏览器不会重新刷新
前端路由分为两种,hash 模式 和 history 模式
history路由原理
-
首次进入项目,得解析url,加载对应的路由
-
项目已经加载了,需要切换路由。如果使用a标签切换,需要阻止默认事件,如果使用js的push方法切换,直接操作:
- window.history.pushState: 在浏览历史记录中增加一条记录
- window.history.replaceState: 把当前浏览地址换成replaceState 之后的地址,浏览记录总长度不变
- window对象中有一个onpopstate方法来监听历史栈的改变,只要有改变,就会触发该事件
- h5中提供的以上方法不会刷新浏览器
代码实现:
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
router: router,
render: h => h(App)
}).$mount('#app')
router.js
import Vue from 'vue'
import VueRouter from './vue-router'
import Home from './views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
}
]
const router = new VueRouter({
routes
})
export default router
vue-router文件夹下的 index.js
let Vue = null;
export default class Router{
static install(_Vue){
Vue = _Vue;
Vue.mixin({
beforeCreate(){
if(this.$options.router){
//根实例挂载了router
// 路由初始化
this.$options.router.init();
}
if(this.$root.$options.router){
this.$router = this.$root.$options.router;
}
}
})
}
constructor(options){
this._options = options;
this._routeMap = {};
this._vm = new Vue({
data: {
currentPath : '/'
}
})
}
init(){
// 解析路由表
this.getRouteMap();
// 注册组件
this.initComponent();
// 解析第一次的路由
this.loadRoute();
// 监听返回的事件,重新装载组件
this.onBack();
}
getRouteMap(){
this._options.routes.forEach(route=>{
this._routeMap[route.path] = route;
})
}
initComponent(){
const self = this;
Vue.component('router-view', {
render(h){
const path = self._vm.currentPath;
const component = self._routeMap[path] && self._routeMap[path].component;
return h(component);
}
});
Vue.component('router-link', {
props: {
to: String
},
render(h){
return h('a', {
class: {
'router-link-exact-active': self._vm.currentPath === this.to
},
attrs: {
href: this.to
},
on: {
// a标签点击事件
click: (ev)=>{
// 阻止默认事件
ev.preventDefault();
// 修改地址栏
window.history.pushState({}, '', this.to);
// 装载组件
self._vm.currentPath = this.to;
}
}
}, this.$slots.default);
}
});
}
loadRoute(){
window.addEventListener('load', ()=>{
//得到路由的地址
let path = window.location.pathname;
// 加载路由对应的组件
this._vm.currentPath = path;
})
}
onBack(){
window.addEventListener('popstate', ()=>{
//得到路由的地址
let path = window.location.pathname;
// 加载路由对应的组件
this._vm.currentPath = path;
})
}
push(path){
// 修改地址栏
window.history.pushState({}, '', path);
// 加载路由对应的组件
this._vm.currentPath = path;
}
back(){
window.history.back();
}
forward(){
window.history.forward();
}
go(n){
window.history.go(n);
}
}
hash 路由的原理
地址栏变化,路由要切换,需要监听onhashchange
js切换路由,实际上是修改地址栏的hash值
- 改变url后面的hash 值,不会向服务器发送请求,因此也就不会刷新页面,每次hash值改变,触发hashchange 事件,因此我们可以通过监听该事件,来知道hash值发生了哪些变化(location.hash)
- 改变hash 不会触发页面跳转,hash 链接是当前页面中的某个片段,那么页面会滚动到对应位置,如果没有,则没有任何效果
代码实现:
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
router: router,
render: h => h(App)
}).$mount('#app')
router.js
import Vue from 'vue'
import VueRouter from './vue-router'
import Home from './views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
}
]
const router = new VueRouter({
routes
})
export default router
vue-router 文件夹下的 index.js
let Vue = null;
export default class Router{
static install(_Vue){
Vue = _Vue;
Vue.mixin({
beforeCreate(){
if(this.$options.router){
//只有vm实例,而且是配置了router的vm实例,会进来
this.$options.router.init();
}
if(this.$root.$options.router){
this.$router = this.$root.$options.router;
}
}
});
}
constructor(options){
this._options = options;
this._routeMap = {};
this._routeEach = [];
this._vm = new Vue({
data: {
//保存当前路由所在的地址
currentPath: '/'
}
});
}
init(){
// 解析路由表
this.getRouteMap();
// 配置组件
this.initComponent();
// 加载路由
this.addEvents();
}
// 解析路由表
getRouteMap(){
this._options.routes.forEach(route=>{
this._routeMap[route.path] = route;
});
}
initComponent(){
let self = this;
Vue.component('router-view', {
render(h){
let currentPath = self._vm.currentPath;
let component = self._routeMap[currentPath].component;
return h(component);
}
});
Vue.component('router-link', {
props: {
to: String
},
render(h){
return h(
'a',
{
class: {
'router-link-exact-active': self._vm.currentPath === this.to
},
attrs: {
href: '#'+this.to
}
},
this.$slots.default
)
}
});
}
// 加载路由
addEvents(){
window.addEventListener('hashchange', this.onHashChange.bind(this));
window.addEventListener('load', this.onHashChange.bind(this));
}
getPath(){
return window.location.hash.slice(1);
}
// 处理路由
onHashChange(ev){
console.log('onHashChange函数调用了.....');
//调用路由拦截
if(this._routeEach.length > 0){
let {newURL, oldURL} = ev;
let to = {};
let from = {};
if(oldURL){
// hashChange事件
// 重新设置currentPath,那么router-view就会重新装载
to = {
path: newURL.split('#')[1]
}
from = {
path: oldURL.split('#')[1]
}
}else{
//load事件
to = {
path: window.location.hash.split('#')[1]
}
from = {
path: null
}
}
this.runEach(to, from);
}
//没有拦截
else{
let path = this.getPath();
let route = this._routeMap[path];
this._vm.currentPath = route.path;
}
}
// 切换路由
push(path){
// this._vm.currentPath = path;
window.location.hash = '#'+path;
}
// 路由拦截
beforeEach(cb){
this._routeEach.push(cb);
}
//执行路由拦截
runEach(to, from, index = 0){
console.log(index);
this._routeEach[index](to, from, ()=>{
index++;
if(index < this._routeEach.length){
this.runEach(to, from, index);
}else{
let path = this.getPath();
let route = this._routeMap[path];
this._vm.currentPath = route.path;
}
});
}
}
history 路由 和 hash 路由的区别:
- hash 只能修改#后面的,而且hash值必须和原先的值不一样
- pushState 能修改路径,查询参数和片段标识符,pushState比hash更符合前端路由的访问方式,更加优雅,但是history模式需要后端配合