From 351aef3cb4ada980f105f98f8b835876e8d4d689 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 5 Aug 2016 01:52:09 -0400 Subject: [PATCH] use comment node as empty placeholder (fix SSR hydration) --- src/core/vdom/patch.js | 23 ++++++++++++------- src/core/vdom/vnode.js | 9 +++++++- src/platforms/web/runtime/node-ops.js | 4 ++++ src/server/render.js | 2 ++ test/ssr/ssr-string.spec.js | 9 ++++++++ .../component/component-async.spec.js | 2 +- .../component/component-keep-alive.spec.js | 12 +++++----- .../unit/features/component/component.spec.js | 2 +- .../transition/transition-mode.spec.js | 12 +++++----- .../features/transition/transition.spec.js | 10 ++++---- 10 files changed, 57 insertions(+), 28 deletions(-) diff --git a/src/core/vdom/patch.js b/src/core/vdom/patch.js index 799e508bd7a..a24c6237789 100644 --- a/src/core/vdom/patch.js +++ b/src/core/vdom/patch.js @@ -30,6 +30,7 @@ function sameVnode (vnode1, vnode2) { return ( vnode1.key === vnode2.key && vnode1.tag === vnode2.tag && + vnode1.isComment === vnode2.isComment && !vnode1.data === !vnode2.data ) } @@ -87,12 +88,7 @@ export function createPatchFunction (backend) { // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(i = vnode.child)) { - if (vnode.data.pendingInsert) { - insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert) - } - vnode.elm = vnode.child.$el - invokeCreateHooks(vnode, insertedVnodeQueue) - setScope(vnode) + initComponent(vnode, insertedVnodeQueue) return vnode.elm } } @@ -127,6 +123,8 @@ export function createPatchFunction (backend) { if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } + } else if (vnode.isComment) { + elm = vnode.elm = nodeOps.createComment(vnode.text) } else { elm = vnode.elm = nodeOps.createTextNode(vnode.text) } @@ -144,6 +142,15 @@ export function createPatchFunction (backend) { } } + function initComponent (vnode, insertedVnodeQueue) { + if (vnode.data.pendingInsert) { + insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert) + } + vnode.elm = vnode.child.$el + invokeCreateHooks(vnode, insertedVnodeQueue) + setScope(vnode) + } + // set scope id attribute for scoped CSS. // this is implemented as a special case to avoid the overhead // of going through the normal attribute patching process. @@ -360,7 +367,7 @@ export function createPatchFunction (backend) { if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode, true /* hydrating */) if (isDef(i = vnode.child)) { // child component. it should have hydrated its own tree. - invokeCreateHooks(vnode, insertedVnodeQueue) + initComponent(vnode, insertedVnodeQueue) return true } } @@ -425,7 +432,7 @@ export function createPatchFunction (backend) { // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. - if (oldVnode.hasAttribute('server-rendered')) { + if (oldVnode.nodeType === 1 && oldVnode.hasAttribute('server-rendered')) { oldVnode.removeAttribute('server-rendered') hydrating = true } diff --git a/src/core/vdom/vnode.js b/src/core/vdom/vnode.js index 08d17f0f577..bcd8737acee 100644 --- a/src/core/vdom/vnode.js +++ b/src/core/vdom/vnode.js @@ -16,6 +16,7 @@ export default class VNode { raw: ?boolean; // contains raw HTML isStatic: ?boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check + isComment: boolean; constructor ( tag?: string, @@ -43,6 +44,7 @@ export default class VNode { this.raw = false this.isStatic = false this.isRootInsert = true + this.isComment = false // apply construct hook. // this is applied during render, before patch happens. // unlike other hooks, this is applied on both client and server. @@ -53,4 +55,9 @@ export default class VNode { } } -export const emptyVNode = () => new VNode(undefined, undefined, undefined, '') +export const emptyVNode = () => { + const node = new VNode() + node.text = '' + node.isComment = true + return node +} diff --git a/src/platforms/web/runtime/node-ops.js b/src/platforms/web/runtime/node-ops.js index 281f3155139..fb2def45f5c 100644 --- a/src/platforms/web/runtime/node-ops.js +++ b/src/platforms/web/runtime/node-ops.js @@ -14,6 +14,10 @@ export function createTextNode (text: string): Text { return document.createTextNode(text) } +export function createComment (text: string): Comment { + return document.createComment(text) +} + export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) { parentNode.insertBefore(newNode, referenceNode) } diff --git a/src/server/render.js b/src/server/render.js index 3db57ef4086..b25b70d34a9 100644 --- a/src/server/render.js +++ b/src/server/render.js @@ -83,6 +83,8 @@ export function createRenderFunction ( } else { if (node.tag) { renderElement(node, write, next, isRoot) + } else if (node.isComment) { + write(``, next) } else { write(node.raw ? node.text : encodeHTML(String(node.text)), next) } diff --git a/test/ssr/ssr-string.spec.js b/test/ssr/ssr-string.spec.js index 1252df10b25..e11039e8593 100644 --- a/test/ssr/ssr-string.spec.js +++ b/test/ssr/ssr-string.spec.js @@ -500,6 +500,15 @@ describe('SSR: renderToString', () => { }) }) + it('comment nodes', done => { + renderVmWithOptions({ + template: '
' + }, result => { + expect(result).toContain(`
`) + done() + }) + }) + it('should catch error', done => { renderToString(new Vue({ render () { diff --git a/test/unit/features/component/component-async.spec.js b/test/unit/features/component/component-async.spec.js index ce13deaa732..8d997050adb 100644 --- a/test/unit/features/component/component-async.spec.js +++ b/test/unit/features/component/component-async.spec.js @@ -40,7 +40,7 @@ describe('Component async', () => { } } }).$mount() - expect(vm.$el.nodeType).toBe(3) + expect(vm.$el.nodeType).toBe(8) expect(vm.$children.length).toBe(0) function next () { expect(vm.$el.nodeType).toBe(1) diff --git a/test/unit/features/component/component-keep-alive.spec.js b/test/unit/features/component/component-keep-alive.spec.js index 7b5a176d04d..4336475680a 100644 --- a/test/unit/features/component/component-keep-alive.spec.js +++ b/test/unit/features/component/component-keep-alive.spec.js @@ -105,16 +105,16 @@ describe('Component keep-alive', () => { vm.view = 'two' waitForUpdate(() => { expect(vm.$el.innerHTML).toBe( - '
one
' + '
one
' ) assertHookCalls(one, [1, 1, 1, 1, 0]) assertHookCalls(two, [0, 0, 0, 0, 0]) }).thenWaitFor(nextFrame).then(() => { expect(vm.$el.innerHTML).toBe( - '
one
' + '
one
' ) }).thenWaitFor(_next => { next = _next }).then(() => { - expect(vm.$el.innerHTML).toBe('') + expect(vm.$el.innerHTML).toBe('') }).thenWaitFor(nextFrame).then(() => { expect(vm.$el.innerHTML).toBe( '
two
' @@ -135,16 +135,16 @@ describe('Component keep-alive', () => { vm.view = 'one' }).then(() => { expect(vm.$el.innerHTML).toBe( - '
two
' + '
two
' ) assertHookCalls(one, [1, 1, 1, 1, 0]) assertHookCalls(two, [1, 1, 1, 1, 0]) }).thenWaitFor(nextFrame).then(() => { expect(vm.$el.innerHTML).toBe( - '
two
' + '
two
' ) }).thenWaitFor(_next => { next = _next }).then(() => { - expect(vm.$el.innerHTML).toBe('') + expect(vm.$el.innerHTML).toBe('') }).thenWaitFor(nextFrame).then(() => { expect(vm.$el.innerHTML).toBe( '
one
' diff --git a/test/unit/features/component/component.spec.js b/test/unit/features/component/component.spec.js index d0bfa1aca49..cb4a5c2c52b 100644 --- a/test/unit/features/component/component.spec.js +++ b/test/unit/features/component/component.spec.js @@ -107,7 +107,7 @@ describe('Component', () => { vm.view = '' }) .then(() => { - expect(vm.$el.nodeType).toBe(3) + expect(vm.$el.nodeType).toBe(8) expect(vm.$el.data).toBe('') }).then(done) }) diff --git a/test/unit/features/transition/transition-mode.spec.js b/test/unit/features/transition/transition-mode.spec.js index e4c20495848..d4dbb7914e2 100644 --- a/test/unit/features/transition/transition-mode.spec.js +++ b/test/unit/features/transition/transition-mode.spec.js @@ -68,14 +68,14 @@ if (!isIE9) { vm.view = 'two' waitForUpdate(() => { expect(vm.$el.innerHTML).toBe( - '
one
' + '
one
' ) }).thenWaitFor(nextFrame).then(() => { expect(vm.$el.innerHTML).toBe( - '
one
' + '
one
' ) }).thenWaitFor(_next => { next = _next }).then(() => { - expect(vm.$el.innerHTML).toBe('') + expect(vm.$el.innerHTML).toBe('') }).thenWaitFor(nextFrame).then(() => { expect(vm.$el.innerHTML).toBe( '
two
' @@ -257,14 +257,14 @@ if (!isIE9) { vm.view = 'two' waitForUpdate(() => { expect(vm.$el.innerHTML).toBe( - '
one
' + '
one
' ) }).thenWaitFor(nextFrame).then(() => { expect(vm.$el.innerHTML).toBe( - '
one
' + '
one
' ) }).thenWaitFor(_next => { next = _next }).then(() => { - expect(vm.$el.innerHTML).toBe('') + expect(vm.$el.innerHTML).toBe('') }).thenWaitFor(nextFrame).then(() => { expect(vm.$el.innerHTML).toBe( '
two
' diff --git a/test/unit/features/transition/transition.spec.js b/test/unit/features/transition/transition.spec.js index b47ac04d2ef..1c03a9a7cb3 100644 --- a/test/unit/features/transition/transition.spec.js +++ b/test/unit/features/transition/transition.spec.js @@ -316,7 +316,7 @@ if (!isIE9) { vm.ok = false waitForUpdate(() => { expect(leaveSpy).toHaveBeenCalled() - expect(vm.$el.innerHTML).toBe('') + expect(vm.$el.innerHTML).toBe('') vm.ok = true }).then(() => { expect(enterSpy).toHaveBeenCalled() @@ -339,9 +339,9 @@ if (!isIE9) { vm.ok = false waitForUpdate(() => { expect(leaveSpy).toHaveBeenCalled() - expect(vm.$el.innerHTML).toBe('
foo
') + expect(vm.$el.innerHTML).toBe('
foo
') }).thenWaitFor(nextFrame).then(() => { - expect(vm.$el.innerHTML).toBe('') + expect(vm.$el.innerHTML).toBe('') vm.ok = true }).then(() => { expect(enterSpy).toHaveBeenCalled() @@ -367,7 +367,7 @@ if (!isIE9) { } }).$mount(el) - expect(vm.$el.innerHTML).toBe('') + expect(vm.$el.innerHTML).toBe('') vm.ok = true waitForUpdate(() => { expect(vm.$el.children[0].className).toBe('test test-enter test-enter-active') @@ -652,7 +652,7 @@ if (!isIE9) { expect(vm.$el.childNodes[0].getAttribute('class')).toBe('test v-leave-active') }).thenWaitFor(duration + 10).then(() => { expect(vm.$el.childNodes.length).toBe(1) - expect(vm.$el.childNodes[0].nodeType).toBe(3) // should be an empty text node + expect(vm.$el.childNodes[0].nodeType).toBe(8) // should be an empty comment node expect(vm.$el.childNodes[0].textContent).toBe('') vm.ok = true }).then(() => {