renderToString.ts 8.1 KB

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