Просмотр исходного кода

improved SSR rendering

- Reduced duplicated code
- Added more test coverage
- Added syncronous component resolving support
- Fixed component lifecycle
- Added raw render method, to allow custom streaming possibilities
Blake Newman 10 лет назад
Родитель
Сommit
a233e4dbfa

+ 1 - 1
src/core/instance/lifecycle.js

@@ -105,7 +105,7 @@ export function lifecycleMixin (Vue) {
   }
 
   Vue.prototype.$forceUpdate = function () {
-    this._watcher.update()
+    this._update(this._render())
   }
 
   Vue.prototype.$destroy = function () {

+ 7 - 3
src/core/vdom/create-component.js

@@ -31,12 +31,15 @@ export function createComponent (Ctor, data, parent, children, context) {
     if (Ctor.resolved) {
       Ctor = Ctor.resolved
     } else {
-      resolveAsyncComponent(Ctor, () => {
+      const resolved = resolveAsyncComponent(Ctor, () => {
         // it's ok to queue this on every render because
         // $forceUpdate is buffered.
         parent.$forceUpdate()
       })
-      return
+      if (!resolved || !resolved.cid) {
+        return
+      }
+      Ctor = resolved
     }
   }
 
@@ -118,7 +121,7 @@ function resolveAsyncComponent (factory, cb) {
   } else {
     factory.requested = true
     const cbs = factory.pendingCallbacks = [cb]
-    factory(function resolve (res) {
+    return factory(function resolve (res) {
       if (isObject(res)) {
         res = Vue.extend(res)
       }
@@ -128,6 +131,7 @@ function resolveAsyncComponent (factory, cb) {
       for (let i = 0, l = cbs.length; i < l; i++) {
         cbs[i](res)
       }
+      return res
     }, function reject (reason) {
       process.env.NODE_ENV !== 'production' && warn(
         `Failed to resolve async component: ${factory}` +

+ 20 - 4
src/server/create-renderer.js

@@ -1,13 +1,29 @@
-import { createSyncRenderer } from './create-sync-renderer'
-import { createStreamingRenderer } from './create-streaming-renderer'
+import RenderStream from './render-stream'
+import { render } from './render'
 
 export function createRenderer ({
   modules = [],
   directives = {},
   isUnaryTag = (() => false)
 } = {}) {
+  function _render (component, write, done) {
+    render(modules, directives, isUnaryTag)(component, write, done)
+  }
+
   return {
-    renderToString: createSyncRenderer(modules, directives, isUnaryTag),
-    renderToStream: createStreamingRenderer(modules, directives, isUnaryTag)
+    renderToString (component) {
+      let result = ''
+      _render(component, (str, next) => {
+        result += str
+        next && next()
+      })
+      return result
+    },
+    renderToStream (component) {
+      return new RenderStream((write, done) => {
+        _render(component, write, done)
+      })
+    },
+    render: _render
   }
 }

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

@@ -1,44 +0,0 @@
-import { renderStartingTag } from './render-starting-tag'
-
-export function createSyncRenderer (modules, directives, isUnaryTag) {
-  function renderComponent (component, isRoot) {
-    component.$mount()
-    return renderNode(component._vnode, isRoot)
-  }
-
-  function renderNode (node, isRoot) {
-    if (node.componentOptions) {
-      node.data.hook.init(node)
-      return renderComponent(node.child, isRoot)
-    } else {
-      return node.tag
-        ? renderElement(node, isRoot)
-        : node.text
-    }
-  }
-
-  function renderElement (el, isRoot) {
-    if (isRoot) {
-      if (!el.data) el.data = {}
-      if (!el.data.attrs) el.data.attrs = {}
-      el.data.attrs['server-rendered'] = true
-    }
-    const startTag = renderStartingTag(el, modules, directives)
-    const endTag = `</${el.tag}>`
-    if (isUnaryTag(el.tag)) {
-      return startTag
-    } else if (!el.children || !el.children.length) {
-      return startTag + endTag
-    } else {
-      let children = ''
-      for (let i = 0; i < el.children.length; i++) {
-        children += renderNode(el.children[i])
-      }
-      return startTag + children + endTag
-    }
-  }
-
-  return function renderToString (component) {
-    return renderComponent(component, true)
-  }
-}

+ 3 - 6
src/server/create-streaming-renderer.js → src/server/render.js

@@ -1,7 +1,6 @@
-import RenderStream from './render-stream'
 import { renderStartingTag } from './render-starting-tag'
 
-export function createStreamingRenderer (modules, directives, isUnaryTag) {
+export function render (modules, directives, isUnaryTag) {
   function renderComponent (component, write, next, isRoot) {
     component.$mount()
     renderNode(component._vnode, write, next, isRoot)
@@ -53,9 +52,7 @@ export function createStreamingRenderer (modules, directives, isUnaryTag) {
     }
   }
 
-  return function renderToStream (component) {
-    return new RenderStream((write, done) => {
-      renderComponent(component, write, done, true)
-    })
+  return function render (component, write, done) {
+    renderComponent(component, write, done, true)
   }
 }

+ 1 - 1
test/ssr/jasmine.json

@@ -1,7 +1,7 @@
 {
   "spec_dir": "test/ssr",
   "spec_files": [
-    "ssr.sync.spec.js",
+    "ssr.string.spec.js",
     "ssr.stream.spec.js"
   ],
   "helpers": [

+ 31 - 12
test/ssr/ssr.stream.spec.js

@@ -9,25 +9,43 @@ describe('SSR: renderToStream', () => {
       template: `
         <div>
           <p class="hi">yoyo</p>
-          <div id="ho" :class="{ red: isRed }"></div>
+          <div id="ho" :class="[testClass, { red: isRed }]"></div>
           <span>{{ test }}</span>
           <input :value="test">
-          <test></test>
+          <b-comp></b-comp>
+          <c-comp></c-comp>
         </div>
       `,
       data: {
         test: 'hi',
-        isRed: true
+        isRed: true,
+        testClass: 'a'
       },
       components: {
-        test: {
-          render: function () {
-            return this.$createElement('div', { class: ['a'] }, 'hahahaha')
+        bComp (resolve) {
+          return resolve({
+            render () {
+              return this.$createElement('test-async-2')
+            },
+            components: {
+              testAsync2 (resolve) {
+                return resolve({
+                  created () { this.$parent.$parent.testClass = 'b' },
+                  render () {
+                    return this.$createElement('div', { class: [this.$parent.$parent.testClass] }, 'test')
+                  }
+                })
+              }
+            }
+          })
+        },
+        cComp: {
+          render () {
+            return this.$createElement('div', { class: [this.$parent.testClass] }, 'test')
           }
         }
       }
     })
-
     let res = ''
     stream.on('data', chunk => {
       res += chunk
@@ -35,11 +53,12 @@ describe('SSR: renderToStream', () => {
     stream.on('end', () => {
       expect(res).toContain(
         '<div server-rendered="true">' +
-          '<p class="hi">yoyo</p>' +
-          '<div id="ho" class="red"></div>' +
-          '<span>hi</span>' +
-          '<input value="hi">' +
-          '<div class="a">hahahaha</div>' +
+        '<p class="hi">yoyo</p>' +
+        '<div id="ho" class="a red"></div>' +
+        '<span>hi</span>' +
+        '<input value="hi">' +
+        '<div class="b">test</div>' +
+        '<div class="b">test</div>' +
         '</div>'
       )
       done()

+ 119 - 9
test/ssr/ssr.sync.spec.js → test/ssr/ssr.string.spec.js

@@ -52,7 +52,9 @@ describe('SSR: renderToString', () => {
         fontSize: 14,
         color: 'red'
       }
-    })).toContain('<div server-rendered="true" style="font-size:14px;color:red;background-color:black"></div>')
+    })).toContain(
+        '<div server-rendered="true" style="font-size:14px;color:red;background-color:black"></div>'
+      )
   })
 
   it('text interpolation', () => {
@@ -75,11 +77,7 @@ describe('SSR: renderToString', () => {
         child: {
           props: ['msg'],
           data () {
-            return { name: 'foo' }
-          },
-          created () {
-            // checking setting state in created hook works in ssr
-            this.name = 'bar'
+            return { name: 'bar' }
           },
           render () {
             const h = this.$createElement
@@ -90,6 +88,109 @@ describe('SSR: renderToString', () => {
     })).toContain('<div server-rendered="true" class="foo bar">hello bar</div>')
   })
 
+  it('has correct lifecycle during render', () => {
+    let lifecycleCount = 1
+    expect(renderVmWithOptions({
+      template: '<div><span>{{ val }}</span><test></test></div>',
+      data: {
+        val: 'hi'
+      },
+      init () {
+        expect(lifecycleCount++).toBe(1)
+      },
+      created () {
+        this.val = 'hello'
+        expect(this.val).toBe('hello')
+        expect(lifecycleCount++).toBe(2)
+      },
+      components: {
+        test: {
+          init () {
+            expect(lifecycleCount++).toBe(3)
+          },
+          created () {
+            expect(lifecycleCount++).toBe(4)
+          },
+          render () {
+            expect(lifecycleCount++).toBeGreaterThan(4)
+            return this.$createElement('span', { class: ['b'] }, 'testAsync')
+          }
+        }
+      }
+    })).toContain(
+      '<div server-rendered="true">' +
+        '<span>hello</span>' +
+        '<span class="b">testAsync</span>' +
+      '</div>'
+    )
+  })
+
+  it('renders asynchronous component', () => {
+    expect(renderVmWithOptions({
+      template: `
+        <div>
+          <test-async></test-async>
+        </div>
+      `,
+      components: {
+        testAsync (resolve) {
+          return resolve({
+            render () {
+              return this.$createElement('span', { class: ['b'] }, 'testAsync')
+            }
+          })
+        }
+      }
+    })).toContain('<div server-rendered="true"><span class="b">testAsync</span></div>')
+  })
+
+  it('renders asynchronous component (hoc)', () => {
+    expect(renderVmWithOptions({
+      template: '<test-async></test-async>',
+      components: {
+        testAsync (resolve) {
+          return resolve({
+            render () {
+              return this.$createElement('span', { class: ['b'] }, 'testAsync')
+            }
+          })
+        }
+      }
+    })).toContain('<span server-rendered="true" class="b">testAsync</span>')
+  })
+
+  it('renders nested asynchronous component', () => {
+    expect(renderVmWithOptions({
+      template: `
+        <div>
+          <test-async></test-async>
+        </div>
+      `,
+      components: {
+        testAsync (resolve) {
+          const options = compileToFunctions(`
+            <span class="b">
+              <test-sub-async></test-sub-async>
+            </span>
+          `, { preserveWhitespace: false })
+
+          options.components = {
+            testSubAsync (resolve) {
+              return resolve({
+                render () {
+                  return this.$createElement('div', { class: ['c'] }, 'testSubAsync')
+                }
+              })
+            }
+          }
+          return resolve(options)
+        }
+      }
+    })).toContain(
+      '<div server-rendered="true"><span class="b"><div class="c">testSubAsync</div></span></div>'
+    )
+  })
+
   it('everything together', () => {
     expect(renderVmWithOptions({
       template: `
@@ -99,6 +200,7 @@ describe('SSR: renderToString', () => {
           <span>{{ test }}</span>
           <input :value="test">
           <test></test>
+          <test-async></test-async>
         </div>
       `,
       data: {
@@ -107,9 +209,16 @@ describe('SSR: renderToString', () => {
       },
       components: {
         test: {
-          render: function () {
-            return this.$createElement('div', { class: ['a'] }, 'hahahaha')
+          render () {
+            return this.$createElement('div', { class: ['a'] }, 'test')
           }
+        },
+        testAsync (resolve) {
+          return resolve({
+            render () {
+              return this.$createElement('span', { class: ['b'] }, 'testAsync')
+            }
+          })
         }
       }
     })).toContain(
@@ -118,7 +227,8 @@ describe('SSR: renderToString', () => {
         '<div id="ho" class="red"></div>' +
         '<span>hi</span>' +
         '<input value="hi">' +
-        '<div class="a">hahahaha</div>' +
+        '<div class="a">test</div>' +
+        '<span class="b">testAsync</span>' +
       '</div>'
     )
   })