render.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. import {
  2. Comment,
  3. Component,
  4. ComponentInternalInstance,
  5. DirectiveBinding,
  6. Fragment,
  7. FunctionalComponent,
  8. mergeProps,
  9. ssrUtils,
  10. Static,
  11. Text,
  12. VNode,
  13. VNodeArrayChildren,
  14. VNodeProps,
  15. warn
  16. } from 'vue'
  17. import {
  18. escapeHtml,
  19. escapeHtmlComment,
  20. isFunction,
  21. isPromise,
  22. isString,
  23. isVoidTag,
  24. ShapeFlags,
  25. isArray,
  26. NOOP
  27. } from '@vue/shared'
  28. import { ssrRenderAttrs } from './helpers/ssrRenderAttrs'
  29. import { ssrCompile } from './helpers/ssrCompile'
  30. import { ssrRenderTeleport } from './helpers/ssrRenderTeleport'
  31. const {
  32. createComponentInstance,
  33. setCurrentRenderingInstance,
  34. setupComponent,
  35. renderComponentRoot,
  36. normalizeVNode
  37. } = ssrUtils
  38. export type SSRBuffer = SSRBufferItem[] & { hasAsync?: boolean }
  39. export type SSRBufferItem = string | SSRBuffer | Promise<SSRBuffer>
  40. export type PushFn = (item: SSRBufferItem) => void
  41. export type Props = Record<string, unknown>
  42. export type SSRContext = {
  43. [key: string]: any
  44. teleports?: Record<string, string>
  45. /**
  46. * @internal
  47. */
  48. __teleportBuffers?: Record<string, SSRBuffer>
  49. /**
  50. * @internal
  51. */
  52. __watcherHandles?: (() => void)[]
  53. }
  54. // Each component has a buffer array.
  55. // A buffer array can contain one of the following:
  56. // - plain string
  57. // - A resolved buffer (recursive arrays of strings that can be unrolled
  58. // synchronously)
  59. // - An async buffer (a Promise that resolves to a resolved buffer)
  60. export function createBuffer() {
  61. let appendable = false
  62. const buffer: SSRBuffer = []
  63. return {
  64. getBuffer(): SSRBuffer {
  65. // Return static buffer and await on items during unroll stage
  66. return buffer
  67. },
  68. push(item: SSRBufferItem) {
  69. const isStringItem = isString(item)
  70. if (appendable && isStringItem) {
  71. buffer[buffer.length - 1] += item as string
  72. } else {
  73. buffer.push(item)
  74. }
  75. appendable = isStringItem
  76. if (isPromise(item) || (isArray(item) && item.hasAsync)) {
  77. // promise, or child buffer with async, mark as async.
  78. // this allows skipping unnecessary await ticks during unroll stage
  79. buffer.hasAsync = true
  80. }
  81. }
  82. }
  83. }
  84. export function renderComponentVNode(
  85. vnode: VNode,
  86. parentComponent: ComponentInternalInstance | null = null,
  87. slotScopeId?: string
  88. ): SSRBuffer | Promise<SSRBuffer> {
  89. const instance = createComponentInstance(vnode, parentComponent, null)
  90. const res = setupComponent(instance, true /* isSSR */)
  91. const hasAsyncSetup = isPromise(res)
  92. const prefetches = instance.sp /* LifecycleHooks.SERVER_PREFETCH */
  93. if (hasAsyncSetup || prefetches) {
  94. let p: Promise<unknown> = hasAsyncSetup
  95. ? (res as Promise<void>)
  96. : Promise.resolve()
  97. if (prefetches) {
  98. p = p
  99. .then(() =>
  100. Promise.all(prefetches.map(prefetch => prefetch.call(instance.proxy)))
  101. )
  102. // Note: error display is already done by the wrapped lifecycle hook function.
  103. .catch(() => {})
  104. }
  105. return p.then(() => renderComponentSubTree(instance, slotScopeId))
  106. } else {
  107. return renderComponentSubTree(instance, slotScopeId)
  108. }
  109. }
  110. function renderComponentSubTree(
  111. instance: ComponentInternalInstance,
  112. slotScopeId?: string
  113. ): SSRBuffer | Promise<SSRBuffer> {
  114. const comp = instance.type as Component
  115. const { getBuffer, push } = createBuffer()
  116. if (isFunction(comp)) {
  117. let root = renderComponentRoot(instance)
  118. // #5817 scope ID attrs not falling through if functional component doesn't
  119. // have props
  120. if (!(comp as FunctionalComponent).props) {
  121. for (const key in instance.attrs) {
  122. if (key.startsWith(`data-v-`)) {
  123. ;(root.props || (root.props = {}))[key] = ``
  124. }
  125. }
  126. }
  127. renderVNode(push, (instance.subTree = root), instance, slotScopeId)
  128. } else {
  129. if (
  130. (!instance.render || instance.render === NOOP) &&
  131. !instance.ssrRender &&
  132. !comp.ssrRender &&
  133. isString(comp.template)
  134. ) {
  135. comp.ssrRender = ssrCompile(comp.template, instance)
  136. }
  137. // perf: enable caching of computed getters during render
  138. // since there cannot be state mutations during render.
  139. for (const e of instance.scope.effects) {
  140. if (e.computed) e.computed._cacheable = true
  141. }
  142. const ssrRender = instance.ssrRender || comp.ssrRender
  143. if (ssrRender) {
  144. // optimized
  145. // resolve fallthrough attrs
  146. let attrs = instance.inheritAttrs !== false ? instance.attrs : undefined
  147. let hasCloned = false
  148. let cur = instance
  149. while (true) {
  150. const scopeId = cur.vnode.scopeId
  151. if (scopeId) {
  152. if (!hasCloned) {
  153. attrs = { ...attrs }
  154. hasCloned = true
  155. }
  156. attrs![scopeId] = ''
  157. }
  158. const parent = cur.parent
  159. if (parent && parent.subTree && parent.subTree === cur.vnode) {
  160. // parent is a non-SSR compiled component and is rendering this
  161. // component as root. inherit its scopeId if present.
  162. cur = parent
  163. } else {
  164. break
  165. }
  166. }
  167. if (slotScopeId) {
  168. if (!hasCloned) attrs = { ...attrs }
  169. attrs![slotScopeId.trim()] = ''
  170. }
  171. // set current rendering instance for asset resolution
  172. const prev = setCurrentRenderingInstance(instance)
  173. try {
  174. ssrRender(
  175. instance.proxy,
  176. push,
  177. instance,
  178. attrs,
  179. // compiler-optimized bindings
  180. instance.props,
  181. instance.setupState,
  182. instance.data,
  183. instance.ctx
  184. )
  185. } finally {
  186. setCurrentRenderingInstance(prev)
  187. }
  188. } else if (instance.render && instance.render !== NOOP) {
  189. renderVNode(
  190. push,
  191. (instance.subTree = renderComponentRoot(instance)),
  192. instance,
  193. slotScopeId
  194. )
  195. } else {
  196. const componentName = comp.name || comp.__file || `<Anonymous>`
  197. warn(`Component ${componentName} is missing template or render function.`)
  198. push(`<!---->`)
  199. }
  200. }
  201. return getBuffer()
  202. }
  203. export function renderVNode(
  204. push: PushFn,
  205. vnode: VNode,
  206. parentComponent: ComponentInternalInstance,
  207. slotScopeId?: string
  208. ) {
  209. const { type, shapeFlag, children } = vnode
  210. switch (type) {
  211. case Text:
  212. push(escapeHtml(children as string))
  213. break
  214. case Comment:
  215. push(
  216. children ? `<!--${escapeHtmlComment(children as string)}-->` : `<!---->`
  217. )
  218. break
  219. case Static:
  220. push(children as string)
  221. break
  222. case Fragment:
  223. if (vnode.slotScopeIds) {
  224. slotScopeId =
  225. (slotScopeId ? slotScopeId + ' ' : '') + vnode.slotScopeIds.join(' ')
  226. }
  227. push(`<!--[-->`) // open
  228. renderVNodeChildren(
  229. push,
  230. children as VNodeArrayChildren,
  231. parentComponent,
  232. slotScopeId
  233. )
  234. push(`<!--]-->`) // close
  235. break
  236. default:
  237. if (shapeFlag & ShapeFlags.ELEMENT) {
  238. renderElementVNode(push, vnode, parentComponent, slotScopeId)
  239. } else if (shapeFlag & ShapeFlags.COMPONENT) {
  240. push(renderComponentVNode(vnode, parentComponent, slotScopeId))
  241. } else if (shapeFlag & ShapeFlags.TELEPORT) {
  242. renderTeleportVNode(push, vnode, parentComponent, slotScopeId)
  243. } else if (shapeFlag & ShapeFlags.SUSPENSE) {
  244. renderVNode(push, vnode.ssContent!, parentComponent, slotScopeId)
  245. } else {
  246. warn(
  247. '[@vue/server-renderer] Invalid VNode type:',
  248. type,
  249. `(${typeof type})`
  250. )
  251. }
  252. }
  253. }
  254. export function renderVNodeChildren(
  255. push: PushFn,
  256. children: VNodeArrayChildren,
  257. parentComponent: ComponentInternalInstance,
  258. slotScopeId: string | undefined
  259. ) {
  260. for (let i = 0; i < children.length; i++) {
  261. renderVNode(push, normalizeVNode(children[i]), parentComponent, slotScopeId)
  262. }
  263. }
  264. function renderElementVNode(
  265. push: PushFn,
  266. vnode: VNode,
  267. parentComponent: ComponentInternalInstance,
  268. slotScopeId: string | undefined
  269. ) {
  270. const tag = vnode.type as string
  271. let { props, children, shapeFlag, scopeId, dirs } = vnode
  272. let openTag = `<${tag}`
  273. if (dirs) {
  274. props = applySSRDirectives(vnode, props, dirs)
  275. }
  276. if (props) {
  277. openTag += ssrRenderAttrs(props, tag)
  278. }
  279. if (scopeId) {
  280. openTag += ` ${scopeId}`
  281. }
  282. // inherit parent chain scope id if this is the root node
  283. let curParent: ComponentInternalInstance | null = parentComponent
  284. let curVnode = vnode
  285. while (curParent && curVnode === curParent.subTree) {
  286. curVnode = curParent.vnode
  287. if (curVnode.scopeId) {
  288. openTag += ` ${curVnode.scopeId}`
  289. }
  290. curParent = curParent.parent
  291. }
  292. if (slotScopeId) {
  293. openTag += ` ${slotScopeId}`
  294. }
  295. push(openTag + `>`)
  296. if (!isVoidTag(tag)) {
  297. let hasChildrenOverride = false
  298. if (props) {
  299. if (props.innerHTML) {
  300. hasChildrenOverride = true
  301. push(props.innerHTML)
  302. } else if (props.textContent) {
  303. hasChildrenOverride = true
  304. push(escapeHtml(props.textContent))
  305. } else if (tag === 'textarea' && props.value) {
  306. hasChildrenOverride = true
  307. push(escapeHtml(props.value))
  308. }
  309. }
  310. if (!hasChildrenOverride) {
  311. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  312. push(escapeHtml(children as string))
  313. } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  314. renderVNodeChildren(
  315. push,
  316. children as VNodeArrayChildren,
  317. parentComponent,
  318. slotScopeId
  319. )
  320. }
  321. }
  322. push(`</${tag}>`)
  323. }
  324. }
  325. function applySSRDirectives(
  326. vnode: VNode,
  327. rawProps: VNodeProps | null,
  328. dirs: DirectiveBinding[]
  329. ): VNodeProps {
  330. const toMerge: VNodeProps[] = []
  331. for (let i = 0; i < dirs.length; i++) {
  332. const binding = dirs[i]
  333. const {
  334. dir: { getSSRProps }
  335. } = binding
  336. if (getSSRProps) {
  337. const props = getSSRProps(binding, vnode)
  338. if (props) toMerge.push(props)
  339. }
  340. }
  341. return mergeProps(rawProps || {}, ...toMerge)
  342. }
  343. function renderTeleportVNode(
  344. push: PushFn,
  345. vnode: VNode,
  346. parentComponent: ComponentInternalInstance,
  347. slotScopeId: string | undefined
  348. ) {
  349. const target = vnode.props && vnode.props.to
  350. const disabled = vnode.props && vnode.props.disabled
  351. if (!target) {
  352. if (!disabled) {
  353. warn(`[@vue/server-renderer] Teleport is missing target prop.`)
  354. }
  355. return []
  356. }
  357. if (!isString(target)) {
  358. warn(
  359. `[@vue/server-renderer] Teleport target must be a query selector string.`
  360. )
  361. return []
  362. }
  363. ssrRenderTeleport(
  364. push,
  365. push => {
  366. renderVNodeChildren(
  367. push,
  368. vnode.children as VNodeArrayChildren,
  369. parentComponent,
  370. slotScopeId
  371. )
  372. },
  373. target,
  374. disabled || disabled === '',
  375. parentComponent
  376. )
  377. }