You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@weex.apache.org by ji...@apache.org on 2017/02/20 06:41:05 UTC

[16/50] [abbrv] incubator-weex git commit: V0.10.0 stable gitlab (#178)

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/runtime/callback-manager.js
----------------------------------------------------------------------
diff --git a/html5/runtime/callback-manager.js b/html5/runtime/callback-manager.js
new file mode 100644
index 0000000..3dad20d
--- /dev/null
+++ b/html5/runtime/callback-manager.js
@@ -0,0 +1,37 @@
+/**
+ * For general callback management of a certain Weex instance.
+ * Because function can not passed into native, so we create callback
+ * callback id for each function and pass the callback id into native
+ * in fact. And when a callback called from native, we can find the real
+ * callback through the callback id we have passed before.
+ */
+export default class CallbackManager {
+  constructor (instanceId) {
+    this.instanceId = instanceId
+    this.lastCallbackId = 0
+    this.callbacks = []
+  }
+  add (callback) {
+    this.lastCallbackId++
+    this.callbacks[this.lastCallbackId] = callback
+    return this.lastCallbackId
+  }
+  remove (callbackId) {
+    const callback = this.callbacks[callbackId]
+    this.callbacks[callbackId] = undefined
+    return callback
+  }
+  consume (callbackId, data, ifKeepAlive) {
+    const callback = this.callbacks[callbackId]
+    if (typeof ifKeepAlive === 'undefined' || ifKeepAlive === false) {
+      this.callbacks[callbackId] = undefined
+    }
+    if (typeof callback === 'function') {
+      return callback(data)
+    }
+    return new Error(`invalid callback id "${callbackId}"`)
+  }
+  close () {
+    this.callbacks = this.callbacks.map(cb => undefined)
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/runtime/config.js
----------------------------------------------------------------------
diff --git a/html5/runtime/config.js b/html5/runtime/config.js
new file mode 100644
index 0000000..d69d44e
--- /dev/null
+++ b/html5/runtime/config.js
@@ -0,0 +1,15 @@
+import { Document, Element, Comment } from './vdom'
+import Listener from './listener'
+import { TaskCenter } from './task-center'
+
+const config = {
+  Document, Element, Comment, Listener,
+  TaskCenter,
+  sendTasks (...args) {
+    return global.callNative(...args)
+  }
+}
+
+Document.handler = config.sendTasks
+
+export default config

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/runtime/handler.js
----------------------------------------------------------------------
diff --git a/html5/runtime/handler.js b/html5/runtime/handler.js
index c285d65..21a68bd 100644
--- a/html5/runtime/handler.js
+++ b/html5/runtime/handler.js
@@ -21,7 +21,7 @@ const handlerMap = {
  * @return {function} taskHandler
  */
 export function createHandler (id, handler) {
-  const defaultHandler = handler || callNative
+  const defaultHandler = handler || global.callNative
 
   /* istanbul ignore if */
   if (typeof defaultHandler !== 'function') {

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/runtime/index.js
----------------------------------------------------------------------
diff --git a/html5/runtime/index.js b/html5/runtime/index.js
index 88c6381..aaa3423 100644
--- a/html5/runtime/index.js
+++ b/html5/runtime/index.js
@@ -6,30 +6,27 @@
  */
 
 import * as shared from '../shared'
-import { Document, Element, Comment } from './vdom'
-import Listener from './listener'
-import init from './init'
 
-const config = {
-  Document, Element, Comment, Listener,
-  sendTasks (...args) {
-    return global.callNative(...args)
-  }
-}
+import init from './init'
+import config from './config'
 
-Document.handler = config.sendTasks
+import {
+  register,
+  unregister,
+  has
+} from './service'
 
 /* istanbul ignore next */
 function freezePrototype () {
   shared.freezePrototype()
 
-  Object.freeze(Element)
-  Object.freeze(Comment)
-  Object.freeze(Listener)
-  Object.freeze(Document.prototype)
-  Object.freeze(Element.prototype)
-  Object.freeze(Comment.prototype)
-  Object.freeze(Listener.prototype)
+  Object.freeze(config.Element)
+  Object.freeze(config.Comment)
+  Object.freeze(config.Listener)
+  Object.freeze(config.Document.prototype)
+  Object.freeze(config.Element.prototype)
+  Object.freeze(config.Comment.prototype)
+  Object.freeze(config.Listener.prototype)
 }
 
 export default {
@@ -37,6 +34,7 @@ export default {
   resetNativeConsole: shared.resetNativeConsole,
   setNativeTimer: shared.setNativeTimer,
   resetNativeTimer: shared.resetNativeTimer,
+  service: { register, unregister, has },
   freezePrototype,
   init,
   config

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/runtime/init.js
----------------------------------------------------------------------
diff --git a/html5/runtime/init.js b/html5/runtime/init.js
index 1cba7ca..ad44460 100644
--- a/html5/runtime/init.js
+++ b/html5/runtime/init.js
@@ -1,4 +1,9 @@
+import { init as initTaskHandler } from './task-center'
+import { registerElement } from './vdom/element-types'
+import { services, register, unregister } from './service'
+
 let frameworks
+let runtimeConfig
 
 const versionRegExp = /^\s*\/\/ *(\{[^}]*\}) *\r?\n/
 
@@ -21,6 +26,26 @@ function checkVersion (code) {
   return info
 }
 
+function createServices (id, env, config) {
+  // Init JavaScript services for this instance.
+  const serviceMap = Object.create(null)
+  serviceMap.service = Object.create(null)
+  services.forEach(({ name, options }) => {
+    if (process.env.NODE_ENV === 'development') {
+      console.debug(`[JS Runtime] create service ${name}.`)
+    }
+    const create = options.create
+    if (create) {
+      const result = create(id, env, config)
+      Object.assign(serviceMap.service, result)
+      Object.assign(serviceMap, result.instance)
+    }
+  })
+  delete serviceMap.service.instance
+  Object.freeze(serviceMap.service)
+  return serviceMap
+}
+
 const instanceMap = {}
 
 /**
@@ -33,24 +58,38 @@ const instanceMap = {}
  */
 function createInstance (id, code, config, data) {
   let info = instanceMap[id]
+
   if (!info) {
+    // Init instance info.
     info = checkVersion(code) || {}
     if (!frameworks[info.framework]) {
       info.framework = 'Weex'
     }
-    info.created = Date.now()
-    instanceMap[id] = info
+
+    // Init instance config.
     config = JSON.parse(JSON.stringify(config || {}))
     config.bundleVersion = info.version
     config.env = JSON.parse(JSON.stringify(global.WXEnvironment || {}))
     console.debug(`[JS Framework] create an ${info.framework}@${config.bundleVersion} instance from ${config.bundleVersion}`)
-    return frameworks[info.framework].createInstance(id, code, config, data)
+
+    const env = {
+      info,
+      config,
+      created: Date.now(),
+      framework: info.framework
+    }
+    env.services = createServices(id, env, runtimeConfig)
+    instanceMap[id] = env
+
+    return frameworks[info.framework].createInstance(id, code, config, data, env)
   }
   return new Error(`invalid instance id "${id}"`)
 }
 
 const methods = {
-  createInstance
+  createInstance,
+  registerService: register,
+  unregisterService: unregister
 }
 
 /**
@@ -59,6 +98,9 @@ const methods = {
  */
 function genInit (methodName) {
   methods[methodName] = function (...args) {
+    if (methodName === 'registerComponents') {
+      checkComponentMethods(args[0])
+    }
     for (const name in frameworks) {
       const framework = frameworks[name]
       if (framework && framework[methodName]) {
@@ -68,6 +110,16 @@ function genInit (methodName) {
   }
 }
 
+function checkComponentMethods (components) {
+  if (Array.isArray(components)) {
+    components.forEach((name) => {
+      if (name && name.type && name.methods) {
+        registerElement(name.type, name.methods)
+      }
+    })
+  }
+}
+
 /**
  * Register methods which will be called for each instance.
  * @param {string} methodName
@@ -78,9 +130,26 @@ function genInstance (methodName) {
     const info = instanceMap[id]
     if (info && frameworks[info.framework]) {
       const result = frameworks[info.framework][methodName](...args)
-      if (methodName === 'destroyInstance') {
+
+      // Lifecycle methods
+      if (methodName === 'refreshInstance') {
+        services.forEach(service => {
+          const refresh = service.options.refresh
+          if (refresh) {
+            refresh(id, { info, runtime: runtimeConfig })
+          }
+        })
+      }
+      else if (methodName === 'destroyInstance') {
+        services.forEach(service => {
+          const destroy = service.options.destroy
+          if (destroy) {
+            destroy(id, { info, runtime: runtimeConfig })
+          }
+        })
         delete instanceMap[id]
       }
+
       return result
     }
     return new Error(`invalid instance id "${id}"`)
@@ -105,7 +174,9 @@ function adaptInstance (methodName, nativeMethodName) {
 }
 
 export default function init (config) {
-  frameworks = config.frameworks || {}
+  runtimeConfig = config || {}
+  frameworks = runtimeConfig.frameworks || {}
+  initTaskHandler()
 
   // Init each framework by `init` method and `config` which contains three
   // virtual-DOM Class: `Document`, `Element` & `Comment`, and a JS bridge method:

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/runtime/service.js
----------------------------------------------------------------------
diff --git a/html5/runtime/service.js b/html5/runtime/service.js
new file mode 100644
index 0000000..0773ed6
--- /dev/null
+++ b/html5/runtime/service.js
@@ -0,0 +1,58 @@
+// JS Services
+
+export const services = []
+
+/**
+ * Register a JavaScript service.
+ * A JavaScript service options could have a set of lifecycle methods
+ * for each Weex instance. For example: create, refresh, destroy.
+ * For the JS framework maintainer if you want to supply some features
+ * which need to work well in different Weex instances, even in different
+ * frameworks separately. You can make a JavaScript service to init
+ * its variables or classes for each Weex instance when it's created
+ * and recycle them when it's destroyed.
+ * @param {object} options Could have { create, refresh, destroy }
+ *                         lifecycle methods. In create method it should
+ *                         return an object of what variables or classes
+ *                         would be injected into the Weex instance.
+ */
+export function register (name, options) {
+  if (has(name)) {
+    console.warn(`Service "${name}" has been registered already!`)
+  }
+  else {
+    options = Object.assign({}, options)
+    services.push({ name, options })
+  }
+}
+
+/**
+ * Unregister a JavaScript service by name
+ * @param {string} name
+ */
+export function unregister (name) {
+  services.some((service, index) => {
+    if (service.name === name) {
+      services.splice(index, 1)
+      return true
+    }
+  })
+}
+
+/**
+ * Check if a JavaScript service with a certain name existed.
+ * @param  {string}  name
+ * @return {Boolean}
+ */
+export function has (name) {
+  return indexOf(name) >= 0
+}
+
+/**
+ * Find the index of a JavaScript service by name
+ * @param  {string} name
+ * @return {number}
+ */
+function indexOf (name) {
+  return services.map(service => service.name).indexOf(name)
+}

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/runtime/task-center.js
----------------------------------------------------------------------
diff --git a/html5/runtime/task-center.js b/html5/runtime/task-center.js
new file mode 100644
index 0000000..c39192a
--- /dev/null
+++ b/html5/runtime/task-center.js
@@ -0,0 +1,129 @@
+import CallbackManager from './callback-manager'
+import Element from './vdom/element'
+
+let fallback = function () {}
+
+// The API of TaskCenter would be re-design.
+export class TaskCenter {
+  constructor (id, sendTasks) {
+    Object.defineProperty(this, 'instanceId', {
+      enumerable: true,
+      value: id
+    })
+    Object.defineProperty(this, 'callbackManager', {
+      enumerable: true,
+      value: new CallbackManager()
+    })
+    fallback = sendTasks || function () {}
+  }
+
+  callback (callbackId, data, ifKeepAlive) {
+    return this.callbackManager.consume(callbackId, data, ifKeepAlive)
+  }
+
+  destroyCallback () {
+    return this.callbackManager.close()
+  }
+
+  typof (v) {
+    const s = Object.prototype.toString.call(v)
+    return s.substring(8, s.length - 1).toLowerCase()
+  }
+
+  /**
+   * Normalize a value. Specially, if the value is a function, then generate a function id
+   * and save it to `CallbackManager`, at last return the function id.
+   * @param  {any}        v
+   * @param  {object}     app
+   * @return {primitive}
+   */
+  normalize (v) {
+    const type = this.typof(v)
+
+    switch (type) {
+      case 'undefined':
+      case 'null':
+        return ''
+      case 'regexp':
+        return v.toString()
+      case 'date':
+        return v.toISOString()
+      case 'number':
+      case 'string':
+      case 'boolean':
+      case 'array':
+      case 'object':
+        if (v instanceof Element) {
+          return v.ref
+        }
+        return v
+      case 'function':
+        return this.callbackManager.add(v).toString()
+      /* istanbul ignore next */
+      default:
+        return JSON.stringify(v)
+    }
+  }
+
+  send (type, options, args) {
+    const { action, component, ref, module, method } = options
+
+    args = args.map(arg => this.normalize(arg))
+
+    switch (type) {
+      case 'dom':
+        return this[action](this.instanceId, args)
+      case 'component':
+        return this.componentHandler(this.instanceId, ref, method, args, { component })
+      default:
+        return this.moduleHandler(this.instanceId, module, method, args, {})
+    }
+  }
+
+  callDOM (action, args) {
+    return this[action](this.instanceId, args)
+  }
+
+  callComponent (ref, method, args) {
+    return this.componentHandler(this.instanceId, ref, method, args, {})
+  }
+
+  callModule (module, method, args) {
+    return this.moduleHandler(this.instanceId, module, method, args, {})
+  }
+}
+
+export function init () {
+  const DOM_METHODS = {
+    createFinish: global.callCreateFinish,
+    updateFinish: global.callUpdateFinish,
+    refreshFinish: global.callRefreshFinish,
+
+    createBody: global.callCreateBody,
+
+    addElement: global.callAddElement,
+    removeElement: global.callRemoveElement,
+    moveElement: global.callMoveElement,
+    updateAttrs: global.callUpdateAttrs,
+    updateStyle: global.callUpdateStyle,
+
+    addEvent: global.callAddEvent,
+    removeEvent: global.callRemoveEvent
+  }
+  const proto = TaskCenter.prototype
+
+  for (const name in DOM_METHODS) {
+    const method = DOM_METHODS[name]
+    proto[name] = method ?
+      (id, args) => method(id, ...args) :
+      (id, args) => fallback(id, [{ module: 'dom', method: name, args }], '-1')
+  }
+
+  proto.componentHandler = global.callNativeComponent ||
+    ((id, ref, method, args, options) =>
+      fallback(id, [{ component: options.component, ref, method, args }]))
+
+  proto.moduleHandler = global.callNativeModule ||
+    ((id, module, method, args) =>
+      fallback(id, [{ module, method, args }]))
+}

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/runtime/vdom/document.js
----------------------------------------------------------------------
diff --git a/html5/runtime/vdom/document.js b/html5/runtime/vdom/document.js
index cf6b097..34e8936 100644
--- a/html5/runtime/vdom/document.js
+++ b/html5/runtime/vdom/document.js
@@ -7,6 +7,7 @@ import '../../shared/objectAssign'
 import Comment from './comment'
 import Element from './element'
 import Listener from '../listener'
+import { TaskCenter } from '../task-center'
 import { createHandler } from '../handler'
 import { addDoc, removeDoc, appendBody, setBody } from './operation'
 
@@ -18,7 +19,8 @@ export default function Document (id, url, handler) {
   addDoc(id, this)
   this.nodeMap = {}
   const L = Document.Listener || Listener
-  this.listener = new L(id, handler || createHandler(id, Document.handler))
+  this.listener = new L(id, handler || createHandler(id, Document.handler)) // deprecated
+  this.taskCenter = new TaskCenter(id, handler ? (id, ...args) => handler(...args) : Document.handler)
   this.createDocumentElement()
 }
 

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/runtime/vdom/element-types.js
----------------------------------------------------------------------
diff --git a/html5/runtime/vdom/element-types.js b/html5/runtime/vdom/element-types.js
new file mode 100644
index 0000000..a4a24cc
--- /dev/null
+++ b/html5/runtime/vdom/element-types.js
@@ -0,0 +1,65 @@
+import { getTaskCenter } from './operation'
+
+let Element
+
+export function setElement (El) {
+  Element = El
+}
+
+/**
+ * A map which stores all type of elements.
+ * @type {Object}
+ */
+export const elementTypes = {}
+
+/**
+ * Register an extended element type with component methods.
+ * @param  {string} type    component type
+ * @param  {array}  methods a list of method names
+ */
+export function registerElement (type, methods) {
+  // Skip when no special component methods.
+  if (!methods || !methods.length) {
+    return
+  }
+
+  // Init constructor.
+  const XElement = function (props) {
+    Element.call(this, type, props, true)
+  }
+
+  // Init prototype.
+  XElement.prototype = Object.create(Element.prototype)
+  Object.defineProperty(XElement.prototype, 'constructor', {
+    configurable: false,
+    enumerable: false,
+    writable: false,
+    value: Element
+  })
+
+  // Add methods to prototype.
+  methods.forEach(methodName => {
+    XElement.prototype[methodName] = function (...args) {
+      const taskCenter = getTaskCenter(this.docId)
+      if (taskCenter) {
+        return taskCenter.send('component', {
+          ref: this.ref,
+          component: type,
+          method: methodName
+        }, args)
+      }
+    }
+  })
+
+  // Add to element type map.
+  elementTypes[type] = XElement
+}
+
+/**
+ * Clear all element types. Only for testing.
+ */
+export function clearElementTypes () {
+  for (const type in elementTypes) {
+    delete elementTypes[type]
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/runtime/vdom/element.js
----------------------------------------------------------------------
diff --git a/html5/runtime/vdom/element.js b/html5/runtime/vdom/element.js
index 2af5054..e7a7526 100644
--- a/html5/runtime/vdom/element.js
+++ b/html5/runtime/vdom/element.js
@@ -7,7 +7,7 @@ import '../../shared/objectAssign'
 import Node from './node'
 import {
   getDoc,
-  getListener,
+  getTaskCenter,
   uniqueId,
   linkParent,
   nextElement,
@@ -16,10 +16,18 @@ import {
   moveIndex,
   removeIndex
 } from './operation'
+import {
+  elementTypes,
+  setElement
+} from './element-types'
 
 const DEFAULT_TAG_NAME = 'div'
 
-export default function Element (type = DEFAULT_TAG_NAME, props) {
+export default function Element (type = DEFAULT_TAG_NAME, props, isExtended) {
+  const XElement = elementTypes[type]
+  if (XElement && !isExtended) {
+    return new XElement(props)
+  }
   props = props || {}
   this.nodeType = 1
   this.nodeId = uniqueId()
@@ -41,6 +49,8 @@ function registerNode (docId, node) {
   doc.nodeMap[node.nodeId] = node
 }
 
+setElement(Element)
+
 Object.assign(Element.prototype, {
   /**
    * Append a child node.
@@ -60,9 +70,13 @@ Object.assign(Element.prototype, {
       }
       if (node.nodeType === 1) {
         insertIndex(node, this.pureChildren, this.pureChildren.length)
-        const listener = getListener(this.docId)
-        if (listener) {
-          return listener.addElement(node, this.ref, -1)
+        const taskCenter = getTaskCenter(this.docId)
+        if (taskCenter) {
+          return taskCenter.send(
+            'dom',
+            { action: 'addElement' },
+            [this.ref, node.toJSON(), -1]
+          )
         }
       }
     }
@@ -70,9 +84,13 @@ Object.assign(Element.prototype, {
       moveIndex(node, this.children, this.children.length, true)
       if (node.nodeType === 1) {
         const index = moveIndex(node, this.pureChildren, this.pureChildren.length)
-        const listener = getListener(this.docId)
-        if (listener && index >= 0) {
-          return listener.moveElement(node.ref, this.ref, index)
+        const taskCenter = getTaskCenter(this.docId)
+        if (taskCenter && index >= 0) {
+          return taskCenter.send(
+            'dom',
+            { action: 'moveElement' },
+            [node.ref, this.ref, index]
+          )
         }
       }
     }
@@ -106,9 +124,13 @@ Object.assign(Element.prototype, {
           ? this.pureChildren.indexOf(pureBefore)
           : this.pureChildren.length
         )
-        const listener = getListener(this.docId)
-        if (listener) {
-          return listener.addElement(node, this.ref, index)
+        const taskCenter = getTaskCenter(this.docId)
+        if (taskCenter) {
+          return taskCenter.send(
+            'dom',
+            { action: 'addElement' },
+            [this.ref, node.toJSON(), index]
+          )
         }
       }
     }
@@ -116,6 +138,7 @@ Object.assign(Element.prototype, {
       moveIndex(node, this.children, this.children.indexOf(before), true)
       if (node.nodeType === 1) {
         const pureBefore = nextElement(before)
+        /* istanbul ignore next */
         const index = moveIndex(
           node,
           this.pureChildren,
@@ -123,9 +146,13 @@ Object.assign(Element.prototype, {
           ? this.pureChildren.indexOf(pureBefore)
           : this.pureChildren.length
         )
-        const listener = getListener(this.docId)
-        if (listener && index >= 0) {
-          return listener.moveElement(node.ref, this.ref, index)
+        const taskCenter = getTaskCenter(this.docId)
+        if (taskCenter && index >= 0) {
+          return taskCenter.send(
+            'dom',
+            { action: 'moveElement' },
+            [node.ref, this.ref, index]
+          )
         }
       }
     }
@@ -157,10 +184,14 @@ Object.assign(Element.prototype, {
           this.pureChildren,
           this.pureChildren.indexOf(previousElement(after)) + 1
         )
-        const listener = getListener(this.docId)
+        const taskCenter = getTaskCenter(this.docId)
         /* istanbul ignore else */
-        if (listener) {
-          return listener.addElement(node, this.ref, index)
+        if (taskCenter) {
+          return taskCenter.send(
+            'dom',
+            { action: 'addElement' },
+            [this.ref, node.toJSON(), index]
+          )
         }
       }
     }
@@ -172,9 +203,13 @@ Object.assign(Element.prototype, {
           this.pureChildren,
           this.pureChildren.indexOf(previousElement(after)) + 1
         )
-        const listener = getListener(this.docId)
-        if (listener && index >= 0) {
-          return listener.moveElement(node.ref, this.ref, index)
+        const taskCenter = getTaskCenter(this.docId)
+        if (taskCenter && index >= 0) {
+          return taskCenter.send(
+            'dom',
+            { action: 'moveElement' },
+            [node.ref, this.ref, index]
+          )
         }
       }
     }
@@ -190,9 +225,13 @@ Object.assign(Element.prototype, {
       removeIndex(node, this.children, true)
       if (node.nodeType === 1) {
         removeIndex(node, this.pureChildren)
-        const listener = getListener(this.docId)
-        if (listener) {
-          listener.removeElement(node.ref)
+        const taskCenter = getTaskCenter(this.docId)
+        if (taskCenter) {
+          taskCenter.send(
+            'dom',
+            { action: 'removeElement' },
+            [node.ref]
+          )
         }
       }
     }
@@ -205,11 +244,15 @@ Object.assign(Element.prototype, {
    * Clear all child nodes.
    */
   clear () {
-    const listener = getListener(this.docId)
+    const taskCenter = getTaskCenter(this.docId)
     /* istanbul ignore else */
-    if (listener) {
+    if (taskCenter) {
       this.pureChildren.forEach(node => {
-        listener.removeElement(node.ref)
+        taskCenter.send(
+          'dom',
+          { action: 'removeElement' },
+          [node.ref]
+        )
       })
     }
     this.children.forEach(node => {
@@ -230,9 +273,15 @@ Object.assign(Element.prototype, {
       return
     }
     this.attr[key] = value
-    const listener = getListener(this.docId)
-    if (!silent && listener) {
-      listener.setAttr(this.ref, key, value)
+    const taskCenter = getTaskCenter(this.docId)
+    if (!silent && taskCenter) {
+      const result = {}
+      result[key] = value
+      taskCenter.send(
+        'dom',
+        { action: 'updateAttrs' },
+        [this.ref, result]
+      )
     }
   },
 
@@ -247,9 +296,15 @@ Object.assign(Element.prototype, {
       return
     }
     this.style[key] = value
-    const listener = getListener(this.docId)
-    if (!silent && listener) {
-      listener.setStyle(this.ref, key, value)
+    const taskCenter = getTaskCenter(this.docId)
+    if (!silent && taskCenter) {
+      const result = {}
+      result[key] = value
+      taskCenter.send(
+        'dom',
+        { action: 'updateStyle' },
+        [this.ref, result]
+      )
     }
   },
 
@@ -264,9 +319,13 @@ Object.assign(Element.prototype, {
     }
 
     Object.assign(this.classStyle, classStyle)
-    const listener = getListener(this.docId)
-    if (listener) {
-      listener.setStyles(this.ref, this.toStyle())
+    const taskCenter = getTaskCenter(this.docId)
+    if (taskCenter) {
+      taskCenter.send(
+        'dom',
+        { action: 'updateStyle' },
+        [this.ref, this.toStyle()]
+      )
     }
   },
 
@@ -278,9 +337,13 @@ Object.assign(Element.prototype, {
   addEvent (type, handler) {
     if (!this.event[type]) {
       this.event[type] = handler
-      const listener = getListener(this.docId)
-      if (listener) {
-        listener.addEvent(this.ref, type)
+      const taskCenter = getTaskCenter(this.docId)
+      if (taskCenter) {
+        taskCenter.send(
+          'dom',
+          { action: 'addEvent' },
+          [this.ref, type]
+        )
       }
     }
   },
@@ -292,9 +355,13 @@ Object.assign(Element.prototype, {
   removeEvent (type) {
     if (this.event[type]) {
       delete this.event[type]
-      const listener = getListener(this.docId)
-      if (listener) {
-        listener.removeEvent(this.ref, type)
+      const taskCenter = getTaskCenter(this.docId)
+      if (taskCenter) {
+        taskCenter.send(
+          'dom',
+          { action: 'removeEvent' },
+          [this.ref, type]
+        )
       }
     }
   },

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/runtime/vdom/index.js
----------------------------------------------------------------------
diff --git a/html5/runtime/vdom/index.js b/html5/runtime/vdom/index.js
index 67a85b4..e2f0ad7 100644
--- a/html5/runtime/vdom/index.js
+++ b/html5/runtime/vdom/index.js
@@ -1,9 +1,15 @@
 import Node from './node'
-import Comment from './comment'
 import Element from './element'
+import Comment from './comment'
 import Document from './document'
 
 export {
+  elementTypes,
+  registerElement,
+  clearElementTypes
+} from './element-types'
+
+export {
   Document,
   Node,
   Element,

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/runtime/vdom/operation.js
----------------------------------------------------------------------
diff --git a/html5/runtime/vdom/operation.js b/html5/runtime/vdom/operation.js
index e9bdd99..7f8c65f 100644
--- a/html5/runtime/vdom/operation.js
+++ b/html5/runtime/vdom/operation.js
@@ -29,6 +29,7 @@ export function removeDoc (id) {
 }
 
 /**
+ * @deprecated
  * Get listener by document id.
  * @param {string} id
  * @return {object} listener
@@ -42,6 +43,19 @@ export function getListener (id) {
 }
 
 /**
+ * Get TaskCenter instance by id.
+ * @param {string} id
+ * @return {object} TaskCenter
+ */
+export function getTaskCenter (id) {
+  const doc = docMap[id]
+  if (doc && doc.taskCenter) {
+    return doc.taskCenter
+  }
+  return null
+}
+
+/**
  * Get a unique id.
  */
 let nextNodeRef = 1
@@ -88,7 +102,7 @@ export function appendBody (doc, node, before) {
       delete doc.nodeMap[node.nodeId]
     }
     documentElement.pureChildren.push(node)
-    doc.listener.createBody(node)
+    sendBody(doc, node)
   }
   else {
     node.parentNode = documentElement
@@ -96,6 +110,19 @@ export function appendBody (doc, node, before) {
   }
 }
 
+function sendBody (doc, node) {
+  const body = node.toJSON()
+  const children = body.children
+  delete body.children
+  let result = doc.taskCenter.send('dom', { action: 'createBody' }, [body])
+  if (children) {
+    children.forEach(child => {
+      result = doc.taskCenter.send('dom', { action: 'addElement' }, [body.ref, child, -1])
+    })
+  }
+  return result
+}
+
 /**
  * Set up body node.
  * @param {object} document

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/services/amd/index.js
----------------------------------------------------------------------
diff --git a/html5/services/amd/index.js b/html5/services/amd/index.js
new file mode 100644
index 0000000..dd5a0eb
--- /dev/null
+++ b/html5/services/amd/index.js
@@ -0,0 +1,86 @@
+/**
+ * @fileOverview
+ * A simple implementation of AMD for weex.
+ */
+
+/**
+ * amd modules (<id, module> pair)
+ *  - id : instance id.
+ *  - module: {
+ *        name: module name
+ *        , factory: module's factory function
+ *        , cached: cached module,
+ *        , deps: module's dependencies, always be [] under most circumstances.
+ *      }
+ */
+const modules = {}
+
+const amdService = {
+
+  // create a amd service.
+  create: (id, env, config) => {
+    if (!env.framework.match(/Vue/i)) {
+      return
+    }
+
+    const mod = {}
+    modules[id] = mod
+    const amdObject = {
+
+      /**
+       * define a module.
+       * @param  {String} name: module name.
+       * @param  {Array} deps: dependencies. Always be empty array.
+       * @param  {function} factory: factory function.
+       */
+      define (name, deps, factory) {
+        if (mod[name]) {
+          console.warn(`[amdService] already defined module: '${name}'`)
+        }
+        if (typeof deps === 'function') {
+          factory = deps
+          deps = []
+        }
+        mod[name] = { name, factory, cached: false, deps }
+      },
+
+      /**
+       * require a module.
+       * @param  {string} name - module name.
+       */
+      require (name) {
+        const servMod = mod[name]
+        if (!servMod) {
+          return console.warn(`[amdService] module '${name}' is not defined.`)
+        }
+        if (servMod.cached) {
+          return servMod.cached
+        }
+        const exports = {}
+        const module = { exports }
+        const { deps } = servMod
+        let ret
+        if (deps && deps.length >= 1) {
+          /**
+           * to support:
+           *   define(name, ['foo', 'bar'], function (foo, bar) { ... })
+           */
+          const args = deps.map(depName => require(depName))
+          ret = servMod.factory(...args)
+        }
+        else {
+          ret = servMod.factory(amdObject.require, exports, module)
+        }
+        servMod.cached = ret || module.exports
+        return servMod.cached
+      }
+    }
+    return { instance: amdObject }
+  },
+
+  destroy: (id, env) => {
+    delete modules[id]
+  }
+}
+
+export default amdService

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/services/broadcast-channel/index.js
----------------------------------------------------------------------
diff --git a/html5/services/broadcast-channel/index.js b/html5/services/broadcast-channel/index.js
new file mode 100644
index 0000000..2e04e30
--- /dev/null
+++ b/html5/services/broadcast-channel/index.js
@@ -0,0 +1,106 @@
+/**
+ * @fileOverview
+ * The polyfill of BroadcastChannel API.
+ * This api can be used to achieve inter-instance communications.
+ *
+ * https://html.spec.whatwg.org/multipage/comms.html#broadcasting-to-other-browsing-contexts
+ */
+
+import { MessageEvent } from './message-event'
+
+const channels = {}
+const instances = {}
+
+/**
+ * An empty constructor for BroadcastChannel polyfill.
+ * The real constructor will be defined when a Weex instance created because
+ * we need to track the channel by Weex instance id.
+ */
+function BroadcastChannel () {}
+
+/**
+ * Sends the given message to other BroadcastChannel objects set up for this channel.
+ * @param {any} message
+ */
+BroadcastChannel.prototype.postMessage = function (message) {
+  if (this._closed) {
+    throw new Error(`BroadcastChannel "${this.name}" is closed.`)
+  }
+
+  const subscribers = channels[this.name]
+  if (subscribers && subscribers.length) {
+    for (let i = 0; i < subscribers.length; ++i) {
+      const member = subscribers[i]
+
+      if (member._closed || member === this) continue
+
+      if (typeof member.onmessage === 'function') {
+        member.onmessage(new MessageEvent('message', { data: message }))
+      }
+    }
+  }
+}
+
+/**
+ * Closes the BroadcastChannel object, opening it up to garbage collection.
+ */
+BroadcastChannel.prototype.close = function () {
+  if (this._closed) {
+    return
+  }
+
+  this._closed = true
+
+  // remove itself from channels.
+  if (channels[this.name]) {
+    const subscribers = channels[this.name].filter(x => x !== this)
+    if (subscribers.length) {
+      channels[this.name] = subscribers
+    }
+    else {
+      delete channels[this.name]
+    }
+  }
+}
+
+export default {
+  create: (id, env, config) => {
+    instances[id] = []
+    if (typeof global.BroadcastChannel === 'function') {
+      return {}
+    }
+    const serviceObject = {
+      /**
+       * Returns a new BroadcastChannel object via which messages for the given
+       * channel name can be sent and received.
+       * @param {string} name
+       */
+      BroadcastChannel: function (name) {
+        // the name property is readonly
+        Object.defineProperty(this, 'name', {
+          configurable: false,
+          enumerable: true,
+          writable: false,
+          value: String(name)
+        })
+
+        this._closed = false
+        this.onmessage = null
+
+        if (!channels[this.name]) {
+          channels[this.name] = []
+        }
+        channels[this.name].push(this)
+        instances[id].push(this)
+      }
+    }
+    serviceObject.BroadcastChannel.prototype = BroadcastChannel.prototype
+    return {
+      instance: serviceObject
+    }
+  },
+  destroy: (id, env) => {
+    instances[id].forEach(channel => channel.close())
+    delete instances[id]
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/services/broadcast-channel/message-event.js
----------------------------------------------------------------------
diff --git a/html5/services/broadcast-channel/message-event.js b/html5/services/broadcast-channel/message-event.js
new file mode 100644
index 0000000..437728e
--- /dev/null
+++ b/html5/services/broadcast-channel/message-event.js
@@ -0,0 +1,21 @@
+/**
+ * Mock MessageEvent type
+ * @param {string} type
+ * @param {object} dict { data, origin, source, ports }
+ *
+ * This type has been simplified.
+ * https://html.spec.whatwg.org/multipage/comms.html#messageevent
+ * https://dom.spec.whatwg.org/#interface-event
+ */
+export function MessageEvent (type, dict = {}) {
+  this.type = type || 'message'
+
+  this.data = dict.data || null
+  this.origin = dict.origin || ''
+  this.source = dict.source || null
+  this.ports = dict.ports || []
+
+  // inherit properties
+  this.target = null
+  this.timeStamp = Date.now()
+}

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/services/index.js
----------------------------------------------------------------------
diff --git a/html5/services/index.js b/html5/services/index.js
new file mode 100644
index 0000000..266a4d3
--- /dev/null
+++ b/html5/services/index.js
@@ -0,0 +1,5 @@
+import BroadcastChannel from './broadcast-channel/index'
+
+export default {
+  BroadcastChannel
+}

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/shared/setTimeout.js
----------------------------------------------------------------------
diff --git a/html5/shared/setTimeout.js b/html5/shared/setTimeout.js
index cc3790f..c0fd472 100644
--- a/html5/shared/setTimeout.js
+++ b/html5/shared/setTimeout.js
@@ -40,3 +40,5 @@ export function resetNativeTimer () {
   global.setTimeout = originalSetTimeout
   global.setTimeoutCallback = null
 }
+
+setNativeTimer()

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/case/basic/dynamic-id.output.js
----------------------------------------------------------------------
diff --git a/html5/test/case/basic/dynamic-id.output.js b/html5/test/case/basic/dynamic-id.output.js
new file mode 100644
index 0000000..5029a30
--- /dev/null
+++ b/html5/test/case/basic/dynamic-id.output.js
@@ -0,0 +1,44 @@
+{
+  type: 'div',
+  children: [
+    {
+      type: 'text',
+      attr: {
+        value: 'Hello',
+        a: 1
+      }
+    },
+    {
+      type: 'text',
+      attr: {
+        value: 'Hello',
+        a: 1,
+        b: 1
+      }
+    },
+    {
+      type: 'text',
+      attr: {
+        value: 'Hello'
+      }
+    },
+    {
+      type: 'text',
+      attr: {
+        value: 'Hello'
+      }
+    },
+    {
+      type: 'text',
+      attr: {
+        value: 'Hello'
+      }
+    },
+    {
+      type: 'text',
+      attr: {
+        value: 'Hello'
+      }
+    }
+  ]
+}

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/case/basic/dynamic-id.source.js
----------------------------------------------------------------------
diff --git a/html5/test/case/basic/dynamic-id.source.js b/html5/test/case/basic/dynamic-id.source.js
new file mode 100644
index 0000000..d9abf61
--- /dev/null
+++ b/html5/test/case/basic/dynamic-id.source.js
@@ -0,0 +1,76 @@
+define('@weex-component/id', function (require, exports, module) {
+
+;
+  module.exports = {
+    ready: function () {
+      if (this.$el('x')) {
+        this.$el('x').setAttr('a', 1)
+      }
+      if (this.$el('y')) {
+        this.$el('y').setAttr('a', 1)
+      }
+      if (this.$el('')) {
+        this.$el('').setAttr('a', 1)
+      }
+      if (this.$el('0')) {
+        this.$el('0').setAttr('a', 1)
+      }
+      if (this.$el(0)) {
+        this.$el(0).setAttr('b', 1)
+      }
+    }
+  }
+
+;module.exports.template = {
+  "type": "div",
+  "children": [
+    {
+      "type": "text",
+      "id": function () { return "x" },
+      "attr": {
+        "value": "Hello"
+      }
+    },
+    {
+      "type": "text",
+      "id": function () { return 0 },
+      "attr": {
+        "value": "Hello"
+      }
+    },
+    {
+      "type": "text",
+      "id": function () { return '' },
+      "attr": {
+        "value": "Hello"
+      }
+    },
+    {
+      "type": "text",
+      "id": function () { return null },
+      "attr": {
+        "value": "Hello"
+      }
+    },
+    {
+      "type": "text",
+      "id": function () { return NaN },
+      "attr": {
+        "value": "Hello"
+      }
+    },
+    {
+      "type": "text",
+      "id": function () { return },
+      "attr": {
+        "value": "Hello"
+      }
+    }
+  ]
+}
+
+;})
+
+// require module
+
+bootstrap('@weex-component/id')

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/case/basic/global-weex-object.output.js
----------------------------------------------------------------------
diff --git a/html5/test/case/basic/global-weex-object.output.js b/html5/test/case/basic/global-weex-object.output.js
new file mode 100644
index 0000000..3e1fa19
--- /dev/null
+++ b/html5/test/case/basic/global-weex-object.output.js
@@ -0,0 +1,6 @@
+{
+    type: 'div',
+    attr: {
+        a: 'WeexDemo'
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/case/basic/global-weex-object.source.js
----------------------------------------------------------------------
diff --git a/html5/test/case/basic/global-weex-object.source.js b/html5/test/case/basic/global-weex-object.source.js
new file mode 100644
index 0000000..3a8e99f
--- /dev/null
+++ b/html5/test/case/basic/global-weex-object.source.js
@@ -0,0 +1,19 @@
+weex.define('@weex-component/foo', function (require, exports, module) {
+  module.exports = {
+    data: function () {
+      return {
+        x: weex.config.env.appName
+      }
+    },
+    template: {
+      "type": "div",
+      "attr": {
+        "a": function () {
+          return this.x
+        }
+      }
+    }
+  }
+})
+
+weex.bootstrap('@weex-component/foo')

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/case/basic/id.output.js
----------------------------------------------------------------------
diff --git a/html5/test/case/basic/id.output.js b/html5/test/case/basic/id.output.js
new file mode 100644
index 0000000..7ce073c
--- /dev/null
+++ b/html5/test/case/basic/id.output.js
@@ -0,0 +1,32 @@
+{
+  type: 'div',
+  children: [
+    {
+      type: 'text',
+      attr: {
+        value: 'Hello',
+        a: 1
+      }
+    },
+    {
+      type: 'text',
+      attr: {
+        value: 'Hello',
+        a: 1,
+        b: 1
+      }
+    },
+    {
+      type: 'text',
+      attr: {
+        value: 'Hello'
+      }
+    },
+    {
+      type: 'text',
+      attr: {
+        value: 'Hello'
+      }
+    }
+  ]
+}

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/case/basic/id.source.js
----------------------------------------------------------------------
diff --git a/html5/test/case/basic/id.source.js b/html5/test/case/basic/id.source.js
new file mode 100644
index 0000000..4a5fb65
--- /dev/null
+++ b/html5/test/case/basic/id.source.js
@@ -0,0 +1,62 @@
+define('@weex-component/id', function (require, exports, module) {
+
+;
+  module.exports = {
+    ready: function () {
+      if (this.$el('x')) {
+        this.$el('x').setAttr('a', 1)
+      }
+      if (this.$el('y')) {
+        this.$el('y').setAttr('a', 1)
+      }
+      if (this.$el('')) {
+        this.$el('').setAttr('a', 1)
+      }
+      if (this.$el('0')) {
+        this.$el('0').setAttr('a', 1)
+      }
+      if (this.$el(0)) {
+        this.$el(0).setAttr('b', 1)
+      }
+    }
+  }
+
+;module.exports.template = {
+  "type": "div",
+  "children": [
+    {
+      "type": "text",
+      "id": "x",
+      "attr": {
+        "value": "Hello"
+      }
+    },
+    {
+      "type": "text",
+      "id": "0",
+      "attr": {
+        "value": "Hello"
+      }
+    },
+    {
+      "type": "text",
+      "id": '',
+      "attr": {
+        "value": "Hello"
+      }
+    },
+    {
+      "type": "text",
+      "id": null,
+      "attr": {
+        "value": "Hello"
+      }
+    }
+  ]
+}
+
+;})
+
+// require module
+
+bootstrap('@weex-component/id')

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/case/prepare.js
----------------------------------------------------------------------
diff --git a/html5/test/case/prepare.js b/html5/test/case/prepare.js
index fd92918..9629dbb 100644
--- a/html5/test/case/prepare.js
+++ b/html5/test/case/prepare.js
@@ -11,6 +11,7 @@ import {
 import shared from '../../shared'
 import { Document, Element, Comment } from '../../runtime/vdom'
 import Listener from '../../runtime/listener'
+import { TaskCenter, init } from '../../runtime/task-center'
 
 // load framework
 import * as defaultFramework from '../../frameworks/legacy'
@@ -27,10 +28,13 @@ global.callAddElement = function (id, ref, json, index) {
   return callNativeHandler(id, [{ module: 'dom', method: 'addElement', args: [ref, json, index] }])
 }
 
+init()
+
 // create test driver runtime
 export function createRuntime () {
   const config = {
     Document, Element, Comment, Listener,
+    TaskCenter,
     sendTasks (...args) {
       return callNativeHandler(...args)
     }

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/case/tester.js
----------------------------------------------------------------------
diff --git a/html5/test/case/tester.js b/html5/test/case/tester.js
index 490a3a4..9c2b299 100644
--- a/html5/test/case/tester.js
+++ b/html5/test/case/tester.js
@@ -46,6 +46,8 @@ describe('test input and output', function () {
       app.$destroy()
     }
 
+    it('global Weex object', () => checkOutput(app, 'global-weex-object'))
+
     it('single case', () => checkOutput(app, 'foo'))
     it('foo2 case', () => checkOutput(app, 'foo2'))
     it('foo3 case', () => checkOutput(app, 'foo3'))
@@ -78,6 +80,9 @@ describe('test input and output', function () {
     it('repeat with array non-obj case', () => checkOutput(app, 'repeat-array-non-obj'))
     it('repeat watch case', () => checkOutput(app, 'repeat-watch'))
 
+    it('id case', () => checkOutput(app, 'id'))
+    it('dynamic id case', () => checkOutput(app, 'dynamic-id'))
+
     it('reset style case', () => checkOutput(app, 'reset-style'))
     it('dynamic type case', () => checkOutput(app, 'dynamic-type'))
     it('dynamic property case', () => checkOutput(app, 'dynamic-property'))

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/unit/default/api/methods.js
----------------------------------------------------------------------
diff --git a/html5/test/unit/default/api/methods.js b/html5/test/unit/default/api/methods.js
index 56f4ce0..6a7d26a 100644
--- a/html5/test/unit/default/api/methods.js
+++ b/html5/test/unit/default/api/methods.js
@@ -35,17 +35,17 @@ describe('built-in methods', () => {
         differ: new Differ(),
         requireModule: (name) => {
           requireSpy(name)
-
           const module = requireModule(this, name)
+          const mockModule = {}
           for (const moduleName in module) {
-            module[moduleName] = function (...args) {
+            mockModule[moduleName] = function (...args) {
               moduleSpy(...args)
               if (typeof args[args.length - 1] === 'function') {
                 args[args.length - 1]()
               }
             }
           }
-          return module
+          return mockModule
         }
       },
       _setStyle: function () {},

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/unit/default/app/bundle.js
----------------------------------------------------------------------
diff --git a/html5/test/unit/default/app/bundle.js b/html5/test/unit/default/app/bundle.js
index 195b457..183ba49 100644
--- a/html5/test/unit/default/app/bundle.js
+++ b/html5/test/unit/default/app/bundle.js
@@ -4,9 +4,6 @@ import sinonChai from 'sinon-chai'
 const { expect } = chai
 chai.use(sinonChai)
 
-global.callNative = function () {}
-global.callAddElement = function () {}
-
 import * as bundle from '../../../../frameworks/legacy/app/bundle'
 import * as register from '../../../../frameworks/legacy/app/register'
 import { removeWeexPrefix } from '../../../../frameworks/legacy/util'
@@ -46,19 +43,16 @@ describe('parsing a bundle file', () => {
       const id = Date.now()
       callTasksSpy = sinon.spy()
 
-      const doc = new Document(id, '', (tasks, callback) => {
-        app.callTasks(tasks, callback)
-      }, Listener)
+      const doc = new Document(id, '', (tasks) => {
+        app.callTasks(tasks)
+      })
 
       app = {
         id, doc,
         customComponentMap: {},
         commonModules: {},
         callbacks: {},
-        callTasks: (tasks, callback) => {
-          callTasksSpy(tasks)
-          callback && callback()
-        },
+        callTasks: callTasksSpy,
         requireModule: function (name) {
           return register.requireModule(this, name)
         }
@@ -213,7 +207,6 @@ describe('parsing a bundle file', () => {
         expect(callTasksSpy.calledTwice).to.be.true
 
         expect(ready.calledOnce).to.be.true
-
         const task1 = callTasksSpy.firstCall.args[0][0]
         expect(task1.module).to.be.equal('dom')
         expect(task1.method).to.be.equal('createBody')
@@ -256,6 +249,21 @@ describe('parsing a bundle file', () => {
         )
         expect(result).instanceof(Error)
       })
+
+      it('with viewport config', () => {
+        bundle.bootstrap(
+          app,
+          '@weex-component/undefined',
+          {
+            viewport: { width: 640 }
+          }
+        )
+        expect(callTasksSpy.callCount).to.be.equal(1)
+        const tasks = callTasksSpy.lastCall.args[0]
+        expect(tasks[0].module).to.be.equal('meta')
+        expect(tasks[0].method).to.be.equal('setViewport')
+        expect(tasks[0].args).to.deep.equal([{ width: 640 }])
+      })
     })
   })
 

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/unit/default/app/ctrl.js
----------------------------------------------------------------------
diff --git a/html5/test/unit/default/app/ctrl.js b/html5/test/unit/default/app/ctrl.js
index 9f69431..f7d91f2 100644
--- a/html5/test/unit/default/app/ctrl.js
+++ b/html5/test/unit/default/app/ctrl.js
@@ -4,13 +4,9 @@ import sinonChai from 'sinon-chai'
 const { expect } = chai
 chai.use(sinonChai)
 
-global.callNative = function () {}
-global.callAddElement = function () {}
-
 import * as ctrl from '../../../../frameworks/legacy/app/ctrl'
 import Differ from '../../../../frameworks/legacy/app/differ'
 import { Document } from '../../../../runtime/vdom'
-import Listener from '../../../../runtime/listener'
 
 describe('the api of app', () => {
   let app
@@ -26,15 +22,14 @@ describe('the api of app', () => {
       registerComponent: function () {},
       // define: sinon.spy(),
       // bootstrap: sinon.stub(),
-      callbacks: {
-        1: spy2
-      },
       vm: {},
       differ: new Differ(id)
     }
 
-    app.doc = new Document(id, '', spy1, Listener)
+    app.doc = new Document(id, '', spy1)
     app.doc.createBody('div')
+
+    app.doc.taskCenter.callbackManager.add(spy2)
     // app.bootstrap.returns()
 
     return app
@@ -129,8 +124,6 @@ describe('the api of app', () => {
       const data = { a: 'b' }
       ctrl.callback(app, '1', data, true)
       expect(spy2.calledOnce).to.be.true
-      expect(spy2.args[0][0]).to.deep.equal(data)
-      expect(app.callbacks[1]).to.be.a('function')
 
       const task = spy1.firstCall.args[0][0]
       expect(task.module).to.be.equal('dom')
@@ -142,13 +135,9 @@ describe('the api of app', () => {
       const data = { a: 'b' }
       ctrl.callback(app, '1', data, true)
       expect(spy2.calledTwice).to.be.true
-      expect(spy2.args[0][0]).to.deep.equal(data)
-      expect(app.callbacks[1]).to.be.a('function')
 
       ctrl.callback(app, '1', data, false)
       expect(spy2.calledThrice).to.be.true
-      expect(spy2.args[0][0]).to.deep.equal(data)
-      expect(app.callbacks[1]).to.be.undefined
     })
 
     it('error', () => {
@@ -158,31 +147,6 @@ describe('the api of app', () => {
     })
   })
 
-  describe('updateActions', () => {
-    let originalCallNative
-
-    before(() => {
-      originalCallNative = global.callNative
-      global.callNative = function () {}
-    })
-
-    after(() => {
-      global.callNative = originalCallNative
-    })
-
-    it('update actions in listener', () => {
-      app.doc.listener.updates = [
-        {
-          method () {},
-          args: [undefined, null, /\.x/i, new Date(), 2, '3', true, ['']]
-        }
-      ]
-      ctrl.updateActions(app)
-
-      expect(app.doc.listener.updates).to.deep.equal([])
-    })
-  })
-
   describe('refreshData', () => {
     it('a simple data', () => {
       const data = { b: 'c' }
@@ -219,7 +183,6 @@ describe('the api of app', () => {
       expect(app.vm).to.be.null
       expect(app.doc).to.be.null
       expect(app.customComponentMap).to.be.null
-      expect(app.callbacks).to.be.null
     })
     it('the incomplete data', () => {
       const appx = createApp()
@@ -230,7 +193,6 @@ describe('the api of app', () => {
       expect(appx.vm).to.be.null
       expect(appx.doc).to.be.null
       expect(appx.customComponentMap).to.be.null
-      expect(appx.callbacks).to.be.null
     })
     it('clear vms', () => {
       const appy = createApp()
@@ -245,7 +207,6 @@ describe('the api of app', () => {
       expect(appy.vm).to.be.null
       expect(appy.doc).to.be.null
       expect(appy.customComponentMap).to.be.null
-      expect(appy.callbacks).to.be.null
     })
   })
 })

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/unit/default/app/index.js
----------------------------------------------------------------------
diff --git a/html5/test/unit/default/app/index.js b/html5/test/unit/default/app/index.js
index c8c57c0..53a6938 100644
--- a/html5/test/unit/default/app/index.js
+++ b/html5/test/unit/default/app/index.js
@@ -4,43 +4,22 @@ import sinonChai from 'sinon-chai'
 const { expect } = chai
 chai.use(sinonChai)
 
-global.callNative = function () {}
-global.callAddElement = function () {}
-
 import App from '../../../../frameworks/legacy/app'
-import { Element } from '../../../../runtime/vdom'
+import { Element, Document } from '../../../../runtime/vdom'
 
 describe('App Instance', () => {
-  const oriCallNative = global.callNative
-  const oriCallAddElement = global.callAddElement
-  const callNativeSpy = sinon.spy()
-  const callAddElementSpy = sinon.spy()
+  const oriDocumentHandler = Document.handler
+  const sendTasksSpy = sinon.spy()
   let app
 
-  before(() => {
-    global.callNative = (id, tasks, callbackId) => {
-      callNativeSpy(id, tasks, callbackId)
-      /* istanbul ignore if */
-      if (callbackId !== '-1') {
-        app.callbacks[callbackId] && app.callbacks[callbackId]()
-      }
-    }
-    global.callAddElement = (name, ref, json, index, callbackId) => {
-      callAddElementSpy(name, ref, json, index, callbackId)
-      /* istanbul ignore if */
-      if (callbackId !== '-1') {
-        app.callbacks[callbackId] && app.callbacks[callbackId]()
-      }
-    }
-  })
-
   beforeEach(() => {
-    app = new App(Date.now() + '')
+    Document.handler = sendTasksSpy
+    const id = Date.now() + ''
+    app = new App(id, {})
   })
 
-  after(() => {
-    global.callNative = oriCallNative
-    global.callAddElement = oriCallAddElement
+  afterEach(() => {
+    Document.handler = oriDocumentHandler
   })
 
   describe('normal check', () => {
@@ -75,7 +54,7 @@ describe('App Instance', () => {
       }]
 
       app.callTasks(tasks)
-      expect(callNativeSpy.lastCall.args[1]).to.deep.equal(tasks)
+      expect(sendTasksSpy.lastCall.args[1]).to.deep.equal(tasks)
     })
 
     it('with callback', (done) => {
@@ -86,11 +65,13 @@ describe('App Instance', () => {
       }]
 
       app.callTasks(tasks)
-      expect(callNativeSpy.lastCall.args[1]).to.deep.equal(tasks)
+      expect(sendTasksSpy.lastCall.args[1]).to.deep.equal(tasks)
       done()
     })
 
     it('with function arg', (done) => {
+      const callbackId = '1'
+
       const tasks = [{
         module: 'dom',
         method: 'createBody',
@@ -98,8 +79,11 @@ describe('App Instance', () => {
       }]
 
       app.callTasks(tasks)
-      expect(callNativeSpy.lastCall.args[1]).to.deep.equal(tasks)
-      expect(callNativeSpy.lastCall.args[1][0].args[0]).to.be.a('string')
+      expect(sendTasksSpy.lastCall.args[1]).to.deep.equal([{
+        module: 'dom',
+        method: 'createBody',
+        args: [callbackId]
+      }])
       done()
     })
 
@@ -114,12 +98,17 @@ describe('App Instance', () => {
       }]
 
       app.callTasks(tasks)
-      expect(callNativeSpy.lastCall.args[1]).to.deep.equal(tasks)
-      expect(callNativeSpy.lastCall.args[1][0].args[0]).to.be.equal('1')
+      expect(sendTasksSpy.lastCall.args[1]).to.deep.equal([{
+        module: 'dom',
+        method: 'createBody',
+        args: [node.ref]
+      }])
       done()
     })
 
     it('with callback after close', (done) => {
+      const callbackId = '1'
+
       const tasks = [{
         module: 'dom',
         method: 'createBody',
@@ -129,8 +118,11 @@ describe('App Instance', () => {
       app.doc.close()
 
       app.callTasks(tasks)
-      expect(callNativeSpy.lastCall.args[1]).to.deep.equal(tasks)
-      expect(callNativeSpy.lastCall.args[1][0].args[0]).to.be.a('string')
+      expect(sendTasksSpy.lastCall.args[1]).to.deep.equal([{
+        module: 'dom',
+        method: 'createBody',
+        args: [callbackId]
+      }])
       done()
     })
   })

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/unit/default/app/viewport.js
----------------------------------------------------------------------
diff --git a/html5/test/unit/default/app/viewport.js b/html5/test/unit/default/app/viewport.js
new file mode 100644
index 0000000..ea914a3
--- /dev/null
+++ b/html5/test/unit/default/app/viewport.js
@@ -0,0 +1,61 @@
+import chai from 'chai'
+import sinon from 'sinon'
+import sinonChai from 'sinon-chai'
+const { expect } = chai
+chai.use(sinonChai)
+
+import * as viewport from '../../../../frameworks/legacy/app/viewport'
+
+describe('viewport', function () {
+  const originalCallNative = global.callNative
+  const { setViewport, validateViewport } = viewport
+  const mockApp = {
+    id: 'mock',
+    callTasks (...args) {
+      global.callNative(...args)
+    }
+  }
+
+  before(() => {
+    sinon.stub(console, 'warn')
+  })
+
+  beforeEach(() => {
+    global.callNative = sinon.spy()
+  })
+  afterEach(() => {
+    global.callNative = originalCallNative
+    console.warn.reset()
+  })
+
+  it('invalid setViewport', () => {
+    setViewport()
+    expect(global.callNative.callCount).to.be.equal(0)
+    setViewport({})
+    expect(global.callNative.callCount).to.be.equal(0)
+  })
+
+  it('setViewport', () => {
+    setViewport(mockApp, {})
+    expect(global.callNative.callCount).to.be.equal(1)
+    setViewport(mockApp, { width: 640 })
+    expect(global.callNative.callCount).to.be.equal(2)
+    setViewport(mockApp, { width: 'device-width' })
+    expect(global.callNative.callCount).to.be.equal(3)
+  })
+
+  it('validateViewport', () => {
+    expect(validateViewport()).to.be.false
+    expect(console.warn.callCount).to.be.equal(1)
+    expect(validateViewport({})).to.be.false
+    expect(console.warn.callCount).to.be.equal(2)
+
+    expect(validateViewport({ width: 200 })).to.be.true
+    expect(console.warn.callCount).to.be.equal(2)
+    expect(validateViewport({ width: 'device-width' })).to.be.true
+    expect(console.warn.callCount).to.be.equal(2)
+
+    expect(validateViewport({ width: 'initial-width' })).to.be.false
+    expect(console.warn.callCount).to.be.equal(3)
+  })
+})

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/unit/default/runtime.js
----------------------------------------------------------------------
diff --git a/html5/test/unit/default/runtime.js b/html5/test/unit/default/runtime.js
index 67a59ab..57a36b2 100644
--- a/html5/test/unit/default/runtime.js
+++ b/html5/test/unit/default/runtime.js
@@ -9,6 +9,7 @@ chai.use(sinonChai)
 import runtime from '../../../runtime'
 import frameworks from '../../../frameworks'
 import defaultConfig from '../../../frameworks/legacy/config'
+import { init as resetTaskHandler } from '../../../runtime/task-center'
 
 const { init, config } = runtime
 config.frameworks = frameworks
@@ -29,8 +30,10 @@ function clearRefs (json) {
 describe('framework entry', () => {
   const oriCallNative = global.callNative
   const oriCallAddElement = global.callAddElement
+  const oriDocumentHandler = config.Document.handler
   const callNativeSpy = sinon.spy()
   const callAddElementSpy = sinon.spy()
+  const documentHandlerSpy = sinon.spy()
   const instanceId = Date.now() + ''
 
   before(() => {
@@ -44,7 +47,6 @@ describe('framework entry', () => {
         }])
       }
     }
-    config.Document.handler = global.callNative
     global.callAddElement = (name, id, ref, json, index, callbackId) => {
       callAddElementSpy(name, ref, json, index, callbackId)
       /* istanbul ignore if */
@@ -55,15 +57,18 @@ describe('framework entry', () => {
         }])
       }
     }
+    config.Document.handler = oriDocumentHandler
+    resetTaskHandler()
   })
 
   afterEach(() => {
     callNativeSpy.reset()
     callAddElementSpy.reset()
+    documentHandlerSpy.reset()
   })
 
   after(() => {
-    config.Document.handler = function () {}
+    config.Document.handler = oriDocumentHandler
     global.callNative = oriCallNative
     global.callAddElement = oriCallAddElement
   })
@@ -104,7 +109,6 @@ describe('framework entry', () => {
         bootstrap('@weex-component/main')
       `
       framework.createInstance(instanceId, code)
-
       expect(callNativeSpy.callCount).to.be.equal(2)
       expect(callAddElementSpy.callCount).to.be.equal(1)
 
@@ -168,7 +172,7 @@ describe('framework entry', () => {
       expect(frameworks.xxx.createInstance.callCount).equal(1)
       expect(frameworks.yyy.createInstance.callCount).equal(0)
       expect(frameworks.Weex.createInstance.callCount).equal(0)
-      expect(frameworks.xxx.createInstance.firstCall.args).eql([
+      expect(frameworks.xxx.createInstance.firstCall.args.slice(0, 4)).eql([
         instanceId + '~',
         code,
         { bundleVersion: '0.3.1', env: {}},
@@ -191,7 +195,7 @@ describe('framework entry', () => {
       expect(frameworks.xxx.createInstance.callCount).equal(2)
       expect(frameworks.yyy.createInstance.callCount).equal(0)
       expect(frameworks.Weex.createInstance.callCount).equal(1)
-      expect(frameworks.Weex.createInstance.firstCall.args).eql([
+      expect(frameworks.Weex.createInstance.firstCall.args.slice(0, 4)).eql([
         instanceId + '~~~',
         code,
         { bundleVersion: undefined, env: {}},
@@ -212,7 +216,7 @@ describe('framework entry', () => {
       expect(frameworks.xxx.createInstance.callCount).equal(2)
       expect(frameworks.yyy.createInstance.callCount).equal(1)
       expect(frameworks.Weex.createInstance.callCount).equal(1)
-      expect(frameworks.yyy.createInstance.firstCall.args).eql([
+      expect(frameworks.yyy.createInstance.firstCall.args.slice(0, 4)).eql([
         instanceId + '~~~~',
         code,
         { bundleVersion: undefined, env: {}},
@@ -225,7 +229,7 @@ describe('framework entry', () => {
       expect(frameworks.xxx.createInstance.callCount).equal(2)
       expect(frameworks.yyy.createInstance.callCount).equal(1)
       expect(frameworks.Weex.createInstance.callCount).equal(2)
-      expect(frameworks.Weex.createInstance.secondCall.args).eql([
+      expect(frameworks.Weex.createInstance.secondCall.args.slice(0, 4)).eql([
         instanceId + '~~~~~',
         code,
         { bundleVersion: undefined, env: {}},
@@ -238,7 +242,7 @@ describe('framework entry', () => {
       expect(frameworks.xxx.createInstance.callCount).equal(2)
       expect(frameworks.yyy.createInstance.callCount).equal(1)
       expect(frameworks.Weex.createInstance.callCount).equal(3)
-      expect(frameworks.Weex.createInstance.thirdCall.args).eql([
+      expect(frameworks.Weex.createInstance.thirdCall.args.slice(0, 4)).eql([
         instanceId + '~~~~~~',
         code,
         { bundleVersion: undefined, env: {}},
@@ -320,14 +324,13 @@ describe('framework entry', () => {
       const textRef = json.children[0].ref
       framework.refreshInstance(instanceId, { showText: false })
       expect(callNativeSpy.callCount).to.be.equal(2)
-
       expect(callNativeSpy.firstCall.args[0]).to.be.equal(instanceId)
       expect(callNativeSpy.firstCall.args[1]).to.deep.equal([{
         module: 'dom',
         method: 'removeElement',
         args: [textRef]
       }])
-      expect(callNativeSpy.firstCall.args[2]).to.be.equal('-1')
+      // expect(callNativeSpy.firstCall.args[2]).to.be.equal('-1')
 
       expect(callNativeSpy.secondCall.args[0]).to.be.equal(instanceId)
       expect(callNativeSpy.secondCall.args[1]).to.deep.equal([{
@@ -335,7 +338,7 @@ describe('framework entry', () => {
         method: 'refreshFinish',
         args: []
       }])
-      expect(callNativeSpy.secondCall.args[2]).to.be.equal('-1')
+      // expect(callNativeSpy.secondCall.args[2]).to.be.equal('-1')
     })
 
     it('with a non-exist instanceId', () => {
@@ -384,6 +387,21 @@ describe('framework entry', () => {
       })
       expect(defaultConfig.nativeComponentMap).not.contain.keys('e')
     })
+
+    it('with methods', () => {
+      const components = [{
+        type: 'x',
+        methods: ['foo', 'bar']
+      }, {
+        type: 'y',
+        methods: []
+      }, {
+        type: 'z',
+        methods: null
+      }]
+      framework.registerComponents(components)
+      expect(defaultConfig.nativeComponentMap).to.contain.keys('x', 'y', 'z')
+    })
   })
 
   describe('register modules', () => {

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/unit/default/vm/dom-helper.js
----------------------------------------------------------------------
diff --git a/html5/test/unit/default/vm/dom-helper.js b/html5/test/unit/default/vm/dom-helper.js
index 1f4c9c8..fbcd8d2 100644
--- a/html5/test/unit/default/vm/dom-helper.js
+++ b/html5/test/unit/default/vm/dom-helper.js
@@ -1,9 +1,6 @@
 import chai from 'chai'
 const { expect } = chai
 
-global.callNative = function () {}
-global.callAddElement = function () {}
-
 import {
   createElement,
   createBlock,
@@ -104,10 +101,13 @@ describe('help attach target', () => {
   })
 
   it('attach body to documentElement', () => {
+    const oriCallnative = global.callNative
+    global.callNative = function () {}
     const target = createBody(vm, 'bar')
     const dest = vm._app.doc.documentElement
     attachTarget(vm, target, dest)
     expect(dest.children).eql([target])
+    global.callNative = oriCallnative
   })
 
   it('attach element to body', () => {
@@ -329,12 +329,15 @@ describe('help remove target', () => {
   })
 
   it('remove body', () => {
+    const oriCallnative = global.callNative
+    global.callNative = function () {}
     const parent = vm._app.doc.documentElement
     const element = createBody(vm, 'baz')
     parent.appendChild(element)
     expect(parent.children).eql([element])
     removeTarget(vm, element)
     expect(parent.children).eql([])
+    global.callNative = oriCallnative
   })
 
   it('remove element', () => {

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/unit/default/vm/events.js
----------------------------------------------------------------------
diff --git a/html5/test/unit/default/vm/events.js b/html5/test/unit/default/vm/events.js
index 28e9a59..e065e7f 100644
--- a/html5/test/unit/default/vm/events.js
+++ b/html5/test/unit/default/vm/events.js
@@ -4,12 +4,8 @@ import sinonChai from 'sinon-chai'
 const { expect } = chai
 chai.use(sinonChai)
 
-global.callNative = function () {}
-global.callAddElement = function () {}
-
 import Vm from '../../../../frameworks/legacy/vm'
 import { Document } from '../../../../runtime/vdom'
-import Listener from '../../../../runtime/listener'
 
 describe('bind and fire events', () => {
   let doc, customComponentMap, spy
@@ -26,9 +22,7 @@ describe('bind and fire events', () => {
 
   beforeEach(() => {
     spy = sinon.spy()
-    doc = new Document('test', '', (actions) => {
-      spy(actions)
-    }, Listener)
+    doc = new Document('test', '', spy)
     customComponentMap = {}
   })
 
@@ -59,7 +53,6 @@ describe('bind and fire events', () => {
     const vm = new Vm('foo', customComponentMap.foo, { _app: app })
 
     checkReady(vm, function () {
-      doc.close()
       expect(doc.body.event.click).a('function')
 
       const el = doc.body
@@ -68,10 +61,9 @@ describe('bind and fire events', () => {
       expect(doc.listener.updates.length).eql(0)
 
       el.event.click({ xxx: 1 })
-
       expect(el.attr.a).eql(2)
-      expect(spy.args.length).eql(1)
-      expect(doc.listener.updates).eql([
+      expect(spy.args.length).eql(2)
+      expect(spy.args[1][0]).eql([
         { module: 'dom', method: 'updateAttrs', args: [el.ref, { a: 2 }] }
       ])
 

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/unit/default/vm/vm.js
----------------------------------------------------------------------
diff --git a/html5/test/unit/default/vm/vm.js b/html5/test/unit/default/vm/vm.js
index e12b068..ffd9eb7 100644
--- a/html5/test/unit/default/vm/vm.js
+++ b/html5/test/unit/default/vm/vm.js
@@ -4,14 +4,13 @@ import sinonChai from 'sinon-chai'
 const { expect } = chai
 chai.use(sinonChai)
 
-global.callNative = function () {}
-global.callAddElement = function () {}
-
 import Vm from '../../../../frameworks/legacy/vm'
 import { Document } from '../../../../runtime/vdom'
-import Listener from '../../../../runtime/listener'
+import { init as resetTaskHandler } from '../../../../runtime/task-center'
 import Differ from '../../../../frameworks/legacy/app/differ'
 
+const oriCallNative = global.callNative
+
 describe('generate virtual dom for a single vm', () => {
   const spy = sinon.spy()
   const spy1 = sinon.spy()
@@ -25,7 +24,7 @@ describe('generate virtual dom for a single vm', () => {
       actions.forEach((action) => {
         spy.apply(null, ['test', action.method].concat(action.args))
       })
-    }, Listener)
+    })
     customComponentMap = {}
   })
 
@@ -942,13 +941,16 @@ describe('generate virtual dom for sub vm', () => {
   let differ
 
   beforeEach(() => {
-    doc = new Document('test', null, null, Listener)
+    global.callNative = function () {}
+    resetTaskHandler()
+    doc = new Document('test', null, null)
     customComponentMap = {}
     differ = new Differ('test')
   })
 
   afterEach(() => {
     doc.destroy()
+    global.callNative = oriCallNative
   })
 
   it('generate sub elements', () => {
@@ -1588,7 +1590,7 @@ describe('generate dom actions', () => {
       actions.forEach((action) => {
         spy.apply(null, ['bar', action.method].concat(action.args))
       })
-    }, Listener)
+    })
     differ = new Differ('foo')
     customComponentMap = {}
     app = { doc, customComponentMap, differ }

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/unit/shared/BroadcastChannel.js
----------------------------------------------------------------------
diff --git a/html5/test/unit/shared/BroadcastChannel.js b/html5/test/unit/shared/BroadcastChannel.js
new file mode 100644
index 0000000..3fda443
--- /dev/null
+++ b/html5/test/unit/shared/BroadcastChannel.js
@@ -0,0 +1,220 @@
+import { expect } from 'chai'
+import sinon from 'sinon'
+
+import service from '../../../services/broadcast-channel/index'
+import { MessageEvent } from '../../../services/broadcast-channel/message-event'
+const { BroadcastChannel } = service.create('foo').instance
+
+describe('BroadcastChannel', () => {
+  it('is a function', () => {
+    expect(BroadcastChannel).is.an('function')
+  })
+
+  it('has standard APIs', () => {
+    const JamesBond = new BroadcastChannel('007')
+    expect(JamesBond.name).is.an('string')
+    expect(JamesBond.onmessage).is.an('null')
+    expect(JamesBond.postMessage).is.an('function')
+    expect(JamesBond.close).is.an('function')
+
+    expect(JamesBond).to.have.ownProperty('name')
+    expect(JamesBond).to.have.ownProperty('onmessage')
+    expect(JamesBond).not.to.have.ownProperty('postMessage')
+    expect(JamesBond).not.to.have.ownProperty('close')
+    JamesBond.close()
+  })
+
+  it.skip('inherit APIs', () => {
+    const ProfessorX = new BroadcastChannel('Charles')
+    expect(ProfessorX.addEventListener).is.an('function')
+    expect(ProfessorX.removeEventListener).is.an('function')
+    expect(ProfessorX.dispatchEvent).is.an('function')
+  })
+
+  it('name attribute is readonly', () => {
+    const Wolverine = new BroadcastChannel('Logan')
+    expect(Wolverine.name).to.equal('Logan')
+    Wolverine.name = 'Wolverine'
+    expect(Wolverine.name).to.equal('Logan')
+    Wolverine.close()
+  })
+
+  describe('basci usage', () => {
+    const Hulk = new BroadcastChannel('Avengers')
+    const Stack = new BroadcastChannel('Avengers')
+    const Steven = new BroadcastChannel('Avengers')
+    const Logan = new BroadcastChannel('Mutants')
+    const Erik = new BroadcastChannel('Mutants')
+
+    beforeEach(() => {
+      Hulk.onmessage = sinon.spy()
+      Stack.onmessage = sinon.spy()
+      Steven.onmessage = sinon.spy()
+      Logan.onmessage = sinon.spy()
+    })
+
+    afterEach(() => {
+      Hulk.onmessage = null
+      Stack.onmessage = null
+      Steven.onmessage = null
+      Logan.onmessage = null
+      Erik.onmessage = null
+    })
+
+    it('trigger onmessage', () => {
+      Hulk.postMessage('Hulk Smash !!!')
+      expect(Hulk.onmessage.callCount).to.be.equal(0)
+      expect(Logan.onmessage.callCount).to.be.equal(0)
+      expect(Stack.onmessage.callCount).to.be.equal(1)
+      expect(Steven.onmessage.callCount).to.be.equal(1)
+    })
+
+    it('don\'t trigger onmessage itself', () => {
+      Logan.postMessage('Any one here ?')
+      expect(Hulk.onmessage.callCount).to.be.equal(0)
+      expect(Logan.onmessage.callCount).to.be.equal(0)
+      expect(Stack.onmessage.callCount).to.be.equal(0)
+      expect(Steven.onmessage.callCount).to.be.equal(0)
+    })
+
+    it('send multi messages', () => {
+      Hulk.postMessage('Hulk Smash !!!')
+      Logan.postMessage('I will fight you !')
+      Stack.postMessage('whatever')
+      Hulk.postMessage('whatever')
+      Stack.postMessage('whatever')
+      Steven.postMessage('whatever')
+      Stack.postMessage('whatever')
+
+      expect(Hulk.onmessage.callCount).to.be.equal(4)
+      expect(Logan.onmessage.callCount).to.be.equal(0)
+      expect(Stack.onmessage.callCount).to.be.equal(3)
+      expect(Steven.onmessage.callCount).to.be.equal(5)
+    })
+
+    it('send string message', () => {
+      Stack.postMessage('I am Iron-Man.')
+
+      expect(Hulk.onmessage.callCount).to.be.equal(1)
+      expect(Steven.onmessage.callCount).to.be.equal(1)
+
+      const event = Hulk.onmessage.firstCall.args[0]
+      expect(event).is.an('object')
+      expect(event.data).is.a('string')
+      expect(event.data).to.be.equal('I am Iron-Man.')
+    })
+
+    it('send object message', () => {
+      const message = {
+        type: 'SOKOVIA ACCORDS',
+        approvedCountry: 117,
+        content: 'The Avengers shall no longer be a private organization.'
+      }
+
+      Stack.postMessage(message)
+
+      const event = Steven.onmessage.firstCall.args[0]
+      expect(event).is.an('object')
+      expect(event.data).is.a('object')
+      expect(event.data).to.deep.equal(message)
+    })
+
+    it('close channel', () => {
+      Hulk.close()
+
+      Steven.postMessage('come to fight !')
+      expect(Hulk.onmessage.callCount).to.be.equal(0)
+      expect(Stack.onmessage.callCount).to.be.equal(1)
+      expect(Steven.onmessage.callCount).to.be.equal(0)
+    })
+
+    it('send message after close', () => {
+      Hulk.close()
+
+      expect(() => { Hulk.postMessage('I am leaving.') }).to.throw(Error)
+
+      expect(Hulk.onmessage.callCount).to.be.equal(0)
+      expect(Logan.onmessage.callCount).to.be.equal(0)
+      expect(Stack.onmessage.callCount).to.be.equal(0)
+      expect(Steven.onmessage.callCount).to.be.equal(0)
+    })
+
+    it('MessageEvent dafault parameters', () => {
+      const event = new MessageEvent()
+
+      expect(event).is.an('object')
+      expect(event.data).to.be.null
+      expect(event.type).to.be.equal('message')
+      expect(event.origin).to.be.equal('')
+      expect(event.target).to.be.null
+      expect(event.source).to.be.null
+      expect(event.timeStamp).to.be.a('number')
+      expect(event.ports).to.be.an('array')
+    })
+
+    it('MessageEvent constructor', () => {
+      const source = { type: 'empty' }
+      const event = new MessageEvent('custom', {
+        source,
+        data: 'Nothing',
+        origin: 'http://127.0.0.1',
+        ports: ['8080']
+      })
+
+      expect(event).is.an('object')
+      expect(event.data).to.be.equal('Nothing')
+      expect(event.type).to.be.equal('custom')
+      expect(event.origin).to.be.equal('http://127.0.0.1')
+      expect(event.target).to.be.null
+      expect(event.source).to.deep.equal(source)
+      expect(event.timeStamp).to.be.a('number')
+      expect(event.ports).to.deep.equal(['8080'])
+    })
+
+    it('use MessageEvent', () => {
+      Steven.postMessage('Be Together.')
+
+      const event = Stack.onmessage.firstCall.args[0]
+      expect(event).is.an('object')
+      expect(event.data).to.be.equal('Be Together.')
+      expect(event.type).to.be.equal('message')
+      expect(event.origin).to.be.equal('')
+      expect(event.target).to.be.null
+      expect(event.timeStamp).to.be.a('number')
+    })
+
+    it('invalid usage', () => {
+      const stranger = {
+        name: 'stranger',
+        close: Erik.close,
+        postMessage: Erik.postMessage
+      }
+
+      stranger.postMessage('hello world.')
+
+      expect(Hulk.onmessage.callCount).to.be.equal(0)
+      expect(Logan.onmessage.callCount).to.be.equal(0)
+      expect(Stack.onmessage.callCount).to.be.equal(0)
+      expect(Steven.onmessage.callCount).to.be.equal(0)
+
+      stranger.close()
+    })
+
+    it('close all', () => {
+      Hulk.close()
+      Stack.close()
+      Steven.close()
+      Logan.close()
+      Erik.close()
+
+      // close again
+      expect(() => {
+        Hulk.close()
+        Stack.close()
+        Steven.close()
+        Logan.close()
+        Erik.close()
+      }).to.not.throw
+    })
+  })
+})

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/unit/vanilla/index.js
----------------------------------------------------------------------
diff --git a/html5/test/unit/vanilla/index.js b/html5/test/unit/vanilla/index.js
index 24584da..a0e84e8 100644
--- a/html5/test/unit/vanilla/index.js
+++ b/html5/test/unit/vanilla/index.js
@@ -1,8 +1,5 @@
 import { expect } from 'chai'
 
-global.callNative = function () {}
-global.callAddElement = function () {}
-
 import vanilla from '../../../frameworks/vanilla'
 import runtime from '../../../runtime'
 

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/unit/vdom/index.js
----------------------------------------------------------------------
diff --git a/html5/test/unit/vdom/index.js b/html5/test/unit/vdom/index.js
index 886d7e6..7c8d854 100644
--- a/html5/test/unit/vdom/index.js
+++ b/html5/test/unit/vdom/index.js
@@ -4,28 +4,15 @@ import sinonChai from 'sinon-chai'
 const { expect } = chai
 chai.use(sinonChai)
 
-global.callNative = function () {}
-global.callAddElement = function () {}
-
 import {
   Document,
   Element,
-  Comment
+  Comment,
+  elementTypes,
+  registerElement,
+  clearElementTypes
 } from '../../../runtime/vdom'
 
-global.callNative = function () {}
-global.callAddElement = function () {}
-
-const tempHandler = Document.handler
-
-before(() => {
-  Document.handler = global.callNative
-})
-
-after(() => {
-  Document.handler = tempHandler
-})
-
 describe('document constructor', () => {
   it('create & destroy document', () => {
     const doc = new Document('foo', 'http://path/to/url')
@@ -38,6 +25,49 @@ describe('document constructor', () => {
   })
 })
 
+describe('component methods management', () => {
+  before(() => {
+    registerElement('x', ['foo', 'bar'])
+    registerElement('y', [])
+    registerElement('z')
+  })
+
+  after(() => {
+    clearElementTypes()
+  })
+
+  it('has registered element types', () => {
+    expect(Object.keys(elementTypes)).eql(['x'])
+  })
+
+  it('will call component method', () => {
+    const spy = sinon.spy()
+    const doc = new Document('test', '', spy)
+    const x = new Element('x')
+    const y = new Element('y')
+    const z = new Element('z')
+    const n = new Element('n')
+    expect(x.foo).is.function
+    expect(x.bar).is.function
+    expect(x.baz).is.undefined
+    expect(y.foo).is.undefined
+    expect(z.foo).is.undefined
+    expect(n.foo).is.undefined
+
+    doc.createBody('r')
+    doc.documentElement.appendChild(doc.body)
+    doc.body.appendChild(x)
+    doc.body.appendChild(y)
+    doc.body.appendChild(z)
+    doc.body.appendChild(n)
+    expect(spy.args.length).eql(5)
+
+    x.foo(1, 2, 3)
+    expect(spy.args.length).eql(6)
+    expect(spy.args[5]).eql([[{ component: 'x', method: 'foo', ref: x.ref, args: [1, 2, 3] }]])
+  })
+})
+
 describe('document methods', () => {
   let doc
 
@@ -435,12 +465,12 @@ describe('complicated situations', () => {
     doc = new Document('foo', '', spy)
     doc.createBody('r')
     doc.documentElement.appendChild(doc.body)
-    el = new Element('bar', null, doc)
-    el2 = new Element('baz', null, doc)
-    el3 = new Element('qux', null, doc)
-    c = new Comment('aaa', doc)
-    c2 = new Comment('bbb', doc)
-    c3 = new Comment('ccc', doc)
+    el = new Element('bar')
+    el2 = new Element('baz')
+    el3 = new Element('qux')
+    c = new Comment('aaa')
+    c2 = new Comment('bbb')
+    c3 = new Comment('ccc')
   })
 
   afterEach(() => {

http://git-wip-us.apache.org/repos/asf/incubator-weex/blob/b5123119/html5/test/unit/vdom/listener.js
----------------------------------------------------------------------
diff --git a/html5/test/unit/vdom/listener.js b/html5/test/unit/vdom/listener.js
index 1f260b6..84de22b 100644
--- a/html5/test/unit/vdom/listener.js
+++ b/html5/test/unit/vdom/listener.js
@@ -4,15 +4,9 @@ import sinonChai from 'sinon-chai'
 const { expect } = chai
 chai.use(sinonChai)
 
-global.callNative = function () {}
-global.callAddElement = function () {}
-
 import { Document } from '../../../runtime/vdom'
 import Listener from '../../../runtime/listener'
 
-global.callNative = function () {}
-global.callAddElement = function () {}
-
 describe('dom listener basic', () => {
   it('works with no id', () => {
     const doc = new Document(null, null, null)
@@ -20,6 +14,10 @@ describe('dom listener basic', () => {
   })
 
   it('works with no handler', () => {
+    const oriCallNative = global.callNative
+    const oriCallAddElement = global.callAddElement
+    const oriDocumentHandler = Document.handler
+
     Document.handler = null
     global.callNative = function () { return -1 }
     global.callAddElement = function () { return -1 }
@@ -30,6 +28,10 @@ describe('dom listener basic', () => {
     const el = doc.createElement('a')
     doc.body.appendChild(el)
     doc.destroy()
+
+    global.callNative = oriCallNative
+    global.callAddElement = oriCallAddElement
+    Document.handler = oriDocumentHandler
   })
 
   it('works with an handler', () => {
@@ -77,7 +79,7 @@ describe('dom listener details', () => {
     expect(spy.args[0]).eql([[{
       module: 'dom', method: 'createBody',
       args: [{ type: 'r', ref: '_root', attr: { a: 1 }, style: { b: 2 }}]
-    }]])
+    }], '-1'])
     done()
   })
 
@@ -108,7 +110,7 @@ describe('dom listener details', () => {
     expect(spy.args[0]).eql([[{
       module: 'dom', method: 'createBody',
       args: [{ type: 'r', ref: '_root', attr: { a: 1 }, style: { b: 2 }}]
-    }]])
+    }], '-1'])
     done()
   })
 
@@ -192,7 +194,7 @@ describe('dom listener details', () => {
     expect(spy.args[0]).eql([[{
       module: 'dom', method: 'createBody',
       args: [body.toJSON()]
-    }]])
+    }], '-1'])
 
     const el = doc.createElement('a')
     el.setAttr('x', 1)
@@ -202,7 +204,7 @@ describe('dom listener details', () => {
     expect(spy.args[1]).eql([[{
       module: 'dom', method: 'addElement',
       args: ['_root', el.toJSON(), -1]
-    }]])
+    }], '-1'])
 
     const el2 = doc.createElement('b')
     doc.body.insertBefore(el2, el) // [el2, el]
@@ -214,16 +216,16 @@ describe('dom listener details', () => {
     expect(spy.args[2]).eql([[{
       module: 'dom', method: 'addElement',
       args: ['_root', el2.toJSON(), 0]
-    }]])
+    }], '-1'])
     expect(spy.args[3]).eql([[{
       module: 'dom', method: 'addElement',
       args: ['_root', el3.toJSON(), 2]
-    }]])
+    }], '-1'])
 
     done()
   })
 
-  it('batch when document closed', (done) => {
+  it.skip('batch when document closed', (done) => {
     const body = doc.createBody('r')
 
     doc.documentElement.appendChild(body)
@@ -348,16 +350,14 @@ describe('dom listener details', () => {
       args: [el.ref, 'appear']
     }])
 
-    doc.close()
-
     el.setAttr('a', 1)
     el.setStyle('a', 2)
     el.setClassStyle({ a: 3, b: 4 })
     el.addEvent('click', () => {})
     el.addEvent('appear', () => {})
     el.removeEvent('appear')
-    expect(spy.args.length).eql(10)
-    expect(doc.listener.updates).eql([
+    expect(spy.args.length).eql(16)
+    expect(spy.args.slice(10).map(c => c[0][0])).eql([
       { module: 'dom', method: 'updateAttrs', args: [el.ref, { a: 1 }] },
       { module: 'dom', method: 'updateStyle', args: [el.ref, { a: 2 }] },
       { module: 'dom', method: 'updateStyle', args: [el.ref, { a: 2, b: 4 }] },