静态路由
前端定义完整路由表,页面根据对应的路由访问。
动态路由
1.前端这边把路由写好,登录的时候根据用户的角色权限来动态展示路由(前端控制路由)
2.后台传来当前用户对应权限的路由表,前端通过调接口拿到后处理(后端处理路由)
Vue 动态路由的实现(后台传递路由,前端拿到并生成侧边栏)
vue-router3源码分析
入口文件app.js中传入了VueRouter的实例,所以我们从VueRouter的构造函数开始分析。
import Router from 'vue-router'
new Vue({
el: '#app',
store,
router: new Router(routerConfig),
components: { app },
template: '<app/>'
})
VueRouter构造函数
var VueRouter = function VueRouter (options) {
if ( options === void 0 ) options = {};
this.app = null;
this.apps = [];
this.options = options;
this.beforeHooks = [];
this.resolveHooks = [];
this.afterHooks = [];
this.matcher = createMatcher(options.routes || [], this);
var 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));
}
}};
VueRouter原型上挂载了一些函数
var prototypeAccessors = { currentRoute: { configurable: true } };
VueRouter.prototype.match = function match ( raw, current, redirectedFrom) {
return this.matcher.match(raw, current, redirectedFrom)
};
prototypeAccessors.currentRoute.get = function () {
return this.history && this.history.current
};
VueRouter.prototype.init = function init (app /* Vue component instance */) {
var this$1 = this;
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);
// main app already initialized.
if (this.app) {
return
}
this.app = app;
var history = this.history;
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation());
} else if (history instanceof HashHistory) {
var setupHashListener = function () {
history.setupListeners();
};
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
);
}
history.listen(function (route) {
this$1.apps.forEach(function (app) {
app._route = route;
});
});
};
VueRouter.prototype.beforeEach = function beforeEach (fn) {
return registerHook(this.beforeHooks, fn)
};
VueRouter.prototype.beforeResolve = function beforeResolve (fn) {
return registerHook(this.resolveHooks, fn)
};
VueRouter.prototype.afterEach = function afterEach (fn) {
return registerHook(this.afterHooks, fn)
};
VueRouter.prototype.onReady = function onReady (cb, errorCb) {
this.history.onReady(cb, errorCb);
};
VueRouter.prototype.onError = function onError (errorCb) {
this.history.onError(errorCb);
};
VueRouter.prototype.push = function push (location, onComplete, onAbort) {
this.history.push(location, onComplete, onAbort);
};
VueRouter.prototype.replace = function replace (location, onComplete, onAbort) {
this.history.replace(location, onComplete, onAbort);
};
VueRouter.prototype.go = function go (n) {
this.history.go(n);
};
VueRouter.prototype.back = function back () {
this.go(-1);
};
VueRouter.prototype.forward = function forward () {
this.go(1);
};
VueRouter.prototype.getMatchedComponents = function getMatchedComponents (to) {
var route = to
? to.matched
? to
: this.resolve(to).route
: this.currentRoute;
if (!route) {
return []
}
return [].concat.apply([], route.matched.map(function (m) {
return Object.keys(m.components).map(function (key) {
return m.components[key]
})
}))
};
VueRouter.prototype.resolve = function resolve ( to, current, append) {
var location = normalizeLocation(
to,
current || this.history.current,
append,
this
);
var route = this.match(location, current);
var fullPath = route.redirectedFrom || route.fullPath;
var base = this.history.base;
var href = createHref(base, fullPath, this.mode);
return {
location: location,
route: route,
href: href,
// for backwards compat
normalizedTo: location,
resolved: route
}
};
VueRouter.prototype.addRoutes = function addRoutes (routes) {
this.matcher.addRoutes(routes);
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation());
}
};
Object.defineProperties( VueRouter.prototype, prototypeAccessors );
function registerHook (list, fn) {
list.push(fn);
return function () {
var i = list.indexOf(fn);
if (i > -1) { list.splice(i, 1); }
}
}
function createHref (base, fullPath, mode) {
var path = mode === 'hash' ? '#' + fullPath : fullPath;
return base ? cleanPath(base + '/' + path) : path
}
注意看构造函数里面有两大关键点,一个是createMatcher,一个是history实例化。
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) {
var record = nameMap[name];
if (process.env.NODE_ENV !== 'production') {
warn(record, ("Route with name '" + name + "' does not exist"));
}
if (!record) { return _createRoute(null, location) }
var paramNames = record.regex.keys
.filter(function (key) { return !key.optional; })
.map(function (key) { return key.name; });
if (typeof location.params !== 'object') {
location.params = {};
}
if (currentRoute && typeof currentRoute.params === 'object') {
for (var key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key];
}
}
}
if (record) {
location.path = fillParams(record.path, location.params, ("named route \"" + name + "\"")); return _createRoute(record, location, redirectedFrom) } } else if (location.path) { location.params = {}; for (var i = 0; i < pathList.length; i++) {
var path = pathList[i];
var record$1 = pathMap[path];
if (matchRoute(record$1.regex, location.path, location.params)) {
return _createRoute(record$1, location, redirectedFrom)
}
}
}
// no match
return _createRoute(null, location)
}
function redirect ( record, location ) {
var originalRedirect = record.redirect;
var 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)
}
var re = redirect;
var name = re.name;
var path = re.path;
var query = location.query;
var hash = location.hash;
var params = location.params;
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
var targetRecord = nameMap[name];
if (process.env.NODE_ENV !== 'production') {
assert(targetRecord, ("redirect failed: named route \"" + name + "\" not found."));
}
return match({
_normalized: true,
name: name,
query: query,
hash: hash,
params: params
}, undefined, location)
} else if (path) {
// 1. resolve relative redirect
var rawPath = resolveRecordPath(path, record);
// 2. resolve params
var resolvedPath = fillParams(rawPath, params, ("redirect route with path \"" + rawPath + "\""));
// 3. rematch with existing query and hash
return match({
_normalized: true,
path: resolvedPath,
query: query,
hash: 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, location, matchAs ) {
var aliasedPath = fillParams(matchAs, location.params, ("aliased route with path \"" + matchAs + "\""));
var aliasedMatch = match({
_normalized: true,
path: aliasedPath
});
if (aliasedMatch) {
var matched = aliasedMatch.matched;
var aliasedRecord = matched[matched.length - 1];
location.params = aliasedMatch.params;
return _createRoute(aliasedRecord, location)
}
return _createRoute(null, location)
}
function _createRoute ( record, location, redirectedFrom ) {
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: match,
addRoutes: addRoutes
}
}
HTML5History
var HTML5History = (function (History$$1) {
function HTML5History (router, base) {
var this$1 = this;
History$$1.call(this, router, base);
var expectScroll = router.options.scrollBehavior;
if (expectScroll) {
setupScroll();
}
var initLocation = getLocation(this.base);
window.addEventListener('popstate', function (e) {
var current = this$1.current;
// Avoiding first `popstate` event dispatched in some browsers but first
// history route not updated since async guard at the same time.
var location = getLocation(this$1.base);
if (this$1.current === START && location === initLocation) {
return
}
this$1.transitionTo(location, function (route) {
if (expectScroll) {
handleScroll(router, route, current, true);
}
});
});
}
if ( History$$1 ) HTML5History.__proto__ = History$$1;
HTML5History.prototype = Object.create( History$$1 && History$$1.prototype );
HTML5History.prototype.constructor = HTML5History;
HTML5History.prototype.go = function go (n) {
window.history.go(n);
};
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);
};
HTML5History.prototype.ensureURL = function ensureURL (push) {
if (getLocation(this.base) !== this.current.fullPath) {
var current = cleanPath(this.base + this.current.fullPath);
push ? pushState(current) : replaceState(current);
}
};
HTML5History.prototype.getCurrentLocation = function getCurrentLocation () {
return getLocation(this.base)
};
return HTML5History;
}(History));
HashHistory
var HashHistory = (function (History$$1) {
function HashHistory (router, base, fallback) {
History$$1.call(this, router, base);
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash();
}
if ( History$$1 ) HashHistory.__proto__ = History$$1;
HashHistory.prototype = Object.create( History$$1 && History$$1.prototype );
HashHistory.prototype.constructor = HashHistory;
// this is delayed until the app mounts
// to avoid the hashchange listener being fired too early
HashHistory.prototype.setupListeners = function setupListeners () {
var this$1 = this;
var router = this.router;
var expectScroll = router.options.scrollBehavior;
var supportsScroll = supportsPushState && expectScroll;
if (supportsScroll) {
setupScroll();
}
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', function () {
var current = this$1.current;
if (!ensureSlash()) {
return
}
this$1.transitionTo(getHash(), function (route) {
if (supportsScroll) {
handleScroll(this$1.router, route, current, true);
}
if (!supportsPushState) {
replaceHash(route.fullPath);
}
});
});
};
HashHistory.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;
var ref = this;
var fromRoute = ref.current;
this.transitionTo(location, function (route) {
pushHash(route.fullPath);
handleScroll(this$1.router, route, fromRoute, false);
onComplete && onComplete(route);
}, onAbort);
};
HashHistory.prototype.replace = function replace (location, onComplete, onAbort) {
var this$1 = this;
var ref = this;
var fromRoute = ref.current;
this.transitionTo(location, function (route) {
replaceHash(route.fullPath);
handleScroll(this$1.router, route, fromRoute, false);
onComplete && onComplete(route);
}, onAbort);
};
HashHistory.prototype.go = function go (n) {
window.history.go(n);
};
HashHistory.prototype.ensureURL = function ensureURL (push) {
var current = this.current.fullPath;
if (getHash() !== current) {
push ? pushHash(current) : replaceHash(current);
}
};
HashHistory.prototype.getCurrentLocation = function getCurrentLocation () {
return getHash()
};
return HashHistory;
}(History));
AbstractHistory
var AbstractHistory = (function (History$$1) {
function AbstractHistory (router, base) {
History$$1.call(this, router, base);
this.stack = [];
this.index = -1;
}
if ( History$$1 ) AbstractHistory.__proto__ = History$$1;
AbstractHistory.prototype = Object.create( History$$1 && History$$1.prototype );
AbstractHistory.prototype.constructor = AbstractHistory;
AbstractHistory.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;
this.transitionTo(location, function (route) {
this$1.stack = this$1.stack.slice(0, this$1.index + 1).concat(route);
this$1.index++;
onComplete && onComplete(route);
}, onAbort);
};
AbstractHistory.prototype.replace = function replace (location, onComplete, onAbort) {
var this$1 = this;
this.transitionTo(location, function (route) {
this$1.stack = this$1.stack.slice(0, this$1.index).concat(route);
onComplete && onComplete(route);
}, onAbort);
};
AbstractHistory.prototype.go = function go (n) {
var this$1 = this;
var targetIndex = this.index + n;
if (targetIndex < 0 || targetIndex >= this.stack.length) {
return
}
var route = this.stack[targetIndex];
this.confirmTransition(route, function () {
this$1.index = targetIndex;
this$1.updateRoute(route);
});
};
AbstractHistory.prototype.getCurrentLocation = function getCurrentLocation () {
var current = this.stack[this.stack.length - 1];
return current ? current.fullPath : '/'
};
AbstractHistory.prototype.ensureURL = function ensureURL () {
// noop
};
return AbstractHistory;
}(History));
History
var History = function History (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 = [];
};
History.prototype.listen = function listen (cb) {
this.cb = cb;
};
History.prototype.onReady = function onReady (cb, errorCb) {
if (this.ready) {
cb();
} else {
this.readyCbs.push(cb);
if (errorCb) {
this.readyErrorCbs.push(errorCb);
}
}
};
History.prototype.onError = function onError (errorCb) {
this.errorCbs.push(errorCb);
};
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); });
}
});
};
History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) {
var this$1 = this;
var current = this.current;
var abort = function (err) {
if (isError(err)) {
if (this$1.errorCbs.length) {
this$1.errorCbs.forEach(function (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()
}
var ref = resolveQueue(this.current.matched, route.matched);
var updated = ref.updated;
var deactivated = ref.deactivated;
var activated = ref.activated;
var queue = [].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(function (m) { return m.beforeEnter; }),
// async components
resolveAsyncComponents(activated)
);
this.pending = route;
var iterator = function (hook, next) {
if (this$1.pending !== route) {
return abort()
}
try {
hook(route, current, function (to) {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this$1.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();
if (typeof to === 'object' && to.replace) {
this$1.replace(to);
} else {
this$1.push(to);
}
} else {
// confirm transition and pass on the value
next(to);
}
});
} catch (e) {
abort(e);
}
};
runQueue(queue, iterator, function () {
var postEnterCbs = [];
var isValid = function () { return this$1.current === route; };
// wait until async components are resolved before
// extracting in-component enter guards
var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
var queue = enterGuards.concat(this$1.router.resolveHooks);
runQueue(queue, iterator, function () {
if (this$1.pending !== route) {
return abort()
}
this$1.pending = null;
onComplete(route);
if (this$1.router.app) {
this$1.router.app.$nextTick(function () {
postEnterCbs.forEach(function (cb) { cb(); });
});
}
});
});
};
History.prototype.updateRoute = function updateRoute (route) {
var prev = this.current;
this.current = route;
this.cb && this.cb(route);
this.router.afterHooks.forEach(function (hook) {
hook && hook(route, prev);
});
};
install绑定router-view和router-link
VueRouter.install = install;
install方法
var _Vue;
function install (Vue) {
if (install.installed && _Vue === Vue) { return }
install.installed = true;
_Vue = Vue;
var isDef = function (v) { return v !== undefined; };
var registerInstance = function (vm, callVal) {
var i = vm.$options._parentVnode;
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal);
}
};
Vue.mixin({
beforeCreate: function 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: function destroyed () {
registerInstance(this);
}
});
Object.defineProperty(Vue.prototype, '$router', {
get: function get () { return this._routerRoot._router }
});
Object.defineProperty(Vue.prototype, '$route', {
get: function get () { return this._routerRoot._route }
});
Vue.component('router-view', View);
Vue.component('router-link', Link);
var strats = Vue.config.optionMergeStrategies;
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}
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;
// directly use parent context's createElement() function
// so that components rendered by router-view can resolve named slots
var h = parent.$createElement;
var name = props.name;
var route = parent.$route;
var cache = parent._routerViewCache || (parent._routerViewCache = {});
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
var depth = 0;
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;
// render previous view if the tree is inactive and kept-alive
if (inactive) {
return h(cache[name], data, children)
}
var matched = route.matched[depth];
// render empty node if no matched route
if (!matched) {
cache[name] = null;
return h()
}
var component = cache[name] = matched.components[name];
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
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;
}
}
// also register instance in prepatch hook
// in case the same component instance is reused across different routes
;(data.hook || (data.hook = {})).prepatch = function (_, vnode) {
matched.instances[name] = vnode.componentInstance;
};
// resolve props
data.props = resolveProps(route, matched.props && matched.props[name]);
data.attrs = {};
for (var key in data.props) {
if (!('props' in component) || !(key in component.props)) {
data.attrs[key] = data.props[key];
delete data.props[key];
}
}
return h(component, data, children)
}
};
router-link
var Link = {
name: 'router-link',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
exact: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
event: {
type: eventTypes,
default: 'click'
}
},
render: function render (h) {
var this$1 = this;
var router = this.$router;
var current = this.$route;
var ref = router.resolve(this.to, current, this.append);
var location = ref.location;
var route = ref.route;
var href = ref.href;
var classes = {};
var globalActiveClass = router.options.linkActiveClass;
var globalExactActiveClass = router.options.linkExactActiveClass;
// Support global empty active class
var activeClassFallback = globalActiveClass == null
? 'router-link-active'
: globalActiveClass;
var exactActiveClassFallback = globalExactActiveClass == null
? 'router-link-exact-active'
: globalExactActiveClass;
var activeClass = this.activeClass == null
? activeClassFallback
: this.activeClass;
var exactActiveClass = this.exactActiveClass == null
? exactActiveClassFallback
: this.exactActiveClass;
var compareTarget = location.path
? createRoute(null, location, null, router)
: route;
classes[exactActiveClass] = isSameRoute(current, compareTarget);
classes[activeClass] = this.exact
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget);
var handler = function (e) {
if (guardEvent(e)) {
if (this$1.replace) {
router.replace(location);
} else {
router.push(location);
}
}
};
var on = { click: guardEvent };
if (Array.isArray(this.event)) {
this.event.forEach(function (e) { on[e] = handler; });
} else {
on[this.event] = handler;
}
var data = {
class: classes
};
if (this.tag === 'a') {
data.on = on;
data.attrs = { href: href };
} else {
// find the first <a> child and apply listener and href
var a = findAnchor(this.$slots.default);
if (a) {
// in case the <a> is a static node
a.isStatic = false;
var extend = _Vue.util.extend;
var aData = a.data = extend({}, a.data);
aData.on = on;
var aAttrs = a.data.attrs = extend({}, a.data.attrs);
aAttrs.href = href;
} else {
// doesn't have <a> child, apply listener to self
data.on = on;
}
}
return h(this.tag, data, this.$slots.default)
}
};