client.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. import Vue from 'vue'
  2. import fetch from 'unfetch'
  3. import middleware from './middleware.js'
  4. import {
  5. applyAsyncData,
  6. promisify,
  7. middlewareSeries,
  8. sanitizeComponent,
  9. resolveRouteComponents,
  10. getMatchedComponents,
  11. getMatchedComponentsInstances,
  12. flatMapComponents,
  13. setContext,
  14. getLocation,
  15. compile,
  16. getQueryDiff,
  17. globalHandleError,
  18. isSamePath
  19. } from './utils.js'
  20. import { createApp, NuxtError } from './index.js'
  21. import fetchMixin from './mixins/fetch.client'
  22. import NuxtLink from './components/nuxt-link.client.js' // should be included after ./index.js
  23. // Fetch mixin
  24. if (!Vue.__nuxt__fetch__mixin__) {
  25. Vue.mixin(fetchMixin)
  26. Vue.__nuxt__fetch__mixin__ = true
  27. }
  28. // Component: <NuxtLink>
  29. Vue.component(NuxtLink.name, NuxtLink)
  30. Vue.component('NLink', NuxtLink)
  31. if (!global.fetch) { global.fetch = fetch }
  32. // Global shared references
  33. let _lastPaths = []
  34. let app
  35. let router
  36. let store
  37. // Try to rehydrate SSR data from window
  38. const NUXT = window.__NUXT__ || {}
  39. Object.assign(Vue.config, {"productionTip":true,"devtools":false,"silent":true,"performance":false})
  40. const errorHandler = Vue.config.errorHandler || console.error
  41. // Create and mount App
  42. createApp(null, NUXT.config).then(mountApp).catch(errorHandler)
  43. function componentOption (component, key, ...args) {
  44. if (!component || !component.options || !component.options[key]) {
  45. return {}
  46. }
  47. const option = component.options[key]
  48. if (typeof option === 'function') {
  49. return option(...args)
  50. }
  51. return option
  52. }
  53. function mapTransitions (toComponents, to, from) {
  54. const componentTransitions = (component) => {
  55. const transition = componentOption(component, 'transition', to, from) || {}
  56. return (typeof transition === 'string' ? { name: transition } : transition)
  57. }
  58. const fromComponents = from ? getMatchedComponents(from) : []
  59. const maxDepth = Math.max(toComponents.length, fromComponents.length)
  60. const mergedTransitions = []
  61. for (let i=0; i<maxDepth; i++) {
  62. // Clone original objects to prevent overrides
  63. const toTransitions = Object.assign({}, componentTransitions(toComponents[i]))
  64. const transitions = Object.assign({}, componentTransitions(fromComponents[i]))
  65. // Combine transitions & prefer `leave` properties of "from" route
  66. Object.keys(toTransitions)
  67. .filter(key => typeof toTransitions[key] !== 'undefined' && !key.toLowerCase().includes('leave'))
  68. .forEach((key) => { transitions[key] = toTransitions[key] })
  69. mergedTransitions.push(transitions)
  70. }
  71. return mergedTransitions
  72. }
  73. async function loadAsyncComponents (to, from, next) {
  74. // Check if route changed (this._routeChanged), only if the page is not an error (for validate())
  75. this._routeChanged = Boolean(app.nuxt.err) || from.name !== to.name
  76. this._paramChanged = !this._routeChanged && from.path !== to.path
  77. this._queryChanged = !this._paramChanged && from.fullPath !== to.fullPath
  78. this._diffQuery = (this._queryChanged ? getQueryDiff(to.query, from.query) : [])
  79. if ((this._routeChanged || this._paramChanged) && this.$loading.start && !this.$loading.manual) {
  80. this.$loading.start()
  81. }
  82. try {
  83. if (this._queryChanged) {
  84. const Components = await resolveRouteComponents(
  85. to,
  86. (Component, instance) => ({ Component, instance })
  87. )
  88. // Add a marker on each component that it needs to refresh or not
  89. const startLoader = Components.some(({ Component, instance }) => {
  90. const watchQuery = Component.options.watchQuery
  91. if (watchQuery === true) {
  92. return true
  93. }
  94. if (Array.isArray(watchQuery)) {
  95. return watchQuery.some(key => this._diffQuery[key])
  96. }
  97. if (typeof watchQuery === 'function') {
  98. return watchQuery.apply(instance, [to.query, from.query])
  99. }
  100. return false
  101. })
  102. if (startLoader && this.$loading.start && !this.$loading.manual) {
  103. this.$loading.start()
  104. }
  105. }
  106. // Call next()
  107. next()
  108. } catch (error) {
  109. const err = error || {}
  110. const statusCode = err.statusCode || err.status || (err.response && err.response.status) || 500
  111. const message = err.message || ''
  112. // Handle chunk loading errors
  113. // This may be due to a new deployment or a network problem
  114. if (/^Loading( CSS)? chunk (\d)+ failed\./.test(message)) {
  115. window.location.reload(true /* skip cache */)
  116. return // prevent error page blinking for user
  117. }
  118. this.error({ statusCode, message })
  119. this.$nuxt.$emit('routeChanged', to, from, err)
  120. next()
  121. }
  122. }
  123. function applySSRData (Component, ssrData) {
  124. if (NUXT.serverRendered && ssrData) {
  125. applyAsyncData(Component, ssrData)
  126. }
  127. Component._Ctor = Component
  128. return Component
  129. }
  130. // Get matched components
  131. function resolveComponents (router) {
  132. const path = getLocation(router.options.base, router.options.mode)
  133. return flatMapComponents(router.match(path), async (Component, _, match, key, index) => {
  134. // If component is not resolved yet, resolve it
  135. if (typeof Component === 'function' && !Component.options) {
  136. Component = await Component()
  137. }
  138. // Sanitize it and save it
  139. const _Component = applySSRData(sanitizeComponent(Component), NUXT.data ? NUXT.data[index] : null)
  140. match.components[key] = _Component
  141. return _Component
  142. })
  143. }
  144. function callMiddleware (Components, context, layout) {
  145. let midd = ["initialize"]
  146. let unknownMiddleware = false
  147. // If layout is undefined, only call global middleware
  148. if (typeof layout !== 'undefined') {
  149. midd = [] // Exclude global middleware if layout defined (already called before)
  150. layout = sanitizeComponent(layout)
  151. if (layout.options.middleware) {
  152. midd = midd.concat(layout.options.middleware)
  153. }
  154. Components.forEach((Component) => {
  155. if (Component.options.middleware) {
  156. midd = midd.concat(Component.options.middleware)
  157. }
  158. })
  159. }
  160. midd = midd.map((name) => {
  161. if (typeof name === 'function') {
  162. return name
  163. }
  164. if (typeof middleware[name] !== 'function') {
  165. unknownMiddleware = true
  166. this.error({ statusCode: 500, message: 'Unknown middleware ' + name })
  167. }
  168. return middleware[name]
  169. })
  170. if (unknownMiddleware) {
  171. return
  172. }
  173. return middlewareSeries(midd, context)
  174. }
  175. async function render (to, from, next) {
  176. if (this._routeChanged === false && this._paramChanged === false && this._queryChanged === false) {
  177. return next()
  178. }
  179. // Handle first render on SPA mode
  180. let spaFallback = false
  181. if (to === from) {
  182. _lastPaths = []
  183. spaFallback = true
  184. } else {
  185. const fromMatches = []
  186. _lastPaths = getMatchedComponents(from, fromMatches).map((Component, i) => {
  187. return compile(from.matched[fromMatches[i]].path)(from.params)
  188. })
  189. }
  190. // nextCalled is true when redirected
  191. let nextCalled = false
  192. const _next = (path) => {
  193. if (from.path === path.path && this.$loading.finish) {
  194. this.$loading.finish()
  195. }
  196. if (from.path !== path.path && this.$loading.pause) {
  197. this.$loading.pause()
  198. }
  199. if (nextCalled) {
  200. return
  201. }
  202. nextCalled = true
  203. next(path)
  204. }
  205. // Update context
  206. await setContext(app, {
  207. route: to,
  208. from,
  209. next: _next.bind(this)
  210. })
  211. this._dateLastError = app.nuxt.dateErr
  212. this._hadError = Boolean(app.nuxt.err)
  213. // Get route's matched components
  214. const matches = []
  215. const Components = getMatchedComponents(to, matches)
  216. // If no Components matched, generate 404
  217. if (!Components.length) {
  218. // Default layout
  219. await callMiddleware.call(this, Components, app.context)
  220. if (nextCalled) {
  221. return
  222. }
  223. // Load layout for error page
  224. const errorLayout = (NuxtError.options || NuxtError).layout
  225. const layout = await this.loadLayout(
  226. typeof errorLayout === 'function'
  227. ? errorLayout.call(NuxtError, app.context)
  228. : errorLayout
  229. )
  230. await callMiddleware.call(this, Components, app.context, layout)
  231. if (nextCalled) {
  232. return
  233. }
  234. // Show error page
  235. app.context.error({ statusCode: 404, message: 'This page could not be found' })
  236. return next()
  237. }
  238. // Update ._data and other properties if hot reloaded
  239. Components.forEach((Component) => {
  240. if (Component._Ctor && Component._Ctor.options) {
  241. Component.options.asyncData = Component._Ctor.options.asyncData
  242. Component.options.fetch = Component._Ctor.options.fetch
  243. }
  244. })
  245. // Apply transitions
  246. this.setTransitions(mapTransitions(Components, to, from))
  247. try {
  248. // Call middleware
  249. await callMiddleware.call(this, Components, app.context)
  250. if (nextCalled) {
  251. return
  252. }
  253. if (app.context._errored) {
  254. return next()
  255. }
  256. // Set layout
  257. let layout = Components[0].options.layout
  258. if (typeof layout === 'function') {
  259. layout = layout(app.context)
  260. }
  261. layout = await this.loadLayout(layout)
  262. // Call middleware for layout
  263. await callMiddleware.call(this, Components, app.context, layout)
  264. if (nextCalled) {
  265. return
  266. }
  267. if (app.context._errored) {
  268. return next()
  269. }
  270. // Call .validate()
  271. let isValid = true
  272. try {
  273. for (const Component of Components) {
  274. if (typeof Component.options.validate !== 'function') {
  275. continue
  276. }
  277. isValid = await Component.options.validate(app.context)
  278. if (!isValid) {
  279. break
  280. }
  281. }
  282. } catch (validationError) {
  283. // ...If .validate() threw an error
  284. this.error({
  285. statusCode: validationError.statusCode || '500',
  286. message: validationError.message
  287. })
  288. return next()
  289. }
  290. // ...If .validate() returned false
  291. if (!isValid) {
  292. this.error({ statusCode: 404, message: 'This page could not be found' })
  293. return next()
  294. }
  295. let instances
  296. // Call asyncData & fetch hooks on components matched by the route.
  297. await Promise.all(Components.map(async (Component, i) => {
  298. // Check if only children route changed
  299. Component._path = compile(to.matched[matches[i]].path)(to.params)
  300. Component._dataRefresh = false
  301. const childPathChanged = Component._path !== _lastPaths[i]
  302. // Refresh component (call asyncData & fetch) when:
  303. // Route path changed part includes current component
  304. // Or route param changed part includes current component and watchParam is not `false`
  305. // Or route query is changed and watchQuery returns `true`
  306. if (this._routeChanged && childPathChanged) {
  307. Component._dataRefresh = true
  308. } else if (this._paramChanged && childPathChanged) {
  309. const watchParam = Component.options.watchParam
  310. Component._dataRefresh = watchParam !== false
  311. } else if (this._queryChanged) {
  312. const watchQuery = Component.options.watchQuery
  313. if (watchQuery === true) {
  314. Component._dataRefresh = true
  315. } else if (Array.isArray(watchQuery)) {
  316. Component._dataRefresh = watchQuery.some(key => this._diffQuery[key])
  317. } else if (typeof watchQuery === 'function') {
  318. if (!instances) {
  319. instances = getMatchedComponentsInstances(to)
  320. }
  321. Component._dataRefresh = watchQuery.apply(instances[i], [to.query, from.query])
  322. }
  323. }
  324. if (!this._hadError && this._isMounted && !Component._dataRefresh) {
  325. return
  326. }
  327. const promises = []
  328. const hasAsyncData = (
  329. Component.options.asyncData &&
  330. typeof Component.options.asyncData === 'function'
  331. )
  332. const hasFetch = Boolean(Component.options.fetch) && Component.options.fetch.length
  333. const loadingIncrease = (hasAsyncData && hasFetch) ? 30 : 45
  334. // Call asyncData(context)
  335. if (hasAsyncData) {
  336. const promise = promisify(Component.options.asyncData, app.context)
  337. promise.then((asyncDataResult) => {
  338. applyAsyncData(Component, asyncDataResult)
  339. if (this.$loading.increase) {
  340. this.$loading.increase(loadingIncrease)
  341. }
  342. })
  343. promises.push(promise)
  344. }
  345. // Check disabled page loading
  346. this.$loading.manual = Component.options.loading === false
  347. // Call fetch(context)
  348. if (hasFetch) {
  349. let p = Component.options.fetch(app.context)
  350. if (!p || (!(p instanceof Promise) && (typeof p.then !== 'function'))) {
  351. p = Promise.resolve(p)
  352. }
  353. p.then((fetchResult) => {
  354. if (this.$loading.increase) {
  355. this.$loading.increase(loadingIncrease)
  356. }
  357. })
  358. promises.push(p)
  359. }
  360. return Promise.all(promises)
  361. }))
  362. // If not redirected
  363. if (!nextCalled) {
  364. if (this.$loading.finish && !this.$loading.manual) {
  365. this.$loading.finish()
  366. }
  367. next()
  368. }
  369. } catch (err) {
  370. const error = err || {}
  371. if (error.message === 'ERR_REDIRECT') {
  372. return this.$nuxt.$emit('routeChanged', to, from, error)
  373. }
  374. _lastPaths = []
  375. globalHandleError(error)
  376. // Load error layout
  377. let layout = (NuxtError.options || NuxtError).layout
  378. if (typeof layout === 'function') {
  379. layout = layout(app.context)
  380. }
  381. await this.loadLayout(layout)
  382. this.error(error)
  383. this.$nuxt.$emit('routeChanged', to, from, error)
  384. next()
  385. }
  386. }
  387. // Fix components format in matched, it's due to code-splitting of vue-router
  388. function normalizeComponents (to, ___) {
  389. flatMapComponents(to, (Component, _, match, key) => {
  390. if (typeof Component === 'object' && !Component.options) {
  391. // Updated via vue-router resolveAsyncComponents()
  392. Component = Vue.extend(Component)
  393. Component._Ctor = Component
  394. match.components[key] = Component
  395. }
  396. return Component
  397. })
  398. }
  399. function setLayoutForNextPage (to) {
  400. // Set layout
  401. let hasError = Boolean(this.$options.nuxt.err)
  402. if (this._hadError && this._dateLastError === this.$options.nuxt.dateErr) {
  403. hasError = false
  404. }
  405. let layout = hasError
  406. ? (NuxtError.options || NuxtError).layout
  407. : to.matched[0].components.default.options.layout
  408. if (typeof layout === 'function') {
  409. layout = layout(app.context)
  410. }
  411. this.setLayout(layout)
  412. }
  413. function checkForErrors (app) {
  414. // Hide error component if no error
  415. if (app._hadError && app._dateLastError === app.$options.nuxt.dateErr) {
  416. app.error()
  417. }
  418. }
  419. // When navigating on a different route but the same component is used, Vue.js
  420. // Will not update the instance data, so we have to update $data ourselves
  421. function fixPrepatch (to, ___) {
  422. if (this._routeChanged === false && this._paramChanged === false && this._queryChanged === false) {
  423. return
  424. }
  425. const instances = getMatchedComponentsInstances(to)
  426. const Components = getMatchedComponents(to)
  427. Vue.nextTick(() => {
  428. instances.forEach((instance, i) => {
  429. if (!instance || instance._isDestroyed) {
  430. return
  431. }
  432. if (
  433. instance.constructor._dataRefresh &&
  434. Components[i] === instance.constructor &&
  435. instance.$vnode.data.keepAlive !== true &&
  436. typeof instance.constructor.options.data === 'function'
  437. ) {
  438. const newData = instance.constructor.options.data.call(instance)
  439. for (const key in newData) {
  440. Vue.set(instance.$data, key, newData[key])
  441. }
  442. // Ensure to trigger scroll event after calling scrollBehavior
  443. window.$nuxt.$nextTick(() => {
  444. window.$nuxt.$emit('triggerScroll')
  445. })
  446. }
  447. })
  448. checkForErrors(this)
  449. })
  450. }
  451. function nuxtReady (_app) {
  452. window.onNuxtReadyCbs.forEach((cb) => {
  453. if (typeof cb === 'function') {
  454. cb(_app)
  455. }
  456. })
  457. // Special JSDOM
  458. if (typeof window._onNuxtLoaded === 'function') {
  459. window._onNuxtLoaded(_app)
  460. }
  461. // Add router hooks
  462. router.afterEach((to, from) => {
  463. // Wait for fixPrepatch + $data updates
  464. Vue.nextTick(() => _app.$nuxt.$emit('routeChanged', to, from))
  465. })
  466. }
  467. async function mountApp (__app) {
  468. // Set global variables
  469. app = __app.app
  470. router = __app.router
  471. store = __app.store
  472. // Create Vue instance
  473. const _app = new Vue(app)
  474. // Load layout
  475. const layout = NUXT.layout || 'default'
  476. await _app.loadLayout(layout)
  477. _app.setLayout(layout)
  478. // Mounts Vue app to DOM element
  479. const mount = () => {
  480. _app.$mount('#__nuxt')
  481. // Add afterEach router hooks
  482. router.afterEach(normalizeComponents)
  483. router.afterEach(setLayoutForNextPage.bind(_app))
  484. router.afterEach(fixPrepatch.bind(_app))
  485. // Listen for first Vue update
  486. Vue.nextTick(() => {
  487. // Call window.{{globals.readyCallback}} callbacks
  488. nuxtReady(_app)
  489. })
  490. }
  491. // Resolve route components
  492. const Components = await Promise.all(resolveComponents(router))
  493. // Enable transitions
  494. _app.setTransitions = _app.$options.nuxt.setTransitions.bind(_app)
  495. if (Components.length) {
  496. _app.setTransitions(mapTransitions(Components, router.currentRoute))
  497. _lastPaths = router.currentRoute.matched.map(route => compile(route.path)(router.currentRoute.params))
  498. }
  499. // Initialize error handler
  500. _app.$loading = {} // To avoid error while _app.$nuxt does not exist
  501. if (NUXT.error) {
  502. _app.error(NUXT.error)
  503. }
  504. // Add beforeEach router hooks
  505. router.beforeEach(loadAsyncComponents.bind(_app))
  506. router.beforeEach(render.bind(_app))
  507. // Fix in static: remove trailing slash to force hydration
  508. // Full static, if server-rendered: hydrate, to allow custom redirect to generated page
  509. // Fix in static: remove trailing slash to force hydration
  510. if (NUXT.serverRendered && isSamePath(NUXT.routePath, _app.context.route.path)) {
  511. return mount()
  512. }
  513. // First render on client-side
  514. const clientFirstMount = () => {
  515. normalizeComponents(router.currentRoute, router.currentRoute)
  516. setLayoutForNextPage.call(_app, router.currentRoute)
  517. checkForErrors(_app)
  518. // Don't call fixPrepatch.call(_app, router.currentRoute, router.currentRoute) since it's first render
  519. mount()
  520. }
  521. // fix: force next tick to avoid having same timestamp when an error happen on spa fallback
  522. await new Promise(resolve => setTimeout(resolve, 0))
  523. render.call(_app, router.currentRoute, router.currentRoute, (path) => {
  524. // If not redirected
  525. if (!path) {
  526. clientFirstMount()
  527. return
  528. }
  529. // Add a one-time afterEach hook to
  530. // mount the app wait for redirect and route gets resolved
  531. const unregisterHook = router.afterEach((to, from) => {
  532. unregisterHook()
  533. clientFirstMount()
  534. })
  535. // Push the path and let route to be resolved
  536. router.push(path, undefined, (err) => {
  537. if (err) {
  538. errorHandler(err)
  539. }
  540. })
  541. })
  542. }