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

This commit is contained in:
Evan You 2016-05-08 14:51:11 -04:00
commit 270b0ac021
7 changed files with 223 additions and 53 deletions

View File

@ -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
const staticRenderFns = this.$options.staticRenderFns
if (staticRenderFns) {
@ -47,13 +59,6 @@ export function lifecycleMixin (Vue) {
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
}
@ -105,7 +110,7 @@ export function lifecycleMixin (Vue) {
}
Vue.prototype.$forceUpdate = function () {
this._watcher.update()
this._update(this._render())
}
Vue.prototype.$destroy = function () {

View File

@ -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
}
}
@ -110,31 +113,37 @@ function destroy (vnode) {
function resolveAsyncComponent (factory, cb) {
if (factory.resolved) {
// cached
cb(factory.resolved)
return factory.resolved
} else if (factory.requested) {
// pool callbacks
factory.pendingCallbacks.push(cb)
} else {
factory.requested = true
const cbs = factory.pendingCallbacks = [cb]
factory(function resolve (res) {
factory.resolved = factory(function resolve (res) {
if (isObject(res)) {
res = Vue.extend(res)
}
// cache resolved
factory.resolved = res
// invoke callbacks
for (let i = 0, l = cbs.length; i < l; i++) {
cbs[i](res)
// Reset pending callbacks
factory.pendingCallbacks = []
}
return res
}, function reject (reason) {
process.env.NODE_ENV !== 'production' && warn(
`Failed to resolve async component: ${factory}` +
(reason ? `\nReason: ${reason}` : '')
)
})
return factory.resolved
}
}
function extractProps (data, Ctor) {

View File

@ -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
}
}

View File

@ -1,16 +1,28 @@
import RenderStream from './render-stream'
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) {
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 {
if (node.tag) {
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)
}
}

View File

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

View File

@ -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()

View File

@ -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: `
@ -98,8 +199,9 @@ describe('SSR: renderToString', () => {
<div id="ho" :class="{ red: isRed }"></div>
<span>{{ test }}</span>
<input :value="test">
<test></test>
<img :src="imageUrl">
<test></test>
<test-async></test-async>
</div>
`,
data: {
@ -109,9 +211,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(
@ -120,8 +229,9 @@ describe('SSR: renderToString', () => {
'<div id="ho" class="red"></div>' +
'<span>hi</span>' +
'<input value="hi">' +
'<div class="a">hahahaha</div>' +
'<img src="https://vuejs.org/images/logo.png">' +
'<div class="a">test</div>' +
'<span class="b">testAsync</span>' +
'</div>'
)
})