vue-router

351 阅读2分钟

前端路由 router 原理及实现

核心都是改变url,但不刷新页面,不向服务器发送请求

hash 路由

  • url 中带有一个 #,# 只是客户端的状态,不会传递给服务端
http://a.com/web#order   =>   http://a.com/web
http://a.com/#/list/detail/1   =>   http://a.com
  • hash 改变时,页面不会刷新
// 在调试器中
location.hash = '#/news'
location.replace('#/detail') // 替换当前记录
// https://www.baidu.com -> https://www.baidu.com/#news
  • hash 值的更改,会在浏览器访问历史中添加一条记录,可通过浏览器的前进、返回按钮控制 hash 的切换

  • 监听 hash 更改事件: hashchange

// 案例
<div>
    <a href="#/list">列表页</a>
    <a href="#/detail">详情页</a>
    <a href="#/other">404</a>
</div>
<div id="app" style="border: 1px solid black; min-height: 200px;"></div>

// 定义路由映射表
var routerObj = {
    '#/list': '<div>列表页</div>',
    '#/detail': '<div>详情页</div>',
    '#/other': '<div>404</div>',
}
// hashchange 监听事件
window.addEventListener('hashchange', function () {
    document.getElementById('app').innerHTML = routerObj[location.hash];
})
  • 高级写法
<div class="container">
  <a href="#gray">灰色</a>
  <a href="#green">绿色</a>
  <a href="#">白色</a>
  <button onclick="window.history.go(-1)">返回</button>
</div>


class BaseRouter {
    constructor() {
        this.routes = {}; // 存储path以及callback的对应关系
        this.refresh = this.refresh.bind(this);
        window.addEventListener('load',this.refresh);
        window.addEventListener('hashchange',this.refresh);
    }

    route(path,callback) {
        this.routes[path] = callback || function(){};
    }
    refresh() { // 刷新当前页面。渲染当前路径对应的操作
        const path = `/${location.hash.slice(1) || ''}`;
        this.routes[path]();
    }
}

const body = document.querySelector('body');

function changeBgColor(color) {
    body.style.backgroundColor = color;
}

const Router = new BaseRouter();

Router.route('/',function() {
    changeBgColor('white');
})
Router.route('/green',function() {
    changeBgColor('green');
})
Router.route('/gray',function() {
    changeBgColor('gray');
})

history 路由

  • 需要服务端配合,避免刷新后导致页面404
// 对于后端来说可能是两个页面,要做一个通配符识别,将 /web* 后面的统一返回某个 html 中
http://a.com/web/order
http://a.com/web/goods
  • 用法
window.history.back(); // 后退一步 window.history.go(-1)
window.history.forward(); // 前进一步 window.history.go(1);
window.history.go(-3); // 后退 3 步
window.history.pushState(); // location.href 页面的浏览记录离会添加一个历史记录
window.history.replaceState(); // location.replace 替换掉当前的历史记录

pushState / replaceState 的参数
window.hisory.pushState(null,'new',path);
// state: 一个对象,与指定网址有关
// title:新页面名字
// url: 新页面地址
  • history 路由没有 hash 路由类似的 hashchange 事件
<div id="history-box">
    <h1>history 路由</h1>
    <a href="/web/list">列表页</a>
    <a href="/web/detail">详情页</a>
    <a href="/web/other">404</a>
</div>
<div id="app" style="border: 1px solid black; min-height: 200px;"></div>

// history 路由demo
var routerHistoryObj = {
    '/web/list': '<div>history 列表页</div>',
    '/web/detail': '<div>history 详情页</div>'
}
// 为每个链接添加点击事件
var length = document.querySelectorAll('#history-box a[href]').length
for (var i = 0; i < length; i++) {
    document.querySelectorAll('#history-box a[href]')[i].addEventListener('click', function (event) {
        event.preventDefault();
        window.history.pushState({}, null, event.currentTarget.getAttribute('href'));
        handleHref();
    })
}
// 监听前进/后退 引起的posstate事件
window.addEventListener('popstate', handleHref);
// 根据新的路由,显示新的组件
function handleHref() {
    document.getElementById('app').innerHTML = routerHistoryObj[location.pathname] || '404页面'
}

区别

  1. hash 有 #, history 没有
  2. hash 的 # 部分内容不会传给服务器,history 的所有 url 内容服务端都可以获取到
  3. history 路由,应用在部署的时候,需要注意 HTML 文件的访问
  4. hash 通过 hashchange 监听变化, history 通过 popstate 监听变化

问题

  • pushState 时,会触发 popState 事件吗? 答:不会,需要手动触发页面的重新渲染。
  • popState 什么时候才会触发? 答:点击浏览器的前进、后退按钮;js中触发 back forward go 方法。

vue-router

vue2.x版本

使用方法

  • 通过 cnpm i vue-router --save 下载路由
  • 新建 router.js 配置路由
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

// 路由关系表
const routes = [
    {
        path: '/',
        name: 'home', // 命名路由
        component: ()=> import('@/components/Home'),
        meta: {
            title: '首页'
        }
    },
    { // 404 路由
        path: '*',
        component: () => import('@/components/404'),
        // redirect: '/' // 重定向到首页
    }
]

let router = new Router({
    // 默认hash 模式,设置后改为 history 模式,本地项目切换路由后页面依然没问题
    // (因为vue帮我们处理了),但是线上项目需要后台配置
    mode: 'history',
    routes,
    // 记录位置,返回到上个页面点击的位置
    scrollBehavior:(to,from,savedPosition)=>{
        return savedPosition;
    }
    // 坑:通过 router-link 并不能记住位置
})

export default router;
  • 在 main.js 中配置路由
import router from './router/router'

new Vue({
    router,
    ...
}).$mount('#app')

知识点

  • Vue.use(Router) 引入了两个组件 router-linkrouter-view,及全局混入了$route$router
  1. this.$route 带属性:params、query、matched、path
  2. this.$router 带方法:push(location)、replace(location)...
// routr-link 和 router-view
<div>
    <router-link :to="{name: 'home'}">首页</router-link>
    <router-link :to="'/login'">登录页</router-link>
    <router-link to="/news">新闻页</router-link>
    <router-link to="/login">登录页</router-link>
    // router-link 默认为 a 标签,可用 tag 改变属性
    // <router-link to="/login" tag='p'>登录页</router-link>
    // 用 <a> 标签跳转会刷新页面,所以尽量不要用
</div>
// 路由的出口,匹配到的路由在此渲染,transition 添加动画
<transition name='fade'>
    <router-view class='router-view'></router-view>
</transition>

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.75s ease;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
.router-view {
  position: absolute;
  transition: all 0.75s cubic-bezier(0.55,0,0.1,1);
}
  • 命名路由
  1. 可直接通过名字跳转,后续如果更改了 path,则不影响 name 的跳转
  2. 设置了默认的子路由,则子路由的 name 会被警告,通过name跳转父路由则不会显示默认的子路由
  • 子路由 和 动态匹配路由
  1. 默认子路由: path: ''
  2. 子路由中的path是否以 '/' 开头的区别,加 '/' 是绝对路径,不加是相对
const routes = [
    {
        path: '/news',
        // name: 'news', // 有默认子路由时,父路由不要设置name
        component: () => import('@/components/News'),
        meta: {
            title: '新闻',
            requiredAuth: true,
        },
        children: [
            { // 定义默认路由为 添加新闻 页面
                path: '',
                component: () => import('@/components/news/NewAdd'),
                meta: {
                    title: '新闻',
                },
            },
            {
                path: 'add', // 注意:不加 '/'
                name: 'newAdd',
                component: () => import('@/components/news/NewAdd'),
                meta: {
                    title: '添加新闻'
                },
            },
            { // 动态匹配路由
                path: 'detail/:id',
                name: 'newDetail',
                component: () => import('@/components/news/NewDetail'),
                meta: {
                    title: '新闻详情' 
                },
            },
        ]
    },
]

// 动态匹配路由,此时页面中
<router-link :to="{name: 'newDetail',params: {id: 1}}">新闻详情-1</router-link>
<router-link :to="{name: 'newDetail',params: {id: 2}}">新闻详情-2</router-link>

// 此时对应路由页面内
// 可通过 this.$route.params.id 获取对应 id 内容。
// 但是由于在同一页面,mounted 后路由没有销毁,所以切换路由 detail/1 和 detail/2 时页面延迟导致内容串了。可通过 watch 监听
watch: {
  '$route.params.id'() {
      this.getNews();
  }
},
// 也可用组件路由守卫
beforeRouteUpdate(to, from, next) {
    console.log("新闻详情: 组件中 - beforeRouteUpdate");
    this.getNews(to.params.id);
    next();
},
methods: {
    getNews(id = this.$route.params.id) {
      this.content = `这是新闻ID为: ${id}的内容`;
    },
},
  • 跳转页面的方式
// 页面中
<router-link :to="{name: 'newDetail',params: {id: 1}}">新闻详情-1</router-link>
// js 中
this.$router.push({ name: "my", query: { type: 'like' } });

// 注:query 是 ? 后面内容,params 是 & 后面内容

路由守卫

分类

  • 全局守卫: beforeEachbeforeResolveafterEach
  • 路由独享守卫: beforeEnter
  • 组件守卫: beforeRouteLeavebeforeRouteEnterbeforeRouteUpdate
// 全局守卫,写在 router.js 内
router.beforeEach((to,from,next)=>{})
export default router;

// 路由独享守卫,写在router.js 内 定义在路由映射表里
{
    path: 'detail',
    name: 'newDetail',
    component: () => import('@/components/news/NewDetail'),
    beforeEnter(to,from,next) {
        next();
    }
},

// 组件守卫,写在 组件 内
beforeRouteEnter(to, from, next) {
    // 这里无法访问 this,因为没有创建实例,但是可以在next里面添加回调,唯一一个支持给next传递回调的守卫
    next((vm) => {
      // 通过 `vm` 访问组件实例
    });
},
beforeRouteUpdate(to, from, next) {
    // 当前路由改变时,该组件被复用时调用(比如:统一路由不同id的时候)
    next();
},
beforeRouteLeave(to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消
    const answer = window.confirm("确定是否离开?");
    if (answer) {
      next();
    } else {
      next(false);
    }
},

注:

  1. 必须调用 next() 才可继续
  2. afterEach(to, from) 无 next 参数,不会改变导航,因为导航已被确认

执行顺序

  1. 【组件】前一个组件的 beforRouteLeave
  2. 【全局】的 beforeEach
  3. 【组件】如果是路由参数变化,触发 beforeRouteUpdate
  4. 【配置文件】里, 下一个的 beforeEnter
  5. 【组件】内部声明的 beforeRouteEnter
  6. 【全局】的 afterEach

注意:当路由执行完,就开始页面 vue 的生命周期了(beforeCreate...)

路由元信息 meta

鉴权:鉴定是否有权限 如果需要鉴权页面过多,可以用meta属性添加是否鉴权

const routes = [{
    path: '/',
    meta: {
        title: '首页',
        requiredAuth: true,
    },
  },
]
// 路由全局守卫
router.beforeEach((to,from,next)=>{
    console.log('路由全局守卫: router - beforeEach:',to)
    // if(to.path.includes('news')) { 需要鉴权内容过多时,用 meta
    if( to.matched.some(record=>record.meta.requiredAuth) ) {
        if(localStorage.getItem('userId')) {
            next()
        } else {
            next({name: 'about'})
        }
    } else {
        next()
    }
    
})

异步组件

异步组件 即 实现了路由懒加载

 // 未用异步组件写法,所有资源都会被打包到 app.js 里面去
 import News from './components/News';
 {
     path: '/news',
     component: News
 }
 
// 下面写法自动实现了异步组件加载
// webpackChunkName 改变懒加载时的js名字,比如 news 页面 js 名字为 news.js
 {
    path: '/news',
    component: () => import(/* webpackChunkName: "news" */'@/components/News'),
 }
  • prefetch: 页面中可能用的js,浏览器在空闲的时候加载
  • preload: 页面中用到的js,优先加载,相当于提高优先级
// 默认为以下情况 
<link href="/js/app.js" rel="preload" as="script"> // 具体大小
<link href="/js/login.js" rel="prefetch"> // (prefetch cache)

// Network 中按需加载的 js的 size 大小显示为:(prefetch cache)
  • 去除 prefetch
// 在 package.js 同级目录下新建 vue.config.js (可修改 webpack 配置)
module.exports = {
    // 删除 HTML 相关 webpack 插件
    chainWebpack: config =>{
        // prefetch,当前页面可能会用到的资源,在浏览器空闲时加载
        config.plugins.delete('prefetch')
    }
}
// 变为:
<link href="/js/app.js" rel="preload" as="script">
<script charset="utf-8" src="/js/home.js"></script>
// Network 中按需加载的 js的 size 大小显示为具体大小

注:

  1. 除了首页,都可以用懒加载路由
  2. 如果直接加载某页面,则该页面之前的页面也可能被加载

其他

  • 直接使用 a 链接与使用 router-link 的区别? 使用 a 链接会刷新页面,使用 router-link 不会刷新页面
  • Vue路由怎么跳转打开新窗口?
const obj = {
    path: xxx, // 路由地址
    query: {
        mid: data.id // 可以带参数
    }
};
const {href} = this.$router.resolve(obj);
window.open(href, '_blank');
  • 路由组件和路由为什么解耦,怎么解耦? 当路由中使用到 this.$route.params.id 时,依赖于上个页面传入的属性id,此时不能单独拿出该页面,所以需要解耦
// 在 具体页面的js中添加所用到的属性
export default {
    props: ['id'],
    data() {
        return {
            // id: this.$route.params.id
        }
    }
}
// 在 路由关系表里对应路由添加 props: true
{
    path: '/detail/:id',
    props: true,
    name: 'detail',
    component: ()=> import(/* webpackChunkName: 'detail' */'@/components/Detail')
},

具体代码请到git: vue模板