Skip to content
Go back

Vue3 Router的简易实现

Published:  at  04:15 PM

文章目录

Router的状态管理和跳转实现

生成当前url

根据地址栏创建当前位置的url。

需要注意的是,vue-router3的hash模式和history模式的实现区别,仅仅是hash模式在地址栏中会多一个#。其他逻辑完全相同。

function createCurrentLocation(base = "") {
  const { pathname, search, hash } = window.location;
  const hasHash = base.indexOf('#') > -1;
  if (hasHash) {
    return base.slice(1) || '/';
  }
  return pathname + search + hash;
}

构建状态

这是要传递给history.pushState或者replaceState的state对象。使我们前进后退的时候,可以知晓从哪里来,到哪里去。而不只是一个简单的url地址。

function buildState(back, current, forward, replace = false, computeScroll = false) {
  return {
    back,
    current,
    forward,
    replace,
    scroll: computeScroll ? { left: window.pageXOffset, top: window.pageYOffset } : null,
    position: window.history.length - 1,
  }
}

维护当前位置和历史状态

function useHistoryStateNavigation(base) {
  const currentLocation = {
    value: createCurrentLocation(base),
  };
  const currentLocationState = {
    value: window.history.state,
  }

  // 维护当前位置和历史状态
  // push跳转还是replace跳转,跳转后的位置
  if (currentLocationState.value === null) {
    changeLocation(currentLocation.value, buildState(null, currentLocation.value, null), true)
  }

  // 地址栏变更操作,更新当前位置和历史状态
  function changeLocation(to, state, replace = false) {
    const hasHash = base.indexOf('#') > -1;
    const url = hasHash ? base + to : to;
    window.history[replace ? 'replaceState' : 'pushState'](state, '', url)
    currentLocationState.value = state;
  }

  // 入栈操作,更新当前位置和历史状态
  function push(to, data = {}) {
    // a -> b
    // 更新跳转之前的状态,保存下页面位置,然后替换掉当前状态
    const currentState = Object.assign(
      {},
      currentLocationState.value,
      { forward: to, scroll: { left: window.pageXOffset, top: window.pageYOffset } }
    );
    // 本质没跳转,这是在跳转前更新上一次的状态,方便后退时恢复到当前页面位置。
    changeLocation(currentState.current, currentState, true);

    const state = Object.assign(
      {},
      buildState(currentLocation.value, to, null),
      { postition: currentState.postition + 1 },
      data
    );

    // 执行跳转,新的页面位置入栈,更新当前位置和历史状态。
    changeLocation(to, state, false);
    currentLocation.value = to;
  }

  // 替换当前位置操作,更新历史状态
  function replace(to, data) {
    const state = Object.assign(
      {},
      buildState(currentLocationState.value.back, to, currentLocationState.value.forward, true),
      data
    );
    changeLocation(to, state, true);
    currentLocation.value = to; // 更新当前位置
  }

  return {
    location: currentLocation,
    state: currentLocationState,
    push,
    replace
  };
}

监听浏览器前进后退事件,更新当前位置和历史状态

注意:replaceState和pushState方法的调用并不会触发popstate事件,只有浏览器行为(前进、后退)才会触发。


function useHistoryListeners(base, currentLocationState, currentLocation) {
  let listeners = []

  // popstate事件触发时,更新当前位置和历史状态
  // 
  const popHandler = ({ state }) => {

    // 此函数被触发的时候,地址栏的路由已完成了跳转,但是vue-router的路由对象信息还没更新

    const toUrl = createCurrentLocation(base);      // 此时地址栏已更新,据此获取最新的地址
    const fromUrl = currentLocation.value;          // vue-router记录的之前的"当前地址",即上一个状态的位置
    const fromState = currentLocationState.value;   // vue-router记录的之前的"当前地址状态对象"
    currentLocation.value = toUrl;                  // 更新vue-router记录的当前地址
    currentLocationState.value = state;             // 更新vue-router记录的当前地址状态对象,注意这里的state是浏览器地址栏最新的状态对象

    const isBack = state.position - fromState.position < 0

    listeners.forEach(listener => {
      listener(toUrl, fromUrl, { isBack })
    })
  }

  window.addEventListener("popstate", popHandler)

  function listen(listener) {
    listeners.push(listener)
  }

  return {
    listen
  }
}

创建history模式对象

function createWebHistory(base = "") {
  const historyNavigation = useHistoryStateNavigation(base);
  const { location, state } = historyNavigation;
  const historyListeners = useHistoryListeners(base, state, location);

  const routerHistory = Object.assign({}, historyNavigation, historyListeners);

  Object.defineProperty(routerHistory, 'state', {
    get: () => state.value,
  })

  Object.defineProperty(routerHistory, 'location', {
    get: () => location.value,
  })

  return routerHistory
}

export {
  createWebHistory
}

创建hash模式对象

function createWebHashHistory(base = "#") {
  return createWebHistory(base);
}

路由核心实现

import { shallowRef, computed, reactive, unref, inject } from 'vue';
import { createWebHashHistory } from './hash';
import { createWebHistory } from './history';
import { RouterLink } from './router-link';
import { RouterView } from './router-view';

const routerSymbol = Symbol('router');
const routeSymbol = Symbol('route');

function normalizeRouteRecord(record) {
  return {
    path: record.path, //状态机 解析路径的分数,算出匹配规则
    meta: record.meta || {},
    beforeEnter: record.beforeEnter,
    name: record.name,
    components: {
      default: record.component,//循环
    },
    children: record.children || []
  }
}

function createRouteRecordMatcher(record, parent) {
  const matcher = {
    path: record.path,
    record,
    parent,
    children: []
  }
  if (parent) {
    parent.children.push(matcher)
  }
  return matcher
}

function createRouterMatcher(routes) {
  const matchers = [];
  function addRoute(route, parent) {
    let normalizedRecord = normalizeRouteRecord(route)
    if (parent) {
      normalizedRecord.path = `${parent.path === '/' ? "" : parent.path}/${normalizedRecord.path}`
    }

    const matcher = createRouteRecordMatcher(normalizedRecord, parent)

    if ('children' in normalizedRecord) {
      let children = normalizedRecord.children;
      for (let i = 0; i < children.length; i++) {
        addRoute(children[i], matcher);
      }
    }

    matchers.push(matcher)
  }

  routes.forEach(route => addRoute(route))

  console.log(matchers, 'matchers')

  function resolve(location) {
    let matched = [];
    let path = location.path;
    let matcher = matchers.find(m => m.path === path);
    while (matcher) {
      matched.unshift(matcher.record);
      matcher = matcher.parent;
    }
    return { path, matched } // 匹配到的路由记录
  }

  return {
    resolve,
    addRoute,
    routes: matchers
  }
}

const START_LOCATION_NORMALIZED = {
  path: '/',
  // params: {},
  // query: {},
  matched: [],
}

function useCallback() {
  const handlers = [];
  function add(fn) {
    handlers.push(fn)
  }

  return {
    add,
    list: () => handlers
  }
}

function extractChangeRecords(to, from) {
  const [leavingRecords, updatingRecords, enteringRecords] = [[], [], []];

  const len = Math.max(to.matched.length, from.matched.length);

  for (let i = 0; i < len; i++) {
    const recordFrom = from.matched[i];
    if (recordFrom) {
      // 来的路径和去的路径有相同的记录,认为是更新操作,否则认为来的路径是离开操作
      if (to.matched.find(record => recordFrom.path === record.path)) {
        updatingRecords.push(recordFrom)
      } else {
        leavingRecords.push(recordFrom)
      }
    }

    const recordTo = to.matched[i];
    if (recordTo) {
      // 去的路径和来的路径没有相同的记录,认为是进入操作
      if (!from.matched.find(record => recordTo.path === record.path)) {
        enteringRecords.push(recordTo)
      }
    }
  }

  return [leavingRecords, updatingRecords, enteringRecords]
}

function guardToPromise(guard, to, from, record) {
  return new Promise((resolve, reject) => {
    const next = () => resolve();
    let guardReturn = guard.call(record, to, from, next)

    // 如果用户不调用next,最终也会调用next,所以这里要兜底处理
    return Promise.resolve(guardReturn).then(next)
  })
}

function extractComponentGuards(matched, guardType, to, from) {
  const guards = []
  for (const record of matched) {
    const rawComponent = record.components.default;
    const guard = rawComponent[guardType];

    // 需要将钩子全部串联
    guard && guards.push(guardToPromise(guard, to, from));
  }

  return guards

}

// Promise组合函数
function runGuardsQueue(guards) {
  console.log(guards);
  if (guards.length === 0) return Promise.resolve()
  return guards.reduce((promise, guard) => promise.then(() => guard), Promise.resolve())
}

function createRouter(options) {
  const routerHistory = options.history;
  const matcher = createRouterMatcher(options.routes)

  // 后续路由改变依靠此ref
  const currentRoute = shallowRef(START_LOCATION_NORMALIZED)

  const beforeGuards = useCallback();
  const beforeResolveGuards = useCallback();
  const afterGuards = useCallback();

  function resolve(to) {
    if (typeof to === 'string') {
      return matcher.resolve({ path: to })
    }
  }

  let ready;
  function markAsReady() {
    if (ready) return
    ready = true;
    routerHistory.listen((to) => {
      const targetLocation = resolve(to);
      const from = currentRoute.value;
      finializeNavigation(targetLocation, from, true)
    })
  }

  function finializeNavigation(to, from, replace) {
    if (from === START_LOCATION_NORMALIZED || replace) {
      routerHistory.replace(to.path);
    } else {
      routerHistory.push(to.path);
    }
    currentRoute.value = to;  // 更新最新路由

    // 如果是初始化,我们还需要注入一个listen去更新currentRoute,数据变化触发视图更新
    markAsReady();

  }

  async function navigate(to, from) {
    // 导航的时候,要清楚离开,进入和更新的组件,分别是哪个
    const [leavingRecords, updatingRecords, enteringRecords] = extractChangeRecords(to, from);
    console.log(leavingRecords, updatingRecords, enteringRecords);

    // 保证路由守卫卸载顺序是先子后父
    let guards = extractComponentGuards(
      leavingRecords.reverse(),
      'beforeRouteLeave',
      to,
      from
    );

    return runGuardsQueue(guards).then(() => {
      guards = [];
      for (const guard of beforeGuards.list()) {
        // 这是全局钩子,在路由跳转前执行
        guards.push(guardToPromise(guard, to, from));
      }
      return runGuardsQueue(guards)
    }).then(() => {
      guards = [];
      guards = extractComponentGuards(
        updatingRecords,
        'beforeRouteUpdate',
        to,
        from
      );
      return runGuardsQueue(guards)
    }).then(() => {
      guards = [];
      for (const record of to.matched) {
        if (record.beforeEnter) {
          // 这是在定义路由表时的钩子,在路由跳转前执行
          guards.push(guardToPromise(record.beforeEnter, to, from, record));
        }
      }
      return runGuardsQueue(guards)
    }).then(() => {
      guards = [];
      guards = extractComponentGuards(
        enteringRecords,
        'beforeRouteEnter',
        to,
        from
      );
      return runGuardsQueue(guards)
    }).then(() => {
      guards = [];
      for (const guard of beforeResolveGuards.list()) {
        // 这是全局钩子,在路由跳转前执行
        guards.push(guardToPromise(guard, to, from));
      }
      return runGuardsQueue(guards)
    })
  }

  function pushWithRedirect(to) {
    const targetLocation = resolve(to);
    const from = currentRoute.value;

    // 路由钩子 在跳转前可以做路由的拦截

    navigate(targetLocation, from).then(() => {
      // 路由导航守卫,全局钩子 路由钩子 组件钩子
      return finializeNavigation(targetLocation, from);
    }).then(() => {
      // 导航切换完成后执行 
      for (let guard of afterGuards.list()) guard(to, from);
    })

    // 最外层的路由守卫,全局前置守卫

  }

  function push(to) {
    return pushWithRedirect(to)
  }

  const router = {
    push,
    beforeEach: beforeGuards.add,
    beforeResolve: beforeResolveGuards.add,
    afterEach: afterGuards.add,
    currentRoute,
    getRoutes: () => matcher.routes,
    install: (app) => {
      // vue2中有两个属性 $router $route, 一个是路由实例,一个是当前路由信息,这里简化处理
      app.config.globalProperties.$router = router;
      Object.defineProperty(app.config.globalProperties, '$route', {
        enumerable: true,
        get: () => unref(currentRoute)
      })

      const reactiveRoute = {};
      for (let key in START_LOCATION_NORMALIZED) {
        reactiveRoute[key] = computed(() => currentRoute.value[key])
      };

      app.provide(routerSymbol, router);
      app.provide(routeSymbol, reactive(reactiveRoute));

      // let router = useRouter();  // 实现原理inject(routerSymbol);
      // let route = useRoute();   // inject(routeSymbol);

      app.component('RouterLink', RouterLink);
      app.component('RouterView', RouterView);

      if (currentRoute.value === START_LOCATION_NORMALIZED) {
        // 初始化路由,首次加载页面时触发
        push(routerHistory.location);
      };

      // 解析路径,router-link,router-view实现,页面钩子,从离开到进入,到解析完成
    }
  }

  return router
}

export {
  createRouter,
  createWebHistory,
  createWebHashHistory,
  routerSymbol,
  routeSymbol
}
// 简化版 router-link 实现
import { h, inject } from "vue";
import { routerSymbol } from "./index.js";

function useLink(props) {
  const router = inject(routerSymbol);
  function navigate() {
    router.push(props.to)
  }

  return { navigate }
}

export const RouterLink = {
  name: "RouterLink",
  props: {
    to: {
      type: [String, Object],
      required: true
    }
  },
  setup: (props, { slots }) => {
    const link = useLink(props)
    return () => {
      return h(
      "a", 
      {
        onClick: link.navigate
      },
      slots.default?.())
    }
  }
}

路由核心组件 RouterView 实现

import { h, provide, inject, computed } from "vue";
import { routeSymbol } from "./index.js";

export const RouterView = {
  name: "RouterView",
  setup(props, { slots }) {
    // 层级深度,嵌套路由时使用,获取父级的深度
    const depth = inject('depth', 0);
    const injectRoute = inject(routeSymbol)
    const matchedRouteRef = computed(() => injectRoute.matched[depth]);

    // 传递深度给子组件的RouterView组件使用
    provide('depth', depth + 1);

    return () => {
      const matchRoute = matchedRouteRef.value;
      const viewComponent = matchRoute && matchRoute.components.default;
      if (!viewComponent) {
        return slots.default?.()
      }

      return h(viewComponent)
    }
  }
}


Previous Post
Promise的实现原理详解
Next Post
docker常用命令