Procházet zdrojové kódy

Merge branch 'ssr-improvements' of git://github.com/blake-newman/vue into ssr

Evan You před 10 roky
rodič
revize
270b0ac021

+ 13 - 8
src/core/instance/lifecycle.js

@@ -39,6 +39,18 @@ export function lifecycleMixin (Vue) {
         }
         }
       }
       }
     }
     }
+    this.renderStaticTrees()
+    this._watcher = new Watcher(this, this._render, this._update)
+    this._update(this._watcher.value)
+    this._mounted = true
+    // root instance, call ready on self
+    if (this.$root === this) {
+      callHook(this, 'ready')
+    }
+    return this
+  }
+
+  Vue.prototype._renderStaticTrees = function () {
     // render static sub-trees for once on mount
     // render static sub-trees for once on mount
     const staticRenderFns = this.$options.staticRenderFns
     const staticRenderFns = this.$options.staticRenderFns
     if (staticRenderFns) {
     if (staticRenderFns) {
@@ -47,13 +59,6 @@ export function lifecycleMixin (Vue) {
         this._staticTrees[i] = staticRenderFns[i].call(this._renderProxy)
         this._staticTrees[i] = staticRenderFns[i].call(this._renderProxy)
       }
       }
     }
     }
-    this._watcher = new Watcher(this, this._render, this._update)
-    this._update(this._watcher.value)
-    this._mounted = true
-    // root instance, call ready on self
-    if (this.$root === this) {
-      callHook(this, 'ready')
-    }
     return this
     return this
   }
   }
 
 
@@ -105,7 +110,7 @@ export function lifecycleMixin (Vue) {
   }
   }
 
 
   Vue.prototype.$forceUpdate = function () {
   Vue.prototype.$forceUpdate = function () {
-    this._watcher.update()
+    this._update(this._render())
   }
   }
 
 
   Vue.prototype.$destroy = function () {
   Vue.prototype.$destroy = function () {

+ 14 - 5
src/core/vdom/create-component.js

@@ -31,12 +31,15 @@ export function createComponent (Ctor, data, parent, children, context) {
     if (Ctor.resolved) {
     if (Ctor.resolved) {
       Ctor = Ctor.resolved
       Ctor = Ctor.resolved
     } else {
     } else {
-      resolveAsyncComponent(Ctor, () => {
+      const resolved = resolveAsyncComponent(Ctor, () => {
         // it's ok to queue this on every render because
         // it's ok to queue this on every render because
         // $forceUpdate is buffered.
         // $forceUpdate is buffered.
         parent.$forceUpdate()
         parent.$forceUpdate()
       })
       })
-      return
+      if (!resolved || !resolved.cid) {
+        return
+      }
+      Ctor = resolved
     }
     }
   }
   }
 
 
@@ -110,31 +113,37 @@ function destroy (vnode) {
 
 
 function resolveAsyncComponent (factory, cb) {
 function resolveAsyncComponent (factory, cb) {
   if (factory.resolved) {
   if (factory.resolved) {
-    // cached
-    cb(factory.resolved)
+    return factory.resolved
   } else if (factory.requested) {
   } else if (factory.requested) {
     // pool callbacks
     // pool callbacks
     factory.pendingCallbacks.push(cb)
     factory.pendingCallbacks.push(cb)
   } else {
   } else {
     factory.requested = true
     factory.requested = true
     const cbs = factory.pendingCallbacks = [cb]
     const cbs = factory.pendingCallbacks = [cb]
-    factory(function resolve (res) {
+    factory.resolved = factory(function resolve (res) {
       if (isObject(res)) {
       if (isObject(res)) {
         res = Vue.extend(res)
         res = Vue.extend(res)
       }
       }
       // cache resolved
       // cache resolved
       factory.resolved = res
       factory.resolved = res
+
       // invoke callbacks
       // invoke callbacks
       for (let i = 0, l = cbs.length; i < l; i++) {
       for (let i = 0, l = cbs.length; i < l; i++) {
         cbs[i](res)
         cbs[i](res)
+        // Reset pending callbacks
+        factory.pendingCallbacks = []
       }
       }
+
+      return res
     }, function reject (reason) {
     }, function reject (reason) {
       process.env.NODE_ENV !== 'production' && warn(
       process.env.NODE_ENV !== 'production' && warn(
         `Failed to resolve async component: ${factory}` +
         `Failed to resolve async component: ${factory}` +
         (reason ? `\nReason: ${reason}` : '')
         (reason ? `\nReason: ${reason}` : '')
       )
       )
     })
     })
+    return factory.resolved
   }
   }
+
 }
 }
 
 
 function extractProps (data, Ctor) {
 function extractProps (data, Ctor) {

+ 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 ({
 export function createRenderer ({
   modules = [],
   modules = [],
   directives = {},
   directives = {},
   isUnaryTag = (() => false)
   isUnaryTag = (() => false)
 } = {}) {
 } = {}) {
+  function _render (component, write, done) {
+    render(modules, directives, isUnaryTag)(component, write, done)
+  }
+
   return {
   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
   }
   }
 }
 }

+ 24 - 13
src/server/create-streaming-renderer.js → src/server/render.js

@@ -1,16 +1,28 @@
-import RenderStream from './render-stream'
 import { renderStartingTag } from './render-starting-tag'
 import { renderStartingTag } from './render-starting-tag'
 
 
-export function createStreamingRenderer (modules, directives, isUnaryTag) {
-  function renderComponent (component, write, next, isRoot) {
-    component.$mount()
-    renderNode(component._vnode, write, next, isRoot)
-  }
-
+export function render (modules, directives, isUnaryTag) {
   function renderNode (node, write, next, isRoot) {
   function renderNode (node, write, next, isRoot) {
     if (node.componentOptions) {
     if (node.componentOptions) {
-      node.data.hook.init(node)
-      renderComponent(node.child, write, next, isRoot)
+      const { Ctor, propsData, listeners, parent, children } = node.componentOptions
+      const options = {
+        parent,
+        propsData,
+        _parentVnode: node,
+        _parentListeners: listeners,
+        _renderChildren: children
+      }
+      // check inline-template render functions
+      const inlineTemplate = node.data.inlineTemplate
+      if (inlineTemplate) {
+        options.render = inlineTemplate.render
+        options.staticRenderFns = inlineTemplate.staticRenderFns
+      }
+      const child = new Ctor(options)
+      child._mount = () => {
+        child._renderStaticTrees()
+        renderNode(child._render(), write, next)
+      }
+      child.$mount(node.elm)
     } else {
     } else {
       if (node.tag) {
       if (node.tag) {
         renderElement(node, write, next, isRoot)
         renderElement(node, write, next, isRoot)
@@ -53,9 +65,8 @@ 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) {
+    component._renderStaticTrees()
+    renderNode(component._render(), write, done, true)
   }
   }
 }
 }

+ 1 - 1
test/ssr/jasmine.json

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

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

@@ -9,25 +9,43 @@ describe('SSR: renderToStream', () => {
       template: `
       template: `
         <div>
         <div>
           <p class="hi">yoyo</p>
           <p class="hi">yoyo</p>
-          <div id="ho" :class="{ red: isRed }"></div>
+          <div id="ho" :class="[testClass, { red: isRed }]"></div>
           <span>{{ test }}</span>
           <span>{{ test }}</span>
           <input :value="test">
           <input :value="test">
-          <test></test>
+          <b-comp></b-comp>
+          <c-comp></c-comp>
         </div>
         </div>
       `,
       `,
       data: {
       data: {
         test: 'hi',
         test: 'hi',
-        isRed: true
+        isRed: true,
+        testClass: 'a'
       },
       },
       components: {
       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 = ''
     let res = ''
     stream.on('data', chunk => {
     stream.on('data', chunk => {
       res += chunk
       res += chunk
@@ -35,11 +53,12 @@ describe('SSR: renderToStream', () => {
     stream.on('end', () => {
     stream.on('end', () => {
       expect(res).toContain(
       expect(res).toContain(
         '<div server-rendered="true">' +
         '<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>'
         '</div>'
       )
       )
       done()
       done()

+ 120 - 10
test/ssr/ssr.sync.spec.js → test/ssr/ssr.string.spec.js

@@ -52,7 +52,9 @@ describe('SSR: renderToString', () => {
         fontSize: 14,
         fontSize: 14,
         color: 'red'
         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', () => {
   it('text interpolation', () => {
@@ -75,11 +77,7 @@ describe('SSR: renderToString', () => {
         child: {
         child: {
           props: ['msg'],
           props: ['msg'],
           data () {
           data () {
-            return { name: 'foo' }
-          },
-          created () {
-            // checking setting state in created hook works in ssr
-            this.name = 'bar'
+            return { name: 'bar' }
           },
           },
           render () {
           render () {
             const h = this.$createElement
             const h = this.$createElement
@@ -90,6 +88,109 @@ describe('SSR: renderToString', () => {
     })).toContain('<div server-rendered="true" class="foo bar">hello bar</div>')
     })).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', () => {
   it('everything together', () => {
     expect(renderVmWithOptions({
     expect(renderVmWithOptions({
       template: `
       template: `
@@ -98,8 +199,9 @@ describe('SSR: renderToString', () => {
           <div id="ho" :class="{ red: isRed }"></div>
           <div id="ho" :class="{ red: isRed }"></div>
           <span>{{ test }}</span>
           <span>{{ test }}</span>
           <input :value="test">
           <input :value="test">
-          <test></test>
           <img :src="imageUrl">
           <img :src="imageUrl">
+          <test></test>
+          <test-async></test-async>
         </div>
         </div>
       `,
       `,
       data: {
       data: {
@@ -109,9 +211,16 @@ describe('SSR: renderToString', () => {
       },
       },
       components: {
       components: {
         test: {
         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(
     })).toContain(
@@ -120,8 +229,9 @@ describe('SSR: renderToString', () => {
         '<div id="ho" class="red"></div>' +
         '<div id="ho" class="red"></div>' +
         '<span>hi</span>' +
         '<span>hi</span>' +
         '<input value="hi">' +
         '<input value="hi">' +
-        '<div class="a">hahahaha</div>' +
         '<img src="https://vuejs.org/images/logo.png">' +
         '<img src="https://vuejs.org/images/logo.png">' +
+        '<div class="a">test</div>' +
+        '<span class="b">testAsync</span>' +
       '</div>'
       '</div>'
     )
     )
   })
   })