2020vue-b阶段课程(架构)\第2章 手写Vue-Router
使用 vue/cli 初始化一个带 vue-router 的项目
import Vue from "vue";import VueRouter from "vue-router";import Home from "../views/Home.vue";Vue.use(VueRouter); // 使用Vue-Router插件const routes = [{path: "/",name: "Home",component: Home,},{path: "/about",name: "About",// route level code-splitting// this generates a separate chunk (about.[hash].js) for this route// which is lazy-loaded when the route is visited.component: () =>import(/* webpackChunkName: "about" */ "../views/About.vue"),},];const router = new VueRouter({mode: "history",base: process.env.BASE_URL,routes,});export default router; // 创建Vue-router实例,将实例注入到main.js中
import Vue from "vue";import App from "./App.vue";import router from "./router";new Vue({router,render: (h) => h(App),}).$mount("#app");
实现 install 方法
Vue.use = funciton(plugin, options){plugin.install(this, options)}
实现 install 方法
export default function install(Vue) {_Vue = Vue;Vue.mixin({// 给每个组件添加_routerRoot属性beforeCreate() {if (this.$options.router) { // 如果有 router 属性说明是根实例this._routerRoot = this;this._router = this.$options.router;this._router.init(this); // 初始化路由,这里的 this 指向的是根实例} else {// 儿子找爸爸this._routerRoot = this.$parent && this.$parent._routerRoot;}}})}
为了让所有子组件都有_routerRoot(根实例),所有组件都可以通过this._routerRoot._router获取用户的实例化路由对象。
生成路由表
class VueRouter {constructor(options) {// 生成路由映射表// match 匹配方法// addRoutes 动态添加路由this.matcher = createMatcher(options.routes || []);}}VueRouter.install = install;
import { createRouteMap } from "./create-route-map";export function createMatcher(routes) {// 路径和记录匹配 / recordlet { pathMap } = createRouteMap(routes); // 创建映射表 vfunction match(path) {return pathMap[path];};function addRoutes(routes) {createRouteMap(routes, pathMap);}return {addRoutes,match}}
export function createRouteMap(routes, oldPathMap) {// 如果有oldPathMap 我需要将 routes格式化后放到 oldPathMap 中// 如果没有传递 需要生成一个映射表let pathMap = oldPathMap || {}routes.forEach(route => {addRouteRecord(route, pathMap);})return {pathMap}}function addRouteRecord(route, pathMap, parent) {let path = parent ? `${parent.path}/${route.path}` : route.path;// 将记录 和 路径关联起来let record = { // 最终路径 会匹配到这个记录,里面可以自定义属性等path,component: route.component, // 组件props: route.props || {},parent}pathMap[path] = record;route.children && route.children.forEach(childRoute => {addRouteRecord(childRoute, pathMap, record); // 在循环儿子的时候将父路径也同时传入,目的是为了在子路由添加的时候可以拿到父路径})}
路由模式
this.mode = options.mode || 'hash';switch (this.mode) {case 'hash':this.history = new Hash(this)breakcase 'history':this.history = new HTML5History(this);break}// ...init(app) {const history = this.history;// 初始化时,应该先拿到当前路径,进行匹配逻辑// 让路由系统过度到某个路径const setupHashListener = () => {history.setupListener(); // 监听路径变化}history.transitionTo( // 父类提供方法负责跳转history.getCurrentLocation(), // 子类获取对应的路径// 跳转成功后注册路径监听,为视图更新做准备setupHashListener)}
hash
export default class Hash extends History {constructor(router) {super(router);// hash路由初始化的时候 需要增加一个默认hash值 #/ensureHash();}getCurrentLocation() {return getHash();}setUpListener() {window.addEventListener('hashchange', () => {// hash 值变化 再去切换组件 渲染页面this.transitionTo(getHash());})}}function ensureHash() {if (!window.location.hash) {window.location.hash = '/';}}function getHash() {return window.location.hash.slice(1);}
高版本浏览器可以用 popstate 代替 hashchange 事件,性能更好
h5
import History from './base'export default class HTML5History extends History {constructor(router) {super(router);}getCurrentLocation() {return window.location.pathname;// 获取路径}setUpListener() {window.addEventListener('popstate', () => { // 监听前进和后退this.transitionTo(window.location.pathname);})}pushState(location) {history.pushState({}, null, location);}}
// 路由公共的方法都放在这 大家共用function createRoute(record, location) { // 创建路由const matched = [];// 不停的去父级查找if (record) {while (record) {matched.unshift(record);record = record.parent;} // /about/a => [about,aboutA]}return {...location,matched}}export default class History {constructor(router) {this.router = router;// 有一个数据来保存路径的变化// 当前没有匹配到记录this.current = createRoute(null, {path: '/'}); // => {path:'/',matched:[]}}transitionTo(path, cb) {// 前端路由的实现原理 离不开hash h5let record = this.router.match(path); // 匹配到后this.current = createRoute(record, { path });// 路径变化 需要渲染组件 响应式原理// 我们需要将currrent属性变成响应式的,这样后续更改current 就可以渲染组件了// Vue.util.defineReactive() === defineReactive// 我可以在router-view组件中使用current属性,如果路径变化就可以更新router-view了cb && cb(); // 默认第一次cb是hashchange}}
init( )时 先跳转路径,然后开始监听路径变化
transitionTo
根据路径进行组件的渲染
transitionTo(path, cb) { // {path:'/',matched:[record]}// 前端路由的实现原理 离不开hash h5let record = this.router.match(path); // 匹配到后let route = createRoute(record, { path });// 1.保证跳转的路径 和 当前路径一致// 2.匹配的记录个数 应该和 当前的匹配个数一致 说明是相同路由if (path === this.current.path && route.matched.length === this.current.matched.length) {return}// 在跳转前 我需要先走对应的钩子// 修改current _route 实现跳转的let queue = this.router.beforeHooks;const iterator = (hook,next) =>{ // 此迭代函数可以拿到对应的hookhook(route,this.current,next);}runQueue(queue,iterator,()=>{this.updateRoute(route);cb && cb(); // 默认第一次cb是hashchange// 后置的钩子})// 更新current 需要重新渲染视图// Vue.util.defineReactive();// 如果 两次路由一致 不要跳转了}
createRoute 返回的结果 {path:'about/a',matched:[{...'about' },{...'about/a' }]}
嵌套路由时 要写两层 router-view才可以,先渲染about再渲染about/a
需要将 current 属性变化成响应式的,后续 current 变化会更新视图
// vuex中的 state 在哪里使用就会收集对应的 watcher// current 里面的属性在哪使用,就会收集对应的 watcherVue.util.defineReactive(this,'_route',this._router.history.current);
要改变_route需要传回调函数进去,对_route重新赋值
history.listen((route)=>{// 监听 监听如果current变化了 就重新的给 _route赋值app._route = route;})
组件
Object.defineProperty(Vue.prototype,'$router',{ // 方法get(){return this._routerRoot._router}})Object.defineProperty(Vue.prototype,'$route',{ // 属性get(){return this._routerRoot._route}});Vue.component('router-link',RouterLink)Vue.component('router-view',RouterView)
router-view
export default {functional:true,render(h,{parent,data}){ // current = {matched:[]} .$route // data里面我可以增加点标识// 内部current变成了响应式的// 真正用的是$route this.$route = current; current = xxxlet route = parent.$route; // 获取current对象// 依次的将matched 的结果赋予给每个router-view// 父 * 父 * -> 父 * -> 子 *let depth = 0;while (parent) { // 1.得是组件 <router-view></router-view> <app></app>if(parent.$vnode && parent.$vnode.data.routerView ){depth++;}parent = parent.$parent; // 不停的找父亲}// 两个router-view [ /about /about/a] /about/alet record = route.matched[depth]; // 默认肯定先渲染第一层if(!record){return h() // 空}// 渲染匹配到的组件,这里一直渲染的是第一个data.routerView = true;return h(record.component, data); // <router-view routeView=true></router-view>}}
嵌套路由,record 是数组,通过找有几级父亲及渲染标致,判断该渲染 record 第几条记录
router-link
export default {functional: true, // 函数式组件, 会导致render函数中没有this了// 正常组件是一个类 this._init() 如果是函数式组件就是一个普通函数props: { // 属性校验to: {type: String,required: true}},// render的第二个函数 是内部自己声明一个对象render(h, { props, slots, data, parent }) { // render 方法和 template 等价的 -> template语法需要被编译成render函数const click = () => {// 组件中的$routerparent.$router.push(props.to)}// jsx 和 react语法一样 < 开头的表示的是html {} js属性return <a onClick = { click } > { slots().default } </a>}}
钩子
function runQueue(queue,iterator,cb){function step(index){if(index >= queue.length) return cb();let hook = queue[index];iterator(hook,()=>step(index+1)); // 第二个参数什么时候调用就走下一次的}step(0);}
