]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(router): support multiple apps at the same time
authorEduardo San Martin Morote <posva13@gmail.com>
Tue, 23 Jun 2020 15:55:28 +0000 (17:55 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Tue, 23 Jun 2020 15:56:25 +0000 (17:56 +0200)
__tests__/multipleApps.spec.ts [new file with mode: 0644]
e2e/specs/multi-app.js
src/router.ts

diff --git a/__tests__/multipleApps.spec.ts b/__tests__/multipleApps.spec.ts
new file mode 100644 (file)
index 0000000..fc70f43
--- /dev/null
@@ -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<Parameters<typeof createRouter>[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)
+  })
+})
index fe251ed49e44982cd62c869b0a6a7fbb4d531bd7..07fe895751304ca8995990d7664927a4c20cebac 100644 (file)
@@ -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()
   },
index 7f41d61632ede3c96b9cdf1aac819ce281f28ffb..392872ea319cb960461096e7d9d24c2602b7b2d9 100644 (file)
@@ -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<RouteLocationNormalizedLoaded>
   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<NavigationFailure>(
+              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<NavigationFailure>(
-            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<App>()
 
   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)
+      }
     },
   }