一、Vue-Router基本应用
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import About from './views/About.vue'
Vue.use(Router);// 使用Vue-Router插件 讲vueRouter实例 挂到Vue的原型对象上
export default new Router({ // 创建Vue-router实例,将实例注入到main.js中
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: About,
children: [
{
path: 'a', component: {
render(h) {return <h3>about A</h3>}
}
},
{
path: 'b', component: {
render(h) {return <h3>about B</h3>}
}
}
]
}
]
})
new Vue({
router, // 在根实例中注入router实例
render: h => h(App)
}).$mount('#app')
二.手写 Vue-Router
自己写一个Vue-router插件,根据源码做得文件目录创建
├─vue-router
│ ├─components # 存放vue-router的两个核心组件
│ │ ├─link.js
│ │ └─view.js
│ ├─history # 存放浏览器跳转相关逻辑
│ │ ├─base.js
│ │ └─hash.js
│ ├─create-matcher.js # 创建匹配器
│ ├─create-route-map.js # 创建路由映射表
│ ├─index.js # 引用时的入口文件
│ ├─install.js # install方法
1、 index.js 文件
use方法默认会调用当前返回对象的install方法
// index.js
import install from './install'
import HashHistory from './history/hash'
import createMatcher from './creat-matcher'
export default class VueRouter {
constructor (options) {
this.matcher = createMatcher(options.routes || [])
// vue路由有三种模式 hash / h5api /abstract ,为了保证调用时方法一致。
// 我们需要提供一个base类,在分别实现子类,不同模式下通过父类调用对应子类的方法
this.history = new HashHistory(this)
// beforeEach钩子集合
this.beforeHooks = [];
}
beforeEach(fn){ // 将fn注册到队列中
this.beforeHooks.push(fn);
}
match (location) {
return this.matcher.match(location)
}
// 方便页面上函数式跳转
push (url) {
this.history.push(url)
}
init (app) {
const history = this.history
// 初始化时,应该先拿到当前路径,进行匹配逻辑
// 让路由系统过度到某个路径
const setupHashListener = () => {
history.setupListener() // 监听路径变化
}
history.transitionTo(
// 父类提供方法负责跳转
history.getCurrentLocation(), // 子类获取对应的路径
// 跳转成功后注册路径监听,为视图更新做准备
setupHashListener
)
history.listen(route => {
// 需要更新_route属性
app._route = route
})
}
}
VueRouter.install = install // 提供的install方法
2、install.js中 编写install方法
import RouterView from './components/view'
import RouterLink from './components/link'
export let _Vue
export default function install (Vue) {
_Vue = Vue
Vue.mixin({
// 给所有组件的生命周期都增加beforeCreate方法
beforeCreate () {
if (this.$options.router) {
// 如果有router属性说明是根实例
this._routerRoot = this // 将根实例挂载在_routerRoot属性上
this._router = this.$options.router // 将当前router实例挂载在_router上
this._router.init(this) // 初始化路由,这里的this指向的是根实例
Vue.util.defineReactive(this, '_route', this._router.history.current)
// console.log('我是根组件',this)
} else {
// 父组件渲染后会渲染子组件
this._routerRoot = this.$parent && this.$parent._routerRoot
// 保证所有子组件都拥有_routerRoot 属性,指向根实例
// 保证所有组件都可以通过 this._routerRoot._router 拿到用户传递进来的路由实例对象
// console.log('我是子组件组件',this)
}
}
})
Vue.component('router-view', RouterView)
Vue.component('router-link', RouterLink)
// 仅仅是为了更加方便
Object.defineProperty(Vue.prototype, '$route', {
// 每个实例都可以获取到$route属性
get () {
return this._routerRoot._route
}
})
Object.defineProperty(Vue.prototype, '$router', {
// 每个实例都可以获取router实例
get () {
return this._routerRoot._router
}
})
}
2、create-matcher.js 页面上所有的router-view组件 用一个适配器 对应上
import createRouteMap from './create-route-map'
export default function createMatcher(routes) {
// 收集所有的路由路径, 收集路径的对应渲染关系
// pathList = ['/','/about','/about/a','/about/b']
// pathMap = {'/':{path:'/',component,parent},'/about':{path:'/',component,parent}...}
let {pathList,pathMap} = createRouteMap(routes);
// 这个方法就是动态加载路由的方法
function addRoutes(routes){
// 将新增的路由追加到pathList和pathMap中
createRouteMap(routes,pathList,pathMap);
}
function match(location){ // 稍后根据路径找到对应的记录
let record = pathMap[location]
if (record) { // 根据记录创建对应的路由
return createRoute(record,{
path:location
})
}
// 找不到则返回空匹配
return createRoute(null, {
path: location
})
}
return {
addRoutes,
match
}
}
3、create-route-map.js 需要创建路由映射关系
export default function createRouteMap(routes,oldPathList,oldPathMap){
// 当第一次加载的时候没有 pathList 和 pathMap
let pathList = oldPathList || [];
let pathMap = oldPathMap || Object.create(null);
routes.forEach(route=>{
// 添加到路由记录,用户配置可能是无限层级,稍后要递归调用此方法
addRouteRecord(route,pathList,pathMap);
});
return { // 导出映射关系
pathList,
pathMap
}
}
// 将当前路由存储到pathList和pathMap中
function addRouteRecord(route,pathList,pathMap,parent){
// 如果是子路由记录 需要增加前缀
let path = parent?`${parent.path}/${route.path}`:route.path;
let record = { // 提取需要的信息
path,
component:route.component,
parent
}
if(!pathMap[path]){
pathList.push(path);
pathMap[path] = record;
}
if(route.children){ // 递归添加子路由
route.children.forEach(r=>{
// 这里需要标记父亲是谁
addRouteRecord(r,pathList,pathMap,route);
})
}
}
4.与浏览器相关代码
写一个基本的功能的history 方便几种路由扩展
// history/base.js
export function createRoute (record, location) {
// {path:'/',matched:[record,record]}
let res = []
if (record) {
// 如果有记录
while (record) {
res.unshift(record) // 就将当前记录的父亲放到前面
record = record.parent
}
}
return {
...location,
matched: res
}
}
// 钩子迭代器
function runQueue(queue, iterator,cb) {
function step(index){
if(index >= queue.length){
cb();
}else{
let hook = queue[index];
iterator(hook,()=>{ // 将本次迭代到的hook 传递给iterator函数中,将下次的权限也一并传入
step(index+1)
})
}
}
step(0)
}
// 路由的基类
export default class History {
constructor (router) {
this.router = router
// 根据记录和路径返回对象,稍后会用于router-view的匹配
this.current = createRoute(null, {
path: '/'
})
this.cb = null
}
listen (cb) {
this.cb = cb // 注册函数
}
// 核心逻辑
transitionTo (location, onComplete) {
// 去匹配路径
let route = this.router.match(location)
// 相同路径不必过渡
if (location === this.current.path && route.matched.length === this.current.matched.length) {
return
}
this.updateRoute(route) // 更新路由即可
// 不走钩子 onComplete && onComplete()
// 走钩子
const iterator = (hook, next) => {
hook(route,this.current,()=>{ // 分别对应用户 from,to,next参数
next();
});
}
runQueue(queue, iterator, () => { // 依次执行队列 ,执行完毕后更新路由
this.updateRoute(route);
onComplete && onComplete();
});
}
updateRoute (route) {
this.current = route
this.cb && this.cb(route) // 更新current后 更新_route属性
}
}
以hash路由为主,创建hash路由实例
// history/hash.js
import History from './base'
如果是`hash`路由,打开网站如果没有`hash`默认应该添加`#/`
function ensureSlash () {
if (window.location.hash) {
return
}
window.location.hash = '/'
}
// hash路由
function getHash () {
return window.location.hash.slice(1)
}
export default class HashHistory extends History {
constructor (router) {
super(router)
ensureSlash() // 确保有hash
}
push(url){
window.location.hash = url
}
getCurrentLocation () {
return getHash()
}
setupListener () {
window.addEventListener('hashchange', () => {
// 根据当前hash值 过度到对应路径
// console.log(getHash())
this.transitionTo(getHash())
})
}
}
三、编写Router-Link及Router-View组件
1、router-view组件
export default {
functional:true,
render(h,{parent,data}){
let route = parent.$route;
let depth = 0;
data.routerView = true;
while(parent){ // 根据matched 渲染对应的router-view
if (parent.$vnode && parent.$vnode.data.routerView){
depth++;
}
parent = parent.$parent;
}
let record = route.matched[depth];
if(!record){
return h();
}
return h(record.component, data);
}
}
2、 router-link组件
export default {
props:{
to:{
type:String,
required:true
},
tag:{
type:String,
default:'a'
}
},
render(h){
let handler = ()=>{
this.$router.push(this.to);
}
return <a onClick={handler}>{this.$slots.default}</a>
}
}