renderToString.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import {
  2. App,
  3. Component,
  4. ComponentInternalInstance,
  5. VNode,
  6. VNodeArrayChildren,
  7. createVNode,
  8. Text,
  9. Comment,
  10. Fragment,
  11. ssrUtils,
  12. Slots,
  13. warn,
  14. createApp,
  15. ssrContextKey
  16. } from 'vue'
  17. import {
  18. ShapeFlags,
  19. isString,
  20. isPromise,
  21. isArray,
  22. isFunction,
  23. isVoidTag,
  24. escapeHtml,
  25. NO,
  26. generateCodeFrame
  27. } from '@vue/shared'
  28. import { compile } from '@vue/compiler-ssr'
  29. import { ssrRenderAttrs } from './helpers/ssrRenderAttrs'
  30. import { SSRSlots } from './helpers/ssrRenderSlot'
  31. import { CompilerError } from '@vue/compiler-dom'
  32. const {
  33. isVNode,
  34. createComponentInstance,
  35. setCurrentRenderingInstance,
  36. setupComponent,
  37. renderComponentRoot,
  38. normalizeVNode
  39. } = ssrUtils
  40. // Each component has a buffer array.
  41. // A buffer array can contain one of the following:
  42. // - plain string
  43. // - A resolved buffer (recursive arrays of strings that can be unrolled
  44. // synchronously)
  45. // - An async buffer (a Promise that resolves to a resolved buffer)
  46. export type SSRBuffer = SSRBufferItem[]
  47. export type SSRBufferItem =
  48. | string
  49. | ResolvedSSRBuffer
  50. | Promise<ResolvedSSRBuffer>
  51. export type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
  52. export type PushFn = (item: SSRBufferItem) => void
  53. export type Props = Record<string, unknown>
  54. export type SSRContext = {
  55. [key: string]: any
  56. portals?: Record<string, string>
  57. __portalBuffers?: Record<
  58. string,
  59. ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
  60. >
  61. }
  62. export function createBuffer() {
  63. let appendable = false
  64. let hasAsync = false
  65. const buffer: SSRBuffer = []
  66. return {
  67. getBuffer(): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
  68. // If the current component's buffer contains any Promise from async children,
  69. // then it must return a Promise too. Otherwise this is a component that
  70. // contains only sync children so we can avoid the async book-keeping overhead.
  71. return hasAsync ? Promise.all(buffer) : (buffer as ResolvedSSRBuffer)
  72. },
  73. push(item: SSRBufferItem) {
  74. const isStringItem = isString(item)
  75. if (appendable && isStringItem) {
  76. buffer[buffer.length - 1] += item as string
  77. } else {
  78. buffer.push(item)
  79. }
  80. appendable = isStringItem
  81. if (!isStringItem && !isArray(item)) {
  82. // promise
  83. hasAsync = true
  84. }
  85. }
  86. }
  87. }
  88. function unrollBuffer(buffer: ResolvedSSRBuffer): string {
  89. let ret = ''
  90. for (let i = 0; i < buffer.length; i++) {
  91. const item = buffer[i]
  92. if (isString(item)) {
  93. ret += item
  94. } else {
  95. ret += unrollBuffer(item)
  96. }
  97. }
  98. return ret
  99. }
  100. export async function renderToString(
  101. input: App | VNode,
  102. context: SSRContext = {}
  103. ): Promise<string> {
  104. if (isVNode(input)) {
  105. // raw vnode, wrap with app (for context)
  106. return renderToString(createApp({ render: () => input }), context)
  107. }
  108. // rendering an app
  109. const vnode = createVNode(input._component, input._props)
  110. vnode.appContext = input._context
  111. // provide the ssr context to the tree
  112. input.provide(ssrContextKey, context)
  113. const buffer = await renderComponentVNode(vnode)
  114. await resolvePortals(context)
  115. return unrollBuffer(buffer)
  116. }
  117. export function renderComponent(
  118. comp: Component,
  119. props: Props | null = null,
  120. children: Slots | SSRSlots | null = null,
  121. parentComponent: ComponentInternalInstance | null = null
  122. ): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
  123. return renderComponentVNode(
  124. createVNode(comp, props, children),
  125. parentComponent
  126. )
  127. }
  128. function renderComponentVNode(
  129. vnode: VNode,
  130. parentComponent: ComponentInternalInstance | null = null
  131. ): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
  132. const instance = createComponentInstance(vnode, parentComponent)
  133. const res = setupComponent(
  134. instance,
  135. null /* parentSuspense (no need to track for SSR) */,
  136. true /* isSSR */
  137. )
  138. if (isPromise(res)) {
  139. return res.then(() => renderComponentSubTree(instance))
  140. } else {
  141. return renderComponentSubTree(instance)
  142. }
  143. }
  144. type SSRRenderFunction = (
  145. context: any,
  146. push: (item: any) => void,
  147. parentInstance: ComponentInternalInstance
  148. ) => void
  149. const compileCache: Record<string, SSRRenderFunction> = Object.create(null)
  150. function ssrCompile(
  151. template: string,
  152. instance: ComponentInternalInstance
  153. ): SSRRenderFunction {
  154. const cached = compileCache[template]
  155. if (cached) {
  156. return cached
  157. }
  158. const { code } = compile(template, {
  159. isCustomElement: instance.appContext.config.isCustomElement || NO,
  160. isNativeTag: instance.appContext.config.isNativeTag || NO,
  161. onError(err: CompilerError) {
  162. if (__DEV__) {
  163. const message = `Template compilation error: ${err.message}`
  164. const codeFrame =
  165. err.loc &&
  166. generateCodeFrame(
  167. template as string,
  168. err.loc.start.offset,
  169. err.loc.end.offset
  170. )
  171. warn(codeFrame ? `${message}\n${codeFrame}` : message)
  172. } else {
  173. throw err
  174. }
  175. }
  176. })
  177. return (compileCache[template] = Function(code)())
  178. }
  179. function renderComponentSubTree(
  180. instance: ComponentInternalInstance
  181. ): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
  182. const comp = instance.type as Component
  183. const { getBuffer, push } = createBuffer()
  184. if (isFunction(comp)) {
  185. renderVNode(push, renderComponentRoot(instance), instance)
  186. } else {
  187. if (!instance.render && !comp.ssrRender && isString(comp.template)) {
  188. comp.ssrRender = ssrCompile(comp.template, instance)
  189. }
  190. if (comp.ssrRender) {
  191. // optimized
  192. // set current rendering instance for asset resolution
  193. setCurrentRenderingInstance(instance)
  194. comp.ssrRender(instance.proxy, push, instance)
  195. setCurrentRenderingInstance(null)
  196. } else if (instance.render) {
  197. renderVNode(push, renderComponentRoot(instance), instance)
  198. } else {
  199. throw new Error(
  200. `Component ${
  201. comp.name ? `${comp.name} ` : ``
  202. } is missing template or render function.`
  203. )
  204. }
  205. }
  206. return getBuffer()
  207. }
  208. function renderVNode(
  209. push: PushFn,
  210. vnode: VNode,
  211. parentComponent: ComponentInternalInstance
  212. ) {
  213. const { type, shapeFlag, children } = vnode
  214. switch (type) {
  215. case Text:
  216. push(children as string)
  217. break
  218. case Comment:
  219. push(children ? `<!--${children}-->` : `<!---->`)
  220. break
  221. case Fragment:
  222. renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
  223. break
  224. default:
  225. if (shapeFlag & ShapeFlags.ELEMENT) {
  226. renderElement(push, vnode, parentComponent)
  227. } else if (shapeFlag & ShapeFlags.COMPONENT) {
  228. push(renderComponentVNode(vnode, parentComponent))
  229. } else if (shapeFlag & ShapeFlags.PORTAL) {
  230. renderPortal(vnode, parentComponent)
  231. } else if (shapeFlag & ShapeFlags.SUSPENSE) {
  232. // TODO
  233. } else {
  234. console.warn(
  235. '[@vue/server-renderer] Invalid VNode type:',
  236. type,
  237. `(${typeof type})`
  238. )
  239. }
  240. }
  241. }
  242. export function renderVNodeChildren(
  243. push: PushFn,
  244. children: VNodeArrayChildren,
  245. parentComponent: ComponentInternalInstance
  246. ) {
  247. for (let i = 0; i < children.length; i++) {
  248. renderVNode(push, normalizeVNode(children[i]), parentComponent)
  249. }
  250. }
  251. function renderElement(
  252. push: PushFn,
  253. vnode: VNode,
  254. parentComponent: ComponentInternalInstance
  255. ) {
  256. const tag = vnode.type as string
  257. const { props, children, shapeFlag, scopeId } = vnode
  258. let openTag = `<${tag}`
  259. // TODO directives
  260. if (props !== null) {
  261. openTag += ssrRenderAttrs(props, tag)
  262. }
  263. if (scopeId !== null) {
  264. openTag += ` ${scopeId}`
  265. const treeOwnerId = parentComponent && parentComponent.type.__scopeId
  266. // vnode's own scopeId and the current rendering component's scopeId is
  267. // different - this is a slot content node.
  268. if (treeOwnerId != null && treeOwnerId !== scopeId) {
  269. openTag += ` ${treeOwnerId}-s`
  270. }
  271. }
  272. push(openTag + `>`)
  273. if (!isVoidTag(tag)) {
  274. let hasChildrenOverride = false
  275. if (props !== null) {
  276. if (props.innerHTML) {
  277. hasChildrenOverride = true
  278. push(props.innerHTML)
  279. } else if (props.textContent) {
  280. hasChildrenOverride = true
  281. push(escapeHtml(props.textContent))
  282. } else if (tag === 'textarea' && props.value) {
  283. hasChildrenOverride = true
  284. push(escapeHtml(props.value))
  285. }
  286. }
  287. if (!hasChildrenOverride) {
  288. if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  289. push(escapeHtml(children as string))
  290. } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  291. renderVNodeChildren(
  292. push,
  293. children as VNodeArrayChildren,
  294. parentComponent
  295. )
  296. }
  297. }
  298. push(`</${tag}>`)
  299. }
  300. }
  301. function renderPortal(
  302. vnode: VNode,
  303. parentComponent: ComponentInternalInstance
  304. ) {
  305. const target = vnode.props && vnode.props.target
  306. if (!target) {
  307. console.warn(`[@vue/server-renderer] Portal is missing target prop.`)
  308. return []
  309. }
  310. if (!isString(target)) {
  311. console.warn(
  312. `[@vue/server-renderer] Portal target must be a query selector string.`
  313. )
  314. return []
  315. }
  316. const { getBuffer, push } = createBuffer()
  317. renderVNodeChildren(
  318. push,
  319. vnode.children as VNodeArrayChildren,
  320. parentComponent
  321. )
  322. const context = parentComponent.appContext.provides[
  323. ssrContextKey as any
  324. ] as SSRContext
  325. const portalBuffers =
  326. context.__portalBuffers || (context.__portalBuffers = {})
  327. portalBuffers[target] = getBuffer()
  328. }
  329. async function resolvePortals(context: SSRContext) {
  330. if (context.__portalBuffers) {
  331. context.portals = context.portals || {}
  332. for (const key in context.__portalBuffers) {
  333. // note: it's OK to await sequentially here because the Promises were
  334. // created eagerly in parallel.
  335. context.portals[key] = unrollBuffer(await context.__portalBuffers[key])
  336. }
  337. }
  338. }