Skip to content

Commit

Permalink
feat: lazy loading
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Mar 16, 2020
1 parent 49c87ae commit 6ecdc70
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 56 deletions.
7 changes: 7 additions & 0 deletions playground/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions playground/shim.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
declare module '*.vue' {
import { Component } from 'vue'
var component: Component
import { ComponentOptions } from 'vue'
var component: ComponentOptions
export default component
}
4 changes: 2 additions & 2 deletions src/components/View.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand All @@ -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]
)

Expand Down
3 changes: 2 additions & 1 deletion src/injectKeys.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions src/matcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 33 additions & 22 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TODO,
Immutable,
MatcherLocationNormalized,
RouteLocationNormalizedResolved,
} from './types'
import { RouterHistory, parseURL, stringifyURL } from './history/common'
import {
Expand Down Expand Up @@ -45,7 +46,7 @@ type OnReadyCallback = [() => void, (reason?: any) => void]
interface ScrollBehavior {
(
to: RouteLocationNormalized,
from: RouteLocationNormalized,
from: RouteLocationNormalizedResolved,
savedPosition: ScrollToPosition | null
): ScrollPosition | Promise<ScrollPosition>
}
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -134,7 +137,7 @@ export function createRouter({

function resolve(
location: RouteLocation,
currentLocation?: RouteLocationNormalized
currentLocation?: RouteLocationNormalizedResolved
): RouteLocationNormalized {
// const objectLocation = routerLocationAsObject(location)
currentLocation = currentLocation || currentRoute.value
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -224,7 +227,7 @@ export function createRouter({
}

finalizeNavigation(
toLocation,
toLocation as RouteLocationNormalizedResolved,
from,
true,
// RouteLocationNormalized will give undefined
Expand All @@ -241,7 +244,7 @@ export function createRouter({

async function navigate(
to: RouteLocationNormalized,
from: RouteLocationNormalized
from: RouteLocationNormalizedResolved
): Promise<TODO> {
let guards: Lazy<any>[]

Expand Down Expand Up @@ -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
Expand All @@ -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))
Expand All @@ -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
Expand All @@ -325,8 +330,8 @@ export function createRouter({
* - Calls the scrollBehavior
*/
function finalizeNavigation(
toLocation: RouteLocationNormalized,
from: RouteLocationNormalized,
toLocation: RouteLocationNormalizedResolved,
from: RouteLocationNormalizedResolved,
isPush: boolean,
replace?: boolean
) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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[] = []
Expand All @@ -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]
Expand Down
34 changes: 24 additions & 10 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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[]
}

Expand All @@ -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,
Expand Down
Loading

0 comments on commit 6ecdc70

Please sign in to comment.