Vue-router是Vue的核心组件,主要是作为Vue的路由管理器,Vue-router默认hash模式,通过引入Vue-router对象模块时配置mode属性可以启用history模式。
描述Vue-router的hash模式使用URL的Hash来模拟一个完整的URL,当URL改变时页面不会重新加载,而Vue-router的history模式是充分利用history.pushState的API来完成URL跳转,同样在页面跳转时无须重新加载页面,当然也不会对于服务端进行请求,当然对于history模式仍然是需要后端的配置支持,由于应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问URL时就会返回404,所以需要在服务端增加一个覆盖所有情况的候选资源,如果URL匹配不到任何静态资源时,则应该返回同一个index.html应用依赖页面,例如在Nginx下的配置。
location / { try_files $uri $uri/ /index.html; } 分析Vue-router源码的实现比较复杂,会处理各种兼容问题与异常以及各种条件分支,文章分析比较核心的代码部分,精简过后的版本,重要部分做出注释,commit id为560d11d。
首先是在定义Router时调用Vue.use(VueRouter),这是Vue.js插件的经典写法,给插件对象增加install方法用来安装插件具体逻辑,此时会调用VueRouter类上的静态方法,即VueRouter.install = install,install模块主要是保证Vue-router只被use一次,以及通过mixin在Vue的生命周期beforeCreate内注册实例,在destroyed内销毁实例,还有定义$router与$route属性为只读属性以及<router-view>与<router-link>全局组件的注册。
// dev/src/install.js line 6 export function install (Vue) { if (install.installed && _Vue === Vue) return install.installed = true // 保证 Vue-router 只被 use 一次 _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.$options.router 来自于 VueRouter 的实例化 // 判断实例是否已经挂载 this._routerRoot = this this._router = this.$options.router this._router.init(this) // // 调用 VueRouter 的 init 方法 // 后文会说明 init 方法的作用 Vue.util.defineReactive(this, '_route', this._router.history.current) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this // 将组件的 _routerRoot 都指向根 Vue 实例 } registerInstance(this, this) }, destroyed () { // 销毁实例 即挂载undefined registerInstance(this) } }) Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) Vue.component('RouterView', View) // 注册全局组件 <router-view> Vue.component('RouterLink', Link) // 注册全局组件 <router-link> const strats = Vue.config.optionMergeStrategies // use the same hook merging strategy for route hooks strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created }之后是VueRouter对象的构造函数,主要是先获取mode的值,如果mode的值为history但是浏览器不支持history模式,那么就强制设置mode值为hash,接下来根据mode的值,来选择vue-router使用哪种模式。
// dev/src/index.js line 40 constructor (options: RouterOptions = {}) { this.app = null this.apps = [] this.options = options this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] this.matcher = createMatcher(options.routes || [], this) // 创建路由匹配对象 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 switch (mode) { case 'history': this.history = new HTML5History(this, options.base) // 实例化history模式 break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) // 实例化Hash模式 break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } }在构造函数中调用了创建路由匹配对象的方法createMatcher,而在createMatcher中又调用了实际用以创建路由映射表的方法createRouteMap,可以说createMatcher函数的作用就是创建路由映射表,然后通过闭包的方式让addRoutes和match函数能够使用路由映射表的几个对象,最后返回一个Matcher对象。
// dev/src/create-matcher.js line 16 export function createMatcher ( routes: Array<RouteConfig>, router: VueRouter ): Matcher { const { pathList, pathMap, nameMap } = createRouteMap(routes) // 创建路由映射表 function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) } function match ( // 路由匹配 raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { const location = normalizeLocation(raw, currentRoute, false, router) // location 是一个对象,类似于 {"_normalized":true,"path":"http://www.likecs.com/","query":{},"hash":""} const { name } = location if (name) { // 如果有路由名称 就进行nameMap映射 const record = nameMap[name] // nameMap[name] = 路由记录 if (process.env.NODE_ENV !== 'production') { warn(record, `Route with name '${name}' does not exist`) } 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) } else if (location.path) { // 如果路由配置了path,到pathList和PathMap里匹配到路由记录 location.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) } } } // no match return _createRoute(null, location) } function redirect ( // 处理重定向 record: RouteRecord, location: Location ): Route { const originalRedirect = record.redirect let redirect = typeof originalRedirect === 'function' ? originalRedirect(createRoute(record, location, null, router)) : originalRedirect if (typeof redirect === 'string') { redirect = { path: redirect } } if (!redirect || typeof redirect !== 'object') { if (process.env.NODE_ENV !== 'production') { warn( false, `invalid redirect option: ${JSON.stringify(redirect)}` ) } return _createRoute(null, location) } const re: Object = redirect const { name, path } = re let { query, hash, params } = location query = re.hasOwnProperty('query') ? re.query : query hash = re.hasOwnProperty('hash') ? re.hash : hash params = re.hasOwnProperty('params') ? re.params : params if (name) { // resolved named direct const targetRecord = nameMap[name] if (process.env.NODE_ENV !== 'production') { assert(targetRecord, `redirect failed: named route "${name}" not found.`) } return match({ _normalized: true, name, query, hash, params }, undefined, location) } else if (path) { // 1. resolve relative redirect const rawPath = resolveRecordPath(path, record) // 2. resolve params const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`) // 3. rematch with existing query and hash return match({ _normalized: true, path: resolvedPath, query, hash }, undefined, location) } else { if (process.env.NODE_ENV !== 'production') { warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`) } return _createRoute(null, location) } } function alias ( // 处理别名 record: RouteRecord, location: Location, matchAs: string ): Route { const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`) const aliasedMatch = match({ _normalized: true, path: aliasedPath }) if (aliasedMatch) { const matched = aliasedMatch.matched const aliasedRecord = matched[matched.length - 1] location.params = aliasedMatch.params return _createRoute(aliasedRecord, location) } return _createRoute(null, location) } 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) // 创建路由对象 } return { match, addRoutes } } // dev/src/create-route-map.js line 7 export function createRouteMap ( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord> ): { pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord> } { // the path list is used to control path matching priority const pathList: Array<string> = oldPathList || [] // 创建映射表 // $flow-disable-line const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null) // $flow-disable-line const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null) routes.forEach(route => { // 遍历路由配置,为每个配置添加路由记录 addRouteRecord(pathList, pathMap, nameMap, route) }) // ensure wildcard routes are always at the end for (let i = 0, l = pathList.length; i < l; i++) { // 确保通配符在最后 if (pathList[i] === '*') { pathList.push(pathList.splice(i, 1)[0]) l-- i-- } } if (process.env.NODE_ENV === 'development') { // warn if routes do not include leading slashes const found = pathList // check for missing leading slash .filter(path => path && path.charAt(0) !== '*' && path.charAt(0) !== 'http://www.likecs.com/') if (found.length > 0) { const pathNames = found.map(path => `- ${path}`).join('\n') warn(false, `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`) } } return { pathList, pathMap, nameMap } } function addRouteRecord ( // 添加路由记录 pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string ) { const { path, name } = route // 获得路由配置下的属性 if (process.env.NODE_ENV !== 'production') { assert(path != null, `"path" is required in a route configuration.`) assert( typeof route.component !== 'string', `route config "component" for path: ${String( path || name )} cannot be a ` + `string id. Use an actual component instead.` ) } const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {} const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict) if (typeof route.caseSensitive === 'boolean') { pathToRegexpOptions.sensitive = route.caseSensitive } const record: RouteRecord = { // 生成记录对象 path: normalizedPath, 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 (route.children) { // Warn if route is named, does not redirect and has a default child route. // If users navigate to this route by name, the default child will // not be rendered (GH Issue #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.` ) } } route.children.forEach(child => { // 递归路由配置的 children 属性,添加路由记录 const childMatchAs = matchAs ? cleanPath(`${matchAs}/${child.path}`) : undefined addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs) }) } if (!pathMap[record.path]) { // 如果有多个相同的路径,只有第一个起作用,后面的会被忽略 pathList.push(record.path) pathMap[record.path] = record } if (route.alias !== undefined) { // 如果路由有别名的话,给别名也添加路由记录 const aliases = Array.isArray(route.alias) ? route.alias : [route.alias] for (let i = 0; i < aliases.length; ++i) { const alias = aliases[i] if (process.env.NODE_ENV !== 'production' && alias === path) { warn( false, `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.` ) // skip in dev to make it work continue } const aliasRoute = { path: alias, children: route.children } addRouteRecord( pathList, pathMap, nameMap, aliasRoute, parent, record.path || 'http://www.likecs.com/' // matchAs ) } } 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}" }` ) } } }