VueRouter 원리 조금 만 배 워 볼 게 요.
두 가지 경로 모델 의 기본 원리
vue - router 를 사용 해 보면 두 가지 모드 를 제공 한 다 는 것 을 알 수 있 습 니 다.
hash
과 history
라 우 터 를 통 해 옵션 mode 를 구축 할 수 있 습 니 다.SPA 의 전단 경 로 를 간단하게 이해 하 는 것 은:전단 경로 아래 에서 두 가지 예 를 통 해 이 두 가지 경로 의 가장 기본 적 인 원 리 를 알 아 보 자.
hash 모드
hash 모드 는 URL. hash (즉 url 에서
#
식별 자 를 수정 한 내용) 를 통 해 이 루어 집 니 다.URL. hash 의 변경 사항 은 브 라 우 저 로 페이지 를 불 러 오지 않 지만 history 기록 을 주동 적 으로 수정 합 니 다.
router-hash // hash Home About
// hash window.addEventListener('load', () => { app.innerHTML = location.hash.slice(1) }) // hash window.addEventListener('hashchange', () => { app.innerHTML = location.hash.slice(1) })
history 模式
history 模式 主要原理是使用了浏览器的 history API,主要是 history.pushState()
和 history.replaceState()
两个方法。
通过这两种方法修改 history 的 url 记录时,浏览器不会检查并加载新的 url 。
这两个方法都是接受三个参数:
- 状态对象 -- 可以用来暂存一些数据
- 标题 -- 暂无效 一般写空字符串
- url -- 新的历史 url 记录
两个方法的区别是 replaceState()
仅修改当前记录而非新建。
router-history // go Home About
// history function go(pathname) { history.pushState(null, '', pathname) app.innerHTML = pathname } // window.addEventListener('popstate', () => { app.innerHTML = location.pathname })
手写一个超简易的 VueRouter
看源码之前,先通过一个简易的 VueRouter 了解一下整体的结构和逻辑。
class HistoryRoute {
constructor() {
this.current = null
}
}
class VueRouter {
constructor(opts) {
this.mode = opts.mode || 'hash';
this.routes = opts.routes || [];
//
this.routesMap = this.creatMap(this.routes);
//
this.history = new HistoryRoute();
this.init();
}
// history.current
init() {
if (this.mode === 'hash') {
location.hash ? '' : location.hash = '/';
window.addEventListener('load', () => {
this.history.current = location.hash.slice(1);
});
window.addEventListener('hashchange', () => {
this.history.current = location.hash.slice(1);
});
} else {
location.pathname ? '' : location.pathname = '/';
window.addEventListener('load', () => {
this.history.current = location.pathname;
});
window.addEventListener('popstate', () => {
this.history.current = location.pathname;
})
}
}
//
// {
// '/': HomeComponent,
// '/about': AboutCompontent
// }
creatMap(routes) {
return routes.reduce((memo, current) => {
memo[current.path] = current.component;
return memo;
}, {})
}
}
// Vue.use(Router)
VueRouter.install = function (Vue) {
// $router $route
Object.defineProperty(Vue.prototype, '$router', {
get() {
return this.$root._router;
}
});
Object.defineProperty(Vue.prototype, '$route', {
get() {
return this.$root._route;
}
});
// beforeCreate
Vue.mixin({
beforeCreate() {
// this.$options.router
if (this.$options && this.$options.router) {
this._router = this.$options.router;
// this
// https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js
Vue.util.defineReactive(this, '_route', this._router.history);
}
},
});
// & JSX https://cn.vuejs.org/v2/guide/render-function.html
// router-link
// a
Vue.component('router-link', {
props: {
to: String,
tag: String
},
methods: {
handleClick() {
const mode = this._self.$root._router.mode;
location.href = mode === 'hash' ? `#${this.to}` : this.to;
}
},
render: function (h) {
const mode = this._self.$root._router.mode;
const tag = this.tag || 'a';
return (
{ this.$slots.default }
);
}
});
// router-view
// history.current
Vue.component('router-view', {
render: function (h) {
const current = this._self.$root._route.current;
const routeMap = this._self.$root._router.routesMap;
return h(routeMap[current]);
}
});
}
export default VueRouter;
120 줄 코드 는 가장 기본 적 인 VueRouter 를 실현 하고 전체적인 구 조 를 정리 합 니 다.
Vue.use(VueRouter)
을 사용 할 때 호출 됩 니 다.$router
$route
과 router-view
router-link
두 개의 전역 구성 요 소 를 추가 했다._route
으로 정의 되 었 습 니 다. 이 속성 이 바 뀌 면 의존 하 는 응답 이 렌 더 링 router-view
의 구성 요 소 를 촉발 합 니 다.지금 은 VueRouter 에 대해 기본 적 인 인식 을 가지 게 되 었 고 소스 코드 를 보 러 갈 때 좀 쉬 워 졌 다.
소스 코드 를 맛보다.
다음은 제 가 VueRouter 소스 코드 를 보고 글 과 결합 한 학습 노트 입 니 다.
원본 코드 를 읽 는 과정 에서 이해 하기 쉬 운 주석 을 썼 습 니 다. 원본 코드 를 읽 는 데 도움 이 되 기 를 바 랍 니 다. github: vue - router 소스 코드.
vue - router 의 src 디 렉 터 리 는 다음 과 같 습 니 다. 다음은 이 주요 파일 의 역할 을 순서대로 분석 하 겠 습 니 다.
index.js
VueRouter 의 입구 파일 은 VueRouter 클래스 를 정의 하고 내 보 내 는 역할 을 합 니 다.다음은
index.js
소스 코드 로 flow 와 관련 된 유형 정의 와 함수 의 구체 적 인 실현 을 삭 제 했 습 니 다. 먼저 전체적인 구조 와 각 부분의 기능 을 살 펴 보 겠 습 니 다.// ...
// VueRouter
export default class VueRouter {
//
// install vue ,Vue.use install
static install: () => void;
static version: string;
//
constructor (options) {}
//
match ( raw, current, redirectedFrom ) {}
// history.current
get currentRoute () {}
// ,
init (app) {}
//
beforeEach (fn) {} //
beforeResolve (fn) {} //
afterEach (fn) {} //
onReady (cb, errorCb) {} //
onError (errorCb) {} //
// history
push (location, onComplete, onAbort) {}
replace (location, onComplete, onAbort) {}
go (n) {}
back () {}
forward () {}
//
getMatchedComponents (to) {}
//
resolve (to, current, append) {}
//
addRoutes (routes) {}
}
// ,push
function registerHook (list, fn) {}
// (hash / history) location.href
function createHref (base, fullPath, mode) {}
//
VueRouter.install = install
VueRouter.version = '__VERSION__'
// window.Vue Vue.use
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
다음은 주요 방법의 구체 적 인 실현 을 살 펴 보 자.
constructor
VueRouter 구조 함수, 주로 세 가지 일 을 했 습 니 다.
new VueRouter()
시 들 어 오 는 매개 변 수 를 초기 화 합 니 다.history 인 스 턴 스 및 match 일치 함수 에 대해 서 는 나중에 말씀 드 리 겠 습 니 다.
constructor(options = {}) {
this.app = null // , init
this.apps = [] // , init
this.options = options //
this.beforeHooks = [] //
this.resolveHooks = [] //
this.afterHooks = [] //
this.matcher = createMatcher(options.routes || [], this) // match
let mode = options.mode || 'hash' // hash
// history hash
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
// abstract
if (!inBrowser) {
mode = 'abstract'
}
this.mode = 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}`)
}
}
}
init
install.js
에서 init 함 수 는 루트 구성 요소 인 스 턴 스 의 beforeCreate 수명 주기 함수 에서 호출 되 어 루트 구성 요소 인 스 턴 스 로 전 달 됩 니 다.//
init(app) {
//
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
//
this.apps.push(app)
// app
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', () => {
// , app this.apps ,
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] || null
})
// 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
)
}
// , _route ,
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
install.js
이 파일 은 주로 인 스 톨 방법 을 정의 하고 내 보 냅 니 다.
Vue.use(VueRouter)
에서 호출 됩 니 다.인 스타 그램 방법 은 주로 이 몇 가지 일 을 했다._routerRoot
속성 을 추가 하여 모든 구성 요소 에서 경로 정보 와 방법 을 얻 을 수 있 도록 합 니 다.$router
$route
두 가지 속성 은 각각 경로 인 스 턴 스 와 현재 경로 임 을 나타 낸다.import View from './components/view'
import Link from './components/link'
export let _Vue
export function install (Vue) {
//
if (install.installed && _Vue === Vue) return
install.installed = true
// install Vue _Vue
// Vue
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
// beforeCreate
Vue.mixin({
beforeCreate () {
// router ,
if (isDef(this.$options.router)) {
//
this._routerRoot = this
this._router = this.$options.router
// , VueRouter init
this._router.init(this)
// _router
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// $parent _routerRoot ,
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
// $router $route
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// router-link router-view
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
create-route-map.js
create-route-map.js
파일 은 path 와 name 에 따 른 맵 을 만 드 는 데 사용 되 는 createRouteMap 방법 을 내 보 냅 니 다.// ...
// map
export function createRouteMap(routes, oldPathList, oldPathMap, oldNameMap) {
// pathList
const pathList = oldPathList || []
// path
const pathMap = oldPathMap || Object.create(null)
// name
const nameMap = oldNameMap || Object.create(null)
// pathList pathMap nameMap
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
}
}
function addRouteRecord(pathList, pathMap, nameMap, route, parent, matchAs) {}
function compileRouteRegex(path, pathToRegexpOptions) {}
function normalizePath(path, parent, strict) {}
addRouteRecord
createRouteMap 함수 에서 가장 중요 한 단 계 는 경로 설정 을 옮 겨 다 니 며 맵 에 추 가 된 addRouteRecord 함수 입 니 다.addRoute Record 함수 역할 은 두 개의 맵 표를 생 성 하 는 것 입 니 다. PathMap 과 NameMap 은 각각 path 와 name 을 통 해 해당 하 는 경로 기록 대상 을 조회 할 수 있 습 니 다. 경로 기록 대상 은 meta, props, 그리고 가장 중요 한 components 보기 구성 요소 인 스 턴 스 를 포함 하여
router-view
그룹 에 렌 더 링 할 수 있 습 니 다.//
function addRouteRecord(pathList, pathMap, nameMap, route, parent, matchAs) {
// path, name
const { path, name } = route
//
const pathToRegexpOptions = route.pathToRegexpOptions || {}
// path
// pathToRegexpOptions.strict /
// /
const normalizedPath = normalizePath(
path,
parent,
pathToRegexpOptions.strict // (default: false)
)
// ?( :false)
// caseSensitive pathToRegexpOptions.sensitive
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
//
const record = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
// ,
components: route.components || {
default: route.component
},
instances: {},
name,
parent,
matchAs, // alias path , matchAs
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props == null ? {} : route.components ?
route.props : {
default: route.props
}
}
if (route.children) {
// , , , 。
// name
// https://github.com/vuejs/vue-router/issues/629
if (process.env.NODE_ENV !== 'production') {
if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
warn(
false,
`Named Route '${route.name}' has a default child route. ` +
`When navigating to this named route (:to="{name: '${route.name}'"), ` +
`the default child route will not be rendered. Remove the name from ` +
`this route and use the name of the default child route for named ` +
`links instead.`
)
}
}
// children ,
route.children.forEach(child => {
// path matchAs
const childMatchAs = matchAs ?
cleanPath(`${matchAs}/${child.path}`) :
undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// alias
if (route.alias !== undefined) {
const aliases = Array.isArray(route.alias) ?
route.alias : [route.alias]
aliases.forEach(alias => {
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
})
}
// path map
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
//
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
false,
`Duplicate named routes definition: ` +
`{ name: "${name}", path: "${record.path}" }`
)
}
}
}
재 귀적 인 하위 경로 에는 경로 이름 이 있 고 기본 적 인 하위 경로 가 있 을 때 개발 환경 에 알림 이 있 습 니 다.이 힌트 는 bug 를 피 하 는 데 사 용 됩 니 다. 구체 적 으로 대응 하 는 issue 를 볼 수 있 습 니 다. 쉽게 말 하면 이름 경로 에 기본 하위 경로 가 있 을 때 입 니 다.
routes: [{
path: '/home',
name: 'home',
component: Home,
children: [{
path: '',
name: 'home.index',
component: HomeIndex
}]
}]
사용
to="/home"
은 HomeIndex 기본 하위 경로 로 이동 하고 사용 :to="{ name: 'home' }"
은 Home 으로 만 이동 하 며 HomeIndex 기본 하위 경로 가 표시 되 지 않 습 니 다.위의 addRoute Record 함수 원본 코드 를 통 해 이 두 가지 점프 방식 path 와 name 이 서로 다른 이 유 를 알 수 있 습 니 다. path 와 name 을 통 해 각각 두 개의 맵 표 에서 해당 하 는 경로 기록 을 찾 기 때 문 입 니 다. pathMap 생 성 과정 에서 먼저 하위 경로 로 돌아 갑 니 다. 예 를 들 어 이 하위 경로 의 경로 기록 을 추가 할 때 key 는 /home
입 니 다.하위 경로 추가 후 부모 경로 추가 시 판단 /home
이 존재 하면 pathMap 에 추가 되 지 않 습 니 다.한편, nameMap 의 key 는 name 이 고 home
대응 하 는 것 은 Home 구성 요소 이 며 home.index
대응 하 는 HomeIndex 입 니 다.create-matcher.js
createmacher 함 수 는 경로 설정 에 따라 createRouteMap 방법 으로 맵 표를 만 들 고 일치 하 는 경로 기록 match 와 경로 기록 addRoutes 두 가지 방법 을 제공 합 니 다.
addRoutes 는 루트 설정 을 동적 으로 추가 하 는 데 사 용 됩 니 다.match 는 들 어 오 는 location 와 경로 대상 에 따라 새로운 경로 대상 을 되 돌려 줍 니 다.
// routes VueRouter routes
// router VueRouter
export function createMatcher(routes, router) {
//
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes(routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
//
function match(raw, currentRoute, redirectedFrom) {
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
//
// location record
} else if (location.path) {
//
// location record
}
//
return _createRoute(null, location)
}
function redirect(record, location) {
// ...
}
function alias(record, location, matchAs) {
// ...
}
//
function _createRoute(record, location, redirectedFrom) {
// redirect
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
// alias
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
return {
match,
addRoutes
}
}
history/base.js
history / base. js 에서 History 류 를 정 의 했 는데 주요 한 역할 은 경로 가 변화 할 때 transition To 방법 을 호출 하여 해당 하 는 경로 기록 을 얻 고 일련의 수비 갈고리 함 수 를 순서대로 집행 하 는 것 이다.
export class History {
constructor (router, base) {
this.router = router
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
}
listen (cb) {
this.cb = cb
}
onReady (cb, errorCb) {
if (this.ready) {
cb()
} else {
this.readyCbs.push(cb)
if (errorCb) {
this.readyErrorCbs.push(errorCb)
}
}
}
onError (errorCb) {
this.errorCbs.push(errorCb)
}
// , VueRouter
transitionTo (location, onComplete, onAbort) {
//
const route = this.router.match(location, this.current)
//
this.confirmTransition(route, () => {
//
// , _route ,
// afterHooks
this.updateRoute(route)
// hashchange
onComplete && onComplete(route)
// URL
this.ensureURL()
// ready
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) })
}
})
}
//
confirmTransition (route, onComplete, onAbort) {
const current = this.current
//
const abort = err => {
if (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)
}
//
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()
}
// , ,
const { updated, deactivated, activated } = resolveQueue(this.current.matched, route.matched)
//
const queue = [].concat(
//
extractLeaveGuards(deactivated),
// beforeEach
this.router.beforeHooks,
// ,
extractUpdateHooks(updated),
// enter
activated.map(m => m.beforeEnter),
//
resolveAsyncComponents(activated)
)
//
this.pending = route
// , queue
const iterator = (hook, next) => {
//
if (this.pending !== route) {
return abort()
}
try {
//
hook(route, current, (to) => {
// next,
//
// next()
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' && (
typeof to.path === 'string' ||
typeof to.name === 'string'
))
) {
// next('/') next({ path: '/' }) ->
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// next
// runQueue step(index + 1)
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {
abort(e)
}
}
//
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// , , runQueue cb()
//
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
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) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => { cb() })
})
}
})
})
}
updateRoute (route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
}
레 퍼 런 스
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
전단 자동화 워 크 플 로 의 hooks예 를 들 어 우 리 는 git commt 전에 eslint 코드 검사, npm install 전에 프로젝트 의존 도 를 검사 하고 싶 습 니 다.전형 적 인 상황 에서 각종 도 구 는 특정한 동작 이 발생 할 때 ...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.