]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: lazy loading
authorEduardo San Martin Morote <posva13@gmail.com>
Mon, 16 Mar 2020 13:25:16 +0000 (14:25 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 16 Mar 2020 13:25:16 +0000 (14:25 +0100)
playground/router.ts
playground/shim.d.ts
src/components/View.ts
src/injectKeys.ts
src/matcher/index.ts
src/router.ts
src/types/index.ts
src/utils/index.ts

index e859f4b87c3ad0ac648f7a8a782af73d29b3ac12..1981d6eb6487d153e962f199bbe4b02f1568bcb7 100644 (file)
@@ -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',
index 996ea4df28c1951b2f9e74fadd1f0ba9adc1d2e9..01cae85526bd991f995c0cdfb9ee3f0e728777fe 100644 (file)
@@ -1,5 +1,5 @@
 declare module '*.vue' {
-  import { Component } from 'vue'
-  var component: Component
+  import { ComponentOptions } from 'vue'
+  var component: ComponentOptions
   export default component
 }
index aca948df0ba04f7132ff029b51903a7deb651f10..2efcb593e4d5d8cd56ea19960da282ec3ddf42dd 100644 (file)
@@ -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<Component | undefined>(
+    const ViewComponent = computed<RouteComponent | undefined>(
       () => matchedRoute.value && matchedRoute.value.components[props.name]
     )
 
index e13ed87398e4c0c86554fde8122efed6d04f37af..a79bbb786916f0be100f2edbc6972d54c5f58e82 100644 (file)
@@ -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<Router>
 export const routeKey = ('route' as unknown) as InjectionKey<
-  Ref<RouteLocationNormalized>
+  Ref<RouteLocationNormalizedResolved>
 >
 
 export function useRouter(): Router {
index 4d919ca07bf9de05fd06682e6903545eae6d7a90..002dec0c5b8811fcb6300501ce617a617e11dd06 100644 (file)
@@ -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
index 2701b75f4ccd0e7a72c08c04db38a3945bc6e2b2..c71ce6ecf0b914d02918850224d7b016be5cdd37 100644 (file)
@@ -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<ScrollPosition>
 }
@@ -59,7 +60,7 @@ export interface RouterOptions {
 
 export interface Router {
   history: RouterHistory
-  currentRoute: Ref<Immutable<RouteLocationNormalized>>
+  currentRoute: Ref<Immutable<RouteLocationNormalizedResolved>>
 
   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<RouteLocationNormalized>
-  replace(to: RouteLocation): Promise<RouteLocationNormalized>
+  push(to: RouteLocation): Promise<RouteLocationNormalizedResolved>
+  replace(to: RouteLocation): Promise<RouteLocationNormalizedResolved>
 
   beforeEach(guard: NavigationGuard): ListenerRemover
   afterEach(guard: PostNavigationGuard): ListenerRemover
@@ -91,7 +92,9 @@ export function createRouter({
 
   const beforeGuards = useCallbacks<NavigationGuard>()
   const afterGuards = useCallbacks<PostNavigationGuard>()
-  const currentRoute = ref<RouteLocationNormalized>(START_LOCATION_NORMALIZED)
+  const currentRoute = ref<RouteLocationNormalizedResolved>(
+    START_LOCATION_NORMALIZED
+  )
   let pendingLocation: Immutable<RouteLocationNormalized> = 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<RouteLocationNormalized> {
+  ): Promise<RouteLocationNormalizedResolved> {
     return pushWithRedirect(to, undefined)
   }
 
   async function pushWithRedirect(
     to: RouteLocation | RouteLocationNormalized,
     redirectedFrom: RouteLocationNormalized | undefined
-  ): Promise<RouteLocationNormalized> {
+  ): Promise<RouteLocationNormalizedResolved> {
     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<TODO> {
     let guards: Lazy<any>[]
 
@@ -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<Component>
+
     // 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<any>[]): Promise<void> {
 
 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]
index 173118fb18a42c45e018893d6325d159104dc036..74636cc0d1e74a863c898ab477f130fe69079134 100644 (file)
@@ -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<Vue> | typeof Vue | AsyncComponent
-
 export type Lazy<T> = () => Promise<T>
 export type Override<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U
 
@@ -61,8 +59,25 @@ export type RouteLocation =
   | (RouteQueryAndHash & LocationAsName & RouteLocationOptions)
   | (RouteQueryAndHash & LocationAsRelative & RouteLocationOptions)
 
+export interface RouteLocationMatched extends RouteRecordNormalized {
+  components: Record<string, RouteComponent>
+}
+
 // 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<string | number | symbol, any>
+}
+
 export interface RouteLocationNormalized {
   path: string
   fullPath: string
@@ -108,10 +123,9 @@ export interface RouteComponentInterface {
   beforeRouteUpdate?: NavigationGuard<void>
 }
 
-// TODO: have a real type with augmented properties
-// add async component
-// export type RouteComponent = (Component | ReturnType<typeof defineComponent>) & RouteComponentInterface
-export type RouteComponent = TODO
+// TODO: allow defineComponent export type RouteComponent = (Component | ReturnType<typeof defineComponent>) &
+export type RouteComponent = ComponentOptions & RouteComponentInterface
+export type RawRouteComponent = RouteComponent | Lazy<RouteComponent>
 
 // 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<string, RouteComponent>
+  components: Record<string, RawRouteComponent>
   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,
index e401b24b482bb2e57c9511d76929f4735843c928..e703f2882334e2479842482e3a16d3e804d73cf8 100644 (file)
@@ -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<void>> = []
-  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
 }