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. pushWarningContext,
  38. popWarningContext,
  39. } = ssrUtils
  40. export type SSRBuffer = SSRBufferItem[] & { hasAsync?: boolean }
  41. export type SSRBufferItem = string | SSRBuffer | Promise<SSRBuffer>
  42. export type PushFn = (item: SSRBufferItem) => void
  43. export type Props = Record<string, unknown>
  44. export type SSRContext = {
  45. [key: string]: any
  46. teleports?: Record<string, string>
  47. /**
  48. * @internal
  49. */
  50. __teleportBuffers?: Record<string, SSRBuffer>
  51. /**
  52. * @internal
  53. */
  54. __watcherHandles?: (() => void)[]
  55. }
  56. // Each component has a buffer array.
  57. // A buffer array can contain one of the following:
  58. // - plain string
  59. // - A resolved buffer (recursive arrays of strings that can be unrolled
  60. // synchronously)
  61. // - An async buffer (a Promise that resolves to a resolved buffer)
  62. export function createBuffer() {
  63. let appendable = false
  64. const buffer: SSRBuffer = []
  65. return {
  66. getBuffer(): SSRBuffer {
  67. // Return static buffer and await on items during unroll stage
  68. return buffer
  69. },
  70. push(item: SSRBufferItem): void {
  71. const isStringItem = isString(item)
  72. if (appendable && isStringItem) {
  73. buffer[buffer.length - 1] += item as string
  74. return
  75. }
  76. buffer.push(item)
  77. appendable = isStringItem
  78. if (isPromise(item) || (isArray(item) && item.hasAsync)) {
  79. // promise, or child buffer with async, mark as async.
  80. // this allows skipping unnecessary await ticks during unroll stage
  81. buffer.hasAsync = true
  82. }
  83. },
  84. }
  85. }
  86. export function renderComponentVNode(
  87. vnode: VNode,
  88. parentComponent: ComponentInternalInstance | null = null,
  89. slotScopeId?: string,
  90. ): SSRBuffer | Promise<SSRBuffer> {
  91. const instance = (vnode.component = createComponentInstance(
  92. vnode,
  93. parentComponent,
  94. null,
  95. ))
  96. if (__DEV__) pushWarningContext(vnode)
  97. const res = setupComponent(instance, true /* isSSR */)
  98. if (__DEV__) popWarningContext()
  99. const hasAsyncSetup = isPromise(res)
  100. let prefetches = instance.sp /* LifecycleHooks.SERVER_PREFETCH */
  101. if (hasAsyncSetup || prefetches) {
  102. const p: Promise<unknown> = Promise.resolve(res as Promise<void>)
  103. .then(() => {
  104. // instance.sp may be null until an async setup resolves, so evaluate it here
  105. if (hasAsyncSetup) prefetches = instance.sp
  106. if (prefetches) {
  107. return Promise.all(
  108. prefetches.map(prefetch => prefetch.call(instance.proxy)),
  109. )
  110. }
  111. })
  112. // Note: error display is already done by the wrapped lifecycle hook function.
  113. .catch(NOOP)
  114. return p.then(() => renderComponentSubTree(instance, slotScopeId))
  115. } else {
  116. return renderComponentSubTree(instance, slotScopeId)
  117. }
  118. }
  119. function renderComponentSubTree(
  120. instance: ComponentInternalInstance,
  121. slotScopeId?: string,
  122. ): SSRBuffer | Promise<SSRBuffer> {
  123. if (__DEV__) pushWarningContext(instance.vnode)
  124. const comp = instance.type as Component
  125. const { getBuffer, push } = createBuffer()
  126. if (isFunction(comp)) {
  127. let root = renderComponentRoot(instance)
  128. // #5817 scope ID attrs not falling through if functional component doesn't
  129. // have props
  130. if (!(comp as FunctionalComponent).props) {
  131. for (const key in instance.attrs) {
  132. if (key.startsWith(`data-v-`)) {
  133. ;(root.props || (root.props = {}))[key] = ``
  134. }
  135. }
  136. }
  137. renderVNode(push, (instance.subTree = root), instance, slotScopeId)
  138. } else {
  139. if (
  140. (!instance.render || instance.render === NOOP) &&
  141. !instance.ssrRender &&
  142. !comp.ssrRender &&
  143. isString(comp.template)
  144. ) {
  145. comp.ssrRender = ssrCompile(comp.template, instance)
  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 as ComponentInternalInstance
  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. if (__DEV__) popWarningContext()
  210. return getBuffer()
  211. }
  212. export function renderVNode(
  213. push: PushFn,
  214. vnode: VNode,
  215. parentComponent: ComponentInternalInstance,
  216. slotScopeId?: string,
  217. ): void {
  218. const { type, shapeFlag, children, dirs, props } = vnode
  219. if (dirs) {
  220. vnode.props = applySSRDirectives(vnode, props, dirs)
  221. }
  222. switch (type) {
  223. case Text:
  224. push(escapeHtml(children as string))
  225. break
  226. case Comment:
  227. push(
  228. children
  229. ? `<!--${escapeHtmlComment(children as string)}-->`
  230. : `<!---->`,
  231. )
  232. break
  233. case Static:
  234. push(children as string)
  235. break
  236. case Fragment:
  237. if (vnode.slotScopeIds) {
  238. slotScopeId =
  239. (slotScopeId ? slotScopeId + ' ' : '') + vnode.slotScopeIds.join(' ')
  240. }
  241. push(`<!--[-->`) // open
  242. renderVNodeChildren(
  243. push,
  244. children as VNodeArrayChildren,
  245. parentComponent,
  246. slotScopeId,
  247. )
  248. push(`<!--]-->`) // close
  249. break
  250. default:
  251. if (shapeFlag & ShapeFlags.ELEMENT) {
  252. renderElementVNode(push, vnode, parentComponent, slotScopeId)
  253. } else if (shapeFlag & ShapeFlags.COMPONENT) {
  254. push(renderComponentVNode(vnode, parentComponent, slotScopeId))
  255. } else if (shapeFlag & ShapeFlags.TELEPORT) {
  256. renderTeleportVNode(push, vnode, parentComponent, slotScopeId)
  257. } else if (shapeFlag & ShapeFlags.SUSPENSE) {
  258. renderVNode(push, vnode.ssContent!, parentComponent, slotScopeId)
  259. } else {
  260. warn(
  261. '[@vue/server-renderer] Invalid VNode type:',
  262. type,
  263. `(${typeof type})`,
  264. )
  265. }
  266. }
  267. }
  268. export function renderVNodeChildren(
  269. push: PushFn,
  270. children: VNodeArrayChildren,
  271. parentComponent: ComponentInternalInstance,
  272. slotScopeId?: string,
  273. ): void {
  274. for (let i = 0; i < children.length; i++) {
  275. renderVNode(push, normalizeVNode(children[i]), parentComponent, slotScopeId)
  276. }
  277. }
  278. function renderElementVNode(
  279. push: PushFn,
  280. vnode: VNode,
  281. parentComponent: ComponentInternalInstance,
  282. slotScopeId?: string,
  283. ) {
  284. const tag = vnode.type as string
  285. let { props, children, shapeFlag, scopeId } = vnode
  286. let openTag = `<${tag}`
  287. if (props) {
  288. openTag += ssrRenderAttrs(props, tag)
  289. }
  290. if (scopeId) {
  291. openTag += ` ${scopeId}`
  292. }
  293. // inherit parent chain scope id if this is the root node
  294. let curParent: ComponentInternalInstance | null = parentComponent
  295. let curVnode = vnode
  296. while (curParent && curVnode === curParent.subTree) {
  297. curVnode = curParent.vnode
  298. if (curVnode.scopeId) {
  299. openTag += ` ${curVnode.scopeId}`
  300. }
  301. curParent = curParent.parent as ComponentInternalInstance
  302. }
  303. if (slotScopeId) {
  304. openTag += ` ${slotScopeId}`
  305. }
  306. push(openTag + `>`)
  307. if (!isVoidTag(tag)) {
  308. let hasChildrenOverride = false
  309. if (props) {
  310. if (props.innerHTML) {
  311. hasChildrenOverride = true
  312. push(props.innerHTML)
  313. } else if (props.textContent) {
  314. hasChildrenOverride = true
  315. push(escapeHtml(props.textContent))
  316. } else if (tag === 'textarea' && props.value) {
  317. hasChildrenOverride = true
  318. push(escapeHtml(props.value))
  319. }
  320. }
  321. if (!hasChildrenOverride) {
  322. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  323. push(escapeHtml(children as string))
  324. } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  325. renderVNodeChildren(
  326. push,
  327. children as VNodeArrayChildren,
  328. parentComponent,
  329. slotScopeId,
  330. )
  331. }
  332. }
  333. push(`</${tag}>`)
  334. }
  335. }
  336. function applySSRDirectives(
  337. vnode: VNode,
  338. rawProps: VNodeProps | null,
  339. dirs: DirectiveBinding[],
  340. ): VNodeProps {
  341. const toMerge: VNodeProps[] = []
  342. for (let i = 0; i < dirs.length; i++) {
  343. const binding = dirs[i]
  344. const {
  345. dir: { getSSRProps },
  346. } = binding
  347. if (getSSRProps) {
  348. const props = getSSRProps(binding, vnode)
  349. if (props) toMerge.push(props)
  350. }
  351. }
  352. return mergeProps(rawProps || {}, ...toMerge)
  353. }
  354. function renderTeleportVNode(
  355. push: PushFn,
  356. vnode: VNode,
  357. parentComponent: ComponentInternalInstance,
  358. slotScopeId?: string,
  359. ) {
  360. const target = vnode.props && vnode.props.to
  361. const disabled = vnode.props && vnode.props.disabled
  362. if (!target) {
  363. if (!disabled) {
  364. warn(`[@vue/server-renderer] Teleport is missing target prop.`)
  365. }
  366. return []
  367. }
  368. if (!isString(target)) {
  369. warn(
  370. `[@vue/server-renderer] Teleport target must be a query selector string.`,
  371. )
  372. return []
  373. }
  374. ssrRenderTeleport(
  375. push,
  376. push => {
  377. renderVNodeChildren(
  378. push,
  379. vnode.children as VNodeArrayChildren,
  380. parentComponent,
  381. slotScopeId,
  382. )
  383. },
  384. target,
  385. disabled || disabled === '',
  386. parentComponent,
  387. )
  388. }