render.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import {
  2. Comment,
  3. type Component,
  4. type ComponentInternalInstance,
  5. type DirectiveBinding,
  6. Fragment,
  7. type FunctionalComponent,
  8. Static,
  9. Text,
  10. type VNode,
  11. type VNodeArrayChildren,
  12. type VNodeProps,
  13. mergeProps,
  14. ssrUtils,
  15. warn,
  16. } from 'vue'
  17. import {
  18. NOOP,
  19. ShapeFlags,
  20. escapeHtml,
  21. escapeHtmlComment,
  22. isArray,
  23. isFunction,
  24. isPromise,
  25. isString,
  26. isVoidTag,
  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. return
  73. }
  74. buffer.push(item)
  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(
  101. prefetches.map(prefetch => prefetch.call(instance.proxy)),
  102. ),
  103. )
  104. // Note: error display is already done by the wrapped lifecycle hook function.
  105. .catch(NOOP)
  106. }
  107. return p.then(() => renderComponentSubTree(instance, slotScopeId))
  108. } else {
  109. return renderComponentSubTree(instance, slotScopeId)
  110. }
  111. }
  112. function renderComponentSubTree(
  113. instance: ComponentInternalInstance,
  114. slotScopeId?: string,
  115. ): SSRBuffer | Promise<SSRBuffer> {
  116. const comp = instance.type as Component
  117. const { getBuffer, push } = createBuffer()
  118. if (isFunction(comp)) {
  119. let root = renderComponentRoot(instance)
  120. // #5817 scope ID attrs not falling through if functional component doesn't
  121. // have props
  122. if (!(comp as FunctionalComponent).props) {
  123. for (const key in instance.attrs) {
  124. if (key.startsWith(`data-v-`)) {
  125. ;(root.props || (root.props = {}))[key] = ``
  126. }
  127. }
  128. }
  129. renderVNode(push, (instance.subTree = root), instance, slotScopeId)
  130. } else {
  131. if (
  132. (!instance.render || instance.render === NOOP) &&
  133. !instance.ssrRender &&
  134. !comp.ssrRender &&
  135. isString(comp.template)
  136. ) {
  137. comp.ssrRender = ssrCompile(comp.template, instance)
  138. }
  139. // perf: enable caching of computed getters during render
  140. // since there cannot be state mutations during render.
  141. for (const e of instance.scope.effects) {
  142. if (e.computed) {
  143. e.computed._dirty = true
  144. e.computed._cacheable = true
  145. }
  146. }
  147. const ssrRender = instance.ssrRender || comp.ssrRender
  148. if (ssrRender) {
  149. // optimized
  150. // resolve fallthrough attrs
  151. let attrs = instance.inheritAttrs !== false ? instance.attrs : undefined
  152. let hasCloned = false
  153. let cur = instance
  154. while (true) {
  155. const scopeId = cur.vnode.scopeId
  156. if (scopeId) {
  157. if (!hasCloned) {
  158. attrs = { ...attrs }
  159. hasCloned = true
  160. }
  161. attrs![scopeId] = ''
  162. }
  163. const parent = cur.parent
  164. if (parent && parent.subTree && parent.subTree === cur.vnode) {
  165. // parent is a non-SSR compiled component and is rendering this
  166. // component as root. inherit its scopeId if present.
  167. cur = parent
  168. } else {
  169. break
  170. }
  171. }
  172. if (slotScopeId) {
  173. if (!hasCloned) attrs = { ...attrs }
  174. const slotScopeIdList = slotScopeId.trim().split(' ')
  175. for (let i = 0; i < slotScopeIdList.length; i++) {
  176. attrs![slotScopeIdList[i]] = ''
  177. }
  178. }
  179. // set current rendering instance for asset resolution
  180. const prev = setCurrentRenderingInstance(instance)
  181. try {
  182. ssrRender(
  183. instance.proxy,
  184. push,
  185. instance,
  186. attrs,
  187. // compiler-optimized bindings
  188. instance.props,
  189. instance.setupState,
  190. instance.data,
  191. instance.ctx,
  192. )
  193. } finally {
  194. setCurrentRenderingInstance(prev)
  195. }
  196. } else if (instance.render && instance.render !== NOOP) {
  197. renderVNode(
  198. push,
  199. (instance.subTree = renderComponentRoot(instance)),
  200. instance,
  201. slotScopeId,
  202. )
  203. } else {
  204. const componentName = comp.name || comp.__file || `<Anonymous>`
  205. warn(`Component ${componentName} is missing template or render function.`)
  206. push(`<!---->`)
  207. }
  208. }
  209. return getBuffer()
  210. }
  211. export function renderVNode(
  212. push: PushFn,
  213. vnode: VNode,
  214. parentComponent: ComponentInternalInstance,
  215. slotScopeId?: string,
  216. ) {
  217. const { type, shapeFlag, children } = vnode
  218. switch (type) {
  219. case Text:
  220. push(escapeHtml(children as string))
  221. break
  222. case Comment:
  223. push(
  224. children
  225. ? `<!--${escapeHtmlComment(children as string)}-->`
  226. : `<!---->`,
  227. )
  228. break
  229. case Static:
  230. push(children as string)
  231. break
  232. case Fragment:
  233. if (vnode.slotScopeIds) {
  234. slotScopeId =
  235. (slotScopeId ? slotScopeId + ' ' : '') + vnode.slotScopeIds.join(' ')
  236. }
  237. push(`<!--[-->`) // open
  238. renderVNodeChildren(
  239. push,
  240. children as VNodeArrayChildren,
  241. parentComponent,
  242. slotScopeId,
  243. )
  244. push(`<!--]-->`) // close
  245. break
  246. default:
  247. if (shapeFlag & ShapeFlags.ELEMENT) {
  248. renderElementVNode(push, vnode, parentComponent, slotScopeId)
  249. } else if (shapeFlag & ShapeFlags.COMPONENT) {
  250. push(renderComponentVNode(vnode, parentComponent, slotScopeId))
  251. } else if (shapeFlag & ShapeFlags.TELEPORT) {
  252. renderTeleportVNode(push, vnode, parentComponent, slotScopeId)
  253. } else if (shapeFlag & ShapeFlags.SUSPENSE) {
  254. renderVNode(push, vnode.ssContent!, parentComponent, slotScopeId)
  255. } else {
  256. warn(
  257. '[@vue/server-renderer] Invalid VNode type:',
  258. type,
  259. `(${typeof type})`,
  260. )
  261. }
  262. }
  263. }
  264. export function renderVNodeChildren(
  265. push: PushFn,
  266. children: VNodeArrayChildren,
  267. parentComponent: ComponentInternalInstance,
  268. slotScopeId?: string,
  269. ) {
  270. for (let i = 0; i < children.length; i++) {
  271. renderVNode(push, normalizeVNode(children[i]), parentComponent, slotScopeId)
  272. }
  273. }
  274. function renderElementVNode(
  275. push: PushFn,
  276. vnode: VNode,
  277. parentComponent: ComponentInternalInstance,
  278. slotScopeId?: string,
  279. ) {
  280. const tag = vnode.type as string
  281. let { props, children, shapeFlag, scopeId, dirs } = vnode
  282. let openTag = `<${tag}`
  283. if (dirs) {
  284. props = applySSRDirectives(vnode, props, dirs)
  285. }
  286. if (props) {
  287. openTag += ssrRenderAttrs(props, tag)
  288. }
  289. if (scopeId) {
  290. openTag += ` ${scopeId}`
  291. }
  292. // inherit parent chain scope id if this is the root node
  293. let curParent: ComponentInternalInstance | null = parentComponent
  294. let curVnode = vnode
  295. while (curParent && curVnode === curParent.subTree) {
  296. curVnode = curParent.vnode
  297. if (curVnode.scopeId) {
  298. openTag += ` ${curVnode.scopeId}`
  299. }
  300. curParent = curParent.parent
  301. }
  302. if (slotScopeId) {
  303. openTag += ` ${slotScopeId}`
  304. }
  305. push(openTag + `>`)
  306. if (!isVoidTag(tag)) {
  307. let hasChildrenOverride = false
  308. if (props) {
  309. if (props.innerHTML) {
  310. hasChildrenOverride = true
  311. push(props.innerHTML)
  312. } else if (props.textContent) {
  313. hasChildrenOverride = true
  314. push(escapeHtml(props.textContent))
  315. } else if (tag === 'textarea' && props.value) {
  316. hasChildrenOverride = true
  317. push(escapeHtml(props.value))
  318. }
  319. }
  320. if (!hasChildrenOverride) {
  321. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  322. push(escapeHtml(children as string))
  323. } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  324. renderVNodeChildren(
  325. push,
  326. children as VNodeArrayChildren,
  327. parentComponent,
  328. slotScopeId,
  329. )
  330. }
  331. }
  332. push(`</${tag}>`)
  333. }
  334. }
  335. function applySSRDirectives(
  336. vnode: VNode,
  337. rawProps: VNodeProps | null,
  338. dirs: DirectiveBinding[],
  339. ): VNodeProps {
  340. const toMerge: VNodeProps[] = []
  341. for (let i = 0; i < dirs.length; i++) {
  342. const binding = dirs[i]
  343. const {
  344. dir: { getSSRProps },
  345. } = binding
  346. if (getSSRProps) {
  347. const props = getSSRProps(binding, vnode)
  348. if (props) toMerge.push(props)
  349. }
  350. }
  351. return mergeProps(rawProps || {}, ...toMerge)
  352. }
  353. function renderTeleportVNode(
  354. push: PushFn,
  355. vnode: VNode,
  356. parentComponent: ComponentInternalInstance,
  357. slotScopeId?: string,
  358. ) {
  359. const target = vnode.props && vnode.props.to
  360. const disabled = vnode.props && vnode.props.disabled
  361. if (!target) {
  362. if (!disabled) {
  363. warn(`[@vue/server-renderer] Teleport is missing target prop.`)
  364. }
  365. return []
  366. }
  367. if (!isString(target)) {
  368. warn(
  369. `[@vue/server-renderer] Teleport target must be a query selector string.`,
  370. )
  371. return []
  372. }
  373. ssrRenderTeleport(
  374. push,
  375. push => {
  376. renderVNodeChildren(
  377. push,
  378. vnode.children as VNodeArrayChildren,
  379. parentComponent,
  380. slotScopeId,
  381. )
  382. },
  383. target,
  384. disabled || disabled === '',
  385. parentComponent,
  386. )
  387. }