render.ts 9.7 KB

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