From: Eduardo San Martin Morote Date: Mon, 16 Mar 2020 13:25:16 +0000 (+0100) Subject: feat: lazy loading X-Git-Tag: v4.0.0-alpha.4~52 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6ecdc70baa6361b8614368196ff2652560b6a0ba;p=thirdparty%2Fvuejs%2Frouter.git feat: lazy loading --- diff --git a/playground/router.ts b/playground/router.ts index e859f4b8..1981d6eb 100644 --- a/playground/router.ts +++ b/playground/router.ts @@ -25,6 +25,13 @@ export const router = createRouter({ { path: '/n/:n', name: 'increment', component }, { path: '/multiple/:a/:b', name: 'multiple', component }, { path: '/long-:n', name: 'long', component: LongView }, + { + path: '/lazy', + component: async () => { + await delay(500) + return component + }, + }, { path: '/with-guard/:n', name: 'guarded', diff --git a/playground/shim.d.ts b/playground/shim.d.ts index 996ea4df..01cae855 100644 --- a/playground/shim.d.ts +++ b/playground/shim.d.ts @@ -1,5 +1,5 @@ declare module '*.vue' { - import { Component } from 'vue' - var component: Component + import { ComponentOptions } from 'vue' + var component: ComponentOptions export default component } diff --git a/src/components/View.ts b/src/components/View.ts index aca948df..2efcb593 100644 --- a/src/components/View.ts +++ b/src/components/View.ts @@ -5,12 +5,12 @@ import { defineComponent, PropType, computed, - Component, InjectionKey, Ref, } from 'vue' import { RouteRecordNormalized } from '../matcher/types' import { routeKey } from '../injectKeys' +import { RouteComponent } from '../types' // TODO: make it work with no symbols too for IE export const matchedRouteKey = Symbol() as InjectionKey< @@ -32,7 +32,7 @@ export const View = defineComponent({ provide('routerViewDepth', depth + 1) const matchedRoute = computed(() => route.value.matched[depth]) - const ViewComponent = computed( + const ViewComponent = computed( () => matchedRoute.value && matchedRoute.value.components[props.name] ) diff --git a/src/injectKeys.ts b/src/injectKeys.ts index e13ed873..a79bbb78 100644 --- a/src/injectKeys.ts +++ b/src/injectKeys.ts @@ -1,9 +1,10 @@ import { InjectionKey, Ref, inject } from 'vue' import { Router, RouteLocationNormalized } from '.' +import { RouteLocationNormalizedResolved } from './types' export const routerKey = ('router' as unknown) as InjectionKey export const routeKey = ('route' as unknown) as InjectionKey< - Ref + Ref > export function useRouter(): Router { diff --git a/src/matcher/index.ts b/src/matcher/index.ts index 4d919ca0..002dec0c 100644 --- a/src/matcher/index.ts +++ b/src/matcher/index.ts @@ -64,6 +64,11 @@ export function createRouterMatcher( for (const alias of aliases) { normalizedRecords.push({ ...mainNormalizedRecord, + // this allows us to hold a copy of the `components` option + // so that async components cache is hold on the original record + components: originalRecord + ? originalRecord.record.components + : mainNormalizedRecord.components, path: alias, // we might be the child of an alias aliasOf: originalRecord diff --git a/src/router.ts b/src/router.ts index 2701b75f..c71ce6ec 100644 --- a/src/router.ts +++ b/src/router.ts @@ -10,6 +10,7 @@ import { TODO, Immutable, MatcherLocationNormalized, + RouteLocationNormalizedResolved, } from './types' import { RouterHistory, parseURL, stringifyURL } from './history/common' import { @@ -45,7 +46,7 @@ type OnReadyCallback = [() => void, (reason?: any) => void] interface ScrollBehavior { ( to: RouteLocationNormalized, - from: RouteLocationNormalized, + from: RouteLocationNormalizedResolved, savedPosition: ScrollToPosition | null ): ScrollPosition | Promise } @@ -59,7 +60,7 @@ export interface RouterOptions { export interface Router { history: RouterHistory - currentRoute: Ref> + currentRoute: Ref> addRoute(parentName: string, route: RouteRecord): () => void addRoute(route: RouteRecord): () => void @@ -68,8 +69,8 @@ export interface Router { resolve(to: RouteLocation): RouteLocationNormalized createHref(to: RouteLocationNormalized): string - push(to: RouteLocation): Promise - replace(to: RouteLocation): Promise + push(to: RouteLocation): Promise + replace(to: RouteLocation): Promise beforeEach(guard: NavigationGuard): ListenerRemover afterEach(guard: PostNavigationGuard): ListenerRemover @@ -91,7 +92,9 @@ export function createRouter({ const beforeGuards = useCallbacks() const afterGuards = useCallbacks() - const currentRoute = ref(START_LOCATION_NORMALIZED) + const currentRoute = ref( + START_LOCATION_NORMALIZED + ) let pendingLocation: Immutable = START_LOCATION_NORMALIZED if (isClient && 'scrollRestoration' in window.history) { @@ -134,7 +137,7 @@ export function createRouter({ function resolve( location: RouteLocation, - currentLocation?: RouteLocationNormalized + currentLocation?: RouteLocationNormalizedResolved ): RouteLocationNormalized { // const objectLocation = routerLocationAsObject(location) currentLocation = currentLocation || currentRoute.value @@ -183,18 +186,18 @@ export function createRouter({ function push( to: RouteLocation | RouteLocationNormalized - ): Promise { + ): Promise { return pushWithRedirect(to, undefined) } async function pushWithRedirect( to: RouteLocation | RouteLocationNormalized, redirectedFrom: RouteLocationNormalized | undefined - ): Promise { + ): Promise { const toLocation: RouteLocationNormalized = (pendingLocation = // Some functions will pass a normalized location and we don't need to resolve it again typeof to === 'object' && 'matched' in to ? to : resolve(to)) - const from: RouteLocationNormalized = currentRoute.value + const from: RouteLocationNormalizedResolved = currentRoute.value // @ts-ignore: no need to check the string as force do not exist on a string const force: boolean | undefined = to.force @@ -224,7 +227,7 @@ export function createRouter({ } finalizeNavigation( - toLocation, + toLocation as RouteLocationNormalizedResolved, from, true, // RouteLocationNormalized will give undefined @@ -241,7 +244,7 @@ export function createRouter({ async function navigate( to: RouteLocationNormalized, - from: RouteLocationNormalized + from: RouteLocationNormalizedResolved ): Promise { let guards: Lazy[] @@ -280,7 +283,7 @@ export function createRouter({ // check in components beforeRouteUpdate guards = await extractComponentsGuards( - to.matched.filter(record => from.matched.indexOf(record) > -1), + to.matched.filter(record => from.matched.indexOf(record as any) > -1), 'beforeRouteUpdate', to, from @@ -293,7 +296,7 @@ export function createRouter({ guards = [] for (const record of to.matched) { // do not trigger beforeEnter on reused views - if (record.beforeEnter && from.matched.indexOf(record) < 0) { + if (record.beforeEnter && from.matched.indexOf(record as any) < 0) { if (Array.isArray(record.beforeEnter)) { for (const beforeEnter of record.beforeEnter) guards.push(guardToPromiseFn(beforeEnter, to, from)) @@ -306,10 +309,12 @@ export function createRouter({ // run the queue of per route beforeEnter guards await runGuardQueue(guards) + // TODO: at this point to.matched is normalized and does not contain any () => Promise + // check in-component beforeRouteEnter - // TODO: is it okay to resolve all matched component or should we do it in order guards = await extractComponentsGuards( - to.matched.filter(record => from.matched.indexOf(record) < 0), + // the type does'nt matter as we are comparing an object per reference + to.matched.filter(record => from.matched.indexOf(record as any) < 0), 'beforeRouteEnter', to, from @@ -325,8 +330,8 @@ export function createRouter({ * - Calls the scrollBehavior */ function finalizeNavigation( - toLocation: RouteLocationNormalized, - from: RouteLocationNormalized, + toLocation: RouteLocationNormalizedResolved, + from: RouteLocationNormalizedResolved, isPush: boolean, replace?: boolean ) { @@ -377,7 +382,12 @@ export function createRouter({ try { await navigate(toLocation, from) - finalizeNavigation(toLocation, from, false) + finalizeNavigation( + // after navigation, all matched components are resolved + toLocation as RouteLocationNormalizedResolved, + from, + false + ) } catch (error) { if (NavigationGuardRedirect.is(error)) { // TODO: refactor the duplication of new NavigationCancelled by @@ -451,8 +461,8 @@ export function createRouter({ // Scroll behavior async function handleScroll( - to: RouteLocationNormalized, - from: RouteLocationNormalized, + to: RouteLocationNormalizedResolved, + from: RouteLocationNormalizedResolved, scrollPosition?: ScrollToPosition ) { if (!scrollBehavior) return @@ -524,7 +534,7 @@ async function runGuardQueue(guards: Lazy[]): Promise { function extractChangingRecords( to: RouteLocationNormalized, - from: RouteLocationNormalized + from: RouteLocationNormalizedResolved ) { const leavingRecords: RouteRecordNormalized[] = [] const updatingRecords: RouteRecordNormalized[] = [] @@ -537,7 +547,8 @@ function extractChangingRecords( } for (const record of to.matched) { - if (from.matched.indexOf(record) < 0) enteringRecords.push(record) + // the type doesn't matter because we are comparing per reference + if (from.matched.indexOf(record as any) < 0) enteringRecords.push(record) } return [leavingRecords, updatingRecords, enteringRecords] diff --git a/src/types/index.ts b/src/types/index.ts index 173118fb..74636cc0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,10 +1,8 @@ import { LocationQuery, LocationQueryRaw } from '../utils/query' import { PathParserOptions } from '../matcher/path-parser-ranker' -import { markNonReactive } from 'vue' +import { markNonReactive, ComponentOptions } from 'vue' import { RouteRecordNormalized } from '../matcher/types' -// type Component = ComponentOptions | typeof Vue | AsyncComponent - export type Lazy = () => Promise export type Override = Pick> & U @@ -61,8 +59,25 @@ export type RouteLocation = | (RouteQueryAndHash & LocationAsName & RouteLocationOptions) | (RouteQueryAndHash & LocationAsRelative & RouteLocationOptions) +export interface RouteLocationMatched extends RouteRecordNormalized { + components: Record +} + // A matched record cannot be a redirection and must contain +// matched contains resolved components +export interface RouteLocationNormalizedResolved { + path: string + fullPath: string + query: LocationQuery + hash: string + name: string | null | undefined + params: RouteParams + matched: RouteLocationMatched[] // non-enumerable + redirectedFrom: RouteLocationNormalized | undefined + meta: Record +} + export interface RouteLocationNormalized { path: string fullPath: string @@ -108,10 +123,9 @@ export interface RouteComponentInterface { beforeRouteUpdate?: NavigationGuard } -// TODO: have a real type with augmented properties -// add async component -// export type RouteComponent = (Component | ReturnType) & RouteComponentInterface -export type RouteComponent = TODO +// TODO: allow defineComponent export type RouteComponent = (Component | ReturnType) & +export type RouteComponent = ComponentOptions & RouteComponentInterface +export type RawRouteComponent = RouteComponent | Lazy // TODO: could this be moved to matcher? export interface RouteRecordCommon { @@ -141,12 +155,12 @@ export interface RouteRecordRedirect extends RouteRecordCommon { } export interface RouteRecordSingleView extends RouteRecordCommon { - component: RouteComponent + component: RawRouteComponent children?: RouteRecord[] } export interface RouteRecordMultipleViews extends RouteRecordCommon { - components: Record + components: Record children?: RouteRecord[] } @@ -155,7 +169,7 @@ export type RouteRecord = | RouteRecordMultipleViews | RouteRecordRedirect -export const START_LOCATION_NORMALIZED: RouteLocationNormalized = markNonReactive( +export const START_LOCATION_NORMALIZED: RouteLocationNormalizedResolved = markNonReactive( { path: '/', name: undefined, diff --git a/src/utils/index.ts b/src/utils/index.ts index e401b24b..e703f288 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,38 +1,55 @@ -import { RouteLocationNormalized, RouteParams, Immutable } from '../types' +import { + RouteLocationNormalized, + RouteParams, + Immutable, + RouteComponent, +} from '../types' import { guardToPromiseFn } from './guardToPromiseFn' import { RouteRecordNormalized } from '../matcher/types' import { LocationQueryValue } from './query' export * from './guardToPromiseFn' +const hasSymbol = + typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol' + +function isESModule(obj: any): obj is { default: RouteComponent } { + return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module') +} + type GuardType = 'beforeRouteEnter' | 'beforeRouteUpdate' | 'beforeRouteLeave' +// TODO: remove async export async function extractComponentsGuards( matched: RouteRecordNormalized[], guardType: GuardType, to: RouteLocationNormalized, from: RouteLocationNormalized ) { + // TODO: test to avoid redundant requests for aliases. It should work because we are holding a copy of the `components` option when we create aliases const guards: Array<() => Promise> = [] - await Promise.all( - matched.map(async record => { - // TODO: cache async routes per record - for (const name in record.components) { - const component = record.components[name] - // TODO: handle Vue.extend views - // if ('options' in component) throw new Error('TODO') - const resolvedComponent = component - // TODO: handle async component - // const resolvedComponent = await (typeof component === 'function' - // ? component() - // : component) - const guard = resolvedComponent[guardType] - if (guard) { - guards.push(guardToPromiseFn(guard, to, from)) - } + for (const record of matched) { + for (const name in record.components) { + const rawComponent = record.components[name] + if (typeof rawComponent === 'function') { + // start requesting the chunk already + const componentPromise = rawComponent() + guards.push(async () => { + const resolved = await componentPromise + const resolvedComponent = isESModule(resolved) + ? resolved.default + : resolved + // replace the function with the resolved component + record.components[name] = resolvedComponent + const guard = resolvedComponent[guardType] + return guard && guardToPromiseFn(guard, to, from)() + }) + } else { + const guard = rawComponent[guardType] + guard && guards.push(guardToPromiseFn(guard, to, from)) } - }) - ) + } + } return guards }