UNPKG

21 kBJavaScriptView Raw
1import Vue from 'vue'
2import middleware from './middleware'
3import {
4 applyAsyncData,
5 sanitizeComponent,
6 resolveRouteComponents,
7 getMatchedComponents,
8 getMatchedComponentsInstances,
9 flatMapComponents,
10 setContext,
11 middlewareSeries,
12 promisify,
13 getLocation,
14 compile,
15 getQueryDiff,
16 globalHandleError
17} from './utils'
18import { createApp, NuxtError } from './index'
19
20const noopData = () => { return {} }
21const noopFetch = () => {}
22
23// Global shared references
24let _lastPaths = []
25let app
26let router
27<% if (store) { %>let store<% } %>
28
29// Try to rehydrate SSR data from window
30const NUXT = window.<%= globals.context %> || {}
31
32Object.assign(Vue.config, <%= serialize(vue.config) %>)<%= isTest ? '// eslint-disable-line' : '' %>
33
34<% if (debug || mode === 'spa') { %>
35// Setup global Vue error handler
36if (!Vue.config.$nuxt) {
37 const defaultErrorHandler = Vue.config.errorHandler
38 Vue.config.errorHandler = (err, vm, info, ...rest) => {
39 const nuxtError = {
40 statusCode: err.statusCode || err.name || 'Whoops!',
41 message: err.message || err.toString()
42 }
43
44 // Call other handler if exist
45 let handled = null
46 if (typeof defaultErrorHandler === 'function') {
47 handled = defaultErrorHandler(err, vm, info, ...rest)
48 }
49 if (handled === true) {
50 return handled
51 }
52
53 if (vm && vm.$root) {
54 const nuxtApp = Object.keys(Vue.config.$nuxt)
55 .find(nuxtInstance => vm.$root[nuxtInstance])
56
57 // Show Nuxt Error Page
58 if (nuxtApp && vm.$root[nuxtApp].error && info !== 'render function') {
59 vm.$root[nuxtApp].error(nuxtError)
60 }
61 }
62
63 if (typeof defaultErrorHandler === 'function') {
64 return handled
65 }
66
67 // Log to console
68 if (process.env.NODE_ENV !== 'production') {
69 console.error(err)
70 } else {
71 console.error(err.message || nuxtError.message)
72 }
73 }
74 Vue.config.$nuxt = {}
75}
76Vue.config.$nuxt.<%= globals.nuxt %> = true
77
78<% } %>
79
80// Create and mount App
81createApp()
82 .then(mountApp)
83 .catch((err) => {
84 console.error('[nuxt] Error while initializing app', err)
85 })
86
87function componentOption(component, key, ...args) {
88 if (!component || !component.options || !component.options[key]) {
89 return {}
90 }
91 const option = component.options[key]
92 if (typeof option === 'function') {
93 return option(...args)
94 }
95 return option
96}
97
98function mapTransitions(Components, to, from) {
99 const componentTransitions = (component) => {
100 const transition = componentOption(component, 'transition', to, from) || {}
101 return (typeof transition === 'string' ? { name: transition } : transition)
102 }
103
104 return Components.map((Component) => {
105 // Clone original object to prevent overrides
106 const transitions = Object.assign({}, componentTransitions(Component))
107
108 // Combine transitions & prefer `leave` transitions of 'from' route
109 if (from && from.matched.length && from.matched[0].components.default) {
110 const fromTransitions = componentTransitions(from.matched[0].components.default)
111 Object.keys(fromTransitions)
112 .filter(key => fromTransitions[key] && key.toLowerCase().includes('leave'))
113 .forEach((key) => { transitions[key] = fromTransitions[key] })
114 }
115
116 return transitions
117 })
118}
119
120async function loadAsyncComponents(to, from, next) {
121 // Check if route path changed (this._pathChanged), only if the page is not an error (for validate())
122 this._pathChanged = !!app.nuxt.err || from.path !== to.path
123 this._queryChanged = JSON.stringify(to.query) !== JSON.stringify(from.query)
124 this._diffQuery = (this._queryChanged ? getQueryDiff(to.query, from.query) : [])
125
126 <% if (loading) { %>
127 if (this._pathChanged && this.$loading.start && !this.$loading.manual) {
128 this.$loading.start()
129 }
130 <% } %>
131
132 try {
133 const Components = await resolveRouteComponents(to)
134 <% if (loading) { %>
135 if (!this._pathChanged && this._queryChanged) {
136 // Add a marker on each component that it needs to refresh or not
137 const startLoader = Components.some((Component) => {
138 const watchQuery = Component.options.watchQuery
139 if (watchQuery === true) return true
140 if (Array.isArray(watchQuery)) {
141 return watchQuery.some(key => this._diffQuery[key])
142 }
143 return false
144 })
145 if (startLoader && this.$loading.start && !this.$loading.manual) {
146 this.$loading.start()
147 }
148 }
149 <% } %>
150 // Call next()
151 next()
152 } catch (err) {
153 const error = err || {}
154 const statusCode = (error.statusCode || error.status || (error.response && error.response.status) || 500)
155 this.error({ statusCode, message: error.message })
156 this.<%= globals.nuxt %>.$emit('routeChanged', to, from, error)
157 next(false)
158 }
159}
160
161function applySSRData(Component, ssrData) {
162 if (NUXT.serverRendered && ssrData) {
163 applyAsyncData(Component, ssrData)
164 }
165 Component._Ctor = Component
166 return Component
167}
168
169// Get matched components
170function resolveComponents(router) {
171 const path = getLocation(router.options.base, router.options.mode)
172
173 return flatMapComponents(router.match(path), async (Component, _, match, key, index) => {
174 // If component is not resolved yet, resolve it
175 if (typeof Component === 'function' && !Component.options) {
176 Component = await Component()
177 }
178 // Sanitize it and save it
179 const _Component = applySSRData(sanitizeComponent(Component), NUXT.data ? NUXT.data[index] : null)
180 match.components[key] = _Component
181 return _Component
182 })
183}
184
185function callMiddleware(Components, context, layout) {
186 let midd = <%= devalue(router.middleware) %><%= isTest ? '// eslint-disable-line' : '' %>
187 let unknownMiddleware = false
188
189 // If layout is undefined, only call global middleware
190 if (typeof layout !== 'undefined') {
191 midd = [] // Exclude global middleware if layout defined (already called before)
192 if (layout.middleware) {
193 midd = midd.concat(layout.middleware)
194 }
195 Components.forEach((Component) => {
196 if (Component.options.middleware) {
197 midd = midd.concat(Component.options.middleware)
198 }
199 })
200 }
201
202 midd = midd.map((name) => {
203 if (typeof name === 'function') return name
204 if (typeof middleware[name] !== 'function') {
205 unknownMiddleware = true
206 this.error({ statusCode: 500, message: 'Unknown middleware ' + name })
207 }
208 return middleware[name]
209 })
210
211 if (unknownMiddleware) return
212 return middlewareSeries(midd, context)
213}
214
215async function render(to, from, next) {
216 if (this._pathChanged === false && this._queryChanged === false) return next()
217 // Handle first render on SPA mode
218 if (to === from) _lastPaths = []
219 else {
220 const fromMatches = []
221 _lastPaths = getMatchedComponents(from, fromMatches).map((Component, i) => {
222 return compile(from.matched[fromMatches[i]].path)(from.params)
223 })
224 }
225
226 // nextCalled is true when redirected
227 let nextCalled = false
228 const _next = (path) => {
229 <% if (loading) { %>
230 if (from.path === path.path && this.$loading.finish) {
231 this.$loading.finish()
232 }
233 <% } %>
234 <% if (loading) { %>
235 if (from.path !== path.path && this.$loading.pause) {
236 this.$loading.pause()
237 }
238 <% } %>
239 if (nextCalled) return
240 nextCalled = true
241 next(path)
242 }
243
244 // Update context
245 await setContext(app, {
246 route: to,
247 from,
248 next: _next.bind(this)
249 })
250 this._dateLastError = app.nuxt.dateErr
251 this._hadError = !!app.nuxt.err
252
253 // Get route's matched components
254 const matches = []
255 const Components = getMatchedComponents(to, matches)
256
257 // If no Components matched, generate 404
258 if (!Components.length) {
259 // Default layout
260 await callMiddleware.call(this, Components, app.context)
261 if (nextCalled) return
262 // Load layout for error page
263 const layout = await this.loadLayout(
264 typeof NuxtError.layout === 'function'
265 ? NuxtError.layout(app.context)
266 : NuxtError.layout
267 )
268 await callMiddleware.call(this, Components, app.context, layout)
269 if (nextCalled) return
270 // Show error page
271 app.context.error({ statusCode: 404, message: `<%= messages.error_404 %>` })
272 return next()
273 }
274
275 // Update ._data and other properties if hot reloaded
276 Components.forEach((Component) => {
277 if (Component._Ctor && Component._Ctor.options) {
278 Component.options.asyncData = Component._Ctor.options.asyncData
279 Component.options.fetch = Component._Ctor.options.fetch
280 }
281 })
282
283 // Apply transitions
284 this.setTransitions(mapTransitions(Components, to, from))
285
286 try {
287 // Call middleware
288 await callMiddleware.call(this, Components, app.context)
289 if (nextCalled) return
290 if (app.context._errored) return next()
291
292 // Set layout
293 let layout = Components[0].options.layout
294 if (typeof layout === 'function') {
295 layout = layout(app.context)
296 }
297 layout = await this.loadLayout(layout)
298
299 // Call middleware for layout
300 await callMiddleware.call(this, Components, app.context, layout)
301 if (nextCalled) return
302 if (app.context._errored) return next()
303
304 // Call .validate()
305 let isValid = true
306 try {
307 for (const Component of Components) {
308 if (typeof Component.options.validate !== 'function') {
309 continue
310 }
311
312 isValid = await Component.options.validate(app.context)
313
314 if (!isValid) {
315 break
316 }
317 }
318 } catch (validationError) {
319 // ...If .validate() threw an error
320 this.error({
321 statusCode: validationError.statusCode || '500',
322 message: validationError.message
323 })
324 return next()
325 }
326
327 // ...If .validate() returned false
328 if (!isValid) {
329 this.error({ statusCode: 404, message: `<%= messages.error_404 %>` })
330 return next()
331 }
332
333 // Call asyncData & fetch hooks on components matched by the route.
334 await Promise.all(Components.map((Component, i) => {
335 // Check if only children route changed
336 Component._path = compile(to.matched[matches[i]].path)(to.params)
337 Component._dataRefresh = false
338 // Check if Component need to be refreshed (call asyncData & fetch)
339 // Only if its slug has changed or is watch query changes
340 if ((this._pathChanged && this._queryChanged) || Component._path !== _lastPaths[i]) {
341 Component._dataRefresh = true
342 } else if (!this._pathChanged && this._queryChanged) {
343 const watchQuery = Component.options.watchQuery
344 if (watchQuery === true) {
345 Component._dataRefresh = true
346 } else if (Array.isArray(watchQuery)) {
347 Component._dataRefresh = watchQuery.some(key => this._diffQuery[key])
348 }
349 }
350 if (!this._hadError && this._isMounted && !Component._dataRefresh) {
351 return Promise.resolve()
352 }
353
354 const promises = []
355
356 const hasAsyncData = (
357 Component.options.asyncData &&
358 typeof Component.options.asyncData === 'function'
359 )
360 const hasFetch = !!Component.options.fetch
361 <% if (loading) { %>
362 const loadingIncrease = (hasAsyncData && hasFetch) ? 30 : 45
363 <% } %>
364
365 // Call asyncData(context)
366 if (hasAsyncData) {
367 const promise = promisify(Component.options.asyncData, app.context)
368 .then((asyncDataResult) => {
369 applyAsyncData(Component, asyncDataResult)
370 <% if (loading) { %>
371 if (this.$loading.increase) {
372 this.$loading.increase(loadingIncrease)
373 }
374 <% } %>
375 })
376 promises.push(promise)
377 }
378
379 // Check disabled page loading
380 this.$loading.manual = Component.options.loading === false
381
382 // Call fetch(context)
383 if (hasFetch) {
384 let p = Component.options.fetch(app.context)
385 if (!p || (!(p instanceof Promise) && (typeof p.then !== 'function'))) {
386 p = Promise.resolve(p)
387 }
388 p.then((fetchResult) => {
389 <% if (loading) { %>
390 if (this.$loading.increase) {
391 this.$loading.increase(loadingIncrease)
392 }
393 <% } %>
394 })
395 promises.push(p)
396 }
397
398 return Promise.all(promises)
399 }))
400
401 // If not redirected
402 if (!nextCalled) {
403 <% if (loading) { %>
404 if (this.$loading.finish && !this.$loading.manual) {
405 this.$loading.finish()
406 }
407 <% } %>
408 next()
409 }
410
411 } catch (err) {
412 const error = err || {}
413 if (error.message === 'ERR_REDIRECT') {
414 return this.<%= globals.nuxt %>.$emit('routeChanged', to, from, error)
415 }
416 _lastPaths = []
417 const errorResponseStatus = (error.response && error.response.status)
418 error.statusCode = error.statusCode || error.status || errorResponseStatus || 500
419
420 globalHandleError(error)
421
422 // Load error layout
423 let layout = NuxtError.layout
424 if (typeof layout === 'function') {
425 layout = layout(app.context)
426 }
427 await this.loadLayout(layout)
428
429 this.error(error)
430 this.<%= globals.nuxt %>.$emit('routeChanged', to, from, error)
431 next(false)
432 }
433}
434
435// Fix components format in matched, it's due to code-splitting of vue-router
436function normalizeComponents(to, ___) {
437 flatMapComponents(to, (Component, _, match, key) => {
438 if (typeof Component === 'object' && !Component.options) {
439 // Updated via vue-router resolveAsyncComponents()
440 Component = Vue.extend(Component)
441 Component._Ctor = Component
442 match.components[key] = Component
443 }
444 return Component
445 })
446}
447
448function showNextPage(to) {
449 // Hide error component if no error
450 if (this._hadError && this._dateLastError === this.$options.nuxt.dateErr) {
451 this.error()
452 }
453
454 // Set layout
455 let layout = this.$options.nuxt.err
456 ? NuxtError.layout
457 : to.matched[0].components.default.options.layout
458
459 if (typeof layout === 'function') {
460 layout = layout(app.context)
461 }
462 this.setLayout(layout)
463}
464
465// When navigating on a different route but the same component is used, Vue.js
466// Will not update the instance data, so we have to update $data ourselves
467function fixPrepatch(to, ___) {
468 if (this._pathChanged === false && this._queryChanged === false) return
469
470 Vue.nextTick(() => {
471 const matches = []
472 const instances = getMatchedComponentsInstances(to, matches)
473 const Components = getMatchedComponents(to, matches)
474
475 instances.forEach((instance, i) => {
476 if (!instance) return
477 // if (
478 // !this._queryChanged &&
479 // to.matched[matches[i]].path.indexOf(':') === -1 &&
480 // to.matched[matches[i]].path.indexOf('*') === -1
481 // ) return // If not a dynamic route, skip
482 if (
483 instance.constructor._dataRefresh &&
484 Components[i] === instance.constructor &&
485 typeof instance.constructor.options.data === 'function'
486 ) {
487 const newData = instance.constructor.options.data.call(instance)
488 for (const key in newData) {
489 Vue.set(instance.$data, key, newData[key])
490 }
491 }
492 })
493 showNextPage.call(this, to)
494 <% if (isDev) { %>
495 // Hot reloading
496 setTimeout(() => hotReloadAPI(this), 100)
497 <% } %>
498 })
499}
500
501function nuxtReady(_app) {
502 window.<%= globals.readyCallback %>Cbs.forEach((cb) => {
503 if (typeof cb === 'function') {
504 cb(_app)
505 }
506 })
507 // Special JSDOM
508 if (typeof window.<%= globals.loadedCallback %> === 'function') {
509 window.<%= globals.loadedCallback %>(_app)
510 }
511 // Add router hooks
512 router.afterEach((to, from) => {
513 // Wait for fixPrepatch + $data updates
514 Vue.nextTick(() => _app.<%= globals.nuxt %>.$emit('routeChanged', to, from))
515 })
516}
517
518<% if (isDev) { %>
519// Special hot reload with asyncData(context)
520function getNuxtChildComponents($parent, $components = []) {
521 $parent.$children.forEach(($child) => {
522 if ($child.$vnode.data.nuxtChild && !$components.find(c =>(c.$options.__file === $child.$options.__file))) {
523 $components.push($child)
524 }
525 if ($child.$children && $child.$children.length) {
526 getNuxtChildComponents($child, $components)
527 }
528 })
529
530 return $components
531}
532
533function hotReloadAPI(_app) {
534 if (!module.hot) return
535
536 let $components = getNuxtChildComponents(_app.<%= globals.nuxt %>, [])
537
538 $components.forEach(addHotReload.bind(_app))
539}
540
541function addHotReload($component, depth) {
542 if ($component.$vnode.data._hasHotReload) return
543 $component.$vnode.data._hasHotReload = true
544
545 var _forceUpdate = $component.$forceUpdate.bind($component.$parent)
546
547 $component.$vnode.context.$forceUpdate = async () => {
548 let Components = getMatchedComponents(router.currentRoute)
549 let Component = Components[depth]
550 if (!Component) return _forceUpdate()
551 if (typeof Component === 'object' && !Component.options) {
552 // Updated via vue-router resolveAsyncComponents()
553 Component = Vue.extend(Component)
554 Component._Ctor = Component
555 }
556 this.error()
557 let promises = []
558 const next = function (path) {
559 <%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
560 router.push(path)
561 }
562 await setContext(app, {
563 route: router.currentRoute,
564 isHMR: true,
565 next: next.bind(this)
566 })
567 const context = app.context
568 <% if (loading) { %>
569 if (this.$loading.start && !this.$loading.manual) this.$loading.start()
570 <% } %>
571 callMiddleware.call(this, Components, context)
572 .then(() => {
573 // If layout changed
574 if (depth !== 0) return Promise.resolve()
575 let layout = Component.options.layout || 'default'
576 if (typeof layout === 'function') {
577 layout = layout(context)
578 }
579 if (this.layoutName === layout) return Promise.resolve()
580 let promise = this.loadLayout(layout)
581 promise.then(() => {
582 this.setLayout(layout)
583 Vue.nextTick(() => hotReloadAPI(this))
584 })
585 return promise
586 })
587 .then(() => {
588 return callMiddleware.call(this, Components, context, this.layout)
589 })
590 .then(() => {
591 // Call asyncData(context)
592 let pAsyncData = promisify(Component.options.asyncData || noopData, context)
593 pAsyncData.then((asyncDataResult) => {
594 applyAsyncData(Component, asyncDataResult)
595 <%= (loading ? 'this.$loading.increase && this.$loading.increase(30)' : '') %>
596 })
597 promises.push(pAsyncData)
598 // Call fetch()
599 Component.options.fetch = Component.options.fetch || noopFetch
600 let pFetch = Component.options.fetch(context)
601 if (!pFetch || (!(pFetch instanceof Promise) && (typeof pFetch.then !== 'function'))) { pFetch = Promise.resolve(pFetch) }
602 <%= (loading ? 'pFetch.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %>
603 promises.push(pFetch)
604 return Promise.all(promises)
605 })
606 .then(() => {
607 <%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
608 _forceUpdate()
609 setTimeout(() => hotReloadAPI(this), 100)
610 })
611 }
612}
613<% } %>
614
615async function mountApp(__app) {
616 // Set global variables
617 app = __app.app
618 router = __app.router
619 <% if (store) { %>store = __app.store<% } %>
620
621 // Resolve route components
622 const Components = await Promise.all(resolveComponents(router))
623
624 // Create Vue instance
625 const _app = new Vue(app)
626
627 <% if (mode !== 'spa') { %>
628 // Load layout
629 const layout = NUXT.layout || 'default'
630 await _app.loadLayout(layout)
631 _app.setLayout(layout)
632 <% } %>
633
634 // Mounts Vue app to DOM element
635 const mount = () => {
636 _app.$mount('#<%= globals.id %>')
637
638 // Listen for first Vue update
639 Vue.nextTick(() => {
640 // Call window.{{globals.readyCallback}} callbacks
641 nuxtReady(_app)
642 <% if (isDev) { %>
643 // Enable hot reloading
644 hotReloadAPI(_app)
645 <% } %>
646 })
647 }
648
649 // Enable transitions
650 _app.setTransitions = _app.$options.nuxt.setTransitions.bind(_app)
651 if (Components.length) {
652 _app.setTransitions(mapTransitions(Components, router.currentRoute))
653 _lastPaths = router.currentRoute.matched.map(route => compile(route.path)(router.currentRoute.params))
654 }
655
656 // Initialize error handler
657 _app.$loading = {} // To avoid error while _app.$nuxt does not exist
658 if (NUXT.error) _app.error(NUXT.error)
659
660 // Add router hooks
661 router.beforeEach(loadAsyncComponents.bind(_app))
662 router.beforeEach(render.bind(_app))
663 router.afterEach(normalizeComponents)
664 router.afterEach(fixPrepatch.bind(_app))
665
666 // If page already is server rendered
667 if (NUXT.serverRendered) {
668 mount()
669 return
670 }
671
672 // First render on client-side
673 render.call(_app, router.currentRoute, router.currentRoute, (path) => {
674 // If not redirected
675 if (!path) {
676 normalizeComponents(router.currentRoute, router.currentRoute)
677 showNextPage.call(_app, router.currentRoute)
678 // Don't call fixPrepatch.call(_app, router.currentRoute, router.currentRoute) since it's first render
679 mount()
680 return
681 }
682
683 // Push the path and then mount app
684 router.push(path, () => mount(), (err) => {
685 if (!err) return mount()
686 console.error(err)
687 })
688 })
689}
690
\No newline at end of file