From: Eduardo San Martin Morote Date: Tue, 23 Jun 2020 15:55:28 +0000 (+0200) Subject: feat(router): support multiple apps at the same time X-Git-Tag: v4.0.0-alpha.14~12 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=565ec9d489b4aad347ee466b781ca85aff76bf2d;p=thirdparty%2Fvuejs%2Frouter.git feat(router): support multiple apps at the same time --- diff --git a/__tests__/multipleApps.spec.ts b/__tests__/multipleApps.spec.ts new file mode 100644 index 00000000..fc70f438 --- /dev/null +++ b/__tests__/multipleApps.spec.ts @@ -0,0 +1,57 @@ +import { createRouter, createMemoryHistory } from '../src' +import { h } from 'vue' +import { createDom } from './utils' +// import { mockWarn } from 'jest-mock-warn' + +declare var __DEV__: boolean + +const delay = (t: number) => new Promise(resolve => setTimeout(resolve, t)) + +function newRouter(options: Partial[0]> = {}) { + const history = options.history || createMemoryHistory() + const router = createRouter({ + history, + routes: [ + { + path: '/:pathMatch(.*)', + component: { + render: () => h('div', 'any route'), + }, + }, + ], + ...options, + }) + + return { history, router } +} + +describe('Multiple apps', () => { + beforeAll(() => { + createDom() + const rootEl = document.createElement('div') + rootEl.id = 'app' + document.body.appendChild(rootEl) + }) + + it('does not listen to url changes before being ready', async () => { + const { router, history } = newRouter() + + const spy = jest.fn((to, from, next) => { + next() + }) + router.beforeEach(spy) + + history.push('/foo') + history.push('/bar') + history.go(-1, true) + + await delay(5) + expect(spy).not.toHaveBeenCalled() + + await router.push('/baz') + + history.go(-1, true) + await delay(5) + expect(spy).toHaveBeenCalledTimes(2) + }) +}) diff --git a/e2e/specs/multi-app.js b/e2e/specs/multi-app.js index fe251ed4..07fe8957 100644 --- a/e2e/specs/multi-app.js +++ b/e2e/specs/multi-app.js @@ -72,19 +72,28 @@ module.exports = { .back() .assert.containsText('#guardcount', '4') - /** - * TODO: - * - add in-component guards and check each one of them is called - * - check `this` is the actual instance by injecting a global property - * per app equal to their id and using it somewhere in the template - */ + // unmounting apps should pause guards + // start by navigating 3 times + .click('#app-1 li:nth-child(1) a') + .click('#app-1 li:nth-child(2) a') + .click('#app-1 li:nth-child(1) a') + .assert.containsText('#guardcount', '7') + .click('#unmount1') + .click('#unmount2') + .assert.containsText('#guardcount', '7') + .back() + // one app is still mounted + .assert.containsText('#guardcount', '8') + .click('#unmount3') + .back() + .assert.containsText('#guardcount', '8') - // unmounting apps should end up removing the popstate listener - // .click('#unmount1') - // .click('#unmount2') - // .click('#unmount3') - // TODO: we need a way to hook into unmount - // .assert.containsText('#popcount', '0') + // mounting again should add the listeners again + .click('#mount1') + // the initial navigation + .assert.containsText('#guardcount', '9') + .click('#app-1 li:nth-child(2) a') + .assert.containsText('#guardcount', '10') .end() }, diff --git a/src/router.ts b/src/router.ts index 7f41d616..392872ea 100644 --- a/src/router.ts +++ b/src/router.ts @@ -46,6 +46,7 @@ import { ComputedRef, reactive, unref, + computed, } from 'vue' import { RouteRecord, RouteRecordNormalized } from './matcher/types' import { parseURL, stringifyURL, isSameRouteLocation } from './location' @@ -138,6 +139,9 @@ export interface RouterOptions extends PathParserOptions { } export interface Router { + /** + * @internal + */ readonly history: RouterHistory readonly currentRoute: Ref readonly options: RouterOptions @@ -665,79 +669,82 @@ export function createRouter(options: RouterOptions): Router { markAsReady() } + let removeHistoryListener: () => void // attach listener to history to trigger navigations - routerHistory.listen((to, _from, info) => { - // TODO: in dev try catch to correctly log the matcher error - // cannot be a redirect route because it was in history - const toLocation = resolve(to.fullPath) as RouteLocationNormalized + function setupListeners() { + removeHistoryListener = routerHistory.listen((to, _from, info) => { + // TODO: in dev try catch to correctly log the matcher error + // cannot be a redirect route because it was in history + const toLocation = resolve(to.fullPath) as RouteLocationNormalized + + pendingLocation = toLocation + const from = currentRoute.value + + // TODO: should be moved to web history? + if (isBrowser) { + saveScrollPosition( + getScrollKey(from.fullPath, info.delta), + computeScrollPosition() + ) + } - pendingLocation = toLocation - const from = currentRoute.value + navigate(toLocation, from) + .catch((error: NavigationFailure | NavigationRedirectError) => { + // a more recent navigation took place + if (pendingLocation !== toLocation) { + return createRouterError( + ErrorTypes.NAVIGATION_CANCELLED, + { + from, + to: toLocation, + } + ) + } + if (error.type === ErrorTypes.NAVIGATION_ABORTED) { + return error as NavigationFailure + } + if (error.type === ErrorTypes.NAVIGATION_GUARD_REDIRECT) { + routerHistory.go(-info.delta, false) + // the error is already handled by router.push we just want to avoid + // logging the error + pushWithRedirect( + (error as NavigationRedirectError).to, + toLocation + ).catch(() => { + // TODO: in dev show warning, in prod triggerError, same as initial navigation + }) + // avoid the then branch + return Promise.reject() + } + // TODO: test on different browsers ensure consistent behavior + routerHistory.go(-info.delta, false) + // unrecognized error, transfer to the global handler + return triggerError(error) + }) + .then((failure: NavigationFailure | void) => { + failure = + failure || + finalizeNavigation( + // after navigation, all matched components are resolved + toLocation as RouteLocationNormalizedLoaded, + from, + false + ) - // TODO: should be moved to web history? - if (isBrowser) { - saveScrollPosition( - getScrollKey(from.fullPath, info.delta), - computeScrollPosition() - ) - } + // revert the navigation + if (failure) routerHistory.go(-info.delta, false) - navigate(toLocation, from) - .catch((error: NavigationFailure | NavigationRedirectError) => { - // a more recent navigation took place - if (pendingLocation !== toLocation) { - return createRouterError( - ErrorTypes.NAVIGATION_CANCELLED, - { - from, - to: toLocation, - } - ) - } - if (error.type === ErrorTypes.NAVIGATION_ABORTED) { - return error as NavigationFailure - } - if (error.type === ErrorTypes.NAVIGATION_GUARD_REDIRECT) { - routerHistory.go(-info.delta, false) - // the error is already handled by router.push we just want to avoid - // logging the error - pushWithRedirect( - (error as NavigationRedirectError).to, - toLocation - ).catch(() => { - // TODO: in dev show warning, in prod triggerError, same as initial navigation - }) - // avoid the then branch - return Promise.reject() - } - // TODO: test on different browsers ensure consistent behavior - routerHistory.go(-info.delta, false) - // unrecognized error, transfer to the global handler - return triggerError(error) - }) - .then((failure: NavigationFailure | void) => { - failure = - failure || - finalizeNavigation( - // after navigation, all matched components are resolved + triggerAfterEach( toLocation as RouteLocationNormalizedLoaded, from, - false + failure ) - - // revert the navigation - if (failure) routerHistory.go(-info.delta, false) - - triggerAfterEach( - toLocation as RouteLocationNormalizedLoaded, - from, - failure - ) - }) - .catch(() => { - // TODO: same as above - }) - }) + }) + .catch(() => { + // TODO: same as above + }) + }) + } // Initialization and Errors @@ -780,6 +787,7 @@ export function createRouter(options: RouterOptions): Router { function markAsReady(err?: any): void { if (ready) return ready = true + setupListeners() readyHandlers .list() .forEach(([resolve, reject]) => (err ? reject(err) : resolve())) @@ -828,6 +836,7 @@ export function createRouter(options: RouterOptions): Router { } let started: boolean | undefined + const installedApps = new Set() const router: Router = { currentRoute, @@ -893,6 +902,19 @@ export function createRouter(options: RouterOptions): Router { app.provide(routerKey, router) app.provide(routeLocationKey, reactive(reactiveRoute)) + + let unmountApp = app.unmount + installedApps.add(app) + app.unmount = function () { + installedApps.delete(app) + if (installedApps.size < 1) { + removeHistoryListener() + currentRoute.value = START_LOCATION_NORMALIZED + started = false + ready = false + } + unmountApp.call(this, arguments) + } }, }