Browse Source

fix component client-side hydration

dev
Evan You 9 years ago
parent
commit
d40ab0ca87
  1. 6
      flow/component.js
  2. 1
      flow/vnode.js
  3. 13
      src/core/instance/lifecycle.js
  4. 4
      src/core/vdom/create-component.js
  5. 7
      src/core/vdom/patch.js
  6. 7
      src/entries/web-runtime-with-compiler.js
  7. 8
      src/entries/web-runtime.js
  8. 108
      test/unit/modules/vdom/patch/hydration.spec.js

6
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,

1
flow/vnode.js

@ -26,6 +26,7 @@ declare interface VNodeWithData {
context: Component;
key: string | number | void;
parent?: VNodeWithData;
child?: Component;
}
declare interface VNodeData {

13
src/core/instance/lifecycle.js

@ -24,8 +24,12 @@ export function initLifecycle (vm: Component) {
}
export function lifecycleMixin (Vue: Class<Component>) {
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<Component>) {
}
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<Component>) {
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<Component>) {
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)
}

4
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 (

7
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

7
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)
}
/**

8
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

108
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 = '<span>foo</span><div class="b a"><span>foo qux</span></div>'
const originalNode1 = dom.children[0]
const originalNode2 = dom.children[1]
const vm = new Vue({
template: '<div><span>{{msg}}</span><test class="a" :msg="msg"></test></div>',
data: {
msg: 'foo'
},
components: {
test: {
props: ['msg'],
data () {
return { a: 'qux' }
},
template: '<div class="b"><span>{{msg}} {{a}}</span></div>'
}
}
})
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('<span>bar</span><div class="b a"><span>bar qux</span></div>')
vm.$children[0].a = 'ququx'
}).then(() => {
expect(vm.$el.innerHTML).toBe('<span>bar</span><div class="b a"><span>bar ququx</span></div>')
}).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 = '<div><span>foo</span></div>'
const vm = new Vue({
template: '<div><test :msg="msg"></test></div>',
components: {
test: {
data () {
return { a: 'qux' }
},
template: '<div><span>{{a}}</span></div>'
}
}
})
vm.$mount(dom)
expect('not matching server-rendered content').toHaveBeenWarned()
})
})

Loading…
Cancel
Save