From b8d33ecd9ab8b7a46d8558b4e2caf506235cd165 Mon Sep 17 00:00:00 2001 From: Hanks Date: Mon, 4 Dec 2017 22:59:00 +0800 Subject: [PATCH] feat(weex): WIP implement virtual component (#7165) --- flow/component.js | 2 +- package.json | 2 +- src/core/instance/init.js | 2 +- .../recycle-list/render-component-template.js | 2 + .../runtime/recycle-list/virtual-component.js | 96 +++++++++++++++-- test/weex/cases/cases.spec.js | 67 +++++++++++- .../cases/recycle-list/components/editor.vue | 31 ++++++ .../cases/recycle-list/components/footer.vue | 18 ++++ .../components/stateful-v-model.vdom.js | 48 +++++++++ .../components/stateful-v-model.vue | 21 ++++ .../stateless-multi-components.vdom.js | 100 ++++++++++++++++++ .../components/stateless-multi-components.vue | 30 ++++++ test/weex/helpers/index.js | 21 ++++ 13 files changed, 430 insertions(+), 10 deletions(-) create mode 100644 test/weex/cases/recycle-list/components/editor.vue create mode 100644 test/weex/cases/recycle-list/components/footer.vue create mode 100644 test/weex/cases/recycle-list/components/stateful-v-model.vdom.js create mode 100644 test/weex/cases/recycle-list/components/stateful-v-model.vue create mode 100644 test/weex/cases/recycle-list/components/stateless-multi-components.vdom.js create mode 100644 test/weex/cases/recycle-list/components/stateless-multi-components.vue diff --git a/flow/component.js b/flow/component.js index 41596777..bf984cd6 100644 --- a/flow/component.js +++ b/flow/component.js @@ -48,7 +48,7 @@ declare interface Component { $createElement: (tag?: string | Component, data?: Object, children?: VNodeChildren) => VNode; // private properties - _uid: number; + _uid: number | string; _name: string; // this only exists in dev mode _isVue: true; _self: Component; diff --git a/package.json b/package.json index 48401a53..fe58ff26 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "typescript": "^2.6.1", "uglify-js": "^3.0.15", "webpack": "^3.10.0", - "weex-js-runtime": "^0.23.1", + "weex-js-runtime": "^0.23.3", "weex-styler": "^0.3.0" }, "config": { diff --git a/src/core/instance/init.js b/src/core/instance/init.js index 1a60cc4f..ed2550d0 100644 --- a/src/core/instance/init.js +++ b/src/core/instance/init.js @@ -71,7 +71,7 @@ export function initMixin (Vue: Class) { } } -function initInternalComponent (vm: Component, options: InternalComponentOptions) { +export function initInternalComponent (vm: Component, options: InternalComponentOptions) { const opts = vm.$options = Object.create(vm.constructor.options) // doing this because it's faster than dynamic enumeration. const parentVnode = options._parentVnode diff --git a/src/platforms/weex/runtime/recycle-list/render-component-template.js b/src/platforms/weex/runtime/recycle-list/render-component-template.js index a5cd4b52..17e8c018 100644 --- a/src/platforms/weex/runtime/recycle-list/render-component-template.js +++ b/src/platforms/weex/runtime/recycle-list/render-component-template.js @@ -4,6 +4,7 @@ import { warn } from 'core/util/debug' import { handleError } from 'core/util/error' import { RECYCLE_LIST_MARKER } from 'weex/util/index' import { createComponentInstanceForVnode } from 'core/vdom/create-component' +import { resolveVirtualComponent } from './virtual-component' export function isRecyclableComponent (vnode: VNodeWithData): boolean { return vnode.data.attrs @@ -14,6 +15,7 @@ export function isRecyclableComponent (vnode: VNodeWithData): boolean { export function renderRecyclableComponentTemplate (vnode: MountedComponentVNode): VNode { // $flow-disable-line delete vnode.data.attrs[RECYCLE_LIST_MARKER] + resolveVirtualComponent(vnode) const vm = createComponentInstanceForVnode(vnode) const render = (vm.$options: any)['@render'] if (render) { diff --git a/src/platforms/weex/runtime/recycle-list/virtual-component.js b/src/platforms/weex/runtime/recycle-list/virtual-component.js index da378da8..ed370d8c 100644 --- a/src/platforms/weex/runtime/recycle-list/virtual-component.js +++ b/src/platforms/weex/runtime/recycle-list/virtual-component.js @@ -1,6 +1,90 @@ -// import { -// // id, 'lifecycle', hookname, fn -// // https://github.com/Hanks10100/weex-native-directive/tree/master/component -// registerComponentHook, -// updateComponentData -// } from '../util/index' +/* @flow */ + +// https://github.com/Hanks10100/weex-native-directive/tree/master/component + +import { mergeOptions } from 'core/util/index' +import { initProxy } from 'core/instance/proxy' +import { initState } from 'core/instance/state' +import { initRender } from 'core/instance/render' +import { initEvents } from 'core/instance/events' +import { initProvide, initInjections } from 'core/instance/inject' +import { initLifecycle, mountComponent, callHook } from 'core/instance/lifecycle' +import { initInternalComponent, resolveConstructorOptions } from 'core/instance/init' +import { registerComponentHook, updateComponentData } from '../../util/index' + +let uid = 0 + +// override Vue.prototype._init +function initVirtualComponent (options: Object = {}) { + const vm: Component = this + const componentId = options.componentId + + // virtual component uid + vm._uid = `virtual-component-${uid++}` + + // a flag to avoid this being observed + vm._isVue = true + // merge options + if (options && options._isComponent) { + // optimize internal component instantiation + // since dynamic options merging is pretty slow, and none of the + // internal component options needs special treatment. + initInternalComponent(vm, options) + } else { + vm.$options = mergeOptions( + resolveConstructorOptions(vm.constructor), + options || {}, + vm + ) + } + + /* istanbul ignore else */ + if (process.env.NODE_ENV !== 'production') { + initProxy(vm) + } else { + vm._renderProxy = vm + } + + vm._self = vm + initLifecycle(vm) + initEvents(vm) + initRender(vm) + callHook(vm, 'beforeCreate') + initInjections(vm) // resolve injections before data/props + initState(vm) + initProvide(vm) // resolve provide after data/props + callHook(vm, 'created') + + registerComponentHook(componentId, 'lifecycle', 'attach', () => { + mountComponent(vm) + }) + + registerComponentHook(componentId, 'lifecycle', 'detach', () => { + vm.$destroy() + }) +} + +// override Vue.prototype._update +function updateVirtualComponent (vnode: VNode, hydrating?: boolean) { + // TODO + updateComponentData(this.$options.componentId, {}) +} + +// listening on native callback +export function resolveVirtualComponent (vnode: MountedComponentVNode): VNode { + const BaseCtor = vnode.componentOptions.Ctor + const VirtualComponent = BaseCtor.extend({}) + VirtualComponent.prototype._init = initVirtualComponent + VirtualComponent.prototype._update = updateVirtualComponent + + vnode.componentOptions.Ctor = BaseCtor.extend({ + beforeCreate () { + registerComponentHook(VirtualComponent.cid, 'lifecycle', 'create', componentId => { + // create virtual component + const options = { componentId } + return new VirtualComponent(options) + }) + } + }) +} + diff --git a/test/weex/cases/cases.spec.js b/test/weex/cases/cases.spec.js index ee04604b..72a86ff6 100644 --- a/test/weex/cases/cases.spec.js +++ b/test/weex/cases/cases.spec.js @@ -4,6 +4,8 @@ import { compileVue, compileWithDeps, createInstance, + addTaskHook, + resetTaskHook, getRoot, getEvents, fireEvent @@ -19,6 +21,7 @@ function createRenderTestCase (name) { const instance = createInstance(id, code) setTimeout(() => { expect(getRoot(instance)).toEqual(target) + instance.$destroy() done() }, 50) }).catch(done.fail) @@ -40,6 +43,7 @@ function createEventTestCase (name) { fireEvent(instance, event.ref, event.type, {}) setTimeout(() => { expect(getRoot(instance)).toEqual(after) + instance.$destroy() done() }, 50) }, 50) @@ -79,6 +83,7 @@ describe('Usage', () => { setTimeout(() => { const target = readObject('recycle-list/components/stateless.vdom.js') expect(getRoot(instance)).toEqual(target) + instance.$destroy() done() }, 50) }).catch(done.fail) @@ -94,29 +99,89 @@ describe('Usage', () => { setTimeout(() => { const target = readObject('recycle-list/components/stateless-with-props.vdom.js') expect(getRoot(instance)).toEqual(target) + instance.$destroy() + done() + }, 50) + }).catch(done.fail) + }) + + it('multi stateless components', done => { + compileWithDeps('recycle-list/components/stateless-multi-components.vue', [{ + name: 'banner', + path: 'recycle-list/components/banner.vue' + }, { + name: 'poster', + path: 'recycle-list/components/poster.vue' + }, { + name: 'footer', + path: 'recycle-list/components/footer.vue' + }]).then(code => { + const id = String(Date.now() * Math.random()) + const instance = createInstance(id, code) + setTimeout(() => { + const target = readObject('recycle-list/components/stateless-multi-components.vdom.js') + expect(getRoot(instance)).toEqual(target) + instance.$destroy() done() }, 50) }).catch(done.fail) }) it('stateful component', done => { + const tasks = [] + addTaskHook((_, task) => tasks.push(task)) compileWithDeps('recycle-list/components/stateful.vue', [{ name: 'counter', path: 'recycle-list/components/counter.vue' }]).then(code => { const id = String(Date.now() * Math.random()) const instance = createInstance(id, code) + expect(tasks.length).toEqual(7) + tasks.length = 0 + instance.$triggerHook(2, 'create', ['component-1']) + instance.$triggerHook(2, 'create', ['component-2']) + instance.$triggerHook('component-1', 'attach') + instance.$triggerHook('component-2', 'attach') + expect(tasks.length).toEqual(2) + expect(tasks[0].method).toEqual('updateComponentData') + // expect(tasks[0].args).toEqual([{ count: 42 }]) + expect(tasks[1].method).toEqual('updateComponentData') + // expect(tasks[1].args).toEqual([{ count: 42 }]) setTimeout(() => { const target = readObject('recycle-list/components/stateful.vdom.js') expect(getRoot(instance)).toEqual(target) const event = getEvents(instance)[0] + tasks.length = 0 fireEvent(instance, event.ref, event.type, {}) setTimeout(() => { - expect(getRoot(instance)).toEqual(target) + // expect(tasks.length).toEqual(1) + // expect(tasks[0]).toEqual({ + // module: 'dom', + // method: 'updateComponentData', + // args: [{ count: 43 }] + // }) + instance.$destroy() + resetTaskHook() done() }) }, 50) }).catch(done.fail) }) + + it('stateful component with v-model', done => { + compileWithDeps('recycle-list/components/stateful-v-model.vue', [{ + name: 'editor', + path: 'recycle-list/components/editor.vue' + }]).then(code => { + const id = String(Date.now() * Math.random()) + const instance = createInstance(id, code) + setTimeout(() => { + const target = readObject('recycle-list/components/stateful-v-model.vdom.js') + expect(getRoot(instance)).toEqual(target) + instance.$destroy() + done() + }, 50) + }).catch(done.fail) + }) }) }) diff --git a/test/weex/cases/recycle-list/components/editor.vue b/test/weex/cases/recycle-list/components/editor.vue new file mode 100644 index 00000000..11d3ea1b --- /dev/null +++ b/test/weex/cases/recycle-list/components/editor.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/test/weex/cases/recycle-list/components/footer.vue b/test/weex/cases/recycle-list/components/footer.vue new file mode 100644 index 00000000..b3fe0c8b --- /dev/null +++ b/test/weex/cases/recycle-list/components/footer.vue @@ -0,0 +1,18 @@ + + + diff --git a/test/weex/cases/recycle-list/components/stateful-v-model.vdom.js b/test/weex/cases/recycle-list/components/stateful-v-model.vdom.js new file mode 100644 index 00000000..e4bcf9ba --- /dev/null +++ b/test/weex/cases/recycle-list/components/stateful-v-model.vdom.js @@ -0,0 +1,48 @@ +({ + type: 'recycle-list', + attr: { + listData: [ + { type: 'A' }, + { type: 'A' } + ], + templateKey: 'type', + alias: 'item' + }, + children: [{ + type: 'cell-slot', + attr: { templateType: 'A' }, + children: [{ + type: 'div', + attr: { + '@isComponentRoot': true, + '@componentProps': { + message: 'No binding' + } + }, + children: [{ + type: 'text', + style: { + height: '80px', + fontSize: '60px', + color: '#41B883' + }, + attr: { + value: { '@binding': 'output' } + } + }, { + type: 'input', + event: ['input'], + style: { + fontSize: '50px', + color: '#666666', + borderWidth: '2px', + borderColor: '#41B883' + }, + attr: { + type: 'text', + value: 0 + } + }] + }] + }] +}) diff --git a/test/weex/cases/recycle-list/components/stateful-v-model.vue b/test/weex/cases/recycle-list/components/stateful-v-model.vue new file mode 100644 index 00000000..7a998a8b --- /dev/null +++ b/test/weex/cases/recycle-list/components/stateful-v-model.vue @@ -0,0 +1,21 @@ + + + diff --git a/test/weex/cases/recycle-list/components/stateless-multi-components.vdom.js b/test/weex/cases/recycle-list/components/stateless-multi-components.vdom.js new file mode 100644 index 00000000..6dd60872 --- /dev/null +++ b/test/weex/cases/recycle-list/components/stateless-multi-components.vdom.js @@ -0,0 +1,100 @@ +({ + type: 'recycle-list', + attr: { + listData: [ + { type: 'A' }, + { type: 'B', poster: 'yy', title: 'y' }, + { type: 'A' } + ], + templateKey: 'type', + alias: 'item' + }, + children: [{ + type: 'cell-slot', + attr: { templateType: 'A' }, + children: [{ + type: 'div', + attr: { + '@isComponentRoot': true, + '@componentProps': {} + }, + // style: { + // height: '120px', + // justifyContent: 'center', + // alignItems: 'center', + // backgroundColor: 'rgb(162, 217, 192)' + // }, + children: [{ + type: 'text', + // style: { + // fontWeight: 'bold', + // color: '#41B883', + // fontSize: '60px' + // }, + attr: { value: 'BANNER' } + }] + }, { + type: 'text', + attr: { value: '----' } + }, { + type: 'div', + attr: { + '@isComponentRoot': true, + '@componentProps': {} + }, + style: { height: '80px', justifyContent: 'center', backgroundColor: '#EEEEEE' }, + children: [{ + type: 'text', + style: { color: '#AAAAAA', fontSize: '32px', textAlign: 'center' }, + attr: { value: 'All rights reserved.' } + }] + }] + }, { + type: 'cell-slot', + attr: { templateType: 'B' }, + children: [{ + type: 'div', + attr: { + '@isComponentRoot': true, + '@componentProps': {} + }, + // style: { + // height: '120px', + // justifyContent: 'center', + // alignItems: 'center', + // backgroundColor: 'rgb(162, 217, 192)' + // }, + children: [{ + type: 'text', + // style: { + // fontWeight: 'bold', + // color: '#41B883', + // fontSize: '60px' + // }, + attr: { value: 'BANNER' } + }] + }, { + type: 'div', + attr: { + '@isComponentRoot': true, + '@componentProps': { + imageUrl: { '@binding': 'item.poster' }, + title: { '@binding': 'item.title' } + } + }, + children: [{ + type: 'image', + style: { width: '750px', height: '1000px' }, + attr: { + src: { '@binding': 'imageUrl' } + } + }, { + type: 'text', + style: { fontSize: '80px', textAlign: 'center', color: '#E95659' }, + attr: { + value: { '@binding': 'title' } + } + }] + }] + }] +}) diff --git a/test/weex/cases/recycle-list/components/stateless-multi-components.vue b/test/weex/cases/recycle-list/components/stateless-multi-components.vue new file mode 100644 index 00000000..38fd0823 --- /dev/null +++ b/test/weex/cases/recycle-list/components/stateless-multi-components.vue @@ -0,0 +1,30 @@ + + + diff --git a/test/weex/helpers/index.js b/test/weex/helpers/index.js index 1f46616e..6ed807cb 100644 --- a/test/weex/helpers/index.js +++ b/test/weex/helpers/index.js @@ -161,6 +161,9 @@ export function createInstance (id, code, ...args) { const instance = context.createInstance(id, `// { "framework": "Vue" }\n${code}`, ...args) instance.$refresh = (data) => context.refreshInstance(id, data) instance.$destroy = () => context.destroyInstance(id) + instance.$triggerHook = (id, hook, args) => { + instance.document.taskCenter.triggerHook(id, 'lifecycle', hook, { args }) + } return instance } @@ -197,3 +200,21 @@ export function checkRefresh (instance, data, checker) { }) }) } + +export function addTaskHook (hook) { + global.callNative = function callNative (id, tasks) { + if (Array.isArray(tasks) && typeof hook === 'function') { + tasks.forEach(task => { + hook(id, { + module: task.module, + method: task.method, + args: Array.from(task.args) + }) + }) + } + } +} + +export function resetTaskHook () { + delete global.callNative +}