Evan You 10 лет назад
Родитель
Сommit
d08cf7c320

+ 1 - 0
build/build.js

@@ -68,6 +68,7 @@ var builds = [
   {
     entry: 'src/entries/web-server-renderer.js',
     format: 'cjs',
+    external: ['stream'],
     out: 'dist/server-renderer.js'
   }
 ]

+ 5 - 57
src/server/create-renderer.js

@@ -1,65 +1,13 @@
+import { createSyncRenderer } from './create-sync-renderer'
+import { createStreamingRenderer } from './create-streaming-renderer'
+
 export function createRenderer ({
   modules = [],
   directives = {},
   isUnaryTag = (() => false)
 } = {}) {
-  function renderComponent (component) {
-    component.$mount()
-    return renderNode(component._vnode)
-  }
-
-  function renderNode (node) {
-    if (node.componentOptions) {
-      node.data.hook.init(node)
-      return renderComponent(node.child)
-    } else {
-      return node.tag
-        ? renderElement(node)
-        : node.text
-    }
-  }
-
-  function renderElement (el) {
-    const startTag = renderStartingTag(el)
-    if (isUnaryTag(el.tag)) {
-      return startTag
-    } else {
-      const children = el.children
-        ? el.children.map(renderNode).join('')
-        : ''
-      return startTag + children + `</${el.tag}>`
-    }
-  }
-
-  function renderStartingTag (node) {
-    let markup = `<${node.tag}`
-    if (node.data) {
-      // check directives
-      const dirs = node.data.directives
-      if (dirs) {
-        for (let i = 0; i < dirs.length; i++) {
-          let dirRenderer = directives[dirs[i].name]
-          if (dirRenderer) {
-            // directives mutate the node's data
-            // which then gets rendered by modules
-            dirRenderer(node, dirs[i])
-          }
-        }
-      }
-      // apply other modules
-      for (let i = 0; i < modules.length; i++) {
-        let res = modules[i](node)
-        if (res) {
-          markup += res
-        }
-      }
-    }
-    return markup + '>'
-  }
-
   return {
-    renderToString (component) {
-      return renderComponent(component)
-    }
+    renderToString: createSyncRenderer(modules, directives, isUnaryTag),
+    renderToStream: createStreamingRenderer(modules, directives, isUnaryTag)
   }
 }

+ 55 - 0
src/server/create-streaming-renderer.js

@@ -0,0 +1,55 @@
+import RenderStream from './render-stream'
+import { renderStartingTag } from './render-starting-tag'
+
+export function createStreamingRenderer (modules, directives, isUnaryTag) {
+  function renderComponent (component, write, next) {
+    component.$mount()
+    renderNode(component._vnode, write, next)
+  }
+
+  function renderNode (node, write, next) {
+    if (node.componentOptions) {
+      node.data.hook.init(node)
+      renderComponent(node.child, write, next)
+    } else {
+      if (node.tag) {
+        renderElement(node, write, next)
+      } else {
+        write(node.text, next)
+      }
+    }
+  }
+
+  function renderElement (el, write, next) {
+    const startTag = renderStartingTag(el, modules, directives)
+    if (isUnaryTag(el.tag)) {
+      write(startTag, next)
+    } else if (!el.children || !el.children.length) {
+      write(startTag + `</${el.tag}>`, next)
+    } else {
+      write(startTag, () => {
+        const total = el.children.length
+        let rendered = 0
+
+        function renderChild (child) {
+          renderNode(child, write, () => {
+            rendered++
+            if (rendered < total) {
+              renderChild(el.children[rendered])
+            } else {
+              write(`</${el.tag}>`, next)
+            }
+          })
+        }
+
+        renderChild(el.children[0])
+      })
+    }
+  }
+
+  return function renderToStream (component) {
+    return new RenderStream((write, done) => {
+      renderComponent(component, write, done)
+    })
+  }
+}

+ 33 - 0
src/server/create-sync-renderer.js

@@ -0,0 +1,33 @@
+import { renderStartingTag } from './render-starting-tag'
+
+export function createSyncRenderer (modules, directives, isUnaryTag) {
+  function renderComponent (component) {
+    component.$mount()
+    return renderNode(component._vnode)
+  }
+
+  function renderNode (node) {
+    if (node.componentOptions) {
+      node.data.hook.init(node)
+      return renderComponent(node.child)
+    } else {
+      return node.tag
+        ? renderElement(node)
+        : node.text
+    }
+  }
+
+  function renderElement (el) {
+    const startTag = renderStartingTag(el, modules, directives)
+    if (isUnaryTag(el.tag)) {
+      return startTag
+    } else {
+      const children = el.children
+        ? el.children.map(renderNode).join('')
+        : ''
+      return startTag + children + `</${el.tag}>`
+    }
+  }
+
+  return renderComponent
+}

+ 25 - 0
src/server/render-starting-tag.js

@@ -0,0 +1,25 @@
+export function renderStartingTag (node, modules, directives) {
+  let markup = `<${node.tag}`
+  if (node.data) {
+    // check directives
+    const dirs = node.data.directives
+    if (dirs) {
+      for (let i = 0; i < dirs.length; i++) {
+        let dirRenderer = directives[dirs[i].name]
+        if (dirRenderer) {
+          // directives mutate the node's data
+          // which then gets rendered by modules
+          dirRenderer(node, dirs[i])
+        }
+      }
+    }
+    // apply other modules
+    for (let i = 0; i < modules.length; i++) {
+      let res = modules[i](node)
+      if (res) {
+        markup += res
+      }
+    }
+  }
+  return markup + '>'
+}

+ 68 - 0
src/server/render-stream.js

@@ -0,0 +1,68 @@
+import stream from 'stream'
+
+/**
+ * Original RenderStream implmentation by Sasha Aickin (@aickin)
+ * Licensed under the Apache License, Version 2.0
+ * Modified by Evan You (@yyx990803)
+ */
+
+export default class RenderStream extends stream.Readable {
+  constructor (render, options) {
+    super(options)
+    this.buffer = ''
+    this.render = render
+    this.maxStackDepth = 500
+    this.nextTickCalls = 0
+  }
+
+  _read (n) {
+    var bufferToPush
+    // it's possible that the last chunk added bumped the buffer up to > 2 * n,
+    // which means we will need to go through multiple read calls to drain it
+    // down to < n.
+    if (this.done) {
+      this.push(null)
+      return
+    }
+    if (this.buffer.length >= n) {
+      bufferToPush = this.buffer.substring(0, n)
+      this.buffer = this.buffer.substring(n)
+      this.push(bufferToPush)
+      return
+    }
+    if (!this.next) {
+      this.stackDepth = 0
+      // start the rendering chain.
+      this.render(
+        // write
+        (text, next) => {
+          this.buffer += text
+          if (this.buffer.length >= n) {
+            this.next = next
+            bufferToPush = this.buffer.substring(0, n)
+            this.buffer = this.buffer.substring(n)
+            this.push(bufferToPush)
+          } else {
+            // continue rendering until we have enough text to call this.push().
+            // sometimes do this as process.nextTick to get out of stack overflows.
+            if (this.stackDepth >= this.maxStackDepth) {
+              process.nextTick(next)
+            } else {
+              this.stackDepth++
+              next()
+              this.stackDepth--
+            }
+          }
+        },
+        // done
+        () => {
+          // the rendering is finished; we should push out the last of the buffer.
+          this.done = true
+          this.push(this.buffer)
+        })
+    } else {
+      // continue with the rendering.
+      this.next()
+    }
+  }
+}

+ 8 - 7
test/ssr/jasmine.json

@@ -1,9 +1,10 @@
 {
-    "spec_dir": "test/ssr",
-    "spec_files": [
-        "ssr.spec.js"
-    ],
-    "helpers": [
-      "../../node_modules/babel-register/lib/node.js"
-    ]
+  "spec_dir": "test/ssr",
+  "spec_files": [
+    "ssr.sync.spec.js",
+    "ssr.stream.spec.js"
+  ],
+  "helpers": [
+    "../../node_modules/babel-register/lib/node.js"
+  ]
 }

+ 47 - 0
test/ssr/ssr.stream.spec.js

@@ -0,0 +1,47 @@
+import Vue from '../../dist/vue.common.js'
+import { compileToFunctions } from '../../dist/compiler.common.js'
+import { renderToStream } from '../../dist/server-renderer.js'
+
+describe('SSR: renderToStream', () => {
+  it('should render to a stream', done => {
+    const stream = renderVmWithOptions({
+      template: `
+        <div>
+          <p class="hi">yoyo</p>
+          <div id="ho" :class="{ red: isRed }"></div>
+          <span>{{ test }}</span>
+          <test></test>
+        </div>
+      `,
+      data: {
+        test: 'hi',
+        isRed: true
+      },
+      components: {
+        test: {
+          render: function () {
+            return this.$createElement('div', { class: ['a'] }, 'hahahaha')
+          }
+        }
+      }
+    })
+
+    let res = ''
+    stream.on('data', chunk => {
+      res += chunk
+    })
+    stream.on('end', () => {
+      expect(res).toContain('<div><p class="hi">yoyo</p><div id="ho" class="red"></div><span>hi</span><div class="a">hahahaha</div></div>')
+      done()
+    })
+  })
+})
+
+function renderVmWithOptions (options) {
+  const res = compileToFunctions(options.template, {
+    preserveWhitespace: false
+  })
+  Object.assign(options, res)
+  delete options.template
+  return renderToStream(new Vue(options))
+}

+ 1 - 1
test/ssr/ssr.spec.js → test/ssr/ssr.sync.spec.js

@@ -2,7 +2,7 @@ import Vue from '../../dist/vue.common.js'
 import { compileToFunctions } from '../../dist/compiler.common.js'
 import { renderToString } from '../../dist/server-renderer.js'
 
-describe('Server side rendering', () => {
+describe('SSR: renderToString', () => {
   it('static attributes', () => {
     expect(renderVmWithOptions({
       template: '<div id="foo" bar="123"></div>'