render.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. /* @flow */
  2. import { cached } from 'shared/util'
  3. import { encodeHTML } from 'entities'
  4. import { createComponentInstanceForVnode } from 'core/vdom/create-component'
  5. const encodeHTMLCached = cached(encodeHTML)
  6. const normalizeAsync = (cache, method) => {
  7. const fn = cache[method]
  8. if (!fn) {
  9. return
  10. } else if (fn.length > 1) {
  11. return (key, cb) => fn.call(cache, key, cb)
  12. } else {
  13. return (key, cb) => cb(fn.call(cache, key))
  14. }
  15. }
  16. export function createRenderFunction (
  17. modules: Array<Function>,
  18. directives: Object,
  19. isUnaryTag: Function,
  20. cache: any
  21. ) {
  22. if (cache && (!cache.get || !cache.set)) {
  23. throw new Error('renderer cache must implement at least get & set.')
  24. }
  25. const get = cache && normalizeAsync(cache, 'get')
  26. const has = cache && normalizeAsync(cache, 'has')
  27. function renderNode (
  28. node: VNode,
  29. write: Function,
  30. next: Function,
  31. isRoot: boolean
  32. ) {
  33. if (node.componentOptions) {
  34. // check cache hit
  35. const Ctor = node.componentOptions.Ctor
  36. const getKey = Ctor.options.serverCacheKey
  37. if (getKey && cache) {
  38. const key = Ctor.cid + '::' + getKey(node.componentOptions.propsData)
  39. if (has) {
  40. has(key, hit => {
  41. if (hit) {
  42. get(key, res => write(res, next))
  43. } else {
  44. renderComponentWithCache(node, write, next, isRoot, cache, key)
  45. }
  46. })
  47. } else {
  48. get(key, res => {
  49. if (res) {
  50. write(res, next)
  51. } else {
  52. renderComponentWithCache(node, write, next, isRoot, cache, key)
  53. }
  54. })
  55. }
  56. } else {
  57. if (getKey) {
  58. console.error(
  59. `[vue-server-renderer] Component ${
  60. Ctor.options.name || '(anonymous)'
  61. } implemented serverCacheKey, ` +
  62. 'but no cache was provided to the renderer.'
  63. )
  64. }
  65. renderComponent(node, write, next, isRoot)
  66. }
  67. } else {
  68. if (node.tag) {
  69. renderElement(node, write, next, isRoot)
  70. } else {
  71. write(node.raw ? node.text : encodeHTMLCached(node.text), next)
  72. }
  73. }
  74. }
  75. function renderComponent (node, write, next, isRoot) {
  76. const child = createComponentInstanceForVnode(node)._render()
  77. child.parent = node
  78. renderNode(child, write, next, isRoot)
  79. }
  80. function renderComponentWithCache (node, write, next, isRoot, cache, key) {
  81. write.caching = true
  82. const buffer = write.cacheBuffer
  83. const bufferIndex = buffer.push('') - 1
  84. renderComponent(node, write, () => {
  85. const result = buffer[bufferIndex]
  86. cache.set(key, result)
  87. if (bufferIndex === 0) {
  88. // this is a top-level cached component,
  89. // exit caching mode.
  90. write.caching = false
  91. } else {
  92. // parent component is also being cached,
  93. // merge self into parent's result
  94. buffer[bufferIndex - 1] += result
  95. }
  96. buffer.length = bufferIndex
  97. next()
  98. }, isRoot)
  99. }
  100. function renderElement (el, write, next, isRoot) {
  101. if (isRoot) {
  102. if (!el.data) el.data = {}
  103. if (!el.data.attrs) el.data.attrs = {}
  104. el.data.attrs['server-rendered'] = 'true'
  105. }
  106. const startTag = renderStartingTag(el)
  107. const endTag = `</${el.tag}>`
  108. if (isUnaryTag(el.tag)) {
  109. write(startTag, next)
  110. } else if (!el.children || !el.children.length) {
  111. write(startTag + endTag, next)
  112. } else {
  113. const children: Array<VNode> = el.children || []
  114. write(startTag, () => {
  115. const total = children.length
  116. let rendered = 0
  117. function renderChild (child: VNode) {
  118. renderNode(child, write, () => {
  119. rendered++
  120. if (rendered < total) {
  121. renderChild(children[rendered])
  122. } else {
  123. write(endTag, next)
  124. }
  125. }, false)
  126. }
  127. renderChild(children[0])
  128. })
  129. }
  130. }
  131. function renderStartingTag (node: VNode) {
  132. let markup = `<${node.tag}`
  133. if (node.data) {
  134. // check directives
  135. const dirs = node.data.directives
  136. if (dirs) {
  137. for (let i = 0; i < dirs.length; i++) {
  138. const dirRenderer = directives[dirs[i].name]
  139. if (dirRenderer) {
  140. // directives mutate the node's data
  141. // which then gets rendered by modules
  142. dirRenderer(node, dirs[i])
  143. }
  144. }
  145. }
  146. // apply other modules
  147. for (let i = 0; i < modules.length; i++) {
  148. const res = modules[i](node)
  149. if (res) {
  150. markup += res
  151. }
  152. }
  153. }
  154. // attach scoped CSS ID
  155. let scopeId
  156. if (node.host && (scopeId = node.host.$options._scopeId)) {
  157. markup += ` ${scopeId}`
  158. }
  159. while (node) {
  160. if ((scopeId = node.context.$options._scopeId)) {
  161. markup += ` ${scopeId}`
  162. }
  163. node = node.parent
  164. }
  165. return markup + '>'
  166. }
  167. return function render (
  168. component: Component,
  169. write: (text: string, next: Function) => void,
  170. done: Function
  171. ) {
  172. renderNode(component._render(), write, done, true)
  173. }
  174. }