Vue-router の使い方と原理分析

序章

Vue Router は、Vue.js の公式ルーターです。Vue.js コアとの緊密な統合により、Vue.js を使用したシングルページ アプリケーション (SPA) の構築が容易になります。

使用する

作成

1. Vue Routerの依存関係をインストールした後、それを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,},],},
] 

ソースコードの観点から、ページがどのようにロードされ、ページに表示されるかを見てみましょう

原理

上記の基本的な使用方法からわかるように、主に次の 3 つの手順が含まれます。

1.アプリでこのルートを作成createRouterして使用する 2.テンプレートでタグを使用する3.ページに移動してジャンプするuse
router-view
push

ルーターが宣言する配列構造からわかるように、宣言されたルートは、宣言されたコンポーネントpathを指すルーティング テーブルとして登録され、メソッドが呼び出されると、ルーティング テーブルから対応するコンポーネントが検索されてロードされます。ソース コードがこのプロセスをどのように実装しているかを見てみましょう. Vue Router のソース コード解析バージョンは 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.$routercomponentレンダリング コンポーネント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 = toLocationrouter-view実行後、routeToDisplay中央値の変更をトリガーし、再計算しmatchedRouteRefて新しい値を取得ViewComponentし、ページの更新を完了します。上記の 2 つのポイントがありrouterますresolve. と呼ばれる, 先ほど述べた配列を埋める ,メソッドmatcherナビゲーションでガードを実行します. この 2 つの手順は読みません. 興味のある学生は自分で確認してください. これまでのところ, メインプロセスが分析されました。resolvematchednavigate

参照する

tchedRouteRef 获得新的ViewComponent,完成页面刷新。 上面还有两点,ルーターのリゾルブ会调用到マッチャーリゾルブ,填充刚刚说过的マッチド数组,ナビゲート メソッドは、ナビゲーションのガードを実行します. この 2 つの手順は読みません. 興味のある学生は自分で確認できます. ここまでで主なプロセスは分析されました.

やっと

最近、VUE のさまざまな知識ポイントをまとめ、「Vue 開発が知っておくべき 36 のスキル」にまとめた VUE ドキュメントを見つけました。内容は比較的詳しく、各知識ポイントの解説も整っています。



困っている友達は、下のカードをクリックして無料で受け取り、共有できます

おすすめ

転載: blog.csdn.net/web22050702/article/details/128723312