client.js 19 KB

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