文章目录
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
}
路由核心组件 RouterLink 实现
// 简化版 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)
}
}
}