Introdução
O Vue Router é o roteador oficial do Vue.js. A integração profunda com o núcleo do Vue.js facilita a criação de aplicativos de página única (SPA) com o Vue.js.
usar
Criar
1. Após instalar a dependência do Vue Router, App.vue
importe router-view
-a, é o container para renderização
<div id="app"><router-view></router-view>
</div>
2. Crie uma rotarouter/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
Use o roteamento em
import router from "./router";
const app = createApp(App)
app.use(router)
app.mount('#app')
Então você pode this.$router
acessá-lo em qualquer componente this.$route
e acessar a rota atual como:
// 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')}},},
}
rotas aninhadas
Algumas UIs de aplicativos consistem em várias camadas de componentes aninhados. Nesse caso, os fragmentos da URL geralmente correspondem a estruturas específicas de componentes aninhados, por exemplo:
/user/johnny/profile /user/johnny/posts
+------------------++-----------------+
| User || User|
| +--------------+ || +-------------+ |
| | Profile| |+------------>| | Posts | |
| || || | | |
| +--------------+ || +-------------+ |
+------------------++-----------------+
Sob a camada superior do nó superior do aplicativo router-view
, os componentes contidos são aninhados por si mesmos router-view
, como o user
modelo acima:
const User = {template: `<div class="user"><h2>User {
{ $route.params.id }}</h2><router-view></router-view></div>`,
}
Para renderizar componentes neste ninho router-view
, precisamos configurar na rota 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,},],},
]
Vejamos como a página é carregada e exibida na página do ponto de vista do código-fonte
princípio
Como pode ser visto no método de uso básico acima, ele inclui principalmente três etapas:
1. Crie createRouter
e use
use esta rota no aplicativo 2. Use tags
no modelo 3. Navegue e pule para a páginarouter-view
push
Como pode ser visto na estrutura de array declarada pelos roteadores, a rota declarada path
será registrada como uma tabela de roteamento apontando para component
o componente declarado e, quando o push
método for chamado, o componente correspondente será encontrado na tabela de roteamento e carregado. Vamos dar uma olhada em como o código-fonte implementa esse processo. A versão de análise do código-fonte do Vue Router é 4.1.5
criar instalação
Primeiro, olhe para a createRouter
implementação do método:
/**
* 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 &¤tRoute.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
O método retorna a instância de roteamento atual e inicializa internamente alguns métodos de roteamento comuns, que this.$router
são o mesmo que imprimir a estrutura no componente install
Onde o método é chamado? Chamado durante a instalação app.use(router)
, veja o use
método runtime-core.cjs.prod.js
, abaixo:
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;},
Até agora, a criação e a instalação do roteador global foram concluídas e podem ser usadas no código router-view
e em this.$router
alguns métodos do exemplo, então como exibir o carregado na página component
? router-view
Precisa olhar para a implementação interna do componente de renderização
Renderização
install
O método registra o RouterView
componente e é implementado em RouterView.ts
:
/**
* Component to display the current route the user is at.
*/
export const RouterView = RouterViewImpl as unknown as {// ...
}
RouterViewImpl
concluir:
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)}},
})
O núcleo da implementação do roteamento aninhado é usar o depth
controle de profundidade. A router-view
profundidade inicial é 0 e a profundidade de aninhamento interno é incrementada em 1. Por exemplo, para os seguintes relacionamentos de aninhamento:
const routes = [{path: '/',component: Home,children: [{path: 'product',component: ProductManage},]},{ path: '/login', name: 'login', component: Login }]
Eles resolve
são analisados da routeToDisplay.value
seguinte maneira:
pular
Antes de analisar o processo de salto, primeiro observe a lógica de análise do registro da rota. O createRouter
método é chamado no createRouterMatcher
método. Esse método cria uma correspondência de rota, encapsula internamente a implementação específica do registro e salto da rota e cria o pacote router
correto externamente. matcher
Uma camada fornece a API e protege os detalhes de implementação. Veja a implementação:
/**
* 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" />
}
Resumindo, o método executa o método createRouterMatcher
de cada método , chama-o e insere o gerado no container . o acompanhamento será baseado na busca do array .O elemento que deve ser renderizado. Fluxo de execução do método:routres
addRoute
insertMatcher
matchers
resolve
Matcher.record
MatcherLocation
matched
router-view
depth
push
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
Chamada para fazer o salto final sem falhas , veja a implementação:
/** * - 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
Após a execução, ele acionará router-view
a routeToDisplay
alteração da mediana, recalculará para matchedRouteRef
obter um novo valor ViewComponent
e concluirá a atualização da página. Existem mais dois pontos acima, router
que resolve
serão chamados , preenchendo o array que acabamos de citar matcher
, e o método executará o guarda na navegação. Não vou ler esses dois passos. Os alunos interessados podem verificar por conta própria. Até o momento, os principais processo foi analisado.resolve
matched
navigate
referir-se
O tchedRouteRef 获得新的
ViewComponent ,完成页面刷新。 上面还有两点,
router 的
resolve 会调用到
matcher 的
resolve ,填充刚刚说过的
matched navegue`método 数组,
executará o guarda na navegação. Não vou ler essas duas etapas. Os alunos interessados podem verificar por si mesmos. Até agora, o processo principal foi analisado.
Finalmente
Recentemente, encontrei um documento VUE, que resume os vários pontos de conhecimento do VUE e os organiza em "36 habilidades que o desenvolvimento do Vue deve conhecer". O conteúdo é relativamente detalhado e a explicação de cada ponto de conhecimento também está disponível.
Amigos necessitados, você pode clicar no cartão abaixo para receber e compartilhar gratuitamente