client.js 18 KB

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