この記事では主に vue-router のソース コードの読み取りと学習について説明します。vuex のソース コードを分析するのと同じように、まず簡単な例を通して vue-router がどのように使用されるかを理解し、次に vue-router がどのように実装されているかを分析します。ソースコードが皆さんのお役に立てば幸いです。
例
次の例は、example/basica/app.js からのものです
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const Home = { template: '<div>home</div>' } const Foo = { template: '<div>foo</div>' } const Bar = { template: '<div>bar</div>' } const router = new VueRouter({ mode: 'history', base: __dirname, routes: [ { path: '/', component: Home }, { path: '/foo', component: Foo }, { path: '/bar', component: Bar } ] }) new Vue({ router, template: ` <div id="app"> <h1>Basic</h1> <ul> <li><router-link to="/">/</router-link></li> <li><router-link to="/foo">/foo</router-link></li> <li><router-link to="/bar">/bar</router-link></li> <router-link tag="li" to="/bar" :event="['mousedown', 'touchstart']"> <a>/bar</a> </router-link> </ul> <router-view class="view"></router-view> </div> ` }).$mount('#app')
最初に Vue.use(VueRouter) を呼び出します。Vue.use() メソッドは、Vue がインストールするために使用するメソッドです。これは主に VueRouter のインストールに使用されます。次に、VueRouter がインスタンス化されます。VueRouter コンストラクターが何を行うかを見てみましょう。
ソース コード エントリ ファイル src/index.js から始めます
import type { Matcher } from './create-matcher'export default class VueRouter { 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) 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 (app: any /* Vue component instance */) { this.apps.push(app) // main app already initialized. 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.listen(route => { this.apps.forEach((app) => { app._route = route }) }) } getMatchedComponents (to?: RawLocation | Route): Array<any> { const route: any = to ? to.matched ? to : this.resolve(to).route : this.currentRoute if (!route) { return [] } return [].concat.apply([], route.matched.map(m => { return Object.keys(m.components).map(key => { return m.components[key] }) })) } }
コンストラクター関数の実装から始めて、初期化がどのようなものかを見てみましょう。条件は
- を表します
this.appは現在のVueインスタンスを表します
this.appsはすべてのアプリコンポーネントを表します
this.optionsは受信VueRouterのオプションを表します
this.resolveHooksは配列を表しますフックコールバック関数の解決、解決 ターゲットの場所を解析するために使用されます
this.matcher はマッチング関数を作成します
コードには createMatcher() 関数があります、その実装を見てみましょう
function createMatcher ( routes, router ) { var ref = createRouteMap(routes); var pathList = ref.pathList; var pathMap = ref.pathMap; var nameMap = ref.nameMap; function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap); } function match ( raw, currentRoute, redirectedFrom ) { var location = normalizeLocation(raw, currentRoute, false, router); var name = location.name; // 命名路由处理 if (name) { // nameMap[name]的路由记录 var record = nameMap[name]; ... location.path = fillParams(record.path, location.params, ("named route \"" + name + "\"")); // _createRoute用于创建路由 return _createRoute(record, location, redirectedFrom) } else if (location.path) { // 普通路由处理 } // no match return _createRoute(null, location) } return { match: match, addRoutes: addRoutes } }
createMatcher() には 2 つのパラメータがあります Routes VueRouter の作成時に渡されるルート設定情報を表し、router は VueRouter インスタンスを表します。 createMatcher() の機能は、createRouteMap を通じて受信ルートに対応するマップを作成することと、マップを作成するメソッドです。
まず createRouteMap() メソッドの定義を見てみましょう
function createRouteMap ( routes, oldPathList, oldPathMap, oldNameMap) { // 用于控制匹配优先级 var pathList = oldPathList || []; // name 路由 map var pathMap = oldPathMap || Object.create(null); // name 路由 map var nameMap = oldNameMap || Object.create(null); // 遍历路由配置对象增加路由记录 routes.forEach(function (route) { addRouteRecord(pathList, pathMap, nameMap, route); }); // 确保通配符总是在pathList的最后,保证最后匹配 for (var i = 0, l = pathList.length; i < l; i++) { if (pathList[i] === '*') { pathList.push(pathList.splice(i, 1)[0]); l--; i--; } } return { pathList: pathList, pathMap: pathMap, nameMap: nameMap } }
createRouteMap() には 4 つのパラメータがあります: ルートによって表される構成情報、oldPathList には優先順位を一致させるためのすべてのパスの配列が含まれ、oldNameMap には名前マップ、oldPathMap はパス マップを表します。 createRouteMap は pathList、nameMap、pathMap を更新します。 nameMap は何を表しますか?各属性値名は各レコードのパス属性値であり、属性値はそのパス属性値を持つルーティングレコードです。ルーティングレコードと呼ばれるものがありますが、これは何を意味しますか?ルーティング レコードは、ルート設定配列 (および子の配列) 内のオブジェクトのコピーです。たとえば、上記のコードでは、
const router = new VueRouter({ routes: [ // 下面的对象就是 route record { path: '/foo', component: Foo, children: [ // 这也是个 route record { path: 'bar', component: Bar } ] } ] })
function addRouteRecord ( pathList, pathMap, nameMap, route, parent, matchAs ) { var path = route.path; var name = route.name; var normalizedPath = normalizePath( path, parent ); var record = { path: normalizedPath, regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), components: route.components || { default: route.component }, instances: {}, name: name, parent: parent, matchAs: matchAs, redirect: route.redirect, beforeEnter: route.beforeEnter, meta: route.meta || {}, props: route.props == null ? {} : route.components ? route.props : { default: route.props } }; if (route.children) { route.children.forEach(function (child) { addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs); }); } if (route.alias !== undefined) { // 如果有别名的情况 } if (!pathMap[record.path]) { pathList.push(record.path); pathMap[record.path] = record; } }
History
VueRouter のコンストラクターに戻ります。下にあるのは、history、hash、abstract の 3 つのモードです。 · デフォルトハッシュ: URL ハッシュ値をルートとして使用し、すべてのブラウザをサポートします
· 履歴: HTML5 History API とサーバー構成に依存します · 抽象: Node.js サーバー側など、すべての JavaScript ランタイム環境をサポートします。ブラウザ API が見つからない場合、ルーターは自動的にこのモードを強制します。
デフォルトはハッシュで、ルートは「#」で区切られていますが、プロジェクト内にアンカーリンクやルート内にハッシュ値がある場合、元の「#」がページジャンプに影響を与えるため、履歴モードを使用します。
アプリケーションで一般的に使用されるのは、基本的にヒストリーモードです。 HashHistory のコンストラクターを見てみましょう
var History = function History (router, base) { this.router = router; this.base = normalizeBase(base); this.current = START; this.pending = null; this.ready = false; this.readyCbs = []; this.readyErrorCbs = []; this.errorCbs = []; };
- this.router は VueRouter インスタンスを表します
- this.base はアプリケーションのベースパスを表します。たとえば、単一ページのアプリケーション全体が /app/ で提供される場合、base は
- "/app/" に設定する必要があります。 NormalizeBase() はベースをフォーマットするために使用されます
this.current开始时的route,route使用createRoute()创建
this.pending表示进行时的route
this.ready表示准备状态
this.readyCbs表示准备回调函数
creatRoute()在文件src/util/route.js中,下面是他的实现
function createRoute ( record, location, redirectedFrom, router ) { var stringifyQuery$$1 = router && router.options.stringifyQuery; var query = location.query || {}; try { query = clone(query); } catch (e) {} var route = { name: location.name || (record && record.name), meta: (record && record.meta) || {}, path: location.path || '/', hash: location.hash || '', query: query, params: location.params || {}, fullPath: getFullPath(location, stringifyQuery$$1), matched: record ? formatMatch(record) : [] }; if (redirectedFrom) { route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery$$1); } return Object.freeze(route) }
createRoute有三个参数,record表示路由记录,location,redirectedFrom表示url地址信息对象,router表示VueRouter实例对象。通过传入的参数,返回一个冻结的route对象,route对象里边包含了一些有关location的属性。History包含了一些基本的方法,例如比较重要的方法有transitionTo(),下面是transitionTo()的具体实现。
History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) { var this$1 = this; var route = this.router.match(location, this.current); this.confirmTransition(route, function () { this$1.updateRoute(route); onComplete && onComplete(route); this$1.ensureURL(); // fire ready cbs once if (!this$1.ready) { this$1.ready = true; this$1.readyCbs.forEach(function (cb) { cb(route); }); } }, function (err) { if (onAbort) { onAbort(err); } if (err && !this$1.ready) { this$1.ready = true; this$1.readyErrorCbs.forEach(function (cb) { cb(err); }); } }); };
首先match得到匹配的route对象,route对象在之前已经提到过。然后使用confirmTransition()确认过渡,更新route,ensureURL()的作用就是更新URL。如果ready为false,更改ready的值,然后对readyCbs数组进行遍历回调。下面来看看HTML5History的构造函数
var HTML5History = (function (History$$1) { function HTML5History (router, base) { var this$1 = this; History$$1.call(this, router, base); var initLocation = getLocation(this.base); window.addEventListener('popstate', function (e) { var current = this$1.current; var location = getLocation(this$1.base); if (this$1.current === START && location === initLocation) { return } }); } if ( History$$1 ) HTML5History.__proto__ = History$$1; HTML5History.prototype = Object.create( History$$1 && History$$1.prototype ); HTML5History.prototype.constructor = HTML5History; HTML5History.prototype.push = function push (location, onComplete, onAbort) { var this$1 = this; var ref = this; var fromRoute = ref.current; this.transitionTo(location, function (route) { pushState(cleanPath(this$1.base + route.fullPath)); handleScroll(this$1.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort); }; HTML5History.prototype.replace = function replace (location, onComplete, onAbort) { var this$1 = this; var ref = this; var fromRoute = ref.current; this.transitionTo(location, function (route) { replaceState(cleanPath(this$1.base + route.fullPath)); handleScroll(this$1.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort); }; return HTML5History; }(History))
在HTML5History()中代码多次用到了getLocation()那我们来看看他的具体实现吧
function getLocation (base) { var path = window.location.pathname; if (base && path.indexOf(base) === 0) { path = path.slice(base.length); } return (path || '/') + window.location.search + window.location.hash }
用一个简单的地址来解释代码中各个部分的含义。例如http://example.com:1234/test/test.htm#part2?a=123,window.location.pathname=>/test/test.htm=>?a=123,window.location.hash=>#part2。
把我们继续回到HTML5History()中,首先继承history构造函数。然后监听popstate事件。当活动记录条目更改时,将触发popstate事件。需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。我们来看看HTML5History的push方法。location表示url信息,onComplete表示成功后的回调函数,onAbort表示失败的回调函数。首先获取current属性值,replaceState和pushState用于更新url,然后处理滚动。模式的选择就大概讲完了,我们回到入口文件,看看init()方法,app代表的是Vue的实例,现将app存入this.apps中,如果this.app已经存在就返回,如果不是就赋值。this.history是三种的实例对象,然后分情况进行transtionTo()操作,history方法就是给history.cb赋值穿进去的回调函数。
下面看getMatchedComponents(),唯一需要注意的就是我们多次提到的route.matched是路由记录的数据,最终返回的是每个路由记录的components属性值的值。
Router-View
最后讲讲router-view
var View = { name: 'router-view', functional: true, props: { name: { type: String, default: 'default' } }, render: function render (_, ref) { var props = ref.props; var children = ref.children; var parent = ref.parent; var data = ref.data; // 解决嵌套深度问题 data.routerView = true; var h = parent.$createElement; var name = props.name; // route var route = parent.$route; // 缓存 var cache = parent._routerViewCache || (parent._routerViewCache = {}); // 组件的嵌套深度 var depth = 0; // 用于设置class值 var inactive = false; // 组件的嵌套深度 while (parent && parent._routerRoot !== parent) { if (parent.$vnode && parent.$vnode.data.routerView) { depth++; } if (parent._inactive) { inactive = true; } parent = parent.$parent; } data.routerViewDepth = depth; if (inactive) { return h(cache[name], data, children) } var matched = route.matched[depth]; if (!matched) { cache[name] = null; return h() } var component = cache[name] = matched.components[name]; data.registerRouteInstance = function (vm, val) { // val could be undefined for unregistration var current = matched.instances[name]; if ( (val && current !== vm) || (!val && current === vm) ) { matched.instances[name] = val; } } ;(data.hook || (data.hook = {})).prepatch = function (_, vnode) { matched.instances[name] = vnode.componentInstance; }; var propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]); if (propsToPass) { propsToPass = data.props = extend({}, propsToPass); var attrs = data.attrs = data.attrs || {}; for (var key in propsToPass) { if (!component.props || !(key in component.props)) { attrs[key] = propsToPass[key]; delete propsToPass[key]; } } } return h(component, data, children) } };
router-view比较简单,functional为true使组件无状态 (没有 data ) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使他们更容易渲染。props表示接受属性,下面来看看render函数,首先获取数据,然后缓存,_inactive用于处理keep-alive情况,获取路由记录,注册Route实例,h()用于渲染。很简单我也懒得一一再说。
相关推荐: