UNPKG

react-view-router

Version:
1,445 lines (1,251 loc) 64.1 kB
import { ComponentType } from 'react'; import { createHashHistory, createBrowserHistory, createMemoryHistory, getPossibleHistory, HistoryFix } from './history-fix'; import config from './config'; import { flatten, isAbsoluteUrl, innumerable, isPlainObject, getRouterViewPath, getCompleteRoute, normalizeRoutes, normalizeLocation, resolveRedirect, resolveAbort, copyOwnProperties, matchRoutes, isFunction, isLocation, nextTick, once, isRouteGuardInfoHooks, isReadonly, afterInterceptors, getRouteChildren, readRouteMeta, readonly, getLoactionAction, getHostRouterView, camelize, reverseArray, isMatchedRoutePropsChanged, isRoute, walkRoutes, warn, createEmptyRouteState, DEFAULT_STATE_NAME, hasOwnProp, isString, isNumber, isEmptyRouteState, } from './util'; import { RouteLazy, hasRouteLazy, hasMatchedRouteLazy } from './route-lazy'; import { getGuardsComponent } from './route-guard'; import { RouterViewComponent as RouterView } from './router-view'; import { ReactViewRouterOptions, ReactViewRouterMoreOptions, NormalizedConfigRouteArray, RouteBeforeGuardFn, RouteAfterGuardFn, RouteNextFn, RouteHistoryLocation, RouteGuardInterceptor, RouteEvent, RouteChildrenFn, RouteNextResult, RouteLocation, RouteBranchInfo, matchPathResult, ConfigRoute, RouteErrorCallback, ReactViewRoutePlugin, Route, MatchedRoute, MatchedRouteArray, LazyResolveFn, OnBindInstance, OnGetLazyResovle, RouteComponentToResolveFn, VuelikeComponent, RouteInterceptorCallback, HistoryStackInfo, MatchedRouteGuard, RouteResolveNameFn, onRouteChangeEvent, UserConfigRoute, ParseQueryProps, RouteInterceptorItem, onRouteingNextCallback, // ReactViewRouterScrollBehavior, RouteComputedMeta, } from './types'; import { Action, createHashHref, HistoryType } from './history'; const HISTORY_METHS = ['push', 'replace', 'redirect', 'go', 'back', 'forward', 'block']; let idSeed = 1; // eslint-disable-next-line no-undef const version: string = typeof __packageversion__ === 'undefined' ? undefined : __packageversion__; class ReactViewRouter { static version = version; version: string = version; isReactViewRouterInstance: boolean = true; parent: ReactViewRouter | null = null; children: ReactViewRouter[] = []; options: ReactViewRouterMoreOptions = {}; mode: HistoryType = HistoryType.hash; basename: string = ''; basenameNoSlash: string = ''; name: string = ''; routeNameMap: { [key: string]: string } = {}; routes: NormalizedConfigRouteArray = normalizeRoutes([]); stacks: HistoryStackInfo[] = []; plugins: ReactViewRoutePlugin[] = []; beforeEachGuards: RouteBeforeGuardFn[] = []; afterUpdateGuards: RouteAfterGuardFn[] = []; beforeResolveGuards: RouteAfterGuardFn[] = []; afterEachGuards: RouteAfterGuardFn[] = []; resolveNameFns: RouteResolveNameFn[] = []; prevRoute: Route | null = null; currentRoute: Route | null = null; pendingRoute: RouteHistoryLocation | null = null; initialRoute: Route = { } as any; queryProps: ParseQueryProps = {}; viewRoot: RouterView | null = null; errorCallbacks: RouteErrorCallback[] = []; apps: any[] = []; Apps: React.ComponentClass[] = []; isRunning: boolean = false; isHistoryCreater: boolean = false; rememberInitialRoute: boolean = false; getHostRouterView: typeof getHostRouterView; nextTick: typeof nextTick; // scrollBehavior: ReactViewRouterScrollBehavior|null = null; protected _history: HistoryFix | null = null; protected _unlisten?: () => void; protected _uninterceptor?: () => void; protected id: number; protected _nexting: RouteNextFn | null = null; protected vuelike?: VuelikeComponent; protected _interceptorCounter: number = 0; [key: string]: any; constructor(options: ReactViewRouterOptions = {}) { this.id = idSeed++; this._initRouter(options); this.getHostRouterView = getHostRouterView; this.nextTick = nextTick.bind(this); this._handleRouteInterceptor = this._handleRouteInterceptor.bind(this); this.use(this.options); if (!options.manual) this.start(undefined, true); } _initRouter(options: ReactViewRouterOptions) { let { name, mode, basename, ...moreOptions } = options; if (name != null) this.name = name; if (mode != null) { if (typeof mode !== 'string') { moreOptions.history = mode; mode = mode.type; } if (this.mode !== mode) this.mode = mode; } if (basename !== undefined) { if (basename && !/\/$/.test(basename)) basename += '/'; this.basename = (this.mode === HistoryType.memory && (!moreOptions.history || moreOptions.history.type !== HistoryType.memory)) ? '' : basename.replace(/\/{2,}/g, '/'); this.basenameNoSlash = this.basename ? (this.basename.endsWith('/') ? this.basename.substr(0, basename.length - 1) : this.basename) : this.basename; } Object.assign(this.options, moreOptions); if (this.options.keepAlive && !this.options.renderUtils) { throw new Error('endable keepAlive need "renderUtils", but it cannot be found from router\'s options!'); } } _updateParent(parent: ReactViewRouter | null) { if (parent === this) parent = null; if (this.parent === parent) return; if (parent) { if (parent.children && !parent.children.includes(this)) parent.children.push(this); } else if (this.parent && this.parent.children) { const idx = this.parent.children.indexOf(this); if (~idx) this.parent.children.splice(idx, 1); } this.parent = parent; } get history(): HistoryFix { if (this._history) return this._history; switch (this.mode) { case HistoryType.browser: this._history = createBrowserHistory(this.options as any, this); break; case HistoryType.memory: this._history = createMemoryHistory(this.options as any, this); break; default: this._history = createHashHistory(this.options as any, this); } HISTORY_METHS.forEach(key => { if (!this[key] || !this[key].bindThis) return; this[key] = this[key].bind(this); innumerable(this[key], 'bindThis', true); }); this._history.refresh && this._history.refresh(); this._history && (this._history.destroy = () => this._history = null); return this._history as HistoryFix; } get pluginName(): string { return this.name; } get top(): ReactViewRouter { return this.parent ? this.parent.top : this; } get isBrowserMode() { return this.mode === HistoryType.browser; } get isHashMode() { return this.mode === HistoryType.hash; } get isMemoryMode() { return this.mode === HistoryType.memory; } get isPrepared() { return this.isRunning && Boolean(this.currentRoute && this.viewRoot && this.viewRoot.state.inited && this.viewRoot._isMounted); } _startHistory() { if (this._history && this._history.destroy) this._history.destroy(); this._unlisten = this.history.listen(({ location }, interceptors?: RouteInterceptorItem[]) => { let to = location; if (interceptors && Array.isArray(interceptors)) { const interceptorItem = interceptors.find(v => v.router === this); if (interceptorItem && interceptorItem.payload) to = interceptorItem.payload; } this.updateRoute(to as any); }); this._uninterceptor = this.history.interceptorTransitionTo(this._handleRouteInterceptor, this); this._refreshInitialRoute(); } start(routerOptions: ReactViewRouterOptions = {}, isInit = false) { this.stop({ isInit }); this._callEvent('onStart', this, routerOptions, isInit); this._initRouter(routerOptions); this._startHistory(); this.isRunning = true; } stop(options: { ignoreClearRoute?: boolean, isInit?: boolean } = {}) { this._callEvent('onStop', this, options); if (this._unlisten) { this._unlisten(); this._unlisten = undefined; } if (this._uninterceptor) { this._uninterceptor(); this._uninterceptor = undefined; } if (this._history && this._history.destroy) { this._history.destroy(); } this._history = null; this._interceptorCounter = 0; this.isRunning = false; this.isHistoryCreater = false; if (!options.ignoreClearRoute) { this.initialRoute = { location: {} } as any; this.prevRoute = null; this.currentRoute = null; this.pendingRoute = null; } } use({ routes, inheritProps, rememberInitialRoute, install, queryProps, ...restOptions }: ReactViewRouterMoreOptions) { if (rememberInitialRoute !== undefined) this.rememberInitialRoute = rememberInitialRoute; // if (scrollBehavior !== undefined) this.scrollBehavior = scrollBehavior; if (queryProps) this.queryProps = queryProps; if (routes) { const originRoutes = this.routes; this.routes = routes ? normalizeRoutes(routes) : normalizeRoutes([]); this._callEvent('onRoutesChange', this.routes, originRoutes); this._walkRoutes(this.routes); if (this._history && this.initialRoute) this._refreshInitialRoute(); } if (inheritProps !== undefined) config.inheritProps = inheritProps; Object.assign(config, restOptions); if (install) this.install = install.bind(this); } plugin(plugin: ReactViewRoutePlugin|onRouteChangeEvent) { if (isFunction(plugin)) { plugin = this.plugins.find(p => p.onRouteChange === plugin) || { onRouteChange: plugin } as ReactViewRoutePlugin; } else if (~this.plugins.indexOf(plugin)) return; const idx = this.plugins.findIndex(p => { if (plugin.name) return p.name === plugin.name; return p === plugin; }); if (~idx) { const [old] = this.plugins.splice(idx, 1, plugin); if (old && old.uninstall) old.uninstall(this); } else this.plugins.push(plugin); if ((plugin as ReactViewRoutePlugin).install) (plugin as any).install(this); return () => { const idx = this.plugins.indexOf(plugin); if (~idx) { this.plugins.splice(idx, 1); if ((plugin as ReactViewRoutePlugin).uninstall) (plugin as any).uninstall(this); } }; } _walkRoutes(routes: ConfigRoute[]|RouteChildrenFn, parent?: ConfigRoute) { walkRoutes(routes, (route, routeIndex, rs) => { this._callEvent('onWalkRoute', route, routeIndex, rs); if (route.name) { const name = camelize(route.name); if (this.routeNameMap[name]) { warn(`[react-view-router] route name '${route.name}'(path is [${route.path}]) is duplicate with [${this.routeNameMap[name]}]`); } this.routeNameMap[name] = route.path; } }, parent); } _refreshInitialRoute() { let historyLocation = { ...this.history.realtimeLocation } as RouteHistoryLocation; this.updateRoute(historyLocation); if (this.rememberInitialRoute) { let stack; if (this.basename) { const stacks = reverseArray(this.history.stacks.concat(historyLocation as any)); const basename = this.basenameNoSlash; for (let i = 0; i < stacks.length; i++) { const currentStack = stacks[i]; if (!currentStack.pathname.startsWith(basename)) break; if (i === stacks.length - 1 || !stacks[i + 1].pathname.startsWith(basename)) { stack = currentStack; break; } } } else if (this.history.stacks.length) { stack = this.history.stacks[0]; } if (stack) { historyLocation = this._normalizeLocation({ pathname: stack.pathname, search: stack.search, }) as RouteHistoryLocation; if (!this.isMemoryMode && globalThis?.location?.search) { const query = this.parseQuery(globalThis.location.search, this.queryProps); if (this.isHashMode) { Object.assign(historyLocation.query, query); } else if (this.isBrowserMode) { Object.keys(historyLocation.query).forEach(key => { if (query[key] !== undefined) historyLocation.query[key] = query[key]; }); } } } } this.initialRoute = this.createRoute(this._transformLocation(historyLocation)); // innumerable(this.initialRoute, 'location', location); } _callEvent<E extends Exclude<keyof ReactViewRoutePlugin, 'name'|'install'|'uninstall'>>( event: E, ...args: Parameters<ReactViewRoutePlugin[E]> ): ReturnType<ReactViewRoutePlugin[E]> { let plugin: ReactViewRoutePlugin | null = null; try { let ret: any; this.plugins.forEach(p => { plugin = p; const newRet = p[event] && p[event].call(p, ...args, ret); if (newRet !== undefined) ret = newRet; }); return ret; } catch (ex) { if (plugin && (plugin as ReactViewRoutePlugin).name && ex && (ex as any).message) { (ex as any).message = `[${(plugin as ReactViewRoutePlugin).name}:${event}]${(ex as any).message}`; } throw ex; } } _isVuelikeComponent(comp: any) { return comp && this.vuelike && ( // eslint-disable-next-line no-proto (comp.__proto__ && comp.isVuelikeComponentInstance) || (comp.__vuelike || comp.__vuelikeComponentClass) ); } _getComponentGurads<T extends RouteGuardInterceptor>( mr: MatchedRoute, guardName: string, onBindInstance?: OnBindInstance<Exclude<T, 'LazyResolveFn'>>, onGetLazyResovle?: OnGetLazyResovle|null ) { const ret: T[] = []; const componentInstances = mr.componentInstances; const getGuard = (obj: any, guardName: string): T => { const guard = obj && obj[guardName]; if (guard) innumerable(guard, 'instance', obj); return guard; }; // route config const routeGuardName = guardName.replace('Route', ''); const r = mr.config; const guards = r[routeGuardName]; if (guards) ret.push(guards); const toResovle: RouteComponentToResolveFn<T> = (c, componentKey) => { let ret: T[] = []; if (c) { const cc = c.__component ? getGuardsComponent(c, true) : c; const cg = getGuard(c.__guards, guardName); if (cg) ret.push(cg); let ccg = cc && getGuard(cc.prototype, guardName); if (ccg) { if (this.vuelike && !ccg.isMobxFlow && cc.__flows && ~cc.__flows.indexOf(guardName)) ccg = this.vuelike.flow(ccg); ret.push(ccg); } if (this._isVuelikeComponent(cc) && Array.isArray(cc.mixins)) { cc.mixins.forEach((m: any) => { let ccg = m && (getGuard(m, guardName) || getGuard(m.prototype, guardName)); if (!ccg) return; if (this.vuelike && !ccg.isMobxFlow && m.__flows && ~m.__flows.indexOf(guardName)) ccg = this.vuelike.flow(ccg); ret.push(ccg); }); } } const ci = componentInstances[componentKey]; if (isRouteGuardInfoHooks(ci)) { const cig = getGuard(ci, guardName); if (cig) ret.push(cig); } if (onBindInstance) ret = ret.map(v => onBindInstance(v as any, componentKey, ci, mr)).filter(Boolean) as T[]; else if (ci) { ret = ret.map(v => { const ret = v.bind(ci) as T; ret.instance = v.instance; return ret; }); } ret = flatten(ret); ret.forEach(v => v.route = mr); return ret; }; const replaceInterceptors = (newInterceptors: T[], interceptors: T[], index: number) => { interceptors.splice(index, 1, ...newInterceptors); return interceptors[index] as any; }; // route component r.components && Object.keys(r.components).forEach(key => { const c = r.components[key]; if (!c) return; const isResolved = this._callEvent( 'onGetRouteComponentGurads', ret, r, c, key, guardName, { router: this, onBindInstance, onGetLazyResovle, toResovle, getGuard, replaceInterceptors: replaceInterceptors as any } ); if (isResolved === true) return ret; if (c instanceof RouteLazy) { let lazyResovleCb: () => void; const lazyResovle: LazyResolveFn = async (interceptors: any[], index: number) => { lazyResovleCb && lazyResovleCb(); let nc = await c.toResolve(this, r, key); nc = this._callEvent('onLazyResolveComponent', nc, r) || nc; const ret = toResovle(nc, key); return replaceInterceptors(ret, interceptors, index); }; lazyResovle.lazy = true; lazyResovle.route = mr; onGetLazyResovle && onGetLazyResovle(lazyResovle, cb => lazyResovleCb = cb); ret.push(lazyResovle as any); return; } ret.push(...toResovle(c, key)); }); return ret; } _getSameMatched(route: Route | null, compare?: Route) { const ret: MatchedRoute[] = []; if (!compare) return []; route && route.matched.some((tr, i) => { const fr = compare.matched[i]; if (tr.path !== fr.path) return true; ret.push(tr); }); return ret.filter(r => !r.redirect); } _getChangeMatched(route: Route, route2?: Route | null, options: { containLazy?: boolean, count?: number|((ret: MatchedRoute[], tr: MatchedRoute, fr: MatchedRoute) => number), compare?: null|((tr: MatchedRoute, fr: MatchedRoute) => boolean) } = {}) { const ret: MatchedRoute[] = []; if (!route2) return [...route.matched]; let start = false; route && route.matched.some((tr, i) => { const fr = route2.matched[i]; if (!start) { start = (options.containLazy && hasRouteLazy(tr)) || !fr || fr.path !== tr.path || Boolean(options.compare && options.compare(tr, fr)); if (!start) return; } ret.push(tr); let count = options.count; if (isFunction(count)) count = count(ret, tr, fr); return isNumber(count) && ret.length === count; }); return ret.filter(r => !r.redirect); } _getBeforeEachGuards(to: Route, from: Route | null, current: Route | null = null) { const ret: (RouteBeforeGuardFn|LazyResolveFn)[] = [...this.beforeEachGuards]; if (this.viewRoot && this.viewRoot.props.beforeEach) { ret.push(this.viewRoot.props.beforeEach); } // to.matched.forEach(mr => { // Object.keys(mr.viewInstances).forEach(key => { // let view = mr.viewInstances[key]; // if (!view || view === this.viewRoot) return; // const beforeEach = view.props.beforeEach; // if (beforeEach) ret.push(beforeEach); // }); // }); if (from) { const fm = this._getChangeMatched(from, to).filter(r => Object.keys(r.viewInstances).some(key => r.viewInstances[key])); reverseArray(fm).forEach(mr => { reverseArray(mr.guards.beforeLeave).forEach(g => { if (g.called) return; if (g.instance) { const beforeEnterGuard = mr.guards.beforeEnter.find(g2 => g2.instance === g.instance); if (beforeEnterGuard && !beforeEnterGuard.called) return; } ret.push(g.guard); }); }); } if (to) { const tm = this._getChangeMatched(to, from, { containLazy: true, compare: (tr, fr) => fr.guards.beforeEnter.some(g => !g.called) }); tm.forEach(mr => mr.guards.beforeEnter.forEach(g => { if (!g.lazy && g.called) return; ret.push(g.guard); })); } return flatten(ret); } _getBeforeResolveGuards(to: Route, from: Route | null) { const ret = [...this.beforeResolveGuards]; if (to) { const tm = this._getChangeMatched(to, from, { compare: (tr, fr) => fr.guards.beforeResolve.some(g => !g.called) }); tm.forEach(mr => { mr.guards.beforeResolve.forEach(g => { if (g.called) return; ret.push(g.guard); }); }); } return flatten(ret); } _getRouteUpdateGuards(to: Route, from: Route | null) { const ret: RouteAfterGuardFn[] = [ ...this.afterUpdateGuards, ...this.afterEachGuards.filter(g => g.update) ]; const fm: MatchedRoute[] = []; to && to.matched.some((tr, i) => { const fr = from && from.matched[i]; if (!fr || fr.path !== tr.path) return true; fm.push(fr); }); reverseArray(fm.filter(r => !r.redirect)).forEach(mr => reverseArray(mr.guards.update).forEach(g => { if (g.called) return; ret.push(g.guard); })); return ret; } _getAfterEachGuards(to: Route, from: Route | null) { const ret: RouteAfterGuardFn[] = []; if (from) { const fm = this._getChangeMatched(from, to).filter(r => Object.keys(r.viewInstances) .some(key => r.viewInstances[key])); reverseArray(fm.filter(r => !r.redirect)).forEach(mr => reverseArray(mr.guards.afterLeave).forEach(g => { if (g.called) return; ret.push(g.guard); })); } if (this.viewRoot && this.viewRoot.props.afterEach) { ret.push(this.viewRoot.props.afterEach); } ret.push(...this.afterEachGuards); return flatten(ret); } _isMatchBasename(location: RouteHistoryLocation|Route) { if (!this.basename) return true; if (location.basename && !(location as RouteHistoryLocation).absolute) return location.basename === this.basename; const pathname = location.path || (location as RouteHistoryLocation).pathname; return pathname.startsWith(this.basenameNoSlash); } _transformLocation(location: RouteHistoryLocation|Route) { if (!location || isRoute(location)) return location; if (this.basename) { let pathname = location.pathname; if (!location.absolute && location.basename != null) pathname = location.basename + pathname; else if (pathname.length < this.basename.length) { let parent: ReactViewRouter | null = this.parent; while (parent) { if (pathname.length >= parent.basename.length) { const parentMatched = parent.getMatched(pathname); if (parentMatched.length) { pathname = parentMatched[parentMatched.length - 1].path + parentMatched.unmatchedPath; break; } } parent = parent.parent; } } if (!/\/$/.test(pathname)) pathname += '/'; location = { ...location, absolute: false }; const isCurrentBasename = pathname.startsWith(this.basenameNoSlash); if (isCurrentBasename) { location.pathname = (location.pathname.substr(this.basename.length - 1) || '/'); location.fullPath = location.pathname + location.search; location.basename = this.basename; } else { location.pathname = ''; location.fullPath = ''; } if (location.path != null) location.path = location.pathname; } return location; } async _getInterceptor(interceptors: RouteGuardInterceptor[], index: number) { let interceptor = interceptors[index]; while (interceptor && (interceptor as LazyResolveFn).lazy) { interceptor = await (interceptor as LazyResolveFn)(interceptors, index); } const newInterceptor = this._callEvent('onGetRouteInterceptor', interceptor, interceptors, index); if (newInterceptor && (isFunction(newInterceptor) || newInterceptor.then)) interceptor = newInterceptor; return interceptor as any; } async _routetInterceptors( interceptors: RouteGuardInterceptor[], to: Route, from: Route | null, next?: RouteNextFn ) { let throwError = false; const isBlock = (v: any, interceptor: RouteBeforeGuardFn, update: (v: Route) => void) => { if (throwError) return true; let _isLocation = isString(v) || isLocation(v); if (_isLocation && interceptor) { const _to = isRoute(v) ? v : this._normalizeLocation(v, { route: interceptor.route, }); v = _to && this.createRoute(_to, { action: getLoactionAction(to), from: to, matchedProvider: getCompleteRoute(to) || getCompleteRoute(from), isRedirect: true, }); if (v && v.fullPath === to.fullPath) { v = undefined; _isLocation = false; } else if (v) update(v); } return !this._history || v === false || _isLocation || v instanceof Error; }; async function beforeInterceptor( interceptor: RouteBeforeGuardFn, index: number, to: Route, from: Route | null, next: RouteNextFn ) { if (!interceptor) return next(); const nextWrapper: RouteNextFn = this._nexting = once(async (f1: any) => { if (isBlock.call(this, f1, interceptor, v => { f1 = v; })) return next(f1); if (f1 === true) f1 = undefined; try { const nextInterceptor = await this._getInterceptor(interceptors, ++index); if (!nextInterceptor) return next((res: any) => isFunction(f1) && f1(res)); return await beforeInterceptor.call( this, nextInterceptor, index, to, from, res => { const ret = next(res); if ((interceptor as RouteBeforeGuardFn).global) isFunction(f1) && f1(res); return ret; } ); } catch (ex) { throwError = true; console.error(ex); next(isString(ex) ? new Error(ex) : ex); } }); return await (interceptor as RouteBeforeGuardFn)(to, from, nextWrapper, { route: interceptor.route, router: this }); } if (next) await beforeInterceptor.call(this, await this._getInterceptor(interceptors, 0), 0, to, from, next); else afterInterceptors.call(this, interceptors, to, from); } async _handleRouteInterceptor( location: null | RouteHistoryLocation | Route, callback: (ok: boolean | RouteInterceptorCallback, route?: Route | null) => void, isInit = false ) { if (!this.isRunning) return callback(true); if (location) { const pathname = location.path || (location as RouteHistoryLocation).pathname; if (isInit && this.basename && !location.basename && this.history.location.pathname === pathname) { if (this.parent && this.parent.currentRoute) { let url = this.parent.currentRoute.url; if (url && this.parent.basename) url = this.parent.basename.substr(0, this.parent.basename.length - 1) + url; if (!pathname.startsWith(url)) { if ((location as RouteHistoryLocation).pathname != null) (location as RouteHistoryLocation).pathname = url; if (location.path != null) location.path = url; if (location.query) location.query = this.parent.currentRoute.query; if (!isReadonly(location, 'search')) location.search = this.parent.currentRoute.search; } } } if (this.pendingRoute && this.pendingRoute.fullPath === (location as RouteHistoryLocation).fullPath) return callback(!this._nexting); if (this.basename && (location as RouteHistoryLocation).absolute && !pathname.startsWith(this.basename)) return callback(true); location = this._transformLocation(location as RouteHistoryLocation); } if (!this._isMatchBasename(location as RouteHistoryLocation)) return callback(true); if (!location || (!(location as RouteHistoryLocation).pathname && (this.currentRoute && !this.currentRoute.path))) { return callback(true); } if ((!isInit && !location.onInit) && ( !this.viewRoot || !this.viewRoot.state.inited )) return callback(true); const nexts: onRouteingNextCallback[] = []; let error: unknown; let isContinue = true; this._callEvent('onRouteing', (ok: boolean|onRouteingNextCallback|Route) => { if (ok === false) isContinue = false; else if (isLocation(ok)) location = ok as any; else if (isFunction(ok)) nexts.push(ok); }); try { return isContinue && (await this._internalHandleRouteInterceptor(location, callback, isInit)); } catch (ex) { error = ex; } finally { nexts.forEach(next => next(Boolean(!isContinue || error), error, { location, isInit })); } } _normalizeLocation( to: Parameters<typeof normalizeLocation>[0], options: Parameters<typeof normalizeLocation>[1] = {} ) { return normalizeLocation(to, { basename: this.basename, mode: this.mode, queryProps: this.queryProps, ...options }); } _internalHandleRouteInterceptor( location: RouteHistoryLocation|Route, callback: (ok: boolean | RouteInterceptorCallback, route?: Route | null) => void, isInit = false, ) { let isContinue = false; const interceptorCounter = ++this._interceptorCounter; try { const to = this.createRoute( location, { matchedProvider: (hasOwnProp(location, 'basename') && location.basename !== this.basename) ? this.currentRoute : null } ); const from = isInit ? null : to.redirectedFrom && to.redirectedFrom.basename === this.basename ? to.redirectedFrom : this.currentRoute; const current = this.currentRoute; const checkIsContinue = () => !to.path || (this.isRunning && interceptorCounter === this._interceptorCounter && Boolean(this.viewRoot && this.viewRoot._isMounted)); const afterCallback = (isContinue: boolean, to: Route, isRouteAbort: boolean = true, ok: any = undefined) => { if (isContinue) to.onInit && to.onInit(isContinue, to); else if (isRouteAbort) { this._callEvent('onRouteAbort', to, ok); to.onAbort && to.onAbort(isContinue, to, ok); } this.nextTick(() => { if (!checkIsContinue()) return; if (!isInit && (!current || current.fullPath !== to.fullPath)) { this._routetInterceptors(this._getRouteUpdateGuards(to, current), to, current); } }); }; if (!to) return; const realtimeLocation = this.history.realtimeLocation; if (from && from.isComplete && (!realtimeLocation || (realtimeLocation.pathname === this.history.location.pathname)) && (to.matchedPath === from.matchedPath)) { isContinue = checkIsContinue(); if (isContinue) { callback(newIsContinue => { afterCallback(newIsContinue, to); }, to); } else callback(isContinue, to); return; } let fallbackViews: RouterView[] = []; if (hasMatchedRouteLazy(to.matched)) { this.viewRoot && fallbackViews.push(this.viewRoot); reverseArray(this._getSameMatched(isInit ? null : this.currentRoute, to)).some(m => { const keys = Object.keys(m.viewInstances).filter(key => m.viewInstances[key] && m.viewInstances[key].props.fallback); if (!keys.length) return; return fallbackViews = keys.map(key => m.viewInstances[key]); }); } fallbackViews.forEach(fallbackView => fallbackView._updateResolving(true, to)); this._routetInterceptors(this._getBeforeEachGuards(to, from, current), to, from, ok => { this._nexting = null; fallbackViews.length && setTimeout(() => fallbackViews.forEach( fallbackView => fallbackView._updateResolving(false) ), 0); const resolveOptions = { from: to, isInit: Boolean(isInit || to.onInit) }; if (resolveOptions.isInit && this.pendingRoute && to.fullPath !== this.pendingRoute.fullPath) { ok = this.pendingRoute; this.pendingRoute = null; } // if (isString(ok)) ok = { path: ok }; isContinue = checkIsContinue() && Boolean(ok === undefined || (ok && !(ok instanceof Error) && !isLocation(ok))); if (isContinue) { const toLast = to.matched[to.matched.length - 1]; if (toLast && toLast.config.exact && toLast.redirect) { ok = resolveRedirect(toLast.redirect, toLast, resolveOptions); if (ok) isContinue = false; } } if (isContinue && !isLocation(ok)) { to.matched .filter((mr: MatchedRoute) => mr.abort) .some(r => { const abort = resolveAbort(r.abort, r, resolveOptions); if (abort) { ok = isString(abort) ? new Error(abort) : (abort === true ? undefined : abort); isContinue = false; } return abort; }); } const onNext = (newOk: boolean) => { isContinue = newOk; if (isContinue) this._routetInterceptors(this._getBeforeResolveGuards(to, current), to, current); // callback(isContinue, to); const okIsLocation = isLocation(ok); const isRouteAbort = !isContinue && !okIsLocation; afterCallback(isContinue, to, isRouteAbort, ok); if (!isContinue) { if (okIsLocation) { return this.redirect( (ok as RouteLocation), isRouteAbort ? undefined : to.onComplete, isRouteAbort ? undefined : to.onAbort, to.onInit || (isInit ? callback : null), to ); } if (ok instanceof Error) this.errorCallbacks.forEach(cb => cb(ok as Error)); return; } this.nextTick(() => { if (isFunction(ok)) ok = ok(to); if (to && isFunction(to.onComplete)) to.onComplete(Boolean(ok), to); this._routetInterceptors(this._getAfterEachGuards(to, current), to, current); }); }; if (!isContinue) { try { onNext(isContinue); } finally { callback(isContinue, to); } return; } return callback(onNext, to); }); } catch (ex) { console.error(ex); if (!isContinue) callback(isContinue, null); } } _go( to: string | RouteLocation | Route | null, onComplete?: RouteEvent, onAbort?: RouteEvent, onInit?: RouteEvent | null, replace?: boolean ) { return new Promise((resolve, reject) => { function doComplete(res: any, _to: Route|null) { onComplete && onComplete(res, _to); resolve(res); } function doAbort(res: any, _to: Route|null) { onAbort && onAbort(res, _to); reject(res === false && _to === null ? new Error('to path cannot be empty!') : res); } if (!this.isRunning) { doAbort(new Error('router is not running!'), null); return; } let _to: RouteHistoryLocation|null; try { _to = this._normalizeLocation(to, { route: (to && (to as RouteLocation).route) || this.currentRoute, resolvePathCb: (path, to) => { const newPath: string = path.replace( /\[([A-z0-9.\-_#@$%^&*():|?<>=+]+)\]/g, (m, name) => { const ret = this.nameToPath(name, to, { onComplete: onInit || onComplete, onAbort, }); if (ret == null) throw new Error(`route name [${name}]not be found!`); if (ret === true) throw 'cancel'; return ret; } ); return newPath; } }); } catch (ex) { if (ex === 'cancel') return; throw ex; } if (!_to) { doAbort(false, null); return; } if (isFunction(onComplete)) _to.onComplete = once(doComplete); if (isFunction(onAbort)) _to.onAbort = once(doAbort); if (onInit) _to.onInit = onInit; let holdInitialQueryProps = this.options.holdInitialQueryProps; if (holdInitialQueryProps) { let query = this.initialRoute.query; if (Array.isArray(holdInitialQueryProps)) { query = holdInitialQueryProps.reduce((p, key) => { const value = query[key]; if (value === undefined) return p; p[key] = value; return p; }, {} as Record<string, any>); } else if (isFunction(holdInitialQueryProps)) { query = holdInitialQueryProps(this.initialRoute.query); } copyOwnProperties(_to.query, query); } _to.isReplace = Boolean(replace); if (this._nexting && (!(to as RouteLocation).pendingIfNotPrepared || this.isPrepared)) { this._nexting(_to); return; } if (_to.fullPath && isAbsoluteUrl(_to.fullPath) && globalThis?.location) { if (replace) globalThis.location.replace(_to.fullPath); else globalThis.location.href = _to.fullPath; return; } if (!this.isPrepared && !onInit) { this.pendingRoute = _to; const location = this.history.realtimeLocation; if (_to.fullPath === `${location.pathname}${location.search}`) return; } else { this.pendingRoute = null; } const isContinue = onInit || this._callEvent('onRouteGo', to as any, doComplete, doAbort, Boolean(replace)); if (isContinue === false) return; let history = this.history; if ((_to as RouteLocation).absolute && globalThis.location) { let url = ''; if (this.basename) { if (this.top && !this.top.basename) { history = this.top.history; } else if (!this.isMemoryMode) { url = history.createHref(_to); } } else if (this.isMemoryMode) { let mode = isString(_to.absolute) ? _to.absolute : ''; if (!mode || mode === HistoryType.memory) { const guessHistory = getPossibleHistory(this.options); if (guessHistory) { if (guessHistory.type === HistoryType.memory) history = guessHistory; else { mode = guessHistory.type; url = guessHistory.createHref(_to); } } else { mode = HistoryType.hash; url = createHashHref(_to, this.options.hashType); } warn(`[react-view-router] warning: parent router mode is ${mode || 'unknown'}, it could be '${mode}' mode that to go!`); } else url = history.createHref(_to); // if (mode === HistoryType.hash) url = getBaseHref() + (url.startsWith('#') ? '' : '#') + url; } if (url) { if (replace) globalThis.location.replace(url); else globalThis.location.href = url; return; } } if ((_to as RouteLocation).backIfVisited && _to.basename === this.basename) { const historyIndex = this.history.index; const isMatch: (to: RouteHistoryLocation, s: HistoryStackInfo) => boolean = (_to as RouteLocation).backIfVisited === 'full-matcth' ? (to, s) => to.search === s.search : (to, s) => Object.keys(to.query).every(key => to.query[key] == s.query[key]); const stack = reverseArray(this.stacks).find(s => { const stackPath = this.getMatchedPath(s.pathname); let toPath = (_to as RouteLocation).path || ''; if (this.basename) toPath = toPath.substr(this.basenameNoSlash.length, toPath.length); return stackPath === this.getMatchedPath(toPath) && isMatch(to as RouteHistoryLocation, s) && s.index <= historyIndex; }); if (stack) { this.go(stack); return; } } if (replace) history.replace(_to); else history.push(_to); }); } _replace( to: string | RouteLocation | Route, onComplete?: RouteEvent, onAbort?: RouteEvent, onInit?: RouteEvent | null ) { return this._go(to, onComplete, onAbort, onInit, true); } _push( to: string | RouteLocation | Route, onComplete?: RouteEvent, onAbort?: RouteEvent, onInit?: RouteEvent | null ) { return this._go(to, onComplete, onAbort, onInit); } resolveRouteName(fn: RouteResolveNameFn) { const _off = () => { const idx = this.resolveNameFns.indexOf(fn); if (~idx) this.resolveNameFns.splice(idx, 1); }; _off(); this.resolveNameFns.push(fn); return _off; } nameToPath(name: string, options: { absolute?: boolean, }|RouteHistoryLocation = {}, events: { onComplete?: RouteEvent, onAbort?: RouteEvent } = {}): string|true|null { name = camelize(name); let path: string|true|null = this.routeNameMap[name]; if (path == null) { this.resolveNameFns.some(fn => { const newPath = fn(name, options, this, events); if (typeof newPath !== 'string' && newPath !== true) return; path = newPath; return true; }); } else if (options.absolute) { if (this.basename) path = `${this.basename}${path}`; } if (path == null && options.absolute && this.parent) { path = this.parent.nameToPath(name, options, events); if (path != null && this.parent.basename) { path = `${this.parent.basename}${path}`; } } return path; } updateRouteMeta(route: ConfigRoute|MatchedRoute, newValue: Partial<any>, options: { ignoreConfigRoute?: boolean } = {}) { if (!route || !route.meta) return; let changed = false; const oldValue: Partial<any> = {}; Object.keys(newValue).forEach(key => { if (route.meta[key] === newValue[key]) return; oldValue[key] = route.meta[key]; changed = true; }); if (!changed) return; Object.assign(route.meta, newValue); route.metaComputed = null; // if (route.config && !options.ignoreConfigRoute) Object.assign(route.config.meta, newValue); this._callEvent('onRouteMetaChange', newValue, oldValue, route, this); return changed; } createMatchedRoute(route: ConfigRoute, match: matchPathResult): MatchedRoute { const { url, path = route.path, regx, params } = match || {}; const { subpath, meta = {}, redirect, depth } = route; function guardToGuardInfo<T extends Function>(originGuard: T, instance?: any): MatchedRouteGuard<T> { const guardInfo: MatchedRouteGuard<T> = { guard: (function matchedRouteGuardWrapper() { guardInfo.called = true; return originGuard.apply(this, arguments); }) as any, instance: instance || (originGuard as any).instance, called: false }; copyOwnProperties(guardInfo.guard, originGuard); return guardInfo; } function guardsToMatchedRouteGuards<T extends Function>(guards: T[]): MatchedRouteGuard<T>[] { return guards.filter((v: any) => !v.lazy).map((v: any) => guardToGuardInfo(v)); } let beforeEnter: MatchedRouteGuard<RouteBeforeGuardFn|LazyResolveFn>[]; let beforeResolve: MatchedRouteGuard<RouteAfterGuardFn>[]; let update: MatchedRouteGuard<RouteAfterGuardFn>[]; let beforeLeave: MatchedRouteGuard<RouteBeforeGuardFn>[]; let afterLeave: MatchedRouteGuard<RouteAfterGuardFn>[]; const that = this; const ret: MatchedRoute = { url, path, subpath, depth, regx, redirect, params, componentInstances: {}, viewInstances: {}, guards: { get beforeEnter() { if (beforeEnter) return beforeEnter; beforeEnter = []; that._getComponentGurads<RouteBeforeGuardFn|LazyResolveFn>( ret, 'beforeRouteEnter', (fn, name, ci, r) => { const ret = function beforeRouteEnterWraper(to: Route, from: Route | null, next: RouteNextFn) { return fn(to as any, from as any, (cb, ...args) => { if (isFunction(cb)) { const _cb = cb; r.config._pending && (r.config._pending.completeCallbacks[name] = ci => { const res = _cb(ci); that._callEvent('onRouteEnterNext', r, ci, res); return res; }); cb = undefined; } return next(cb, ...args); }, { route: r, router: that }); }; const guardInfo = guardToGuardInfo(ret, fn.instance); beforeEnter.push(guardInfo); return guardInfo.guard; }, (lazyResovleFn, hook) => { const guardInfo = { lazy: true, guard: lazyResovleFn, }; hook(() => { const idx = beforeEnter.indexOf(guardInfo); if (~idx) beforeEnter.splice(idx, 1); }); return beforeEnter.push(guardInfo); } ); return beforeEnter; }, get beforeResolve() { if (beforeResolve) return beforeResolve; beforeResolve = guardsToMatchedRouteGuards(that._getComponentGurads<RouteAfterGuardFn>(ret, 'beforeRouteResolve')); return beforeResolve; }, get update() { if (update) return update; update = guardsToMatchedRouteGuards(that._getComponentGurads<RouteAfterGuardFn>(ret, 'beforeRouteUpdate')); return update; }, get beforeLeave() { if (beforeLeave) return beforeLeave; beforeLeave = []; that._getComponentGurads<RouteBeforeGuardFn>( ret, 'beforeRouteLeave', (fn, name, ci, r) => { const ret = function beforeRouteLeaveWraper(to: Route, from: Route | null, next: RouteNextFn) { return fn.call(ci, to, from, (cb?: RouteNextResult, ...args: any[]) => { if (isFunction(cb)) { const _cb = cb; cb = (...as: any[]) => { const res = _cb(...as); that._callEvent('onRouteLeaveNext', r, ci, res); return res; }; } return next(cb, ...args); }, { route: r, router: that }); }; const guardInfo = guardToGuardInfo(ret, fn.instance); beforeLeave.push(guardInfo); return guardInfo.guard; } ); return beforeLeave; }, get afterLeave() { if (afterLeave) return afterLeave; afterLeave = guardsToMatchedRouteGuards(that._getComponentGurads<RouteAfterGuardFn>(ret, 'afterRouteLeave')); return afterLeave; }, }, config: route } as any; readonly(ret, 'meta', () => meta); let metaComputed: RouteComputedMeta|undefined; Object.defineProperty(ret, 'metaComputed', { get() { if (metaComputed) return metaComputed; metaComputed = Object.keys(meta).reduce((p, key) => { readonly(p, key, () => readRouteMeta(route, key, { router: this })); return p; }, {} as any); return metaComputed; }, set(v) { metaComputed = v; }, enumerable: true, configurable: true }); if (match.isNull) ret.isNull = true; return ret; } getMatched(to: Route | RouteHistoryLocation | string, from?: Route | null, parent?: ConfigRoute): MatchedRouteArray { if (!from) from = this.currentRoute; // function copyInstance(to: MatchedRoute, from: MatchedRoute | null) { // if (!from) return; // if (from.componentInstances) to.componentInstances = from.componentInstances; // if (from.viewInstances) to.viewInstances = from.viewInstances; // } function isSameMatch(tr: RouteBranchInfo, fr: MatchedRoute): boolean { return fr && tr && fr.path === tr.match.path; } const matched = matchRoutes(this.routes, to, parent, { queryProps: this.queryProps }); const isHistoryLocation = !isRoute(to) && typeof to !== 'string'; const state = (isHistoryLocation && (to as any).state && (to as any).state[this.basename || DEFAULT_STATE_NAME]) || {}; let isSameMatchedRoute = true; const ret = matched.map(({ route, match }, i) => { let ret; let viewInstances: MatchedRoute['viewInstances']|undefined; if (isSameMatchedRoute && from && i < from.matched.length) { const fr: MatchedRoute = from.matched[i]; const tr = matched[i]; isSameMatchedRoute = isSameMatch(tr, fr); if (isSameMatchedRoute) ret = fr; else if (i && isSameMatch(matched[i - 1], from.matched[i - 1])) viewInstances = fr.viewInstances; } if (!ret) { ret = this.createMatchedRoute(route, match); if (viewInstances) ret.viewInstances = viewInstances; ret.state = state[ret.url]; if (!ret.state || !Object.keys(ret.state).length) ret.state = createEmptyRouteState(); } return ret; }); innumerable(ret, 'unmatchedPath', matched.unmatchedPath || ''); innumerable(ret, 'first', ret[0]); innumerable(ret, 'last', ret[ret.length - 1]); return ret as MatchedRouteArray; } getMatchedComponents(to: Route, from?: Route, parent?: ConfigRoute) { const ret: ComponentType[] = []; this.getMatched(to, from, parent).forEach(r => { Array.prototype.push.apply(ret, Object.values(r.componentInstances)); }); return ret.filter(Boolean); } getMatchedViews(to: Route, from?: Route, parent?: ConfigRoute) { const ret: RouterView[] = []; this.getMatched(to, from, parent).map(r => { Array.prototype.push.apply(Object.values(r.viewInstances)); }); return ret.filter(Boolean); } getMatchedPath(path: string = '') { const matched = this.getMatched(path);