diff --git a/flow/component.js b/flow/component.js index 51409a3f..01206aa3 100644 --- a/flow/component.js +++ b/flow/component.js @@ -26,7 +26,7 @@ declare interface Component { $isServer: boolean; // public methods - $mount: (el?: Element | string) => Component; + $mount: (el?: Element | string, hydrating?: boolean) => Component; $forceUpdate: () => void; $destroy: () => void; $watch: (expOrFn: string | Function, cb: Function, options?: Object) => Function; @@ -60,8 +60,8 @@ declare interface Component { // private methods // lifecycle _init: Function; - _mount: () => Component; - _update: (vnode: VNode) => void; + _mount: (el?: Element | void, hydrating?: boolean) => Component; + _update: (vnode: VNode, hydrating?: boolean) => void; _updateListeners: (listeners: Object, oldListeners: ?Object) => void; _updateFromParent: ( propsData: ?Object, diff --git a/flow/vnode.js b/flow/vnode.js index 06708fcf..058aa4d4 100644 --- a/flow/vnode.js +++ b/flow/vnode.js @@ -26,6 +26,7 @@ declare interface VNodeWithData { context: Component; key: string | number | void; parent?: VNodeWithData; + child?: Component; } declare interface VNodeData { diff --git a/src/core/instance/lifecycle.js b/src/core/instance/lifecycle.js index ed2bbab3..00099be7 100644 --- a/src/core/instance/lifecycle.js +++ b/src/core/instance/lifecycle.js @@ -24,8 +24,12 @@ export function initLifecycle (vm: Component) { } export function lifecycleMixin (Vue: Class) { - Vue.prototype._mount = function (): Component { + Vue.prototype._mount = function ( + el?: Element | void, + hydrating?: boolean + ): Component { const vm: Component = this + vm.$el = el if (!vm.$options.render) { vm.$options.render = () => emptyVNode if (process.env.NODE_ENV !== 'production') { @@ -47,8 +51,9 @@ export function lifecycleMixin (Vue: Class) { } callHook(vm, 'beforeMount') vm._watcher = new Watcher(vm, () => { - vm._update(vm._render()) + vm._update(vm._render(), hydrating) }, noop) + hydrating = false vm._isMounted = true // root instance, call mounted on self if (vm.$root === vm) { @@ -57,7 +62,7 @@ export function lifecycleMixin (Vue: Class) { return vm } - Vue.prototype._update = function (vnode: VNode) { + Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this if (vm._isMounted) { callHook(vm, 'beforeUpdate') @@ -65,7 +70,7 @@ export function lifecycleMixin (Vue: Class) { if (!vm._vnode) { // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. - vm.$el = vm.__patch__(vm.$el, vnode) + vm.$el = vm.__patch__(vm.$el, vnode, hydrating) } else { vm.$el = vm.__patch__(vm._vnode, vnode) } diff --git a/src/core/vdom/create-component.js b/src/core/vdom/create-component.js index 382fa737..b2eb2865 100644 --- a/src/core/vdom/create-component.js +++ b/src/core/vdom/create-component.js @@ -96,9 +96,9 @@ export function createComponentInstanceForVnode ( return new vnodeComponentOptions.Ctor(options) } -function init (vnode: VNode) { +function init (vnode: VNodeWithData, hydrating: boolean) { const child = vnode.child = createComponentInstanceForVnode(vnode) - child.$mount() + child.$mount(vnode.elm, hydrating) } function prepatch ( diff --git a/src/core/vdom/patch.js b/src/core/vdom/patch.js index 1e5a4cdc..feb65448 100644 --- a/src/core/vdom/patch.js +++ b/src/core/vdom/patch.js @@ -308,7 +308,7 @@ export function createPatchFunction (backend) { vnode.elm = elm const { tag, data, children } = vnode if (isDef(data)) { - if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode) + 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) @@ -349,7 +349,7 @@ export function createPatchFunction (backend) { } } - return function patch (oldVnode, vnode) { + return function patch (oldVnode, vnode, hydrating) { let elm, parent const insertedVnodeQueue = [] @@ -367,6 +367,9 @@ export function createPatchFunction (backend) { // a successful hydration. if (oldVnode.hasAttribute('server-rendered')) { oldVnode.removeAttribute('server-rendered') + hydrating = true + } + if (hydrating) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(insertedVnodeQueue) return oldVnode diff --git a/src/entries/web-runtime-with-compiler.js b/src/entries/web-runtime-with-compiler.js index 37da5791..ed945d96 100644 --- a/src/entries/web-runtime-with-compiler.js +++ b/src/entries/web-runtime-with-compiler.js @@ -12,7 +12,10 @@ const idToTemplate = cached(id => { }) const mount = Vue.prototype.$mount -Vue.prototype.$mount = function (el: string | Element): Component { +Vue.prototype.$mount = function ( + el?: string | Element, + hydrating?: boolean +): Component { el = el && query(el) const options = this.$options // resolve template/el and convert to render function @@ -44,7 +47,7 @@ Vue.prototype.$mount = function (el: string | Element): Component { options.staticRenderFns = staticRenderFns } } - return mount.call(this, el) + return mount.call(this, el, hydrating) } /** diff --git a/src/entries/web-runtime.js b/src/entries/web-runtime.js index b90c2a6e..32d54d44 100644 --- a/src/entries/web-runtime.js +++ b/src/entries/web-runtime.js @@ -19,9 +19,11 @@ Vue.options.directives = platformDirectives Vue.prototype.__patch__ = config._isServer ? noop : patch // wrap mount -Vue.prototype.$mount = function (el?: string | Element): Vue { - this.$el = el && query(el) - return this._mount() +Vue.prototype.$mount = function ( + el?: string | Element, + hydrating?: boolean +): Component { + return this._mount(el && query(el), hydrating) } export default Vue diff --git a/test/unit/modules/vdom/patch/hydration.spec.js b/test/unit/modules/vdom/patch/hydration.spec.js index beb8f152..4266a72c 100644 --- a/test/unit/modules/vdom/patch/hydration.spec.js +++ b/test/unit/modules/vdom/patch/hydration.spec.js @@ -1,5 +1,5 @@ +import Vue from 'vue' import { patch } from 'web/runtime/patch' -import * as nodeOps from 'web/runtime/node-ops' import VNode from 'core/vdom/vnode' describe('hydration', () => { @@ -13,13 +13,13 @@ describe('hydration', () => { const result = [] function init (vnode) { result.push(vnode) } function createServerRenderedDOM () { - const root = nodeOps.createElement('div') + const root = document.createElement('div') root.setAttribute('server-rendered', 'true') - const span = nodeOps.createElement('span') + const span = document.createElement('span') root.appendChild(span) - const div = nodeOps.createElement('div') - const child1 = nodeOps.createElement('span') - const child2 = nodeOps.createElement('span') + const div = document.createElement('div') + const child1 = document.createElement('span') + const child2 = document.createElement('span') child1.textContent = 'hi' child2.textContent = 'ho' div.appendChild(child1) @@ -62,42 +62,14 @@ describe('hydration', () => { expect(node0.children[0].id).toBe('bar') }) - it('should hydrate components when server-rendered DOM tree is same as virtual DOM tree', () => { - const result = [] - function init (vnode) { result.push(vnode) } - function createServerRenderedDOM () { - const root = nodeOps.createElement('vue-component-1') - root.setAttribute('server-rendered', 'true') - const span = nodeOps.createElement('span') - root.appendChild(span) - const div = nodeOps.createElement('div') - const child1 = nodeOps.createElement('span') - const child2 = nodeOps.createElement('span') - div.appendChild(child1) - div.appendChild(child2) - root.appendChild(div) - return root - } - const node0 = createServerRenderedDOM() - const vnode1 = new VNode('vue-component-1', {}, [ - new VNode('span', {}), - new VNode('div', { hook: { init }}, [ - new VNode('span', {}), - new VNode('span', {}) - ]) - ]) - patch(node0, vnode1) - expect(result.length).toBe(1) - }) - it('should warn message that virtual DOM tree is not matching when hydrate element', () => { function createServerRenderedDOM () { - const root = nodeOps.createElement('div') + const root = document.createElement('div') root.setAttribute('server-rendered', 'true') - const span = nodeOps.createElement('span') + const span = document.createElement('span') root.appendChild(span) - const div = nodeOps.createElement('div') - const child1 = nodeOps.createElement('span') + const div = document.createElement('div') + const child1 = document.createElement('span') div.appendChild(child1) root.appendChild(div) return root @@ -113,4 +85,64 @@ describe('hydration', () => { patch(node0, vnode1) expect('The client-side rendered virtual DOM tree is not matching').toHaveBeenWarned() }) + + // component hydration is better off with a more e2e approach + it('should hydrate components when server-rendered DOM tree is same as virtual DOM tree', done => { + const dom = document.createElement('div') + dom.setAttribute('server-rendered', 'true') + dom.innerHTML = 'foo
foo qux
' + const originalNode1 = dom.children[0] + const originalNode2 = dom.children[1] + + const vm = new Vue({ + template: '
{{msg}}
', + data: { + msg: 'foo' + }, + components: { + test: { + props: ['msg'], + data () { + return { a: 'qux' } + }, + template: '
{{msg}} {{a}}
' + } + } + }) + + expect(() => { vm.$mount(dom) }).not.toThrow() + expect('not matching server-rendered content').not.toHaveBeenWarned() + expect(vm.$el).toBe(dom) + expect(vm.$children[0].$el).toBe(originalNode2) + expect(vm.$el.children[0]).toBe(originalNode1) + expect(vm.$el.children[1]).toBe(originalNode2) + vm.msg = 'bar' + waitForUpdate(() => { + expect(vm.$el.innerHTML).toBe('bar
bar qux
') + vm.$children[0].a = 'ququx' + }).then(() => { + expect(vm.$el.innerHTML).toBe('bar
bar ququx
') + }).then(done) + }) + + it('should warn failed hydration for non-matching DOM in child component', () => { + const dom = document.createElement('div') + dom.setAttribute('server-rendered', 'true') + dom.innerHTML = '
foo
' + + const vm = new Vue({ + template: '
', + components: { + test: { + data () { + return { a: 'qux' } + }, + template: '
{{a}}
' + } + } + }) + + vm.$mount(dom) + expect('not matching server-rendered content').toHaveBeenWarned() + }) })