render.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  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. } 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(
  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. const ssrRender = instance.ssrRender || comp.ssrRender
  140. if (ssrRender) {
  141. // optimized
  142. // resolve fallthrough attrs
  143. let attrs = instance.inheritAttrs !== false ? instance.attrs : undefined
  144. let hasCloned = false
  145. let cur = instance
  146. while (true) {
  147. const scopeId = cur.vnode.scopeId
  148. if (scopeId) {
  149. if (!hasCloned) {
  150. attrs = { ...attrs }
  151. hasCloned = true
  152. }
  153. attrs![scopeId] = ''
  154. }
  155. const parent = cur.parent
  156. if (parent && parent.subTree && parent.subTree === cur.vnode) {
  157. // parent is a non-SSR compiled component and is rendering this
  158. // component as root. inherit its scopeId if present.
  159. cur = parent
  160. } else {
  161. break
  162. }
  163. }
  164. if (slotScopeId) {
  165. if (!hasCloned) attrs = { ...attrs }
  166. const slotScopeIdList = slotScopeId.trim().split(' ')
  167. for (let i = 0; i < slotScopeIdList.length; i++) {
  168. attrs![slotScopeIdList[i]] = ''
  169. }
  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
  217. ? `<!--${escapeHtmlComment(children as string)}-->`
  218. : `<!---->`,
  219. )
  220. break
  221. case Static:
  222. push(children as string)
  223. break
  224. case Fragment:
  225. if (vnode.slotScopeIds) {
  226. slotScopeId =
  227. (slotScopeId ? slotScopeId + ' ' : '') + vnode.slotScopeIds.join(' ')
  228. }
  229. push(`<!--[-->`) // open
  230. renderVNodeChildren(
  231. push,
  232. children as VNodeArrayChildren,
  233. parentComponent,
  234. slotScopeId,
  235. )
  236. push(`<!--]-->`) // close
  237. break
  238. default:
  239. if (shapeFlag & ShapeFlags.ELEMENT) {
  240. renderElementVNode(push, vnode, parentComponent, slotScopeId)
  241. } else if (shapeFlag & ShapeFlags.COMPONENT) {
  242. push(renderComponentVNode(vnode, parentComponent, slotScopeId))
  243. } else if (shapeFlag & ShapeFlags.TELEPORT) {
  244. renderTeleportVNode(push, vnode, parentComponent, slotScopeId)
  245. } else if (shapeFlag & ShapeFlags.SUSPENSE) {
  246. renderVNode(push, vnode.ssContent!, parentComponent, slotScopeId)
  247. } else {
  248. warn(
  249. '[@vue/server-renderer] Invalid VNode type:',
  250. type,
  251. `(${typeof type})`,
  252. )
  253. }
  254. }
  255. }
  256. export function renderVNodeChildren(
  257. push: PushFn,
  258. children: VNodeArrayChildren,
  259. parentComponent: ComponentInternalInstance,
  260. slotScopeId?: string,
  261. ) {
  262. for (let i = 0; i < children.length; i++) {
  263. renderVNode(push, normalizeVNode(children[i]), parentComponent, slotScopeId)
  264. }
  265. }
  266. function renderElementVNode(
  267. push: PushFn,
  268. vnode: VNode,
  269. parentComponent: ComponentInternalInstance,
  270. slotScopeId?: string,
  271. ) {
  272. const tag = vnode.type as string
  273. let { props, children, shapeFlag, scopeId, dirs } = vnode
  274. let openTag = `<${tag}`
  275. if (dirs) {
  276. props = applySSRDirectives(vnode, props, dirs)
  277. }
  278. if (props) {
  279. openTag += ssrRenderAttrs(props, tag)
  280. }
  281. if (scopeId) {
  282. openTag += ` ${scopeId}`
  283. }
  284. // inherit parent chain scope id if this is the root node
  285. let curParent: ComponentInternalInstance | null = parentComponent
  286. let curVnode = vnode
  287. while (curParent && curVnode === curParent.subTree) {
  288. curVnode = curParent.vnode
  289. if (curVnode.scopeId) {
  290. openTag += ` ${curVnode.scopeId}`
  291. }
  292. curParent = curParent.parent
  293. }
  294. if (slotScopeId) {
  295. openTag += ` ${slotScopeId}`
  296. }
  297. push(openTag + `>`)
  298. if (!isVoidTag(tag)) {
  299. let hasChildrenOverride = false
  300. if (props) {
  301. if (props.innerHTML) {
  302. hasChildrenOverride = true
  303. push(props.innerHTML)
  304. } else if (props.textContent) {
  305. hasChildrenOverride = true
  306. push(escapeHtml(props.textContent))
  307. } else if (tag === 'textarea' && props.value) {
  308. hasChildrenOverride = true
  309. push(escapeHtml(props.value))
  310. }
  311. }
  312. if (!hasChildrenOverride) {
  313. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  314. push(escapeHtml(children as string))
  315. } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  316. renderVNodeChildren(
  317. push,
  318. children as VNodeArrayChildren,
  319. parentComponent,
  320. slotScopeId,
  321. )
  322. }
  323. }
  324. push(`</${tag}>`)
  325. }
  326. }
  327. function applySSRDirectives(
  328. vnode: VNode,
  329. rawProps: VNodeProps | null,
  330. dirs: DirectiveBinding[],
  331. ): VNodeProps {
  332. const toMerge: VNodeProps[] = []
  333. for (let i = 0; i < dirs.length; i++) {
  334. const binding = dirs[i]
  335. const {
  336. dir: { getSSRProps },
  337. } = binding
  338. if (getSSRProps) {
  339. const props = getSSRProps(binding, vnode)
  340. if (props) toMerge.push(props)
  341. }
  342. }
  343. return mergeProps(rawProps || {}, ...toMerge)
  344. }
  345. function renderTeleportVNode(
  346. push: PushFn,
  347. vnode: VNode,
  348. parentComponent: ComponentInternalInstance,
  349. slotScopeId?: string,
  350. ) {
  351. const target = vnode.props && vnode.props.to
  352. const disabled = vnode.props && vnode.props.disabled
  353. if (!target) {
  354. if (!disabled) {
  355. warn(`[@vue/server-renderer] Teleport is missing target prop.`)
  356. }
  357. return []
  358. }
  359. if (!isString(target)) {
  360. warn(
  361. `[@vue/server-renderer] Teleport target must be a query selector string.`,
  362. )
  363. return []
  364. }
  365. ssrRenderTeleport(
  366. push,
  367. push => {
  368. renderVNodeChildren(
  369. push,
  370. vnode.children as VNodeArrayChildren,
  371. parentComponent,
  372. slotScopeId,
  373. )
  374. },
  375. target,
  376. disabled || disabled === '',
  377. parentComponent,
  378. )
  379. }