import Vue from 'vue' import { isFn, noop, hasOwn, isPlainObject } from 'uni-shared' export const PAGE_EVENT_HOOKS = [ 'onPullDownRefresh', 'onReachBottom', 'onAddToFavorites', 'onShareTimeline', 'onShareAppMessage', 'onPageScroll', 'onResize', 'onTabItemTap' ] export function initMocks (vm, mocks) { const mpInstance = vm.$mp[vm.mpType] mocks.forEach(mock => { if (hasOwn(mpInstance, mock)) { vm[mock] = mpInstance[mock] } }) } function hasHook (hook, vueOptions) { if (!vueOptions) { return true } if (Vue.options && Array.isArray(Vue.options[hook])) { return true } vueOptions = vueOptions.default || vueOptions if (isFn(vueOptions)) { if (isFn(vueOptions.extendOptions[hook])) { return true } if (vueOptions.super && vueOptions.super.options && Array.isArray(vueOptions.super.options[hook])) { return true } return false } if (isFn(vueOptions[hook])) { return true } const mixins = vueOptions.mixins if (Array.isArray(mixins)) { return !!mixins.find(mixin => hasHook(hook, mixin)) } } export function initHooks (mpOptions, hooks, vueOptions) { hooks.forEach(hook => { if (hasHook(hook, vueOptions)) { mpOptions[hook] = function (args) { return this.$vm && this.$vm.__call_hook(hook, args) } } }) } export function initUnknownHooks (mpOptions, vueOptions, excludes = []) { findHooks(vueOptions).forEach((hook) => initHook(mpOptions, hook, excludes)) } function findHooks (vueOptions, hooks = []) { if (vueOptions) { Object.keys(vueOptions).forEach((name) => { if (name.indexOf('on') === 0 && isFn(vueOptions[name])) { hooks.push(name) } }) } return hooks } function initHook (mpOptions, hook, excludes) { if (excludes.indexOf(hook) === -1 && !hasOwn(mpOptions, hook)) { mpOptions[hook] = function (args) { return this.$vm && this.$vm.__call_hook(hook, args) } } } export function initVueComponent (Vue, vueOptions) { vueOptions = vueOptions.default || vueOptions let VueComponent if (isFn(vueOptions)) { VueComponent = vueOptions } else { VueComponent = Vue.extend(vueOptions) } vueOptions = VueComponent.options return [VueComponent, vueOptions] } export function initSlots (vm, vueSlots) { if (Array.isArray(vueSlots) && vueSlots.length) { const $slots = Object.create(null) vueSlots.forEach(slotName => { $slots[slotName] = true }) vm.$scopedSlots = vm.$slots = $slots } } export function initVueIds (vueIds, mpInstance) { vueIds = (vueIds || '').split(',') const len = vueIds.length if (len === 1) { mpInstance._$vueId = vueIds[0] } else if (len === 2) { mpInstance._$vueId = vueIds[0] mpInstance._$vuePid = vueIds[1] } } export function initData (vueOptions, context) { let data = vueOptions.data || {} const methods = vueOptions.methods || {} if (typeof data === 'function') { try { data = data.call(context) // 支持 Vue.prototype 上挂的数据 } catch (e) { if (process.env.VUE_APP_DEBUG) { console.warn('根据 Vue 的 data 函数初始化小程序 data 失败,请尽量确保 data 函数中不访问 vm 对象,否则可能影响首次数据渲染速度。', data) } } } else { try { // 对 data 格式化 data = JSON.parse(JSON.stringify(data)) } catch (e) { } } if (!isPlainObject(data)) { data = {} } Object.keys(methods).forEach(methodName => { if (context.__lifecycle_hooks__.indexOf(methodName) === -1 && !hasOwn(data, methodName)) { data[methodName] = methods[methodName] } }) return data } const PROP_TYPES = [String, Number, Boolean, Object, Array, null] function createObserver (name) { return function observer (newVal, oldVal) { if (this.$vm) { this.$vm[name] = newVal // 为了触发其他非 render watcher } } } export function initBehaviors (vueOptions, initBehavior) { const vueBehaviors = vueOptions.behaviors const vueExtends = vueOptions.extends const vueMixins = vueOptions.mixins let vueProps = vueOptions.props if (!vueProps) { vueOptions.props = vueProps = [] } const behaviors = [] if (Array.isArray(vueBehaviors)) { vueBehaviors.forEach(behavior => { behaviors.push(behavior.replace('uni://', `${__PLATFORM_PREFIX__}://`)) if (behavior === 'uni://form-field') { if (Array.isArray(vueProps)) { vueProps.push('name') vueProps.push('value') } else { vueProps.name = { type: String, default: '' } vueProps.value = { type: [String, Number, Boolean, Array, Object, Date], default: '' } } } }) } if (__PLATFORM__ === 'mp-alipay') { // alipay 重复定义props会报错,下边的代码对于其他平台也没有意义,保险起见,仅对alipay做处理 return } if (isPlainObject(vueExtends) && vueExtends.props) { behaviors.push( initBehavior({ properties: initProperties(vueExtends.props, true) }) ) } if (Array.isArray(vueMixins)) { vueMixins.forEach(vueMixin => { if (isPlainObject(vueMixin) && vueMixin.props) { behaviors.push( initBehavior({ properties: initProperties(vueMixin.props, true) }) ) } }) } return behaviors } function parsePropType (key, type, defaultValue, file) { // [String]=>String if (Array.isArray(type) && type.length === 1) { return type[0] } if (__PLATFORM__ === 'mp-baidu') { if ( defaultValue === false && Array.isArray(type) && type.length === 2 && type.indexOf(String) !== -1 && type.indexOf(Boolean) !== -1 ) { // [String,Boolean]=>Boolean if (file) { console.warn( `props.${key}.type should use Boolean instead of [String,Boolean] at ${file}` ) } return Boolean } } return type } export function initProperties (props, isBehavior = false, file = '', options) { const properties = {} if (!isBehavior) { properties.vueId = { type: String, value: '' } if (__PLATFORM__ === 'mp-toutiao' || __PLATFORM__ === 'mp-lark') { // 用于字节跳动小程序模拟抽象节点 properties.generic = { type: Object, value: null } } if (__PLATFORM__ === 'mp-weixin' || __PLATFORM__ === 'mp-alipay') { if (__PLATFORM__ === 'mp-alipay' || options.virtualHost) { properties.virtualHostStyle = { type: null, value: '' } properties.virtualHostClass = { type: null, value: '' } } } // scopedSlotsCompiler auto properties.scopedSlotsCompiler = { type: String, value: '' } properties.vueSlots = { // 小程序不能直接定义 $slots 的 props,所以通过 vueSlots 转换到 $slots type: null, value: [], observer: function (newVal, oldVal) { const $slots = Object.create(null) newVal.forEach(slotName => { $slots[slotName] = true }) this.setData({ $slots }) } } } if (Array.isArray(props)) { // ['title'] props.forEach(key => { properties[key] = { type: null, observer: createObserver(key) } }) } else if (isPlainObject(props)) { // {title:{type:String,default:''},content:String} Object.keys(props).forEach(key => { const opts = props[key] if (isPlainObject(opts)) { // title:{type:String,default:''} let value = opts.default if (isFn(value)) { value = value() } opts.type = parsePropType(key, opts.type, value, file) properties[key] = { type: PROP_TYPES.indexOf(opts.type) !== -1 ? opts.type : null, value, observer: createObserver(key) } } else { // content:String const type = parsePropType(key, opts, null, file) properties[key] = { type: PROP_TYPES.indexOf(type) !== -1 ? type : null, observer: createObserver(key) } } }) } return properties } function wrapper (event) { // TODO 又得兼容 mpvue 的 mp 对象 try { event.mp = JSON.parse(JSON.stringify(event)) } catch (e) { } event.stopPropagation = noop event.preventDefault = noop event.target = event.target || {} if (!hasOwn(event, 'detail')) { event.detail = {} } if (hasOwn(event, 'markerId')) { event.detail = typeof event.detail === 'object' ? event.detail : {} event.detail.markerId = event.markerId } if (__PLATFORM__ === 'mp-baidu') { // mp-baidu,checked=>value if ( isPlainObject(event.detail) && hasOwn(event.detail, 'checked') && !hasOwn(event.detail, 'value') ) { event.detail.value = event.detail.checked } } if (isPlainObject(event.detail)) { event.target = Object.assign({}, event.target, event.detail) } return event } function getExtraValue (vm, dataPathsArray) { let context = vm dataPathsArray.forEach(dataPathArray => { const dataPath = dataPathArray[0] const value = dataPathArray[2] if (dataPath || typeof value !== 'undefined') { // ['','',index,'disable'] const propPath = dataPathArray[1] const valuePath = dataPathArray[3] let vFor if (Number.isInteger(dataPath)) { vFor = dataPath } else if (!dataPath) { vFor = context } else if (typeof dataPath === 'string' && dataPath) { if (dataPath.indexOf('#s#') === 0) { vFor = dataPath.substr(3) } else { vFor = vm.__get_value(dataPath, context) } } if (Number.isInteger(vFor)) { context = value } else if (!propPath) { context = vFor[value] } else { if (Array.isArray(vFor)) { context = vFor.find(vForItem => { return vm.__get_value(propPath, vForItem) === value }) } else if (isPlainObject(vFor)) { context = Object.keys(vFor).find(vForKey => { return vm.__get_value(propPath, vFor[vForKey]) === value }) } else { console.error('v-for 暂不支持循环数据:', vFor) } } if (valuePath) { context = vm.__get_value(valuePath, context) } } }) return context } function processEventExtra (vm, extra, event, __args__) { const extraObj = {} if (Array.isArray(extra) && extra.length) { /** *[ * ['data.items', 'data.id', item.data.id], * ['metas', 'id', meta.id] *], *[ * ['data.items', 'data.id', item.data.id], * ['metas', 'id', meta.id] *], *'test' */ extra.forEach((dataPath, index) => { if (typeof dataPath === 'string') { if (!dataPath) { // model,prop.sync extraObj['$' + index] = vm } else { if (dataPath === '$event') { // $event extraObj['$' + index] = event } else if (dataPath === 'arguments') { extraObj['$' + index] = event.detail ? event.detail.__args__ || __args__ : __args__ } else if (dataPath.indexOf('$event.') === 0) { // $event.target.value extraObj['$' + index] = vm.__get_value(dataPath.replace('$event.', ''), event) } else { extraObj['$' + index] = vm.__get_value(dataPath) } } } else { extraObj['$' + index] = getExtraValue(vm, dataPath) } }) } return extraObj } function getObjByArray (arr) { const obj = {} for (let i = 1; i < arr.length; i++) { const element = arr[i] obj[element[0]] = element[1] } return obj } function processEventArgs (vm, event, args = [], extra = [], isCustom, methodName) { let isCustomMPEvent = false // wxcomponent 组件,传递原始 event 对象 // fixed 用户直接触发 mpInstance.triggerEvent const __args__ = isPlainObject(event.detail) ? event.detail.__args__ || [event.detail] : [event.detail] if (isCustom) { // 自定义事件 isCustomMPEvent = event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.comType === 'wx' if (!args.length) { // 无参数,直接传入 event 或 detail 数组 if (isCustomMPEvent) { return [event] } return __args__ } } const extraObj = processEventExtra(vm, extra, event, __args__) const ret = [] args.forEach(arg => { if (arg === '$event') { if (methodName === '__set_model' && !isCustom) { // input v-model value ret.push(event.target.value) } else { if (isCustom && !isCustomMPEvent) { ret.push(__args__[0]) } else { // wxcomponent 组件或内置组件 ret.push(event) } } } else { if (Array.isArray(arg) && arg[0] === 'o') { ret.push(getObjByArray(arg)) } else if (typeof arg === 'string' && hasOwn(extraObj, arg)) { ret.push(extraObj[arg]) } else { ret.push(arg) } } }) return ret } const ONCE = '~' const CUSTOM = '^' function isMatchEventType (eventType, optType) { return (eventType === optType) || ( optType === 'regionchange' && ( eventType === 'begin' || eventType === 'end' ) ) } function getContextVm (vm) { let $parent = vm.$parent // 父组件是 scoped slots 或者其他自定义组件时继续查找 while ($parent && $parent.$parent && ($parent.$options.generic || $parent.$parent.$options.generic || $parent.$scope._$vuePid)) { $parent = $parent.$parent } return $parent && $parent.$parent } export function handleEvent (event) { event = wrapper(event) // [['tap',[['handle',[1,2,a]],['handle1',[1,2,a]]]]] const dataset = (event.currentTarget || event.target).dataset if (!dataset) { return console.warn('事件信息不存在') } const eventOpts = dataset.eventOpts || dataset['event-opts'] // 支付宝 web-view 组件 dataset 非驼峰 if (!eventOpts) { return console.warn('事件信息不存在') } // [['handle',[1,2,a]],['handle1',[1,2,a]]] const eventType = event.type const ret = [] eventOpts.forEach(eventOpt => { let type = eventOpt[0] const eventsArray = eventOpt[1] const isCustom = type.charAt(0) === CUSTOM type = isCustom ? type.slice(1) : type const isOnce = type.charAt(0) === ONCE type = isOnce ? type.slice(1) : type if (eventsArray && isMatchEventType(eventType, type)) { eventsArray.forEach(eventArray => { const methodName = eventArray[0] if (methodName) { let handlerCtx = this.$vm if (handlerCtx.$options.generic) { // mp-weixin,mp-toutiao 抽象节点模拟 scoped slots handlerCtx = getContextVm(handlerCtx) || handlerCtx } if (methodName === '$emit') { handlerCtx.$emit.apply(handlerCtx, processEventArgs( this.$vm, event, eventArray[1], eventArray[2], isCustom, methodName )) return } const handler = handlerCtx[methodName] if (!isFn(handler)) { const type = this.$vm.mpType === 'page' ? 'Page' : 'Component' const path = this.route || this.is throw new Error(`${type} "${path}" does not have a method "${methodName}"`) } if (isOnce) { if (handler.once) { return } handler.once = true } let params = processEventArgs( this.$vm, event, eventArray[1], eventArray[2], isCustom, methodName ) params = Array.isArray(params) ? params : [] // 参数尾部增加原始事件对象用于复杂表达式内获取额外数据 if (/=\s*\S+\.eventParams\s*\|\|\s*\S+\[['"]event-params['"]\]/.test(handler.toString())) { // eslint-disable-next-line no-sparse-arrays params = params.concat([, , , , , , , , , , event]) } ret.push(handler.apply(handlerCtx, params)) } }) } }) if ( eventType === 'input' && ret.length === 1 && typeof ret[0] !== 'undefined' ) { return ret[0] } }