2020vue-b阶段课程(架构)\第2章 手写Vue-Router

使用 vue/cli 初始化一个带 vue-router 的项目

  1. import Vue from "vue";
  2. import VueRouter from "vue-router";
  3. import Home from "../views/Home.vue";
  4. Vue.use(VueRouter); // 使用Vue-Router插件
  5. const routes = [
  6. {
  7. path: "/",
  8. name: "Home",
  9. component: Home,
  10. },
  11. {
  12. path: "/about",
  13. name: "About",
  14. // route level code-splitting
  15. // this generates a separate chunk (about.[hash].js) for this route
  16. // which is lazy-loaded when the route is visited.
  17. component: () =>
  18. import(/* webpackChunkName: "about" */ "../views/About.vue"),
  19. },
  20. ];
  21. const router = new VueRouter({
  22. mode: "history",
  23. base: process.env.BASE_URL,
  24. routes,
  25. });
  26. export default router; // 创建Vue-router实例,将实例注入到main.js中
  1. import Vue from "vue";
  2. import App from "./App.vue";
  3. import router from "./router";
  4. new Vue({
  5. router,
  6. render: (h) => h(App),
  7. }).$mount("#app");

实现 install 方法

  1. Vue.use = funciton(plugin, options){
  2. plugin.install(this, options)
  3. }

实现 install 方法

  1. export default function install(Vue) {
  2. _Vue = Vue;
  3. Vue.mixin({
  4. // 给每个组件添加_routerRoot属性
  5. beforeCreate() {
  6. if (this.$options.router) { // 如果有 router 属性说明是根实例
  7. this._routerRoot = this;
  8. this._router = this.$options.router;
  9. this._router.init(this); // 初始化路由,这里的 this 指向的是根实例
  10. } else {
  11. // 儿子找爸爸
  12. this._routerRoot = this.$parent && this.$parent._routerRoot;
  13. }
  14. }
  15. })
  16. }

为了让所有子组件都有_routerRoot(根实例),所有组件都可以通过this._routerRoot._router获取用户的实例化路由对象。

生成路由表

  1. class VueRouter {
  2. constructor(options) {
  3. // 生成路由映射表
  4. // match 匹配方法
  5. // addRoutes 动态添加路由
  6. this.matcher = createMatcher(options.routes || []);
  7. }
  8. }
  9. VueRouter.install = install;
  1. import { createRouteMap } from "./create-route-map";
  2. export function createMatcher(routes) {
  3. // 路径和记录匹配 / record
  4. let { pathMap } = createRouteMap(routes); // 创建映射表 v
  5. function match(path) {
  6. return pathMap[path];
  7. };
  8. function addRoutes(routes) {
  9. createRouteMap(routes, pathMap);
  10. }
  11. return {
  12. addRoutes,
  13. match
  14. }
  15. }
  1. export function createRouteMap(routes, oldPathMap) {
  2. // 如果有oldPathMap 我需要将 routes格式化后放到 oldPathMap 中
  3. // 如果没有传递 需要生成一个映射表
  4. let pathMap = oldPathMap || {}
  5. routes.forEach(route => {
  6. addRouteRecord(route, pathMap);
  7. })
  8. return {
  9. pathMap
  10. }
  11. }
  12. function addRouteRecord(route, pathMap, parent) {
  13. let path = parent ? `${parent.path}/${route.path}` : route.path;
  14. // 将记录 和 路径关联起来
  15. let record = { // 最终路径 会匹配到这个记录,里面可以自定义属性等
  16. path,
  17. component: route.component, // 组件
  18. props: route.props || {},
  19. parent
  20. }
  21. pathMap[path] = record;
  22. route.children && route.children.forEach(childRoute => {
  23. addRouteRecord(childRoute, pathMap, record); // 在循环儿子的时候将父路径也同时传入,目的是为了在子路由添加的时候可以拿到父路径
  24. })
  25. }

路由模式

  1. this.mode = options.mode || 'hash';
  2. switch (this.mode) {
  3. case 'hash':
  4. this.history = new Hash(this)
  5. break
  6. case 'history':
  7. this.history = new HTML5History(this);
  8. break
  9. }
  10. // ...
  11. init(app) {
  12. const history = this.history;
  13. // 初始化时,应该先拿到当前路径,进行匹配逻辑
  14. // 让路由系统过度到某个路径
  15. const setupHashListener = () => {
  16. history.setupListener(); // 监听路径变化
  17. }
  18. history.transitionTo( // 父类提供方法负责跳转
  19. history.getCurrentLocation(), // 子类获取对应的路径
  20. // 跳转成功后注册路径监听,为视图更新做准备
  21. setupHashListener
  22. )
  23. }

hash

  1. export default class Hash extends History {
  2. constructor(router) {
  3. super(router);
  4. // hash路由初始化的时候 需要增加一个默认hash值 #/
  5. ensureHash();
  6. }
  7. getCurrentLocation() {
  8. return getHash();
  9. }
  10. setUpListener() {
  11. window.addEventListener('hashchange', () => {
  12. // hash 值变化 再去切换组件 渲染页面
  13. this.transitionTo(getHash());
  14. })
  15. }
  16. }
  17. function ensureHash() {
  18. if (!window.location.hash) {
  19. window.location.hash = '/';
  20. }
  21. }
  22. function getHash() {
  23. return window.location.hash.slice(1);
  24. }

高版本浏览器可以用 popstate 代替 hashchange 事件,性能更好

h5

  1. import History from './base'
  2. export default class HTML5History extends History {
  3. constructor(router) {
  4. super(router);
  5. }
  6. getCurrentLocation() {
  7. return window.location.pathname;// 获取路径
  8. }
  9. setUpListener() {
  10. window.addEventListener('popstate', () => { // 监听前进和后退
  11. this.transitionTo(window.location.pathname);
  12. })
  13. }
  14. pushState(location) {
  15. history.pushState({}, null, location);
  16. }
  17. }
  1. // 路由公共的方法都放在这 大家共用
  2. function createRoute(record, location) { // 创建路由
  3. const matched = [];
  4. // 不停的去父级查找
  5. if (record) {
  6. while (record) {
  7. matched.unshift(record);
  8. record = record.parent;
  9. } // /about/a => [about,aboutA]
  10. }
  11. return {
  12. ...location,
  13. matched
  14. }
  15. }
  16. export default class History {
  17. constructor(router) {
  18. this.router = router;
  19. // 有一个数据来保存路径的变化
  20. // 当前没有匹配到记录
  21. this.current = createRoute(null, {
  22. path: '/'
  23. }); // => {path:'/',matched:[]}
  24. }
  25. transitionTo(path, cb) {
  26. // 前端路由的实现原理 离不开hash h5
  27. let record = this.router.match(path); // 匹配到后
  28. this.current = createRoute(record, { path });
  29. // 路径变化 需要渲染组件 响应式原理
  30. // 我们需要将currrent属性变成响应式的,这样后续更改current 就可以渲染组件了
  31. // Vue.util.defineReactive() === defineReactive
  32. // 我可以在router-view组件中使用current属性,如果路径变化就可以更新router-view了
  33. cb && cb(); // 默认第一次cb是hashchange
  34. }
  35. }

init( )时 先跳转路径,然后开始监听路径变化

transitionTo

根据路径进行组件的渲染

  1. transitionTo(path, cb) { // {path:'/',matched:[record]}
  2. // 前端路由的实现原理 离不开hash h5
  3. let record = this.router.match(path); // 匹配到后
  4. let route = createRoute(record, { path });
  5. // 1.保证跳转的路径 和 当前路径一致
  6. // 2.匹配的记录个数 应该和 当前的匹配个数一致 说明是相同路由
  7. if (path === this.current.path && route.matched.length === this.current.matched.length) {
  8. return
  9. }
  10. // 在跳转前 我需要先走对应的钩子
  11. // 修改current _route 实现跳转的
  12. let queue = this.router.beforeHooks;
  13. const iterator = (hook,next) =>{ // 此迭代函数可以拿到对应的hook
  14. hook(route,this.current,next);
  15. }
  16. runQueue(queue,iterator,()=>{
  17. this.updateRoute(route);
  18. cb && cb(); // 默认第一次cb是hashchange
  19. // 后置的钩子
  20. })
  21. // 更新current 需要重新渲染视图
  22. // Vue.util.defineReactive();
  23. // 如果 两次路由一致 不要跳转了
  24. }

createRoute 返回的结果 {path:'about/a',matched:[{...'about' },{...'about/a' }]}

嵌套路由时 要写两层 router-view才可以,先渲染about再渲染about/a

需要将 current 属性变化成响应式的,后续 current 变化会更新视图

  1. // vuex中的 state 在哪里使用就会收集对应的 watcher
  2. // current 里面的属性在哪使用,就会收集对应的 watcher
  3. Vue.util.defineReactive(this,'_route',this._router.history.current);

要改变_route需要传回调函数进去,对_route重新赋值

  1. history.listen((route)=>{
  2. // 监听 监听如果current变化了 就重新的给 _route赋值
  3. app._route = route;
  4. })

组件

  1. Object.defineProperty(Vue.prototype,'$router',{ // 方法
  2. get(){
  3. return this._routerRoot._router
  4. }
  5. })
  6. Object.defineProperty(Vue.prototype,'$route',{ // 属性
  7. get(){
  8. return this._routerRoot._route
  9. }
  10. });
  11. Vue.component('router-link',RouterLink)
  12. Vue.component('router-view',RouterView)

router-view

  1. export default {
  2. functional:true,
  3. render(h,{parent,data}){ // current = {matched:[]} .$route // data里面我可以增加点标识
  4. // 内部current变成了响应式的
  5. // 真正用的是$route this.$route = current; current = xxx
  6. let route = parent.$route; // 获取current对象
  7. // 依次的将matched 的结果赋予给每个router-view
  8. // 父 * 父 * -> 父 * -> 子 *
  9. let depth = 0;
  10. while (parent) { // 1.得是组件 <router-view></router-view> <app></app>
  11. if(parent.$vnode && parent.$vnode.data.routerView ){
  12. depth++;
  13. }
  14. parent = parent.$parent; // 不停的找父亲
  15. }
  16. // 两个router-view [ /about /about/a] /about/a
  17. let record = route.matched[depth]; // 默认肯定先渲染第一层
  18. if(!record){
  19. return h() // 空
  20. }
  21. // 渲染匹配到的组件,这里一直渲染的是第一个
  22. data.routerView = true;
  23. return h(record.component, data); // <router-view routeView=true></router-view>
  24. }
  25. }

嵌套路由,record 是数组,通过找有几级父亲及渲染标致,判断该渲染 record 第几条记录

router-link

  1. export default {
  2. functional: true, // 函数式组件, 会导致render函数中没有this了
  3. // 正常组件是一个类 this._init() 如果是函数式组件就是一个普通函数
  4. props: { // 属性校验
  5. to: {
  6. type: String,
  7. required: true
  8. }
  9. },
  10. // render的第二个函数 是内部自己声明一个对象
  11. render(h, { props, slots, data, parent }) { // render 方法和 template 等价的 -> template语法需要被编译成render函数
  12. const click = () => {
  13. // 组件中的$router
  14. parent.$router.push(props.to)
  15. }
  16. // jsx 和 react语法一样 < 开头的表示的是html {} js属性
  17. return <a onClick = { click } > { slots().default } </a>
  18. }
  19. }

钩子

  1. function runQueue(queue,iterator,cb){
  2. function step(index){
  3. if(index >= queue.length) return cb();
  4. let hook = queue[index];
  5. iterator(hook,()=>step(index+1)); // 第二个参数什么时候调用就走下一次的
  6. }
  7. step(0);
  8. }