index.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. /* @flow */
  2. import { install } from './install'
  3. import { START } from './util/route'
  4. import { assert, warn } from './util/warn'
  5. import { inBrowser } from './util/dom'
  6. import { cleanPath } from './util/path'
  7. import { createMatcher } from './create-matcher'
  8. import { normalizeLocation } from './util/location'
  9. import { supportsPushState } from './util/push-state'
  10. import { handleScroll } from './util/scroll'
  11. import { HashHistory } from './history/hash'
  12. import { HTML5History } from './history/html5'
  13. import { AbstractHistory } from './history/abstract'
  14. import type { Matcher } from './create-matcher'
  15. import { isNavigationFailure, NavigationFailureType } from './util/errors'
  16. export default class VueRouter {
  17. static install: () => void
  18. static version: string
  19. static isNavigationFailure: Function
  20. static NavigationFailureType: any
  21. static START_LOCATION: Route
  22. app: any
  23. apps: Array<any>
  24. ready: boolean
  25. readyCbs: Array<Function>
  26. options: RouterOptions
  27. mode: string
  28. history: HashHistory | HTML5History | AbstractHistory
  29. matcher: Matcher
  30. fallback: boolean
  31. beforeHooks: Array<?NavigationGuard>
  32. resolveHooks: Array<?NavigationGuard>
  33. afterHooks: Array<?AfterNavigationHook>
  34. constructor (options: RouterOptions = {}) {
  35. if (process.env.NODE_ENV !== 'production') {
  36. warn(this instanceof VueRouter, `Router must be called with the new operator.`)
  37. }
  38. this.app = null
  39. this.apps = []
  40. this.options = options
  41. this.beforeHooks = []
  42. this.resolveHooks = []
  43. this.afterHooks = []
  44. this.matcher = createMatcher(options.routes || [], this)
  45. let mode = options.mode || 'hash'
  46. this.fallback =
  47. mode === 'history' && !supportsPushState && options.fallback !== false
  48. if (this.fallback) {
  49. mode = 'hash'
  50. }
  51. if (!inBrowser) {
  52. mode = 'abstract'
  53. }
  54. this.mode = mode
  55. switch (mode) {
  56. case 'history':
  57. this.history = new HTML5History(this, options.base)
  58. break
  59. case 'hash':
  60. this.history = new HashHistory(this, options.base, this.fallback)
  61. break
  62. case 'abstract':
  63. this.history = new AbstractHistory(this, options.base)
  64. break
  65. default:
  66. if (process.env.NODE_ENV !== 'production') {
  67. assert(false, `invalid mode: ${mode}`)
  68. }
  69. }
  70. }
  71. match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
  72. return this.matcher.match(raw, current, redirectedFrom)
  73. }
  74. get currentRoute (): ?Route {
  75. return this.history && this.history.current
  76. }
  77. init (app: any /* Vue component instance */) {
  78. process.env.NODE_ENV !== 'production' &&
  79. assert(
  80. install.installed,
  81. `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
  82. `before creating root instance.`
  83. )
  84. this.apps.push(app)
  85. // set up app destroyed handler
  86. // https://github.com/vuejs/vue-router/issues/2639
  87. app.$once('hook:destroyed', () => {
  88. // clean out app from this.apps array once destroyed
  89. const index = this.apps.indexOf(app)
  90. if (index > -1) this.apps.splice(index, 1)
  91. // ensure we still have a main app or null if no apps
  92. // we do not release the router so it can be reused
  93. if (this.app === app) this.app = this.apps[0] || null
  94. if (!this.app) this.history.teardown()
  95. })
  96. // main app previously initialized
  97. // return as we don't need to set up new history listener
  98. if (this.app) {
  99. return
  100. }
  101. this.app = app
  102. const history = this.history
  103. if (history instanceof HTML5History || history instanceof HashHistory) {
  104. const handleInitialScroll = routeOrError => {
  105. const from = history.current
  106. const expectScroll = this.options.scrollBehavior
  107. const supportsScroll = supportsPushState && expectScroll
  108. if (supportsScroll && 'fullPath' in routeOrError) {
  109. handleScroll(this, routeOrError, from, false)
  110. }
  111. }
  112. const setupListeners = routeOrError => {
  113. history.setupListeners()
  114. handleInitialScroll(routeOrError)
  115. }
  116. history.transitionTo(
  117. history.getCurrentLocation(),
  118. setupListeners,
  119. setupListeners
  120. )
  121. }
  122. history.listen(route => {
  123. this.apps.forEach(app => {
  124. app._route = route
  125. })
  126. })
  127. }
  128. beforeEach (fn: Function): Function {
  129. return registerHook(this.beforeHooks, fn)
  130. }
  131. beforeResolve (fn: Function): Function {
  132. return registerHook(this.resolveHooks, fn)
  133. }
  134. afterEach (fn: Function): Function {
  135. return registerHook(this.afterHooks, fn)
  136. }
  137. onReady (cb: Function, errorCb?: Function) {
  138. this.history.onReady(cb, errorCb)
  139. }
  140. onError (errorCb: Function) {
  141. this.history.onError(errorCb)
  142. }
  143. push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  144. // $flow-disable-line
  145. if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
  146. return new Promise((resolve, reject) => {
  147. this.history.push(location, resolve, reject)
  148. })
  149. } else {
  150. this.history.push(location, onComplete, onAbort)
  151. }
  152. }
  153. replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  154. // $flow-disable-line
  155. if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
  156. return new Promise((resolve, reject) => {
  157. this.history.replace(location, resolve, reject)
  158. })
  159. } else {
  160. this.history.replace(location, onComplete, onAbort)
  161. }
  162. }
  163. go (n: number) {
  164. this.history.go(n)
  165. }
  166. back () {
  167. this.go(-1)
  168. }
  169. forward () {
  170. this.go(1)
  171. }
  172. getMatchedComponents (to?: RawLocation | Route): Array<any> {
  173. const route: any = to
  174. ? to.matched
  175. ? to
  176. : this.resolve(to).route
  177. : this.currentRoute
  178. if (!route) {
  179. return []
  180. }
  181. return [].concat.apply(
  182. [],
  183. route.matched.map(m => {
  184. return Object.keys(m.components).map(key => {
  185. return m.components[key]
  186. })
  187. })
  188. )
  189. }
  190. resolve (
  191. to: RawLocation,
  192. current?: Route,
  193. append?: boolean
  194. ): {
  195. location: Location,
  196. route: Route,
  197. href: string,
  198. // for backwards compat
  199. normalizedTo: Location,
  200. resolved: Route
  201. } {
  202. current = current || this.history.current
  203. const location = normalizeLocation(to, current, append, this)
  204. const route = this.match(location, current)
  205. const fullPath = route.redirectedFrom || route.fullPath
  206. const base = this.history.base
  207. const href = createHref(base, fullPath, this.mode)
  208. return {
  209. location,
  210. route,
  211. href,
  212. // for backwards compat
  213. normalizedTo: location,
  214. resolved: route
  215. }
  216. }
  217. getRoutes () {
  218. return this.matcher.getRoutes()
  219. }
  220. addRoute (parentOrRoute: string | RouteConfig, route?: RouteConfig) {
  221. this.matcher.addRoute(parentOrRoute, route)
  222. if (this.history.current !== START) {
  223. this.history.transitionTo(this.history.getCurrentLocation())
  224. }
  225. }
  226. addRoutes (routes: Array<RouteConfig>) {
  227. if (process.env.NODE_ENV !== 'production') {
  228. warn(false, 'router.addRoutes() is deprecated and has been removed in Vue Router 4. Use router.addRoute() instead.')
  229. }
  230. this.matcher.addRoutes(routes)
  231. if (this.history.current !== START) {
  232. this.history.transitionTo(this.history.getCurrentLocation())
  233. }
  234. }
  235. }
  236. function registerHook (list: Array<any>, fn: Function): Function {
  237. list.push(fn)
  238. return () => {
  239. const i = list.indexOf(fn)
  240. if (i > -1) list.splice(i, 1)
  241. }
  242. }
  243. function createHref (base: string, fullPath: string, mode) {
  244. var path = mode === 'hash' ? '#' + fullPath : fullPath
  245. return base ? cleanPath(base + '/' + path) : path
  246. }
  247. VueRouter.install = install
  248. VueRouter.version = '__VERSION__'
  249. VueRouter.isNavigationFailure = isNavigationFailure
  250. VueRouter.NavigationFailureType = NavigationFailureType
  251. VueRouter.START_LOCATION = START
  252. if (inBrowser && window.Vue) {
  253. window.Vue.use(VueRouter)
  254. }