1. 路由库工程设计
首先,我们需要创建几个核心文件来组织我们的路由库:
src/
router/
index.ts
RouterView.ts
RouterLink.ts
useRouter.ts
injectionsymbols.ts
history.ts
2. injectionSymbols.ts
定义一些注入符号来在应用中共享状态:
import { inject, provide, InjectionKey, Ref } from 'vue';
export interface Router {
currentRoute: Ref<Route>;
push: (to: string) => void;
replace: (to: string) => void;
resolve: (to: string) => string;
}
export interface Route {
path: string;
component: any;
}
export const routerKey: InjectionKey<Router> = Symbol('router');
export const matchedRouteKey: InjectionKey<Ref<Route>> = Symbol('matchedRoute');
export const viewDepthKey: InjectionKey<number> = Symbol('viewDepth');
export function provideRouter(router: Router) {
provide(routerKey, router);
}
export function useRouter(): Router {
return inject(routerKey);
}
export function useMatchedRoute(): Ref<Route> {
return inject(matchedRouteKey);
}
export function provideMatchedRoute(route: Ref<Route>) {
provide(matchedRouteKey, route);
}
export function useViewDepth(): number {
return inject(viewDepthKey, 0);
}
export function provideViewDepth(depth: number) {
provide(viewDepthKey, depth);
}
3. RouterView.ts
实现RouterView组件:
import { defineComponent, h, computed } from 'vue';
import { useMatchedRoute, useViewController, provideViewController } from './injectionsymbols';
export const RouterView = defineComponent({
name: 'RouterView',
setup() {
const depth = useViewController();
const matchedRoute = useMatchedRoute();
const route = computed(() => matchedRoute.value[depth]);
provideViewController(depth + 1);
return () => {
const Component = route.value && route.value.component;
return Component ? h(Component) : null;
};
}
});
4. RouterLink.ts
实现RouterLink组件:
import { defineComponent, h } from 'vue';
import { useRouter } from './injectionsymbols';
export const RouterLink = defineComponent({
name: 'RouterLink',
props: {
to: {
type: [String, Object],
required: true
},
replace: Boolean
},
setup(props, { slots }) {
const router = useRouter();
const navigate = (event: Event) => {
event.preventDefault();
if (props.replace) {
router.replace(props.to as string);
} else {
router.push(props.to as string);
}
};
return () => {
return h('a', {
href: router.resolve(props.to as string),
onClick: navigate
}, slots.default ? slots.default() : '');
};
}
});
5. useRouter.ts
实现useRouter函数:
import { inject } from 'vue';
import { routerKey, Router } from './injectionSymbols';
export function useRouter(): Router {
return inject(routerKey);
}
6. index.ts
实现createRouter函数:
import { ref, reactive, watch, Ref } from 'vue';
import { provideRouter, provideMatchedRoute, Route, Router } from './injectionsymbols';
export function createRouter({ history, routes }: { history: any, routes: Route[] }): Router {
const currentRoute: Ref<Route> = ref({ path: '/', component: null });
function createMatcher(routes: Route[]) {
const matchers = routes.map(route => ({
...route,
regex: new RegExp(`^${route.path}$`)
}));
return (path: string) => matchers.find(route => route.regex.test(path));
}
const matcher = createMatcher(routes);
function push(to: string) {
const route = matcher(to);
if (route) {
currentRoute.value = route;
history.push(to);
}
}
function replace(to: string) {
const route = matcher(to);
if (route) {
currentRoute.value = route;
history.replace(to);
}
}
const router: Router = {
currentRoute,
push,
replace,
resolve: (to: string) => to
};
watch(currentRoute, (route) => {
provideMatchedRoute(ref(route));
});
provideRouter(router);
return router;
}
7. history.ts
实现createWebHistory函数:
export function createWebHistory() {
const listeners: ((path: string) => void)[] = [];
window.addEventListener('popstate', () => {
listeners.forEach(listener => listener(window.location.pathname));
});
function push(path: string) {
window.history.pushState({}, '', path);
listeners.forEach(listener => listener(path));
}
function replace(path: string) {
window.history.replaceState({}, '', path);
listeners.forEach(listener => listener(path));
}
function listen(callback: (path: string) => void) {
listeners.push(callback);
}
return {
push,
replace,
listen
};
}
8. 示例应用
最后,我们可以在一个简单的 Vue 应用中使用我们的自定义路由库:
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { createRouter } from './router';
import { createWebHistory } from './router/history';
const routes = [
{ path: '/', component: () => import('./components/Home.vue') },
{ path: '/about', component: () => import('./components/About.vue') }
];
const history = createWebHistory();
const router = createRouter({ history, routes });
createApp(App).use(router).mount('#app');
以下是在App.vue中
// App.vue
<template>
<div id="app">
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<router-view></router-view>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App'
});
</script>
这个简化版的 Vue Router 库的 TypeScript 版本包含了核心组件 RouterView、RouterLink 以及核心 API createRouter 和 useRouter,实现了基本的路由功能。通过这个实现,你可以了解 Vue Router 的基本工作原理和核心概念。
9. 补充资料
vue-router 官方文档:https://router.vuejs.org/zh/introduction.html
vue-router 相关 api 速查:https://router.vuejs.org/zh/api/
源码:https://github.com/vuejs/router
浏览器历史记录协议:https://developer.mozilla.org/en-US/docs/Web/API/History_API
路由库实现:https://github.com/vuejs/router/blob/v4.3.3/packages/router/src/history/html5.ts