server.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import { stringify } from 'querystring'
  2. import Vue from 'vue'
  3. import fetch from 'node-fetch'
  4. import middleware from './middleware.js'
  5. import {
  6. applyAsyncData,
  7. middlewareSeries,
  8. sanitizeComponent,
  9. getMatchedComponents,
  10. promisify
  11. } from './utils.js'
  12. import fetchMixin from './mixins/fetch.server'
  13. import { createApp, NuxtError } from './index.js'
  14. import NuxtLink from './components/nuxt-link.server.js' // should be included after ./index.js
  15. // Update serverPrefetch strategy
  16. Vue.config.optionMergeStrategies.serverPrefetch = Vue.config.optionMergeStrategies.created
  17. // Fetch mixin
  18. if (!Vue.__nuxt__fetch__mixin__) {
  19. Vue.mixin(fetchMixin)
  20. Vue.__nuxt__fetch__mixin__ = true
  21. }
  22. // Component: <NuxtLink>
  23. Vue.component(NuxtLink.name, NuxtLink)
  24. Vue.component('NLink', NuxtLink)
  25. if (!global.fetch) { global.fetch = fetch }
  26. const noopApp = () => new Vue({ render: h => h('div') })
  27. function urlJoin () {
  28. return Array.prototype.slice.call(arguments).join('/').replace(/\/+/g, '/')
  29. }
  30. const createNext = ssrContext => (opts) => {
  31. // If static target, render on client-side
  32. ssrContext.redirected = opts
  33. if (ssrContext.target === 'static' || !ssrContext.res) {
  34. ssrContext.nuxt.serverRendered = false
  35. return
  36. }
  37. opts.query = stringify(opts.query)
  38. opts.path = opts.path + (opts.query ? '?' + opts.query : '')
  39. const routerBase = '/'
  40. if (!opts.path.startsWith('http') && (routerBase !== '/' && !opts.path.startsWith(routerBase))) {
  41. opts.path = urlJoin(routerBase, opts.path)
  42. }
  43. // Avoid loop redirect
  44. if (opts.path === ssrContext.url) {
  45. ssrContext.redirected = false
  46. return
  47. }
  48. ssrContext.res.writeHead(opts.status, {
  49. Location: opts.path
  50. })
  51. ssrContext.res.end()
  52. }
  53. // This exported function will be called by `bundleRenderer`.
  54. // This is where we perform data-prefetching to determine the
  55. // state of our application before actually rendering it.
  56. // Since data fetching is async, this function is expected to
  57. // return a Promise that resolves to the app instance.
  58. export default async (ssrContext) => {
  59. // Create ssrContext.next for simulate next() of beforeEach() when wanted to redirect
  60. ssrContext.redirected = false
  61. ssrContext.next = createNext(ssrContext)
  62. // Used for beforeNuxtRender({ Components, nuxtState })
  63. ssrContext.beforeRenderFns = []
  64. // Nuxt object (window.{{globals.context}}, defaults to window.__NUXT__)
  65. ssrContext.nuxt = { layout: 'default', data: [], fetch: [], error: null, state: null, serverRendered: true, routePath: '' }
  66. // Remove query from url is static target
  67. if (process.static && ssrContext.url) {
  68. ssrContext.url = ssrContext.url.split('?')[0]
  69. }
  70. // Public runtime config
  71. ssrContext.nuxt.config = ssrContext.runtimeConfig.public
  72. // Create the app definition and the instance (created for each request)
  73. const { app, router, store } = await createApp(ssrContext, { ...ssrContext.runtimeConfig.public, ...ssrContext.runtimeConfig.private })
  74. const _app = new Vue(app)
  75. // Add ssr route path to nuxt context so we can account for page navigation between ssr and csr
  76. ssrContext.nuxt.routePath = app.context.route.path
  77. // Add meta infos (used in renderer.js)
  78. ssrContext.meta = _app.$meta()
  79. // Keep asyncData for each matched component in ssrContext (used in app/utils.js via this.$ssrContext)
  80. ssrContext.asyncData = {}
  81. const beforeRender = async () => {
  82. // Call beforeNuxtRender() methods
  83. await Promise.all(ssrContext.beforeRenderFns.map(fn => promisify(fn, { Components, nuxtState: ssrContext.nuxt })))
  84. ssrContext.rendered = () => {
  85. // Add the state from the vuex store
  86. ssrContext.nuxt.state = store.state
  87. }
  88. }
  89. const renderErrorPage = async () => {
  90. // Don't server-render the page in static target
  91. if (ssrContext.target === 'static') {
  92. ssrContext.nuxt.serverRendered = false
  93. }
  94. // Load layout for error page
  95. const layout = (NuxtError.options || NuxtError).layout
  96. const errLayout = typeof layout === 'function' ? layout.call(NuxtError, app.context) : layout
  97. ssrContext.nuxt.layout = errLayout || 'default'
  98. await _app.loadLayout(errLayout)
  99. _app.setLayout(errLayout)
  100. await beforeRender()
  101. return _app
  102. }
  103. const render404Page = () => {
  104. app.context.error({ statusCode: 404, path: ssrContext.url, message: 'This page could not be found' })
  105. return renderErrorPage()
  106. }
  107. // Components are already resolved by setContext -> getRouteData (app/utils.js)
  108. const Components = getMatchedComponents(router.match(ssrContext.url))
  109. /*
  110. ** Dispatch store nuxtServerInit
  111. */
  112. if (store._actions && store._actions.nuxtServerInit) {
  113. try {
  114. await store.dispatch('nuxtServerInit', app.context)
  115. } catch (err) {
  116. console.debug('Error occurred when calling nuxtServerInit: ', err.message)
  117. throw err
  118. }
  119. }
  120. // ...If there is a redirect or an error, stop the process
  121. if (ssrContext.redirected) {
  122. return noopApp()
  123. }
  124. if (ssrContext.nuxt.error) {
  125. return renderErrorPage()
  126. }
  127. /*
  128. ** Call global middleware (nuxt.config.js)
  129. */
  130. let midd = ["initialize"]
  131. midd = midd.map((name) => {
  132. if (typeof name === 'function') {
  133. return name
  134. }
  135. if (typeof middleware[name] !== 'function') {
  136. app.context.error({ statusCode: 500, message: 'Unknown middleware ' + name })
  137. }
  138. return middleware[name]
  139. })
  140. await middlewareSeries(midd, app.context)
  141. // ...If there is a redirect or an error, stop the process
  142. if (ssrContext.redirected) {
  143. return noopApp()
  144. }
  145. if (ssrContext.nuxt.error) {
  146. return renderErrorPage()
  147. }
  148. /*
  149. ** Set layout
  150. */
  151. let layout = Components.length ? Components[0].options.layout : NuxtError.layout
  152. if (typeof layout === 'function') {
  153. layout = layout(app.context)
  154. }
  155. await _app.loadLayout(layout)
  156. if (ssrContext.nuxt.error) {
  157. return renderErrorPage()
  158. }
  159. layout = _app.setLayout(layout)
  160. ssrContext.nuxt.layout = _app.layoutName
  161. /*
  162. ** Call middleware (layout + pages)
  163. */
  164. midd = []
  165. layout = sanitizeComponent(layout)
  166. if (layout.options.middleware) {
  167. midd = midd.concat(layout.options.middleware)
  168. }
  169. Components.forEach((Component) => {
  170. if (Component.options.middleware) {
  171. midd = midd.concat(Component.options.middleware)
  172. }
  173. })
  174. midd = midd.map((name) => {
  175. if (typeof name === 'function') {
  176. return name
  177. }
  178. if (typeof middleware[name] !== 'function') {
  179. app.context.error({ statusCode: 500, message: 'Unknown middleware ' + name })
  180. }
  181. return middleware[name]
  182. })
  183. await middlewareSeries(midd, app.context)
  184. // ...If there is a redirect or an error, stop the process
  185. if (ssrContext.redirected) {
  186. return noopApp()
  187. }
  188. if (ssrContext.nuxt.error) {
  189. return renderErrorPage()
  190. }
  191. /*
  192. ** Call .validate()
  193. */
  194. let isValid = true
  195. try {
  196. for (const Component of Components) {
  197. if (typeof Component.options.validate !== 'function') {
  198. continue
  199. }
  200. isValid = await Component.options.validate(app.context)
  201. if (!isValid) {
  202. break
  203. }
  204. }
  205. } catch (validationError) {
  206. // ...If .validate() threw an error
  207. app.context.error({
  208. statusCode: validationError.statusCode || '500',
  209. message: validationError.message
  210. })
  211. return renderErrorPage()
  212. }
  213. // ...If .validate() returned false
  214. if (!isValid) {
  215. // Render a 404 error page
  216. return render404Page()
  217. }
  218. // If no Components found, returns 404
  219. if (!Components.length) {
  220. return render404Page()
  221. }
  222. // Call asyncData & fetch hooks on components matched by the route.
  223. const asyncDatas = await Promise.all(Components.map((Component) => {
  224. const promises = []
  225. // Call asyncData(context)
  226. if (Component.options.asyncData && typeof Component.options.asyncData === 'function') {
  227. const promise = promisify(Component.options.asyncData, app.context)
  228. promise.then((asyncDataResult) => {
  229. ssrContext.asyncData[Component.cid] = asyncDataResult
  230. applyAsyncData(Component)
  231. return asyncDataResult
  232. })
  233. promises.push(promise)
  234. } else {
  235. promises.push(null)
  236. }
  237. // Call fetch(context)
  238. if (Component.options.fetch && Component.options.fetch.length) {
  239. promises.push(Component.options.fetch(app.context))
  240. } else {
  241. promises.push(null)
  242. }
  243. return Promise.all(promises)
  244. }))
  245. // datas are the first row of each
  246. ssrContext.nuxt.data = asyncDatas.map(r => r[0] || {})
  247. // ...If there is a redirect or an error, stop the process
  248. if (ssrContext.redirected) {
  249. return noopApp()
  250. }
  251. if (ssrContext.nuxt.error) {
  252. return renderErrorPage()
  253. }
  254. // Call beforeNuxtRender methods & add store state
  255. await beforeRender()
  256. return _app
  257. }