Appearance
封装路由系统
什么是路由系统:
- 1.记录当前"路径"和跳转时所带的"数据",实现跳转功能和替换功能、
- 2.当路径变化时可以”监听路径"的变化
一.创建路由系统
function createWebHistory() { // 创建history模式路由
const historyNavigation = useHistoryStateNavigation(); //实现功能(1)
const historyListeners = useHistoryListeners(); //实现功能(2)
const routerHistory = Object.assign(
{},
historyNavigation,
historyListeners
)// 合并功能导出
return routerHistory
}
createWebHistory();
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
1.useHistoryStateNavigation
实现
function createCurrentLocation() {
const { pathname, search, hash } = location;
return pathname + search + hash;
}
function useHistoryStateNavigation() {
const { history, location } = window; // 获取浏览器history对象和location对象
const currentLocation = {
value: createCurrentLocation() // 完整的路径由 location中路径+查询参数+hash值
}
const historyState = { // 当前跳转路径所带的参数
value: history.state
}
if (!historyState.value) { // 如果没有数据,增添一些默认数据方便后续记录
changeLocation(currentLocation.value, buildState(null, currentLocation.value, null, true), true)
}
function changeLocation(to, state, replace) {
history[replace ? 'replaceState' : 'pushState'](state, '', to);
historyState.value = state;
}
return {
location: currentLocation, // 当前路径状态
state: historyState, // 路由中的状态
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
构建状态信息
function buildState(back, current, forward, repalced = false, computeScroll = false) {
return {
back,
current,
forward,
replaced,
position: window.history.length - 1,
scroll: computeScroll ? { left: window.pageXOffset, top: window.pageYOffset } : null
}
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
实现push和replace方法
function push(to, data) { // 跳转页面
const currentState = Object.assign(
{},
historyState.value,
{ forward: to, scroll:{ left: window.pageXOffset, top: window.pageYOffset } }
)
changeLocation(currentState.current, currentState, true); // 跳转前,更新要去哪里
const state = Object.assign( // 创造一个最终的状态
{},
buildState(currentLocation.value, to, null),
{ position: currentState.position + 1 },
data
)
changeLocation(to, state, false);
currentLocation.value = to; // 更改currentLocation
}
function replace(to, data) { // 替换路径
const state = Object.assign({},
buildState(
historyState.value.back,
to,
historyState.value.forward
), data);
changeLocation(to, state, true);
currentLocation.value = to;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
push方法则是调用
history.pushState
,replace方法则是调用history.replaceState
。会计算最新状态和最新跳转后的路径
最终到出以下四个方法:
{
location: currentLocation, // 当前路径状态
state: historyState, // 路由中的状态
push, // 页面跳转
replace
}
1
2
3
4
5
6
2
3
4
5
6
2.useHistoryListeners
实现
function useHistoryListeners(historyState, currentLocation) {
let listeners = [];
const popStateHandler = ({ state }) => {
const to = createCurrentLocation(); // 获取去哪里
const from = currentLocation.value // 从哪来
const fromState = historyState.value; // 从哪来的状态
currentLocation.value = to; // 更新路径
historyState.value = state; // 更新状态
let isBack = state.position - fromState.position < 0; // 计算是否是后退
listeners.forEach(listener => { // 通知监听者,状态发生变化
listener(currentLocation.value, from, {isBack})
})
}
window.addEventListener('popstate', popStateHandler);
function listen(callback) { // 用于收集监听器
listeners.push(callback)
}
return {
listen
}
}
const historyListeners = useHistoryListeners(
historyNavigation.state,
historyNavigation.location
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
这里最终返回listen方法
3.createWebHistory
实现原理
function createWebHistory() { // 创建history模式路由
const historyNavigation = useHistoryStateNavigation();
const historyListeners = useHistoryListeners(
historyNavigation.state,
historyNavigation.location
);
const routerHistory = Object.assign(
{},
historyNavigation,
historyListeners
)
Object.defineProperty(routerHistory,'location',{ // 简化取值操作
get:()=> historyNavigation.location.value
})
Object.defineProperty(routerHistory,'state',{
get:()=> historyNavigation.state.value
})
return routerHistory;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
4.路由系统使用
<button onclick="go('/a')">去A</button>
<button onclick="go('/b')">去B</button>
<button onclick="go('/a',true)">记录替换A</button>
<button onclick="go('/b',true)">记录替换B</button>
<script>
let routerHistory = createWebHistory();
function go(path, replace) {
if (replace) {
routerHistory.replace(path, { a: 1 })
} else {
routerHistory.push(path, { b: 2 })
}
}
routerHistory.listen((to, from, options) => {
console.log(to, from, options)
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
5.实现hash路由
hash模式的路由只是增添了前缀而已,这样跳转的时候就会增加 #
function createWebHashHistory() {
return createWebHistory('#');
}
function createWebHistory(base = '') {
const historyNavigation = useHistoryStateNavigation(base); // 添加base
const historyListeners = useHistoryListeners(
base // 添加base
historyNavigation.state,
historyNavigation.location
);
}
function createCurrentLocation(base) {
const { pathname, search, hash } = location;
const hashPos = base.indexOf('#');
if (hashPos > -1) {
let pathFromHash = hash.slice(1) || '/';
return pathFromHash; // 路径带hash值 把hash去掉
}
return pathname + search + hash;
}
function useHistoryStateNavigation(base) {
const { history, location } = window; // 获取浏览器history对象和location对象
const currentLocation = {
value: createCurrentLocation(base) // 完整的路径由 location中路径+查询参数+hash值
}
function changeLocation(to, state, replace) {
const hashIndex = base.indexOf('#'); // 如果base是hash的话,跳转时携带hash
const url = hashIndex > -1 ? base + to : to;
history[replace ? 'replaceState' : 'pushState'](state, '', url);
historyState.value = state;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
二.Vue路由系统创建
export function createRouter(options) {
const matcher = createRouterMatcher(options.routes); // 1.创建匹配器
const routerHistory = options.history; // 2.获取路由系统
const router = {
install() {
console.log('Vue路由安装')
}
}
return router;
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
1.创建路由匹配器
function createRouterMatcher(routes) {
// 创建路由匹配器
const matchers = [];
function addRoute(record, parent) {
let normalizedRecord = normalizeRouteRecord(record); // 格式化路由
if(parent){ // 如果有父亲,添加父路径
normalizedRecord.path = parent.record.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)); // 扁平化路由关系 路径 => 记录
return {
addRoute
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
normalizeRouteRecord
记录格式化
function normalizeRouteRecord(record) {
return {
path: record.path, // 路径
name: record.name, // 名称
meta: record.meta || {}, // 批注
beforeEnter: record.beforeEnter, // 里有钩子
children: record.children || [], // 子路由
components: { // 组件
default: record.component
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
createRouteRecordMatcher
创建路径和记录的映射关系
function createRouteRecordMatcher(record, parent) { // 路径对应record
const matcher = {
path: record.path,
parent,
record,
children: []
}
if (parent) {
parent.children.push(matcher)
}
return matcher;
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2.响应式路由创建
const START_LOCATION_NORMALIZED = {
path:'/',
name:undefined,
params:{}, // 路径参数
query:{}, // 查询参数
hash:'',
matched:[],// 匹配的理由记录列表
meta:{}
}
export function createRouter(options) {
const matcher = createRouterMatcher(options.routes);
const routerHistory = options.history;
// 初始化响应式路由系统
const currentRoute = shallowRef(START_LOCATION_NORMALIZED);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
currentRoute
这个对象就是整个vue路由的核心,后续路径变化就可以更新视图
3.Vue
路由安装
const router = {
install(app) {
const router = this;
app.component('RouterLink', RouterLink); // 1.新增2个组件
app.component('RouterView', RouterView);
app.config.globalProperties.$router = router; // 2.在实例上挂载$router属性
Object.defineProperty(app.config.globalProperties, '$route', { // 3.增加$route属性
enumerable: true,
get: () => unref(currentRoute),
});
if(currentRoute.value === START_LOCATION_NORMALIZED){
// 初始化先跳转一次,更新currentRoute属性
push(routerHistory.location)
}
const reactiveRoute = {};
for(let key in START_LOCATION_NORMALIZED){
reactiveRoute[key] = computed(()=> currentRoute.value[key]);
}
app.provide('router',router); // 1.提供router属性
app.provide('route location',reactive(reactiveRoute)); // 2.可以更改路由属性
app.provide('router view location',currentRoute); //3.可以更改路由当前状态
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
4.路由跳转实现
function push(to) {
return pushWithRedirect(to);
}
1
2
3
2
3
用户调用的push方法,核心调用的就是
pushWithRedirect
方法
let ready;
function markAsReady(){
if(ready) return;
ready = true;
routerHistory.listen((to)=>{
let toLocation = resolve(to);
const from = currentRoute.value;
finalizeNavigation(targetLocation,from);
});
}
function finalizeNavigation(toLocation,from){
if(from === START_LOCATION_NORMALIZED){
routerHistory.replace(toLocation.path); // 初始化,调用路由系统跳转
}else{
routerHistory.push(toLocation.path); // 后续走跳转逻辑
}
currentRoute.value = toLocation; // 更改currentRoute值
markAsReady();
}
function createRouterMatcher(routes) {
const matchers = [];
function resolve(location) { // 根据路径解析匹配到的路由
const matched = [];
let path = location.path;
let parentMatcher = matchers.find(m => {
return m.path ==path
});
while (parentMatcher) {
matched.unshift(parentMatcher.record);
parentMatcher = parentMatcher.parent
}
return {
path,
matched
}
}
return {
addRoute,
resolve
}
}
function resolve(rawLocation) {
if (typeof rawLocation === 'string') {
let matchedRoute = matcher.resolve({ path: rawLocation });
return {
...matchedRoute
}
}
}
function pushWithRedirect(to) {
const targetLocation = resolve(to); // 目标
const from = currentRoute.value; // 从哪来的
finalizeNavigation(targetLocation,from)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
最后将push方法和resolve方法也挂载到router对象上
5.RouterLink
实现
import { computed, h, inject, reactive } from "vue";
function useLink(props) {
const router = inject('router');
const currentRoute = inject('route location');
function navigate() {
router.push(props.to);
}
return {
navigate
}
}
export const RouterLink = {
name: 'RouterLink',
props: {
to: {
type: [String, Object],
required: true
},
activeClass: String
},
setup(props, { slots }) {
const link = reactive(useLink(props))
return () => {
const children = slots.default && slots.default();
return h('a', {
onClick: link.navigate
}, children)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
6.RouterView
实现
import { computed, h ,inject,provide} from "vue";
export const RouterView = {
name:'RouterView',
props:{
name:{
type:String,
default:'default'
}
},
setup(ctx,{slots}){
const injectedRoute = inject('router view location');
const depth = inject('router view depth', 0);
const matchedRouteRef = computed(()=>injectedRoute.value.matched[depth]);
provide('router view depth', depth + 1)
return ()=>{
const matchedRoute = matchedRouteRef.value;
const ViewComponent = matchedRoute && matchedRoute.components.default;
if(!ViewComponent){
return slots.default && slots.default();
}
return h(ViewComponent)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
三.路由钩子实现
function useCallbacks() {
const handlers = [];
function add(handler) {handlers.push(handler);}
return {
add,
list: () => handlers
}
}
export function createRouter(options) {
const beforeGuards = useCallbacks();
const beforeResolveGuards = useCallbacks();
const afterGuards = useCallbacks();
const router = {
afterEach: afterGuards.add, // 全局钩子
beforeEach: beforeGuards.add,
beforeResolve: beforeResolveGuards.add
}
return router;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function normalizeRouteRecord(record) {
return {
path: record.path,
name: record.name,
meta: record.meta || {},
beforeEnter: record.beforeEnter,
children: record.children || [],
components: {
default: record.component
},
leaveGuards:new Set(), // 离开守卫
updateGuards:new Set() // 更新守卫
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
添加
leaveGuards
、updateGuards
用于记录组件中,使用路由api
定义的钩子函数
function pushWithRedirect(to) {
const targetLocation = resolve(to);
const from = currentRoute.value;
navigate(targetLocation, from).then(() => { // 执行路由守卫
return finalizeNavigation(targetLocation, from); // 真正发生跳转
}).then(() => {
for (const guard of afterGuards.list()) guard(to, from) // 执行afterEach
})
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
1.路由跳转调用钩子
function navigate(to, from) {
const [leavingRecords, updatingRecords, enteringRecords] = extractChanggingRecords(to, from);// 计算离开、跟新、进入
let guards = extractComponentsGuards( // 1.触发组件内部离开的钩子
leavingRecords.reverse(),
'beforeRouteLeave',
to,
from
);
for (const record of leavingRecords) { // 2.组件内部使用api定义的离开
record.leaveGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
return runGuardQueue(guards).then(() => { // 3.运行钩子
guards = [];
for (const guard of beforeGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
return runGuardQueue(guards);
}).then(() => { // 4.调用组件更新钩子
guards = extractComponentsGuards(
updatingRecords,
'beforeRouteUpdate',
to,
from
)
for (const record of updatingRecords) {// 5.组件内部使用api定义的更新
record.updateGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
return runGuardQueue(guards);
}).then(() => {
guards = []
for (const record of to.matched) { // 6.路由配置中的beforeEnter
if (record.beforeEnter) {
guards.push(guardToPromiseFn(record.beforeEnter, to, from));
}
}
return runGuardQueue(guards)
}).then(() => {
guards = extractComponentsGuards( // 7.组件中的beforeRouteEnter
enteringRecords,
'beforeRouteEnter',
to,
from
)
return runGuardQueue(guards);
}).then(() => { // 8.组件中的确认钩子
guards = [];
for (const guard of beforeResolveGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
return runGuardQueue(guards);
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2.管理组件状态 离开、更新、进入
function extractChanggingRecords(to, from) { // 路由更改时 获取对应记录的状态
const leavingRecords = [];
const updatingRecords = [];
const enteringRecords = [];
const len = Math.max(from.matched.length, to.matched.length)
for (let i = 0; i < len; i++) {
const recordFrom = from.matched[i]; // 去的地方和来的是同一个
if (recordFrom) {
if (to.matched.find(record => record.path === recordFrom.path)) {
updatingRecords.push(recordFrom)
} else {
leavingRecords.push(recordFrom)
}
}
const recordTo = to.matched[i]; // 去的和来的不是一个,就走进入的钩子
if (recordTo) {
if (!from.matched.find(record => record.path == recordTo.path)) {
enteringRecords.push(recordTo);
}
}
}
return [leavingRecords, updatingRecords, enteringRecords]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
3.组件钩子提取
function extractComponentsGuards(matched, guardType, to, from) {
const guards = [];
for (const record of matched) {
let rawComponent = record.components.default; // 取出组件
const guard = rawComponent[guardType]; // 取组件上的钩子方法
// 将这个方法变成promise 放到guards
guard && guards.push(guardToPromiseFn(guard, to, from, record, 'default'))
}
return guards
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
4.包装钩子函数
function guardToPromiseFn(guard, to, from, record, name) {
return () => new Promise((resolve, reject) => {
const next = () => resolve();
const guardReturn = guard.call(record, to, from, next); // 获得返回值
let guardCall = Promise.resolve(guardReturn); // 将返回值包装成promise,自动调用
guardCall.then(next)
})
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8