共计 16210 个字符,预计需要花费 41 分钟才能阅读完成。
vue@3.3.4 源码解析
前端路由次要有 2 局部组成:1、url 的解决;2、组件的加载
[toc]
install 函数
export function install (Vue) {
// 防止反复加载
if (install.installed && _Vue === Vue) return
install.installed = true
_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)
}
}
Vue.mixin({beforeCreate () {if (isDef(this.$options.router)) {
// 根路由组件
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)
}
})
// $router 和 $route 绑定在 Vue 的原型对象上,不便全局拜访
Object.defineProperty(Vue.prototype, '$router', {get () {return this._routerRoot._router}
})
Object.defineProperty(Vue.prototype, '$route', {get () {return this._routerRoot._route}
})
// 注册 router-view 和 router-link 组件
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
}
同 vuex
一样,vue-router
的注入机会也是在 beforeCreated
. 不过在destroyed
的生命周期钩子中多了一步
VueRouter 类
History 类
export class History {
// 定义公有变量
router: Router
base: string
current: Route
pending: ?Route
cb: (r: Route) => void
ready: boolean
readyCbs: Array<Function>
readyErrorCbs: Array<Function>
errorCbs: Array<Function>
listeners: Array<Function>
cleanupListeners: Function
// implemented by sub-classes
+go: (n: number) => void
+push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
+replace: (
loc: RawLocation,
onComplete?: Function,
onAbort?: Function
) => void
+ensureURL: (push?: boolean) => void
+getCurrentLocation: () => string
+setupListeners: Function
constructor (router: Router, base: ?string) {
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 = []
this.listeners = []}
listen (cb: Function) {this.cb = cb}
onReady (cb: Function, errorCb: ?Function) {if (this.ready) {cb()
} else {this.readyCbs.push(cb)
if (errorCb) {this.readyErrorCbs.push(errorCb)
}
}
}
onError (errorCb: Function) {this.errorCbs.push(errorCb)
}
// 重点:transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
let route
// catch redirect option https://github.com/vuejs/vue-router/issues/3201
try {
// 获取匹配的路由对象
route = this.router.match(location, this.current)
} catch (e) {
this.errorCbs.forEach(cb => {cb(e)
})
// Exception should still be thrown
throw e
}
// 保留旧路由对象
const prev = this.current
this.confirmTransition(
route,
() => {
// 更新路由对象
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
this.router.afterHooks.forEach(hook => {hook && hook(route, prev)
})
// 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) {
// Initial redirection should not mark the history as ready yet
// because it's triggered by the redirection instead
// https://github.com/vuejs/vue-router/issues/3225
// https://github.com/vuejs/vue-router/issues/3331
if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
this.ready = true
this.readyErrorCbs.forEach(cb => {cb(err)
})
}
}
}
)
}
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
this.pending = route
const abort = err => {
// changed after adding errors with
// https://github.com/vuejs/vue-router/pull/3047 before that change,
// redirect and aborted navigation would produce an err == null
if (!isNavigationFailure(err) && isError(err)) {if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {cb(err)
})
} else {if (process.env.NODE_ENV !== 'production') {warn(false, 'uncaught error during route navigation:')
}
console.error(err)
}
}
onAbort && onAbort(err)
}
const lastRouteIndex = route.matched.length - 1
const lastCurrentIndex = current.matched.length - 1
if (isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
lastRouteIndex === lastCurrentIndex &&
route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
) {this.ensureURL()
if (route.hash) {handleScroll(this.router, current, route, false)
}
return abort(createNavigationDuplicatedError(current, route))
}
const {updated, deactivated, activated} = resolveQueue(
this.current.matched,
route.matched
)
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// async components
resolveAsyncComponents(activated)
)
const iterator = (hook: NavigationGuard, next) => {if (this.pending !== route) {return abort(createNavigationCancelledError(current, route))
}
try {hook(route, current, (to: any) => {if (to === false) {// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(createNavigationAbortedError(current, route))
} else if (isError(to)) {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
abort(createNavigationRedirectedError(current, route))
if (typeof to === 'object' && to.replace) {this.replace(to)
} else {this.push(to)
}
} else {
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {abort(e)
}
}
runQueue(queue, iterator, () => {
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {if (this.pending !== route) {return abort(createNavigationCancelledError(current, route))
}
this.pending = null
onComplete(route)
if (this.router.app) {this.router.app.$nextTick(() => {handleRouteEntered(route)
})
}
})
})
}
updateRoute (route: Route) {
this.current = route
this.cb && this.cb(route)
}
setupListeners () {// Default implementation is empty}
teardown () {
// clean up event listeners
// https://github.com/vuejs/vue-router/issues/2341
this.listeners.forEach(cleanupListener => {cleanupListener()
})
this.listeners = []
// reset current history route
// https://github.com/vuejs/vue-router/issues/3294
this.current = START
this.pending = null
}
}
HTML5History
export class HTML5History extends History {
_startLocation: string
constructor (router: Router, base: ?string) {super(router, base)
//
this._startLocation = getLocation(this.base)
}
// 设置监听器
setupListeners () {
// 如果存在 listener,间接返回
if (this.listeners.length > 0) {return}
const router = this.router
const expectScroll = router.options.scrollBehavior
// 是否反对 history.pushState api
const supportsScroll = supportsPushState && expectScroll
// 如果浏览器反对 pushState 的 api,则设置 popstate 的监听事件
if (supportsScroll) {this.listeners.push(setupScroll())
}
// 解决路由监听事件函数
const handleRoutingEvent = () => {
const current = this.current
// Avoiding first `popstate` event dispatched in some browsers but first
// history route not updated since async guard at the same time.
// 防止某些浏览器首次加载触发 popstate 事件,导致路由状态更新不同步
const location = getLocation(this.base)
if (this.current === START && location === this._startLocation) {return}
this.transitionTo(location, route => {if (supportsScroll) {
// 保留页面滚动条地位
handleScroll(router, route, current, true)
}
})
}
window.addEventListener('popstate', handleRoutingEvent)
this.listeners.push(() => {window.removeEventListener('popstate', handleRoutingEvent)
})
}
// 封装 history go 办法
go (n: number) {window.history.go(n)
}
//
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {const { current: fromRoute} = this
this.transitionTo(location, route => {
//
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
//
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {const { current: fromRoute} = this
this.transitionTo(location, route => {replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
ensureURL (push?: boolean) {if (getLocation(this.base) !== this.current.fullPath) {const current = cleanPath(this.base + this.current.fullPath)
push ? pushState(current) : replaceState(current)
}
}
getCurrentLocation (): string {return getLocation(this.base)
}
}
export function getLocation (base: string): string {
let path = window.location.pathname
const pathLowerCase = path.toLowerCase()
const baseLowerCase = base.toLowerCase()
// base="/a" shouldn't turn path="/app"into"/a/pp"
// https://github.com/vuejs/vue-router/issues/3555
// so we ensure the trailing slash in the base
if (base && ((pathLowerCase === baseLowerCase) ||
(pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) {path = path.slice(base.length)
}
return (path || '/') + window.location.search + window.location.hash
}
getLocation
获取 url 的 path,不蕴含 base
export function getLocation (base: string): string {
let path = window.location.pathname
const pathLowerCase = path.toLowerCase()
const baseLowerCase = base.toLowerCase()
// base="/a" shouldn't turn path="/app"into"/a/pp"
// https://github.com/vuejs/vue-router/issues/3555
// so we ensure the trailing slash in the base
if (base && ((pathLowerCase === baseLowerCase) ||
(pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) {path = path.slice(base.length)
}
return (path || '/') + window.location.search + window.location.hash
}
setupScroll 函数
export function setupScroll () {
// Prevent browser scroll behavior on History popstate
if ('scrollRestoration' in window.history) {window.history.scrollRestoration = 'manual'}
// Fix for #1585 for Firefox
// Fix for #2195 Add optional third attribute to workaround a bug in safari https://bugs.webkit.org/show_bug.cgi?id=182678
// Fix for #2774 Support for apps loaded from Windows file shares not mapped to network drives: replaced location.origin with
// window.location.protocol + '//' + window.location.host
// location.host contains the port and location.hostname doesn't
const protocolAndPath = window.location.protocol + '//' + window.location.host
const absolutePath = window.location.href.replace(protocolAndPath, '')
// preserve existing history state as it could be overriden by the user
const stateCopy = extend({}, window.history.state)
stateCopy.key = getStateKey()
window.history.replaceState(stateCopy, '', absolutePath)
window.addEventListener('popstate', handlePopState)
return () => {window.removeEventListener('popstate', handlePopState)
}
}
pushState 函数
export function pushState (url?: string, replace?: boolean) {saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {if (replace) {
// preserve existing history state as it could be overriden by the user
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)
}
}
replaceState 函数
export function replaceState (url?: string) {pushState(url, true)
}
HashHistory
export class HashHistory extends History {constructor (router: Router, base: ?string, fallback: boolean) {super(router, base)
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {return}
ensureSlash()}
// this is delayed until the app mounts
// to avoid the hashchange listener being fired too early
setupListeners () {if (this.listeners.length > 0) {return}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {this.listeners.push(setupScroll())
}
const handleRoutingEvent = () => {
const current = this.current
if (!ensureSlash()) {return}
this.transitionTo(getHash(), route => {if (supportsScroll) {handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {replaceHash(route.fullPath)
}
})
}
// 如果反对 popstate 则,应用 popstate 监听,否则应用 hashchange 监听,留神 vue-router4 曾经不反对 hashchange 监听
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
eventType,
handleRoutingEvent
)
this.listeners.push(() => {window.removeEventListener(eventType, handleRoutingEvent)
})
}
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
)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {const { current: fromRoute} = this
this.transitionTo(
location,
route => {replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
go (n: number) {window.history.go(n)
}
ensureURL (push?: boolean) {
const current = this.current.fullPath
if (getHash() !== current) {push ? pushHash(current) : replaceHash(current)
}
}
getCurrentLocation () {return getHash()
}
}
pushHash
function pushHash (path) {if (supportsPushState) {pushState(getUrl(path))
} else {window.location.hash = path}
}
replaceHash
function replaceHash (path) {if (supportsPushState) {replaceState(getUrl(path))
} else {window.location.replace(getUrl(path))
}
}
Hash 路由与 History 的区别
- hash 路由在地址栏 URL 上有 #,而 history 路由没有会难看一点
- 咱们进行回车刷新操作,hash 路由会加载到地址栏对应的页面,而 history 路由个别就 404 报错了,所以 history 在部署的时候,如 nginx,须要只渲染⾸⻚,让⾸⻚依据门路从新跳转。
- hash 路由反对低版本的浏览器,而 history 路由是 HTML5 新增的 API。(IE10 及以上)
- 默认扭转 hash 路由,浏览器端不会发出请求,次要用于锚点;history 路由 go/back/forward 以及浏览器中的后退,后退按钮,个别都会向服务器发动申请
- hash 模式,是不⽀持 SSR 的,然而 history 模式能够做 SSR
History 对象
- pushState / replaceState 都不会触发 popState 事件
-
popState 什么时候触发
- 点击浏览器的后退 / 后退按钮
- back/forward/go
导航守卫的触发程序
- 【组件】- 前一个组件的 beforeRouteLeave
- 【全局】- router.beforeEach
- 【组件】- 如果是路由的参数变动,会触发 beforeRouteUpdate
- 【配置文件】- beforeEnter
- 【组件】外部申明的 beforeRouteEnter
- 【全局】beforeResolve
- 【全局】router.afterEach
手写 Hsitory 路由
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>History 路由 </title>
</head>
<body>
<div id="container">
<button onclick="window.location.hash ='#'"> 首页 </button>
<button onclick="window.location.hash ='#about'"> 对于咱们 </button>
<button onclick="window.location.hash ='#user'"> 用户列表 </button>
</div>
<div id="context"></div>
<script>
class HistoryRouter {constructor() {this.routes = {};
this._bindPopstate();
// this.init();}
init(path) {window.history.replaceState({ path}, null, path);
const cb = this.routes[path];
if (cb) {cb();
}
}
route(path, callback) {this.routes[path] = callback || function () {};
}
go(path) {window.history.pushState({ path}, null, path);
const cb = this.routes[path];
if (cb) {cb();
}
}
_bindPopstate() {window.addEventListener("popstate", (e) => {
const path = e.state && e.state.path;
this.routes[path] && this.routes[path]();});
}
}
const Route = new HistoryRouter();
Route.route("./about", () => changeText("对于咱们页面"));
Route.route("./user", () => changeText("用户列表页"));
Route.route("./", () => changeText("首页"));
function changeText(arg) {document.getElementById("context").innerHTML = arg;
}
container.addEventListener("click", (e) => {if (e.target.tagName === "A") {e.preventDefault();
Route.go(e.target.getAttribute("href"));
}
});
</script>
</body>
</html>
手写 Hash 路由
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hash 路由 </title>
</head>
<body>
<div id="container">
<button onclick="window.location.hash ='#'"> 首页 </button>
<button onclick="window.location.hash ='#about'"> 对于咱们 </button>
<button onclick="window.location.hash ='#user'"> 用户列表 </button>
</div>
<div id="context"></div>
<script>
class HashRouter {constructor() {this.routes = {};
this.refresh = this.refresh.bind(this);
window.addEventListener("load", this.refresh);
window.addEventListener("hashchange", this.refresh);
}
route(path, callback) {this.routes[path] = callback || function () {};
}
refresh() {const path = `/${window.location.hash.slice(1) || ""}`;
this.routes[path]();}
}
const Route = new HashRouter();
Route.route("/about", () => changeText("对于咱们页面"));
Route.route("/user", () => changeText("用户列表页"));
Route.route("/", () => changeText("首页"));
function changeText(arg) {document.getElementById("context").innerHTML = arg;
}
</script>
</body>
</html>
webpack import 函数
vue 路由懒加载时通过 Vue 的异步组件和 Webpack 的代码宰割性能来实现的。
代码宰割是 webpack 最引人注目的个性之一。这个个性容许您将代码宰割成各种包,而后能够按需或并行加载这些包。它能够用来实现更小的 bundle 和管制资源加载优先级,如果应用正确,会对加载工夫产生重大影响。
这里次要剖析 import()
这种代码宰割形式,其余形式疏忽。
- 首先,
webpack
遇到import 办法
时,会将其当成一个代码宰割点,也就是说碰到import 办法
了,那么就去解析import 办法
。 - 而后,
import
援用的文件,webpack
会将其编译成一个jsonp
,也就是一个自执行函数,而后函数外部是援用的文件的内容,因为到时候是通过jsonp
的办法去加载的 - 具体就是,
import
援用文件,会先调用require.ensure
办法 (打包的后果来看叫require.e
),这个办法次要是结构一个promise
,会将resolve
,reject
和promise
放到一个数组中,将promise
放到一个队列中。 - 而后,调用
require.load
(打包后果来看叫require.l
) 办法,这个办法次要是创立一个jsonp
,也就是创立一个script
标签,标签的url
就是文件加载地址,而后塞到document.head
中,一塞进去,就会加载该文件了。 - 加载完,就去执行这段
jsonp
,次要就是把moduleId
和module
内容存到 modules 数组中,而后再去走webpack
内置的require
。 - webpack 内置的 require,次要是先判断缓存,这个 moduleId 是否缓存过了,如果缓存过了,就间接返回。如果没有缓存,再持续往下走,也就是加载 module 内容,而后最终内容会挂在都 module,exports 上,返回 module.exports 就返回了援用文件的最终执行后果。
简略讲就是:promise.all +jsonp + 动态创建 script 标签
参考文章
- window History
- hash 路由和 history 路由的区别
- webpack 动静加载原理 /webpack 异步加载原理 /webpack 动静解析 import()原理
正文完
发表至: vue-router
2022-03-20