Browse Source

feat(weex): WIP implement virtual component (#7165)

dev
Hanks 7 years ago
committed by Evan You
parent
commit
b8d33ecd9a
  1. 2
      flow/component.js
  2. 2
      package.json
  3. 2
      src/core/instance/init.js
  4. 2
      src/platforms/weex/runtime/recycle-list/render-component-template.js
  5. 96
      src/platforms/weex/runtime/recycle-list/virtual-component.js
  6. 67
      test/weex/cases/cases.spec.js
  7. 31
      test/weex/cases/recycle-list/components/editor.vue
  8. 18
      test/weex/cases/recycle-list/components/footer.vue
  9. 48
      test/weex/cases/recycle-list/components/stateful-v-model.vdom.js
  10. 21
      test/weex/cases/recycle-list/components/stateful-v-model.vue
  11. 100
      test/weex/cases/recycle-list/components/stateless-multi-components.vdom.js
  12. 30
      test/weex/cases/recycle-list/components/stateless-multi-components.vue
  13. 21
      test/weex/helpers/index.js

2
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;

2
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": {

2
src/core/instance/init.js

@ -71,7 +71,7 @@ export function initMixin (Vue: Class<Component>) {
}
}
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

2
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) {

96
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)
})
}
})
}

67
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)
})
})
})

31
test/weex/cases/recycle-list/components/editor.vue

@ -0,0 +1,31 @@
<template recyclable="true">
<div>
<text class="output">{{output}}</text>
<input class="input" type="text" v-model="output" />
</div>
</template>
<script>
module.exports = {
props: ['message'],
data () {
return {
output: this.message | ''
}
}
}
</script>
<style scoped>
.output {
height: 80px;
font-size: 60px;
color: #41B883;
}
.input {
font-size: 50px;
color: #666666;
border-width: 2px;
border-color: #41B883;
}
</style>

18
test/weex/cases/recycle-list/components/footer.vue

@ -0,0 +1,18 @@
<template recyclable="true">
<div class="footer">
<text class="copyright">All rights reserved.</text>
</div>
</template>
<style scoped>
.footer {
height: 80px;
justify-content: center;
background-color: #EEEEEE;
}
.copyright {
color: #AAAAAA;
font-size: 32px;
text-align: center;
}
</style>

48
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
}
}]
}]
}]
})

21
test/weex/cases/recycle-list/components/stateful-v-model.vue

@ -0,0 +1,21 @@
<template>
<recycle-list :list-data="longList" template-key="type" alias="item">
<cell-slot template-type="A">
<editor message="No binding"></editor>
</cell-slot>
</recycle-list>
</template>
<script>
// require('./editor.vue')
module.exports = {
data () {
return {
longList: [
{ type: 'A' },
{ type: 'A' }
]
}
}
}
</script>

100
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' }
}
}]
}]
}]
})

30
test/weex/cases/recycle-list/components/stateless-multi-components.vue

@ -0,0 +1,30 @@
<template>
<recycle-list :list-data="longList" template-key="type" alias="item">
<cell-slot template-type="A">
<banner></banner>
<text>----</text>
<footer></footer>
</cell-slot>
<cell-slot template-type="B">
<banner></banner>
<poster :image-url="item.poster" :title="item.title"></poster>
</cell-slot>
</recycle-list>
</template>
<script>
// require('./banner.vue')
// require('./footer.vue')
// require('./poster.vue')
module.exports = {
data () {
return {
longList: [
{ type: 'A' },
{ type: 'B', poster: 'yy', title: 'y' },
{ type: 'A' }
]
}
}
}
</script>

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

Loading…
Cancel
Save