server.js 8.6 KB

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