Browse Source

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

Hanks 8 years ago
parent
commit
b8d33ecd9a

+ 1 - 1
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;

+ 1 - 1
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": {

+ 1 - 1
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 - 0
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) {

+ 90 - 6
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)
+      })
+    }
+  })
+}
+

+ 66 - 1
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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
+}