Skip to content

[Feature]: Async Route Children #1576

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions examples/async-children/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

const routes = [
{ path: '/', component: Home },
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar },
{
path: '/basic',
component: { template: '<div><router-view class="async-view"></router-view></div>' },
loadChildren: () => import('./basic-async').then(asyncConfig => asyncConfig.routes)
},
{
path: '/deep',
component: { template: '<div><router-view class="async-view"></router-view></div>' },
loadChildren: () => import('./deep-async-a').then(asyncConfig => asyncConfig.routes)
},
{
path: '/default-deep',
component: { template: '<div><router-view class="async-view"></router-view></div>' },
loadChildren: () => import('./deep-async-default-a').then(asyncConfig => asyncConfig.routes)
}
]

const router = new VueRouter({
mode: 'history',
base: __dirname,
routes
})

new Vue({
router,
template: `
<div id="app">
<h1>Basic</h1>
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/foo">/foo</router-link></li>
<li><router-link to="/bar">/bar</router-link></li>
<li><router-link to="/basic">/basic</router-link></li>
<li><router-link to="/basic/foo">/basic/foo</router-link></li>
<li><router-link to="/basic/bar">/basic/bar</router-link></li>
<li><router-link to="/deep/a/b">/deep/a/b</router-link></li>
<li><router-link to="/default-deep">/default-deep</router-link></li>
</ul>
<router-view class="view"></router-view>
</div>
`
}).$mount('#app')
9 changes: 9 additions & 0 deletions examples/async-children/basic-async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const BasicDefault = { template: '<div>basic-default</div>' }
const BasicFoo = { template: '<div>basic-foo</div>' }
const BasicBar = { template: '<div>basic-bar</div>' }

export const routes = [
{ path: '', component: BasicDefault },
{ path: 'foo', component: BasicFoo },
{ path: 'bar', component: BasicBar }
]
19 changes: 19 additions & 0 deletions examples/async-children/deep-async-a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const DeepA = { template: '<div><router-view class="async-viewA"></router-view></div>' }

export const routes = [
{
path: '',
component: {},
loadChildren: () => Promise.reject('Default async route loaded when it should not have been.')
},
{
path: 'test',
component: {},
loadChildren: () => Promise.reject('Test async route loaded when it should not have been.')
},
{
path: 'a',
component: DeepA,
loadChildren: () => import('./deep-async-b').then(asyncConfig => asyncConfig.routes)
}
]
18 changes: 18 additions & 0 deletions examples/async-children/deep-async-b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const DeepB = { template: '<div>deep-async-b</div>' }

export const routes = [
{
path: '',
component: {},
loadChildren: () => Promise.reject('Default async route loaded when it should not have been.')
},
{
path: 'test',
component: {},
loadChildren: () => Promise.reject('Test async route loaded when it should not have been.')
},
{
path: 'b',
component: DeepB
}
]
14 changes: 14 additions & 0 deletions examples/async-children/deep-async-default-a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const DeepA = { template: '<div><router-view class="async-viewA"></router-view></div>' }

export const routes = [
{
path: '',
component: DeepA,
loadChildren: () => import('./deep-async-default-b').then(asyncConfig => asyncConfig.routes)
},
{
path: 'test',
component: {},
loadChildren: () => Promise.reject('Test async route loaded when it should not have been.')
}
]
13 changes: 13 additions & 0 deletions examples/async-children/deep-async-default-b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const DeepB = { template: '<div>deep-async-default-b</div>' }

export const routes = [
{
path: '',
component: DeepB
},
{
path: 'test',
component: {},
loadChildren: () => Promise.reject('Test async route loaded when it should not have been.')
}
]
6 changes: 6 additions & 0 deletions examples/async-children/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<link rel="stylesheet" href="/global.css">
<a href="/">&larr; Examples index</a>
<div id="app"></div>
<script src="/__build__/shared.js"></script>
<script src="/__build__/async-children.js"></script>
5 changes: 5 additions & 0 deletions flow/declarations.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ declare type NavigationGuard = (

declare type AfterNavigationHook = (to: Route, from: Route) => any

declare type LoadChildrenPromise = () => Promise<RouteConfig[]>;

type Position = { x: number, y: number };

declare type RouterOptions = {
Expand Down Expand Up @@ -59,6 +61,7 @@ declare type RouteConfig = {
props?: boolean | Object | Function;
caseSensitive?: boolean;
pathToRegexpOptions?: PathToRegexpOptions;
loadChildren?: string | LoadChildrenPromise | null;
}

declare type RouteRecord = {
Expand All @@ -73,6 +76,8 @@ declare type RouteRecord = {
beforeEnter: ?NavigationGuard;
meta: any;
props: boolean | Object | Function | Dictionary<boolean | Object | Function>;
loadChildren?: string | LoadChildrenPromise | null;
routeConfig: RouteConfig;
}

declare type Location = {
Expand Down
34 changes: 33 additions & 1 deletion src/create-matcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import { createRoute } from './util/route'
import { fillParams } from './util/params'
import { createRouteMap } from './create-route-map'
import { normalizeLocation } from './util/location'
import { findParent, addChildren } from './util/async-children'

export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
loadAsyncChildren: (raw: RawLocation, route: Route) => Promise<void>;
};

export function createMatcher (
Expand Down Expand Up @@ -71,6 +73,35 @@ export function createMatcher (
return _createRoute(null, location)
}

function loadAsyncChildren (
raw: RawLocation,
currentRoute: Route
): Promise<any> {
const location = normalizeLocation(raw, currentRoute, false, router)
const asyncMatches = currentRoute.matched.filter(match => match && !!match.loadChildren)
if (!asyncMatches) {
return Promise.reject(new Error('No matched routes have async children.'))
}

return Promise.all([
...asyncMatches.map(match =>
typeof match.loadChildren === 'function'
? match.loadChildren()
: Promise.resolve([])
)
])
.then(allChildren => {
return Promise.all([
...allChildren.map((children, i) => {
const { path } = asyncMatches[i]
const parent = findParent(pathMap[path])
return addChildren(parent.routeConfig, children, path, location.path || '/')
.then(updatedConfig => addRoutes([updatedConfig]))
})
])
})
}

function redirect (
record: RouteRecord,
location: Location
Expand Down Expand Up @@ -168,7 +199,8 @@ export function createMatcher (

return {
match,
addRoutes
addRoutes,
loadAsyncChildren
}
}

Expand Down
52 changes: 42 additions & 10 deletions src/create-route-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
oldNameMap?: Dictionary<RouteRecord>,
parentRoute?: RouteRecord
): {
pathList: Array<string>;
pathMap: Dictionary<RouteRecord>;
Expand All @@ -20,7 +21,7 @@ export function createRouteMap (
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
})

// ensure wildcard routes are always at the end
Expand Down Expand Up @@ -48,20 +49,35 @@ function addRouteRecord (
matchAs?: string
) {
const { path, name } = route
const hasAsyncChildren = typeof route.loadChildren === 'function'
const matchAllChildren = hasAsyncChildren && (
(parent && path !== '' && path !== '/') ||
!parent
)

if (process.env.NODE_ENV !== 'production') {
assert(path != null, `"path" is required in a route configuration.`)
assert(
typeof route.component !== 'string',
`route config "component" for path: ${String(path || name)} cannot be a ` +
`string id. Use an actual component instead.`
)

if (route.loadChildren) {
assert(
hasAsyncChildren,
`route config "loadChildren" for path: ${String(path || name)} cannot be a ` +
`${typeof route.loadChildren}. Use a method that returns a Promise with your child routes.`
)
}
}

const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
const normalizedPath = normalizePath(
path,
parent,
pathToRegexpOptions.strict
pathToRegexpOptions.strict,
hasAsyncChildren
)

if (typeof route.caseSensitive === 'boolean') {
Expand All @@ -70,7 +86,7 @@ function addRouteRecord (

const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions, matchAllChildren),
components: route.components || { default: route.component },
instances: {},
name,
Expand All @@ -83,7 +99,12 @@ function addRouteRecord (
? {}
: route.components
? route.props
: { default: route.props }
: { default: route.props },
routeConfig: route
}

if (hasAsyncChildren) {
record.loadChildren = route.loadChildren
}

if (route.children) {
Expand Down Expand Up @@ -118,6 +139,7 @@ function addRouteRecord (
aliases.forEach(alias => {
const aliasRoute = {
path: alias,
loadChildren: route.loadChildren,
children: route.children
}
addRouteRecord(
Expand All @@ -136,8 +158,18 @@ function addRouteRecord (
pathMap[record.path] = record
}

if (parent) {
// Ensure the parent route is after all child routes
const parentIndex = pathList.indexOf(parent.path)
const childIndex = pathList.indexOf(record.path)

if (parentIndex < childIndex) {
pathList.push(pathList.splice(parentIndex, 1)[0])
}
}

if (name) {
if (!nameMap[name]) {
if (!nameMap[name] || nameMap[name].path === record.path) {
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
Expand All @@ -149,8 +181,8 @@ function addRouteRecord (
}
}

function compileRouteRegex (path: string, pathToRegexpOptions: PathToRegexpOptions): RouteRegExp {
const regex = Regexp(path, [], pathToRegexpOptions)
function compileRouteRegex (path: string, pathToRegexpOptions: PathToRegexpOptions, matchAllChildren?: boolean): RouteRegExp {
const regex = Regexp(`${path}${matchAllChildren ? '(\/{0,1}.*)' : ''}`, [], pathToRegexpOptions)
if (process.env.NODE_ENV !== 'production') {
const keys: any = {}
regex.keys.forEach(key => {
Expand All @@ -161,8 +193,8 @@ function compileRouteRegex (path: string, pathToRegexpOptions: PathToRegexpOptio
return regex
}

function normalizePath (path: string, parent?: RouteRecord, strict?: boolean): string {
if (!strict) path = path.replace(/\/$/, '')
function normalizePath (path: string, parent?: RouteRecord, strict?: boolean, async?: boolean): string {
if (!strict || async) path = path.replace(/\/$/, '')
if (path[0] === '/') return path
if (parent == null) return path
return cleanPath(`${parent.path}/${path}`)
Expand Down
32 changes: 25 additions & 7 deletions src/history/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,32 @@ export class History {
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const route = this.router.match(location, this.current)
this.confirmTransition(route, () => {
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
if (route.loadChildren) {
this.router.matcher.loadAsyncChildren(location, route)
.then(() => {
// Perform transition again to ensure the proper component hierarchy is loaded
this.transitionTo(location, onComplete, onAbort)
})
.catch((err) => {
if (onAbort) {
onAbort(err)
}

// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => { cb(route) })
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => { cb(err) })
}
})
} else {
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()

// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => { cb(route) })
}
}
}, err => {
if (onAbort) {
Expand Down
Loading