Vue-router 사용 및 원리 분석

소개

Vue 라우터는 Vue.js의 공식 라우터입니다. Vue.js 코어와의 긴밀한 통합으로 Vue.js로 단일 페이지 애플리케이션(SPA)을 더 쉽게 구축할 수 있습니다.

사용

창조하다

1. Vue 라우터 종속성을 설치한 후 App.vue가져 오면 router-view렌더링을 위한 컨테이너입니다.

<div id="app"><router-view></router-view>
</div> 

2. 경로 만들기router/index.js

 const routes = [	{ path: '/', component: Home},{ path: '/login', name: 'login', component: Login},
]
constrouter = createRouter({history: createWebHistory(),routes: routes,
})
export default router 

3. main.js라우팅 사용

import router from "./router";
const app = createApp(App)

app.use(router)

app.mount('#app') 

그런 다음 this.$router모든 구성 요소 this.$route에서 액세스하고 다음과 같이 현재 경로에 액세스할 수 있습니다.

// Home.vue
export default {computed: {username() {// 我们很快就会看到 `params` 是什么return this.$route.params.username},},methods: {goToDashboard() {if (isAuthenticated) {this.$router.push('/dashboard')} else {this.$router.push('/login')}},},
} 

중첩 경로

일부 애플리케이션 UI는 중첩 구성 요소의 여러 레이어로 구성됩니다. 이 경우 URL의 조각은 일반적으로 특정 중첩 구성 요소 구조에 해당합니다. 예를 들면 다음과 같습니다.

/user/johnny/profile /user/johnny/posts
+------------------++-----------------+
| User || User|
| +--------------+ || +-------------+ |
| | Profile| |+------------>| | Posts | |
| || || | | |
| +--------------+ || +-------------+ |
+------------------++-----------------+ 

상위 앱 노드의 최상위 레이어 아래에 포함된 구성 요소는 위의 템플릿 과 같이 router-view자체적으로 중첩됩니다 .router-viewuser

const User = {template: `<div class="user"><h2>User {
   
   { $route.params.id }}</h2><router-view></router-view></div>`,
} 

구성 요소를 이 중첩으로 렌더링하려면 router-view경로에서 다음을 구성해야 합니다 children.

const routes = [{path: '/user/:id',component: User,children: [{// 当 /user/:id/profile 匹配成功// UserProfile 将被渲染到 User 的 <router-view> 内部path: 'profile',component: UserProfile,},{// 当 /user/:id/posts 匹配成功// UserPosts 将被渲染到 User 的 <router-view> 内部path: 'posts',component: UserPosts,},],},
] 

소스 코드 관점에서 페이지가 로드되고 페이지에 표시되는 방식을 살펴보겠습니다.

원칙

위의 기본 사용 방법에서 알 수 있듯이 주로 세 단계로 구성됩니다.

1. 앱에서 이 경로 생성 createRouter및 사용 2. 템플릿에서 태그 사용 3. 페이지 탐색 및 이동use
router-view
push

라우터가 선언한 배열 구조에서 알 수 있듯이 선언된 경로 는 선언된 컴포넌트 path를 가리키는 라우팅 테이블로 등록되고 메소드가 호출되면 라우팅 테이블에서 해당 컴포넌트를 찾아 로드하게 됩니다. 소스코드가 이 과정을 어떻게 구현하는지 살펴보자.Vue 라우터 소스코드 분석 버전은 4.1.5이다.componentpush

생성 설치

createRouter먼저 메소드 구현 을 살펴보십시오 .

/**
 * Creates a Router instance that can be used by a Vue app.
 *
 * @param options - {@link RouterOptions}
 */
export function createRouter(options: RouterOptions): Router {const matcher = createRouterMatcher(options.routes, options)// ...function addRoute( parentOrRoute: RouteRecordName | RouteRecordRaw,route?: RouteRecordRaw ) {// ...}function getRoutes() {return matcher.getRoutes().map(routeMatcher => routeMatcher.record)}function hasRoute(name: RouteRecordName): boolean {return !!matcher.getRecordMatcher(name)}function push(to: RouteLocationRaw) {return pushWithRedirect(to)}function replace(to: RouteLocationRaw) {return push(assign(locationAsObject(to), { replace: true }))}// ...const router: Router = {currentRoute,listening: true,addRoute,removeRoute,hasRoute,getRoutes,resolve,options,push,replace,go,back: () => go(-1),forward: () => go(1),beforeEach: beforeGuards.add,beforeResolve: beforeResolveGuards.add,afterEach: afterGuards.add,onError: errorHandlers.add,isReady,// 在app全局安装routerinstall(app: App) {const router = this// 全局注册组件RouterLink、RouterViewapp.component('RouterLink', RouterLink)app.component('RouterView', RouterView)	// 全局声明router实例,this.$router访问app.config.globalProperties.$router = router// 全局注册this.$route 访问当前路由currentRouteObject.defineProperty(app.config.globalProperties, '$route', {enumerable: true,get: () => unref(currentRoute),})// this initial navigation is only necessary on client, on server it doesn't// make sense because it will create an extra unnecessary navigation and could// lead to problemsif (isBrowser &&// used for the initial navigation client side to avoid pushing// multiple times when the router is used in multiple apps!started &&currentRoute.value === START_LOCATION_NORMALIZED) {// see above// 浏览器情况下,push一个初始页面,不指定url默认首页‘/’started = truepush(routerHistory.location).catch(err => {if (__DEV__) warn('Unexpected error when starting the router:', err)})}	// ...app.provide(routerKey, router)app.provide(routeLocationKey, reactive(reactiveRoute))// 全局注入当前路由currentRouteapp.provide(routerViewLocationKey, currentRoute)	// ...},}return router
} 

createRouter이 메서드는 현재 라우팅 인스턴스를 반환하고 구성 요소 this.$router에서 구조를 인쇄하는 것과 동일한 몇 가지 일반적인 라우팅 메서드를 내부적으로 초기화합니다 install. 설치 중에 호출됩니다 . 아래 방법 app.use(router)살펴보십시오 .useruntime-core.cjs.prod.js

use(plugin, ...options) {if (installedPlugins.has(plugin)) ;else if (plugin && shared.isFunction(plugin.install)) {installedPlugins.add(plugin);// 如果是插件,调用插件的install方法,并把当前app传入plugin.install(app, ...options);}else if (shared.isFunction(plugin)) {installedPlugins.add(plugin);plugin(app, ...options);}else ;return app;}, 

router-view지금까지 글로벌 라우터 생성 및 설치가 완료되었으며 코드 에서 사용할 수 있으며 this.$router예제의 일부 방법은 로드된 페이지를 표시하는 방법은 component무엇입니까? 렌더링 구성 요소 router-view의 내부 구현 을 살펴봐야 합니다.

표현

install메서드는 RouterView구성 요소를 등록하고 다음에서 구현됩니다 RouterView.ts.

/**
 * Component to display the current route the user is at.
 */
export const RouterView = RouterViewImpl as unknown as {// ...
} 

RouterViewImpl성취하다:

 export const RouterViewImpl = /*#__PURE__*/ defineComponent({name: 'RouterView',	// ...setup(props, { attrs, slots }) {__DEV__ && warnDeprecatedUsage()	// 拿到之前注册的currentRouteconst injectedRoute = inject(routerViewLocationKey)!// 当前要显示的route,监听route值变化时会刷新const routeToDisplay = computed<RouteLocationNormalizedLoaded>(() => props.route || injectedRoute.value)// 获取当前router-view深度层级,在嵌套路由时使用const injectedDepth = inject(viewDepthKey, 0)// 在当前router-view深度下去匹配要显示的路由matched// matched 是个数组,在resolve方法被赋值,如果有匹配到则在当前router-view渲染const depth = computed<number>(() => {let initialDepth = unref(injectedDepth)const { matched } = routeToDisplay.valuelet matchedRoute: RouteLocationMatched | undefinedwhile ((matchedRoute = matched[initialDepth]) &&!matchedRoute.components) {initialDepth++}return initialDepth})const matchedRouteRef = computed<RouteLocationMatched | undefined>(() => routeToDisplay.value.matched[depth.value])provide(viewDepthKey,computed(() => depth.value + 1))provide(matchedRouteKey, matchedRouteRef)provide(routerViewLocationKey, routeToDisplay)const viewRef = ref<ComponentPublicInstance>()// watch at the same time the component instance, the route record we are// rendering, and the name// 监听匹配路由变化时,刷新 watch(() => [viewRef.value, matchedRouteRef.value, props.name] as const,([instance, to, name], [oldInstance, from, oldName]) => {// ...},{ flush: 'post' })return () => {const route = routeToDisplay.value// we need the value at the time we render because when we unmount, we// navigated to a different location so the value is differentconst currentName = props.nameconst matchedRoute = matchedRouteRef.valueconst ViewComponent =matchedRoute && matchedRoute.components![currentName]if (!ViewComponent) {return normalizeSlot(slots.default, { Component: ViewComponent, route })}	// ...	// 关键:h函数,渲染路由中获得的组件const component = h(ViewComponent,assign({}, routeProps, attrs, {onVnodeUnmounted,ref: viewRef,}))return (// pass the vnode to the slot as a prop.// h and <component :is="..."> both accept vnodesnormalizeSlot(slots.default, { Component: component, route }) ||component)}},
}) 

중첩 라우팅 구현의 핵심은 깊이 depth제어를 사용하는 것입니다.초기 router-view깊이는 0이고 내부 중첩 깊이는 1씩 증가합니다.예를 들어 다음 중첩 관계의 경우:

const routes = [{path: '/',component: Home,children: [{path: 'product',component: ProductManage},]},{ path: '/login', name: 'login', component: Login }] 

다음 과 같이 순서대로 resolve파싱됩니다 .routeToDisplay.value

도약

점프 프로세스를 분석하기 전에 먼저 경로 등록의 구문 분석 논리를 살펴보십시오. createRouter메소드는 메소드에서 호출됩니다 createRouterMatcher. 이 메소드는 경로 일치자를 생성하고 경로 등록 및 점프의 특정 구현을 내부적으로 캡슐화하고 외부에서 router올바른 matcher패키지 를 생성합니다. 계층은 API를 제공하고 구현 세부 정보를 보호합니다. 구현을 살펴보십시오.

 /**
 * Creates a Router Matcher.
 *
 * @internal
 * @param routes - array of initial routes
 * @param globalOptions - global route options
 */
export function createRouterMatcher( routes: Readonly<RouteRecordRaw[]>,globalOptions: PathParserOptions ): RouterMatcher {// normalized ordered array of matchers// 匹配器的两个容器,匹配器Array和命名路由Mapconst matchers: RouteRecordMatcher[] = []const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()function getRecordMatcher(name: RouteRecordName) {return matcherMap.get(name)}function addRoute( record: RouteRecordRaw,parent?: RouteRecordMatcher,originalRecord?: RouteRecordMatcher ) {// ...// 如果记录中声明'alias'别名,把别名当作path,插入一条新的记录if ('alias' in record) {const aliases =typeof record.alias === 'string' ? [record.alias] : record.alias<img src="https://github.com/vuejs/router/issues/1124(matcher.record.path !== matchers[i].record.path ||!isRecordChildOf(matcher, matchers[i])))i++	// 将matcher添加到数组末尾matchers.splice(i, 0, matcher)// only add the original record to the name map// 命名路由添加到路由Mapif (matcher.record.name && !isAliasRecord(matcher))matcherMap.set(matcher.record.name, matcher)}function resolve( location: Readonly<MatcherLocationRaw>,currentLocation: Readonly<MatcherLocation> ): MatcherLocation {let matcher: RouteRecordMatcher | undefinedlet params: PathParams = {}let path: MatcherLocation['path']let name: MatcherLocation['name']if ('name' in location && location.name) {// 命名路由解析出pathmatcher = matcherMap.get(location.name)// ...// throws if cannot be stringifiedpath = matcher.stringify(params)} else if ('path' in location) {// no need to resolve the path with the matcher as it was provided// this also allows the user to control the encodingpath = location.path//...matcher = matchers.find(m => m.re.test(path))// matcher should have a value after the loopif (matcher) {// we know the matcher works because we tested the regexpparams = matcher.parse(path)!name = matcher.record.name}// push相对路径} else {// match by name or path of current routematcher = currentLocation.name? matcherMap.get(currentLocation.name): matchers.find(m => m.re.test(currentLocation.path))if (!matcher)throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {location,currentLocation,})name = matcher.record.name// since we are navigating to the same location, we don't need to pick the// params like when `name` is providedparams = assign({}, currentLocation.params, location.params)path = matcher.stringify(params)}const matched: MatcherLocation['matched'] = []let parentMatcher: RouteRecordMatcher | undefined = matcherwhile (parentMatcher) {// reversed order so parents are at the beginning	// 和当前path匹配的记录,插入到数组头部,让父级先匹配matched.unshift(parentMatcher.record)parentMatcher = parentMatcher.parent}return {name,path,params,matched,meta: mergeMetaFields(matched),}}// 添加初始路由routes.forEach(route => addRoute(route))return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher " style="margin: auto" />
} 

정리 하면 메소드 createRouterMatcher는 각각 의 메소드에 대한 메소드를 routres실행하고 addRoute이를 호출 insertMatcher하여 생성된 메소드 matchers를 컨테이너에 삽입하고 이후 메소드를 통해 레코드가 저장된 배열에 resolve레코드를 매칭 하고 후속 작업은 배열에서 가져오기를 기반으로 합니다. 렌더링해야 하는 요소입니다. 메서드 실행 흐름:Matcher.recordMatcherLocationmatchedrouter-viewdepthpush

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

// ...function pushWithRedirect( to: RouteLocationRaw | RouteLocation,redirectedFrom?: RouteLocation ): Promise<NavigationFailure | void | undefined> {// 解析出目标locationconst targetLocation: RouteLocation = (pendingLocation = resolve(to))const from = currentRoute.valueconst data: HistoryState | undefined = (to as RouteLocationOptions).stateconst force: boolean | undefined = (to as RouteLocationOptions).force// to could be a string where `replace` is a functionconst replace = (to as RouteLocationOptions).replace === trueconst shouldRedirect = handleRedirectRecord(targetLocation)// 重定向逻辑if (shouldRedirect)return pushWithRedirect(assign(locationAsObject(shouldRedirect), {state:typeof shouldRedirect === 'object'? assign({}, data, shouldRedirect.state): data,force,replace,}),// keep original redirectedFrom if it existsredirectedFrom || targetLocation)// if it was a redirect we already called `pushWithRedirect` aboveconst toLocation = targetLocation as RouteLocationNormalized	// ...return (failure ? Promise.resolve(failure) : navigate(toLocation, from)).catch((error: NavigationFailure | NavigationRedirectError) =>// ...).then((failure: NavigationFailure | NavigationRedirectError | void) => {if (failure) {// ...} else {// if we fail we don't finalize the navigationfailure = finalizeNavigation(toLocation as RouteLocationNormalizedLoaded,from,true,replace,data)}triggerAfterEach(toLocation as RouteLocationNormalizedLoaded,from,failure)return failure})} 

실패 없이 finalizeNavigation마지막 점프를 수행하려면 다음 구현을 참조하십시오.

/** * - Cleans up any navigation guards * - Changes the url if necessary * - Calls the scrollBehavior */function finalizeNavigation( toLocation: RouteLocationNormalizedLoaded,from: RouteLocationNormalizedLoaded,isPush: boolean,replace?: boolean,data?: HistoryState ): NavigationFailure | void {// a more recent navigation took placeconst error = checkCanceledNavigation(toLocation, from)if (error) return error// only consider as push if it's not the first navigationconst isFirstNavigation = from === START_LOCATION_NORMALIZEDconst state = !isBrowser ? {} : history.state// change URL only if the user did a push/replace and if it's not the initial navigation because// it's just reflecting the url// 如果是push保存历史到routerHistoryif (isPush) {// on the initial navigation, we want to reuse the scroll position from// history state if it existsif (replace || isFirstNavigation)routerHistory.replace(toLocation.fullPath,assign({scroll: isFirstNavigation && state && state.scroll,},data))else routerHistory.push(toLocation.fullPath, data)}// accept current navigation// 给当前路由赋值,会触发监听的router-view刷新currentRoute.value = toLocationhandleScroll(toLocation, from, isPush, isFirstNavigation)markAsReady()} 

currentRoute.value = toLocation실행 후 중앙값 변경 을 트리거 router-view하고 새 값을 얻기 위해 다시 계산 하고 페이지 새로고침을 완료합니다. 위의 2개의 포인트가 더 있는데 , 방금 언급 한 배열 채우고 메소드가 내비게이션에서 가드를 실행합니다. 이 두 단계는 읽지 않겠습니다. 관심 있는 학생들은 스스로 확인할 수 있습니다. 지금까지 주요 과정을 분석했습니다.routeToDisplaymatchedRouteRefViewComponentrouterresolvematcherresolvematchednavigate

인용하다

tchedRouteRef 获得新的ViewComponent ,完成页面刷新。 上面还有两点,라우터 확인 会调用到매처 해결 ,填充刚刚说过的일치 数组,탐색 방법은 내비게이션에서 가드를 실행합니다. 이 두 단계는 읽지 않겠습니다. 관심 있는 학생이 직접 확인할 수 있습니다. 지금까지 주요 프로세스를 분석했습니다.

마침내

최근에 VUE의 다양한 지식 포인트를 요약하고 "Vue 개발이 알아야 할 36가지 기술"로 정리한 VUE 문서를 발견했습니다. 내용이 비교적 상세하고, 각 지식 포인트에 대한 설명도 자리하고 있습니다.



도움이 필요한 친구는 아래 카드를 클릭하여 무료로 받고 공유할 수 있습니다.

рекомендация

отblog.csdn.net/web22050702/article/details/128723312