vue
Version:
Reactive, component-oriented view layer for modern web interfaces.
423 lines (385 loc) • 12.1 kB
JavaScript
import Vue from 'weex/runtime/index'
import renderer from 'weex/runtime/config'
Vue.weexVersion = '__WEEX_VERSION__'
export { Vue }
const {
instances,
modules,
components
} = renderer
let activeId
const oriIsReservedTag = (Vue && Vue.config && typeof Vue.config.isReservedTag === 'function') ? Vue.config.isReservedTag : function () {}
/**
* Prepare framework config, basically about the virtual-DOM and JS bridge.
* @param {object} cfg
*/
export function init (cfg) {
renderer.Document = cfg.Document
renderer.Element = cfg.Element
renderer.Comment = cfg.Comment
renderer.sendTasks = cfg.sendTasks
}
/**
* Reset framework config and clear all registrations.
*/
export function reset () {
clear(instances)
clear(modules)
clear(components)
delete renderer.Document
delete renderer.Element
delete renderer.Comment
delete renderer.sendTasks
Vue.config.isReservedTag = oriIsReservedTag
}
/**
* Delete all keys of an object.
* @param {object} obj
*/
function clear (obj) {
for (const key in obj) {
delete obj[key]
}
}
/**
* Create an instance with id, code, config and external data.
* @param {string} instanceId
* @param {string} appCode
* @param {object} config
* @param {object} data
* @param {object} env { info, config, services }
*/
export function createInstance (
instanceId,
appCode = '',
config = {},
data,
env = {}
) {
// Set active instance id and put some information into `instances` map.
activeId = instanceId
// Virtual-DOM object.
const document = new renderer.Document(instanceId, config.bundleUrl)
// All function/callback of parameters before sent to native
// will be converted as an id. So `callbacks` is used to store
// these real functions. When a callback invoked and won't be
// called again, it should be removed from here automatically.
const callbacks = []
// The latest callback id, incremental.
const callbackId = 1
instances[instanceId] = {
instanceId, config, data,
document, callbacks, callbackId
}
// Prepare native module getter and HTML5 Timer APIs.
const moduleGetter = genModuleGetter(instanceId)
const timerAPIs = getInstanceTimer(instanceId, moduleGetter)
// Prepare `weex` instance variable.
const weexInstanceVar = {
config,
document,
requireModule: moduleGetter
}
Object.freeze(weexInstanceVar)
// Each instance has a independent `Vue` variable and it should have
// all top-level public APIs.
const subVue = Vue.extend({})
// ensure plain-object components are extended from the subVue
subVue.options._base = subVue
// expose global utility
;['util', 'set', 'delete', 'nextTick', 'version', 'weexVersion', 'config'].forEach(name => {
subVue[name] = Vue[name]
})
// The function which create a closure the JS Bundle will run in.
// It will declare some instance variables like `Vue`, HTML5 Timer APIs etc.
const instanceVars = Object.assign({
Vue: subVue,
weex: weexInstanceVar,
__weex_require_module__: weexInstanceVar.requireModule // deprecated
}, timerAPIs)
callFunction(instanceVars, appCode)
// Send `createFinish` signal to native.
renderer.sendTasks(instanceId + '', [{ module: 'dom', method: 'createFinish', args: [] }], -1)
}
/**
* Destroy an instance with id. It will make sure all memory of
* this instance released and no more leaks.
* @param {string} instanceId
*/
export function destroyInstance (instanceId) {
const instance = instances[instanceId] || {}
if (instance.app instanceof Vue) {
instance.app.$destroy()
}
delete instances[instanceId]
}
/**
* Refresh an instance with id and new top-level component data.
* It will use `Vue.set` on all keys of the new data. So it's better
* define all possible meaningful keys when instance created.
* @param {string} instanceId
* @param {object} data
*/
export function refreshInstance (instanceId, data) {
const instance = instances[instanceId] || {}
if (!(instance.app instanceof Vue)) {
return new Error(`refreshInstance: instance ${instanceId} not found!`)
}
for (const key in data) {
Vue.set(instance.app, key, data[key])
}
// Finally `refreshFinish` signal needed.
renderer.sendTasks(instanceId + '', [{ module: 'dom', method: 'refreshFinish', args: [] }], -1)
}
/**
* Get the JSON object of the root element.
* @param {string} instanceId
*/
export function getRoot (instanceId) {
const instance = instances[instanceId] || {}
if (!(instance.app instanceof Vue)) {
return new Error(`getRoot: instance ${instanceId} not found!`)
}
return instance.app.$el.toJSON()
}
/**
* Receive tasks from native. Generally there are two types of tasks:
* 1. `fireEvent`: an device actions or user actions from native.
* 2. `callback`: invoke function which sent to native as a parameter before.
* @param {string} instanceId
* @param {array} tasks
*/
export function receiveTasks (instanceId, tasks) {
const instance = instances[instanceId] || {}
if (!(instance.app instanceof Vue)) {
return new Error(`receiveTasks: instance ${instanceId} not found!`)
}
const { callbacks, document } = instance
tasks.forEach(task => {
// `fireEvent` case: find the event target and fire.
if (task.method === 'fireEvent') {
const [nodeId, type, e, domChanges] = task.args
const el = document.getRef(nodeId)
document.fireEvent(el, type, e, domChanges)
}
// `callback` case: find the callback by id and call it.
if (task.method === 'callback') {
const [callbackId, data, ifKeepAlive] = task.args
const callback = callbacks[callbackId]
if (typeof callback === 'function') {
callback(data)
// Remove the callback from `callbacks` if it won't called again.
if (typeof ifKeepAlive === 'undefined' || ifKeepAlive === false) {
callbacks[callbackId] = undefined
}
}
}
})
// Finally `updateFinish` signal needed.
renderer.sendTasks(instanceId + '', [{ module: 'dom', method: 'updateFinish', args: [] }], -1)
}
/**
* Register native modules information.
* @param {object} newModules
*/
export function registerModules (newModules) {
for (const name in newModules) {
if (!modules[name]) {
modules[name] = {}
}
newModules[name].forEach(method => {
if (typeof method === 'string') {
modules[name][method] = true
} else {
modules[name][method.name] = method.args
}
})
}
}
/**
* Register native components information.
* @param {array} newComponents
*/
export function registerComponents (newComponents) {
const config = Vue.config
const newConfig = {}
if (Array.isArray(newComponents)) {
newComponents.forEach(component => {
if (!component) {
return
}
if (typeof component === 'string') {
components[component] = true
newConfig[component] = true
} else if (typeof component === 'object' && typeof component.type === 'string') {
components[component.type] = component
newConfig[component.type] = true
}
})
const oldIsReservedTag = config.isReservedTag
config.isReservedTag = name => {
return newConfig[name] || oldIsReservedTag(name)
}
}
}
// Hack `Vue` behavior to handle instance information and data
// before root component created.
Vue.mixin({
beforeCreate () {
const options = this.$options
const parentOptions = (options.parent && options.parent.$options) || {}
// root component (vm)
if (options.el) {
// record instance info
const instance = instances[activeId] || {}
this.$instanceId = activeId
options.instanceId = activeId
this.$document = instance.document
// set external data of instance
const dataOption = options.data
const internalData = (typeof dataOption === 'function' ? dataOption() : dataOption) || {}
options.data = Object.assign(internalData, instance.data)
// record instance by id
instance.app = this
activeId = undefined
} else {
this.$instanceId = options.instanceId = parentOptions.instanceId
}
}
})
/**
* @deprecated Just instance variable `weex.config`
* Get instance config.
* @return {object}
*/
Vue.prototype.$getConfig = function () {
const instance = instances[this.$instanceId] || {}
if (instance.app instanceof Vue) {
return instance.config
}
}
/**
* Generate native module getter. Each native module has several
* methods to call. And all the hebaviors is instance-related. So
* this getter will return a set of methods which additionally
* send current instance id to native when called. Also the args
* will be normalized into "safe" value. For example function arg
* will be converted into a callback id.
* @param {string} instanceId
* @return {function}
*/
function genModuleGetter (instanceId) {
const instance = instances[instanceId]
return function (name) {
const nativeModule = modules[name] || []
const output = {}
for (const methodName in nativeModule) {
output[methodName] = (...args) => {
const finalArgs = args.map(value => {
return normalize(value, instance)
})
renderer.sendTasks(instanceId + '', [{ module: name, method: methodName, args: finalArgs }], -1)
}
}
return output
}
}
/**
* Generate HTML5 Timer APIs. An important point is that the callback
* will be converted into callback id when sent to native. So the
* framework can make sure no side effect of the callabck happened after
* an instance destroyed.
* @param {[type]} instanceId [description]
* @param {[type]} moduleGetter [description]
* @return {[type]} [description]
*/
function getInstanceTimer (instanceId, moduleGetter) {
const instance = instances[instanceId]
const timer = moduleGetter('timer')
const timerAPIs = {
setTimeout: (...args) => {
const handler = function () {
args[0](...args.slice(2))
}
timer.setTimeout(handler, args[1])
return instance.callbackId.toString()
},
setInterval: (...args) => {
const handler = function () {
args[0](...args.slice(2))
}
timer.setInterval(handler, args[1])
return instance.callbackId.toString()
},
clearTimeout: (n) => {
timer.clearTimeout(n)
},
clearInterval: (n) => {
timer.clearInterval(n)
}
}
return timerAPIs
}
/**
* Call a new function body with some global objects.
* @param {object} globalObjects
* @param {string} code
* @return {any}
*/
function callFunction (globalObjects, body) {
const globalKeys = []
const globalValues = []
for (const key in globalObjects) {
globalKeys.push(key)
globalValues.push(globalObjects[key])
}
globalKeys.push(body)
const result = new Function(...globalKeys)
return result(...globalValues)
}
/**
* Convert all type of values into "safe" format to send to native.
* 1. A `function` will be converted into callback id.
* 2. An `Element` object will be converted into `ref`.
* The `instance` param is used to generate callback id and store
* function if necessary.
* @param {any} v
* @param {object} instance
* @return {any}
*/
function normalize (v, instance) {
const type = 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 renderer.Element) {
return v.ref
}
return v
case 'function':
instance.callbacks[++instance.callbackId] = v
return instance.callbackId.toString()
default:
return JSON.stringify(v)
}
}
/**
* Get the exact type of an object by `toString()`. For example call
* `toString()` on an array will be returned `[object Array]`.
* @param {any} v
* @return {string}
*/
function typof (v) {
const s = Object.prototype.toString.call(v)
return s.substring(8, s.length - 1).toLowerCase()
}