Фронтенд-маршрутизация — ключевая технология для создания одностраничных приложений. Она позволяет изменить URL-адрес браузера, не запрашивая сервер, и позволить странице повторно отобразить желаемый результат.Vue-RouterЭто интерфейсный плагин маршрутизации для приложений Vue, давайте рассмотрим принцип его реализации.
Пример маршрутизации
Чтобы лучше читать исходный код, мы можем использовать простой пример маршрутизации, чтобы увидеть результаты ключевых шагов:
// main.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
Vue.use(VueRouter);
const Foo = {
template: `
<div>
<p>Foo</p>
<router-link to="/foo/bar">Go to Bar</router-link>
<router-view></router-view>
</div>`
};
const Bar = {
template: `<div><p>Bar</p></div>`
};
const routes = [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar
}
]
}
];
const router = new VueRouter({
routes
});
new Vue({
el: '#app',
render: h => h(App),
router
});
Код, соответствующий компоненту App:
<template>
<div class="app">
<h1>Vue Router App</h1>
<p>
<router-link to="/foo">Go to foo</router-link>
</p>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'app'
};
</script>
Установка плагина
Vue предоставляет один для всех плагиновVue.use()Чтобы установить зарегистрированный плагин, этот метод вызовет объект экспорта плагина.installMethod и передайте функцию VUE в качестве первого параметра. существуетsrc/install.jsВ файле находится установщик для Vue Router. В процессе установки есть несколько основных этапов:
- пройти через
Vue.mixin()глобальный миксинbeforeCreateа такжеdestroyedкрюк:
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
// new Vue 实例
this._routerRoot = this
this._router = this.$options.router
this._router.init(this) // 初始化
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
существуетbeforeCreateВ хуке для корневого экземпляра установите_routerRootдля самого экземпляра,_routerэкземпляр для VueRouter и вызовinitметод для инициализации, а затем передатьdefineReactiveПучок_routeАтрибут для обработки ответа, который является ключом к навигации по пути, ведущей к отображению представления. Для экземпляров компонентов через отношение цепочки родитель-потомокthis.$parent && this.$parent._routerRootнастраивать_routerRootАтрибуты. Вызывается в конце функцииregisterInstanceГлавное привязать экземпляр компонента к правилам роутинга, что потом пригодится.
- существует
Vue.prototypeсвойства монтирования
// 每个组件可以vm.$router获取VueRouter实例
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
// 每个组件可以vm.$route获取当前的路由路径Route
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
Вот почему мы можем передать в каждом компонентеvm.$routerа такжеvm.$routeЭкземпляр маршрута метода и причина текущего пути маршрута
- Глобальная регистрация компонентов маршрутизации
// 全局注册router-view和router-link组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
Затем мы можем использовать его в любом компонентеrouter-linkДля маршрутизации переходов используйтеrouter-viewСмонтируйте компонент маршрутизации.
VueRouter
После установки плагина объявляются и передаются правила конфигурации маршрутизации.new VueRouter(options)Создайте новый экземпляр маршрутизации. существуетsrc/index.jsОпределите класс VueRouter и инициализируйте некоторые свойства в конструкторе:
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
затем следует шаблон маршрутизацииmodeобработка:
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false // 是否降级
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
Это по умолчаниюhash. для установкиhistoryрежим, а браузер не поддерживаетhistory.pushStateи нет настройки для запрета автоматического перехода на более раннюю версиюfallback=false, будет автоматически использоватьсяhashЗамена шаблона. Если его нет на стороне браузера, он будет использоватьсяabstract, например, в среде узла он в основном использует массив для имитации стека записей просмотра. Наконец, по разнымmodeДавайте создадим новый экземпляр History, который является классом для коммутации маршрутов и управления записями:
// 根据mode创建不同的history实例
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
Все они определены вsrc/historyВ файле наследуются классы истории разных режимов и определяются вbase.jsбазовый класс.
init
В предыдущей установке маршрутизации она будет смешана в глобальномbeforeCreateХук и корневой экземпляр компонента будут вызыватьrouterпримерinitметод:
this._router.init(this) // 初始化
существуетinitПервый метод — это управление экземпляром Vue, который регистрирует маршрут:
this.apps.push(app)
if (this.app) {
return
}
this.app = app
При условии, что приложение имеет значение, оно вернется напрямую. Это связано с тем, что привязка события, связанного с переключением маршрута, обрабатывается только один раз:
const history = this.history
if (history instanceof HTML5History) {
history.`transitionTo`是跳转到指定路由位置的入口,(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
потому чтоhistoryНа этапе строительства уже по разнымmodeИнициализирован, поэтому он вызывает разные способы перехода к начальному корневому маршруту.transitionToэто запись для перехода к указанному местоположению маршрутизации,setupListenersМетод заключается в отслеживании изменений URL-адреса браузера, и мы проанализируем детали реализации позже.
matcher
Экземпляры маршрута имеют ключевое свойствоmatcher, представляющий сопоставитель маршрутов, который управляет записями маршрутов и новыми совпадениями маршрутов. передать в конструктореcreateMatcherметод инициализации:
this.matcher = createMatcher(options.routes || [], this)
Этот метод определен вsrc/create-matcher.js, который определяет некоторые методы обработки записей маршрутов и, наконец, экспортируетmatchа такжеaddRoutesОбъект метода:
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
// 创建RouteRecord映射
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
// ...
return {
match,
addRoutes
}
}
addRoutesМетод заключается в добавлении объекта конфигурации маршрутизации, поскольку не все правила маршрутизации определены заранее в нашем процессе разработки. В основном звонитеcreateRouteMapметод положитьroutesПравила конфигурации маршрутизации генерируют записи маршрутизации и сохраняют их в соответствующих переменных. запись маршрутизацииRouteRecord— объект форматирования для нашего объекта конфигурации маршрутизации и определение его типа:
declare type RouteRecord = {
path: string;
regex: RouteRegExp;
components: Dictionary<any>;
instances: Dictionary<any>;
name: ?string;
parent: ?RouteRecord;
redirect: ?RedirectOption;
matchAs: ?string;
beforeEnter: ?NavigationGuard;
meta: any;
props: boolean | Object | Function | Dictionary<boolean | Object | Function>;
}
посмотриcreateRouteMapОпределение метода:
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// 这里优先获取传入的存取对象
const pathList: Array<string> = oldPathList || []
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
// 遍历每个路由配置对象, 生成对象的RouteRecord对象
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
})
// 把通配符记录挪到最后
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
return {
pathList,
pathMap,
nameMap
}
}
Здесь сначала получите входящий объект доступа, в противном случае создайте некоторые значения по умолчанию, что является ключом к добавлению конфигурации маршрутизации позже. Затем просмотрите каждый объект конфигурации правила маршрутизации и вызовитеaddRouteRecordметод. Этот метод предназначен для создания объекта записи маршрутизации и сохранения соответствующего местоположения.
const record: RouteRecord = {
path: normalizedPath,
// 利用path-to-regexp库创建路径正则表达式
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
}
Для вложенных правил маршрутизации такжеchidlrenв каждом правиле маршрутизации и рекурсивно вызыватьaddRouteRecordметод:
if (route.children) {
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
наконец,pathListпредставляет собой массив всех путей правил маршрутизации,pathMapэто сопоставление путей к записям маршрутизации,nameMapЭто сопоставление имен маршрутов с записями маршрутов.
Для нашего примера сгенерированные результаты выглядят следующим образом:
Далее давайте посмотрим наmatchопределение метода.matchметод основан на текущемrouteа где ответ прыгаетlocationРассчитать новыйroute. Во-первых, чтобы отформатировать новую позицию прыжка:
const location = normalizeLocation(raw, currentRoute, false, router)
потому что мыroute-linkизtoсобственность илиrouter.pushПередаваемый метод может быть строкой или объектом, и он единообразно отформатирован какlocationобъект. Для определения его типа:
declare type Location = {
_normalized?: boolean;
name?: string;
path?: string;
hash?: string;
query?: Dictionary<string>;
params?: Dictionary<string>;
append?: boolean;
replace?: boolean;
}
соответствующийnameатрибутlocation, мы напрямуюnameMapСоответствующие записи маршрутизации можно получить в файле . Затем обработайте параметры маршрутизации, чтобы сгенерировать окончательный маршрут маршрута:
if (name) {
// 根据name拿到路由记录
const record = nameMap[name]
if (!record) return _createRoute(null, location)
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name)
if (typeof location.params !== 'object') {
location.params = {}
}
if (currentRoute && typeof currentRoute.params === 'object') {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key]
}
}
}
// 生成完成的路径字符串
location.path = fillParams(record.path, location.params, `named route "${name}"`)
return _createRoute(record, location, redirectedFrom)
}
только дляpathИнформация о местоположении атрибута должна быть пройдена через каждую запись для регулярного сопоставления, и сопоставление должно быть извлечено.pathпараметры вparamsВ объекте:
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i];
const record = pathMap[path];
if (matchRoute(record.regex, location.path, location.params)) {
// 匹配成功
return _createRoute(record, location, redirectedFrom)
}
}
В обоих случаях он будет вызван после нахождения записи маршрутизации для_createRouteметод для создания объекта маршрутаroute:
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
вredirectа такжеaliasэто обработка редиректов и псевдонимов, которые вызываются в них или при обычных обстоятельствахcreateRouteчтобы вернуть объект пути маршрута.
export function createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
const stringifyQuery = router && router.options.stringifyQuery
let query: any = location.query || {}
try {
query = clone(query)
} catch (e) {}
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
return Object.freeze(route)
}
объект маршрутаrouteи информация о местоположенииlocationСамое большое отличие состоит в том, что он содержит правило маршрутизации, соответствующее информации о пути.matched. это проходитformatMatchМетод генерирует:
// 构建route中matched,先父后子
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
Очевидно, он продолжит поиск отца записи, а затем бросит его в начало массива, поэтомуmatchedПорядок сопоставления сохраняется сначала родительским. Это свойство особенно важно для последующего рендеринга вида. Наконец, нашmatchМетод получает новый объект маршрутизации на основе новой информации о местоположении и текущего объекта маршрутизации.
history
В программной навигации по маршруту мы можем пройтиrouter.push()чтобы перейти к новому пути и отобразить новое представление. Давайте посмотрим на весь процесс со стороны входа.pushразные типыhistoryсобственное воплощение, дляHashHistoryопределяется вsrc/history/hash.js:
// 编程跳转
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
Этот метод в основном вызывает определение базового классаtransitionToметод:
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
const route = this.router.match(location, this.current) // 根据跳转的location计算出新的route
this.confirmTransition(
route,
() => {
// 更新route
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
err => {
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => {
cb(err)
})
}
}
)
}
навигационная охрана
существуетtransitionToсначала вызов методаmatchметод вычисляет новый объект маршрута, а затем вызываетconfirmTransitionметод, этот метод в основном обрабатывает логику навигационной защиты. Во-первых, он определит, равен ли новый объект маршрутизации старому объекту маршрутизации.Если он равен, он вызоветaboartфункция и выход:
const current = this.current;
const abort = err => {
if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err);
});
} else {
warn(false, 'uncaught error during route navigation:');
console.error(err);
}
}
onAbort && onAbort(err);
};
// 相同的route
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
this.ensureURL();
return abort(new NavigationDuplicated(route));
}
Затем по новым и старым объектам маршрутизации вычислить записи маршрутизации, которые необходимо обновить, входя и выходя:
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
resolveQueueОпределение метода:
// 根据新旧的matched得出不同类型的RouteRecord部分
// 比如 /foo/bar 和 /foo/baz 的 matched比较
// /foo => updated /foo/baz => activated /foo/bar => deactivated
function resolveQueue (
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
updated: Array<RouteRecord>,
activated: Array<RouteRecord>,
deactivated: Array<RouteRecord>
} {
let i
const max = Math.max(current.length, next.length)
for (i = 0; i < max; i++) {
if (current[i] !== next[i]) {
break
}
}
return {
updated: next.slice(0, i),
activated: next.slice(i),
deactivated: current.slice(i)
}
}
С помощью этих трех переменных мы можем легко извлечь хуки маршрутизации, соответствующие записям маршрутизации, включая хуки выхода, обновления или входа:
// 构建导航守卫函数队列
const queue: Array<?NavigationGuard> = [].concat(
// 组件beforeRouteLeave钩子
extractLeaveGuards(deactivated),
// 全局beforeEach
this.router.beforeHooks,
// 组件beforeRouteUpdate钩子
extractUpdateHooks(updated),
// 路由配置的组件beforeEnter钩子
activated.map(m => m.beforeEnter),
// 解析异步组件的钩子
resolveAsyncComponents(activated)
)
Этот массив функций ловушки будет использоваться какrunQueueВызов функции с аргументами:
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
const step = index => {
if (index >= queue.length) {
cb()
} else {
if (queue[index]) {
fn(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
step(0)
}
Этот метод просматривает хуки маршрутизации с нуля, и для каждого хука будет возвращено значениеfnПараметр функции вызывается, а во втором параметре функции рекурсивно вызывается логика выполнения следующего хука.Очевидно, что второй параметр аналогичен нашей функции хука.next.fnэто итератор, определенный для выполнения функции ловушки:
// 执行导航守卫钩子
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort();
}
try {
hook(route, current, (to: any) => {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
// next(false)情况
this.ensureURL(true);
abort(to);
} else if (
typeof to === 'string' ||
(typeof to === 'object' && (typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
// 在导航守卫中next重定向
abort();
if (typeof to === 'object' && to.replace) {
this.replace(to);
} else {
this.push(to);
}
} else {
// confirm transition and pass on the value
// 执行queue中的下一个钩子
next(to);
}
});
} catch (e) {
abort(e);
}
};
Эта функция очень проста: сначала она выполняет нашу функцию-ловушку маршрутизации, а затем решает, что она переданаnextаргументы функции, если ониfalseВ случае звонка напрямуюabortФункция прерывает навигацию, и, если это строка, она также переходит на новый путь и, наконец, успешно выполняет следующую функцию ловушки. существуетrunQueueПосле выполнения очереди ловушек метода будет выполнена следующая логика обратного вызова:
const postEnterCbs = [];
const isValid = () => this.current === route;
// 提取组件的beforeRouteEnter
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
// 全局beforeresolve
const queue = enterGuards.concat(this.router.resolveHooks);
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort();
}
this.pending = null;
onComplete(route);
if (this.router.app) {
// 在route-view组件更新完后执行beforeRouteEnter钩子传给next函数的回调
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => {
cb();
});
});
}
});
вpostEnterCbsправдаbeforeRouteEnterперешел на крючокnextОбработка параметров обратного вызова, он будет в$nextTickПосле визуализации и выполнения представления в это время может быть получен экземпляр объекта соответствующего компонента. в полном исполненииbeforeresolveПосле хука он будет выполнен первымonComplete(route)Обратный вызов успешного выполнения метода. В этом методе будет выполнятьсяupdateRouteобновить маршрут:
// 更新route
updateRoute (route: Route) {
const prev = this.current
this.current = route
// 修改app._route,通知视图更新
this.cb && this.cb(route)
// 执行全局beforeEach
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
Сначала обновите этот методthis.currentДля нового объекта маршрута выполните функцию cb, чтобы установить зарегистрированный маршрут приложения._routeметод. это вinitметод определен:
// init
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
// base.js
listen (cb: Function) {
this.cb = cb
}
Поскольку в корневом экземпляре приложения_routeобрабатывается реактивно, поэтому сброс вызовет егоsetter, что приведет к обновлению его зависимостей. Что касается этих зависимостей, то они фактически используютсяroute-viewОтрисовка экземпляров компонентовwatcher, то экземпляры выполняются повторноrenderфункция, согласно последнимrouteобъект для расчета компонентов маршрутизации для рендеринга, и, наконец, выход маршрутизации монтирует последний вид. существуетupdateRouteВ конце метода глобальныйafterEachкрюк.
URL Update
в исполненииupdateRouteПосле метода он также выполнит переданныйtransitionToУспешный обратный вызов метода. Для объектов HashHistory он будет выполнятьсяpushHashспособ обновления последнего пути маршрутизации:
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
Этот метод сначала определяет, поддерживает ли браузерpushState, если поддерживаетсяgetUrlметод получает заполненный URL-адрес и выполняетpushStateнастраивать. в противном случае используйтеwindow.location.hashЧтобы установить хэш URL-адреса браузера:
// 根据path获取完成的url
function getUrl (path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
export function pushState (url?: string, replace?: boolean) {
const history = window.history
try {
if (replace) {
// 传递之前的数据对象
const stateCopy = extend({}, history.state)
stateCopy.key = getStateKey()
history.replaceState(stateCopy, '', url)
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
Таким образом, когда мы выполняем маршрутную навигацию, URL-адрес браузера будет меняться все больше и больше. Также бывает ситуация, когда мы вручную вводим url, и хотим прослушать события ответа на навигацию по маршруту и обновить представление, для типа хэша оно находится вinitсерединаsetupListenersфункция для привязки:
setupListeners () {
const router = this.router
// 监听url变化事件
window.addEventListener(
supportsPushState ? 'popstate' : 'hashchange',
() => {
const current = this.current
if (!ensureSlash()) {
return
}
// 根据最新的hash进行路由切换
this.transitionTo(getHash(), route => {
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
)
}
вensureSlashФункция предназначена для обработки случая, когда путь отсутствует, он будет автоматически заменен на#/:
// 不存在hash的情况,默认为#/
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
компонент маршрутизации
router-view
Vue будет вrouter-viewВ компоненте правильный компонент отображается в соответствии с текущим объектом маршрутизации, так как же достигается этот процесс? Давайте посмотрим непосредственно на определенную функцию рендеринга этого компонента:
render(_, { props, children, parent, data }) {
data.routerView = true
const h = parent.$createElement
// 默认为defualt
const name = props.name
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
}
потому чтоrouter-viewЭто функциональный компонент, и в процессе рендеринга нет экземпляра компонента. По сравнению с обычным рендерингом компонента, он размещается в узле-заполнителе компонента.initСоздайте новый экземпляр компонента в хуке, а затем вызовите экземплярrender函数进行渲染。 Функциональный компонентrenderФункция выполняется непосредственно при создании узла-заполнителя, то есть текущий экземпляр находится вroute-viewОкружение компонента, которое передается функциональному компонентуrenderВторой параметр функцииparentАтрибуты.
По аналогии с нашим примером, первыйroute-viewво время выполненияparentявляется экземпляром компонента App, а второй — экземпляром компонента Foo.
с последующим расчетом токаrouter-viewГлубина, чтобы определить, имеет ли местозаполнитель vnode текущего компонентаrouterViewсвойства судить:
let depth = 0;
while (parent && parent._routerRoot !== parent) {
// route-view所在组件环境的占位vnode
const vnodeData = parent.$vnode && parent.$vnode.data;
if (vnodeData) {
if (vnodeData.routerView) {
depth++;
}
}
parent = parent.$parent;
}
data.routerViewDepth = depth;
Например, второй наш примерrouter-viewКомпонент Foo, поэтомуparent.$vnodeЭто vnode-заполнитель компонента Foo, поскольку он монтируется на первомrouter-view, так что этоrouterViewдляtrue, поэтому глубина равна 1;
Найдя глубину, вы можетеrouteв соответствующем пути объектаmatchedСвойства получают визуализированный объект компонентаcomponents, а затем получить соответствующий объект компонента в соответствии с конкретным именем представления:
const matched = route.matched[depth]
// render empty node if no matched route
if (!matched) {
cache[name] = null
return h()
}
const component = cache[name] = matched.components[name]
Затем на vnode монтируемого компонентаdataнастраиватьregisterRouteInstanceфункция, эта функция будет в компонентеbeforeCreateВызывается в ловушке и устанавливает соответствующую запись маршрутаinstancesатрибут, чтобы его можно было использовать в маршрутизацииbeforeRouteEnterПолучите соответствующий экземпляр компонента из:
data.registerRouteInstance = (vm, val) => {
// val could be undefined for unregistration
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
Наконец, верните плейсхолдер vnode монтируемого компонента:
return h(component, data, children)
потому что мыrouter-viewизrenderиспользуется в функцииvm.$routeсвойство, поэтому рендеринг текущего экземпляраwatcherсобирается как зависимость, т.е.router-viewэкземпляра среды, в котором находится компонентrenderФункция будет повторно выполняться при переключении маршрута. Вот почему представление обновляется, когда мы выполняем навигацию по маршруту.
router-link
router-linkКомпонент является глобальным компонентом, используемым для прыжков, и его внутренний принцип реализации также называетсяrouter.pushметод маршрута для навигации. посмотриrenderфункция:
const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(
this.to,
current,
this.append
)
первый звонокrouter.resolveметод для расчета нового объекта маршрута:
resolve (
to: RawLocation,
current?: Route,
append?: boolean
): {
location: Location,
route: Route,
href: string,
// for backwards compat
normalizedTo: Location,
resolved: Route
} {
current = current || this.history.current
const location = normalizeLocation(
to,
current,
append,
this
)
const route = this.match(location, current)
const fullPath = route.redirectedFrom || route.fullPath
const base = this.history.base
const href = createHref(base, fullPath, this.mode)
return {
location,
route,
href,
// for backwards compat
normalizedTo: location,
resolved: route
}
}
Затем есть группа, которая обрабатывает стили «нажми и соединись». Затем определите охранную функциюguardEvent, в некоторых случаях он вернется напрямую без навигации:
// 守卫函数
function guardEvent (e) {
// don't redirect with control keys
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
// don't redirect when preventDefault called
if (e.defaultPrevented) return
// don't redirect on right click
if (e.button !== undefined && e.button !== 0) return
// don't redirect if `target="_blank"`
if (e.currentTarget && e.currentTarget.getAttribute) {
const target = e.currentTarget.getAttribute('target')
if (/\b_blank\b/i.test(target)) return
}
// this may be a Weex event which doesn't have this method
if (e.preventDefault) {
e.preventDefault()
}
return true
}
Затем определите функцию обработчика щелчка соединения, которая будет вызываться в соответствии с оценкой конфигурации.pushещеreplaceметод:
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location, noop)
} else {
router.push(location, noop)
}
}
}
const on = { click: guardEvent };
if (Array.isArray(this.event)) {
// 处理配置的其他事件
this.event.forEach(e => {
on[e] = handler;
});
} else {
on[this.event] = handler;
}
дляtagнетaВ случае с этикетками также необходимо иметь дело сslotЗдесьaЕсли есть метка, поставьdataОн прикреплен к телу, в противном случае назначение на внешнем слое. Наконец, вернутьсяtagСоздал vnode:
return h(this.tag, data, this.$slots.default)
Суммировать
Слишком далеко,Vue-RouterИсходный код был грубо проанализирован, на самом деле, есть много деталей реализации, которые еще не были вычтены, например, как форматироватьlocation, как извлечь роутинг хук соответствующего компонента, прокрутку обработки и т.д., но это не влияет на основной процесс роутинга изменений для рендеринга вида. Весь процесс можно резюмировать следующим образом: