server.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import { stringify } from 'querystring'
  2. import Vue from 'vue'
  3. import omit from 'lodash/omit'
  4. import middleware from './middleware'
  5. import { applyAsyncData, sanitizeComponent, getMatchedComponents, getContext, middlewareSeries, promisify, urlJoin } from './utils'
  6. import { createApp, NuxtError } from './index'
  7. const debug = require('debug')('nuxt:render')
  8. debug.color = 4 // force blue color
  9. const isDev = false
  10. const noopApp = () => new Vue({ render: h => h('div') })
  11. const createNext = ssrContext => (opts) => {
  12. ssrContext.redirected = opts
  13. // If nuxt generate
  14. if (!ssrContext.res) {
  15. ssrContext.nuxt.serverRendered = false
  16. return
  17. }
  18. opts.query = stringify(opts.query)
  19. opts.path = opts.path + (opts.query ? '?' + opts.query : '')
  20. const routerBase = '/'
  21. if (!opts.path.startsWith('http') && (routerBase !== '/' && !opts.path.startsWith(routerBase))) {
  22. opts.path = urlJoin(routerBase, opts.path)
  23. }
  24. // Avoid loop redirect
  25. if (opts.path === ssrContext.url) {
  26. ssrContext.redirected = false
  27. return
  28. }
  29. ssrContext.res.writeHead(opts.status, {
  30. 'Location': opts.path
  31. })
  32. ssrContext.res.end()
  33. }
  34. // This exported function will be called by `bundleRenderer`.
  35. // This is where we perform data-prefetching to determine the
  36. // state of our application before actually rendering it.
  37. // Since data fetching is async, this function is expected to
  38. // return a Promise that resolves to the app instance.
  39. export default async (ssrContext) => {
  40. // Create ssrContext.next for simulate next() of beforeEach() when wanted to redirect
  41. ssrContext.redirected = false
  42. ssrContext.next = createNext(ssrContext)
  43. // Used for beforeNuxtRender({ Components, nuxtState })
  44. ssrContext.beforeRenderFns = []
  45. // Nuxt object (window{{globals.context}}, defaults to window.__NUXT__)
  46. ssrContext.nuxt = { layout: 'default', data: [], error: null, serverRendered: true }
  47. // Create the app definition and the instance (created for each request)
  48. const { app, router } = await createApp(ssrContext)
  49. const _app = new Vue(app)
  50. // Add meta infos (used in renderer.js)
  51. ssrContext.meta = _app.$meta()
  52. // Keep asyncData for each matched component in ssrContext (used in app/utils.js via this.$ssrContext)
  53. ssrContext.asyncData = {}
  54. const beforeRender = async () => {
  55. // Call beforeNuxtRender() methods
  56. await Promise.all(ssrContext.beforeRenderFns.map(fn => promisify(fn, { Components, nuxtState: ssrContext.nuxt })))
  57. }
  58. const renderErrorPage = async () => {
  59. // Load layout for error page
  60. const errLayout = (typeof NuxtError.layout === 'function' ? NuxtError.layout(app.context) : NuxtError.layout)
  61. ssrContext.nuxt.layout = errLayout || 'default'
  62. await _app.loadLayout(errLayout)
  63. _app.setLayout(errLayout)
  64. await beforeRender()
  65. return _app
  66. }
  67. const render404Page = () => {
  68. app.context.error({ statusCode: 404, path: ssrContext.url, message: `This page could not be found` })
  69. return renderErrorPage()
  70. }
  71. // Components are already resolved by setContext -> getRouteData (app/utils.js)
  72. const Components = getMatchedComponents(router.match(ssrContext.url))
  73. /*
  74. ** Call global middleware (nuxt.config.js)
  75. */
  76. let midd = []
  77. midd = midd.map((name) => {
  78. if (typeof name === 'function') return name
  79. if (typeof middleware[name] !== 'function') {
  80. app.context.error({ statusCode: 500, message: 'Unknown middleware ' + name })
  81. }
  82. return middleware[name]
  83. })
  84. await middlewareSeries(midd, app.context)
  85. // ...If there is a redirect or an error, stop the process
  86. if (ssrContext.redirected) return noopApp()
  87. if (ssrContext.nuxt.error) return renderErrorPage()
  88. /*
  89. ** Set layout
  90. */
  91. let layout = Components.length ? Components[0].options.layout : NuxtError.layout
  92. if (typeof layout === 'function') layout = layout(app.context)
  93. await _app.loadLayout(layout)
  94. if (ssrContext.nuxt.error) return renderErrorPage()
  95. layout = _app.setLayout(layout)
  96. ssrContext.nuxt.layout = _app.layoutName
  97. /*
  98. ** Call middleware (layout + pages)
  99. */
  100. midd = []
  101. if (layout.middleware) midd = midd.concat(layout.middleware)
  102. Components.forEach((Component) => {
  103. if (Component.options.middleware) {
  104. midd = midd.concat(Component.options.middleware)
  105. }
  106. })
  107. midd = midd.map((name) => {
  108. if (typeof name === 'function') return name
  109. if (typeof middleware[name] !== 'function') {
  110. app.context.error({ statusCode: 500, message: 'Unknown middleware ' + name })
  111. }
  112. return middleware[name]
  113. })
  114. await middlewareSeries(midd, app.context)
  115. // ...If there is a redirect or an error, stop the process
  116. if (ssrContext.redirected) return noopApp()
  117. if (ssrContext.nuxt.error) return renderErrorPage()
  118. /*
  119. ** Call .validate()
  120. */
  121. let isValid = true
  122. try {
  123. for (const Component of Components) {
  124. if (typeof Component.options.validate !== 'function') {
  125. continue
  126. }
  127. isValid = await Component.options.validate(app.context)
  128. if (!isValid) {
  129. break
  130. }
  131. }
  132. } catch (validationError) {
  133. // ...If .validate() threw an error
  134. app.context.error({
  135. statusCode: validationError.statusCode || '500',
  136. message: validationError.message
  137. })
  138. return renderErrorPage()
  139. }
  140. // ...If .validate() returned false
  141. if (!isValid) {
  142. // Don't server-render the page in generate mode
  143. if (ssrContext._generate) ssrContext.nuxt.serverRendered = false
  144. // Render a 404 error page
  145. return render404Page()
  146. }
  147. // If no Components found, returns 404
  148. if (!Components.length) return render404Page()
  149. // Call asyncData & fetch hooks on components matched by the route.
  150. const asyncDatas = await Promise.all(Components.map((Component) => {
  151. const promises = []
  152. // Call asyncData(context)
  153. if (Component.options.asyncData && typeof Component.options.asyncData === 'function') {
  154. const promise = promisify(Component.options.asyncData, app.context)
  155. promise.then((asyncDataResult) => {
  156. ssrContext.asyncData[Component.cid] = asyncDataResult
  157. applyAsyncData(Component)
  158. return asyncDataResult
  159. })
  160. promises.push(promise)
  161. } else {
  162. promises.push(null)
  163. }
  164. // Call fetch(context)
  165. if (Component.options.fetch) {
  166. promises.push(Component.options.fetch(app.context))
  167. } else {
  168. promises.push(null)
  169. }
  170. return Promise.all(promises)
  171. }))
  172. // datas are the first row of each
  173. ssrContext.nuxt.data = asyncDatas.map(r => r[0] || {})
  174. // ...If there is a redirect or an error, stop the process
  175. if (ssrContext.redirected) return noopApp()
  176. if (ssrContext.nuxt.error) return renderErrorPage()
  177. // Call beforeNuxtRender methods & add store state
  178. await beforeRender()
  179. return _app
  180. }