uniapp 在业内名气口碑都挺不错的,选择这个框架,能够让开发者快速出活,依托 vue 的生态,相关开发者也多,企业也容易招聘。然而小程序和web毕竟还是有点区别,某些时候还是需要对uniapp 有一定的理解,才能够顺利的搬砖。本篇,我们尝试解决 uniapp 的两个问题,一窥其内在机理。

uniapp 小程序插件的使用

uniapp 论坛有不少这样的帖子,支付宝插件在子组件无法使用。
官方的用法是这样:
1. 引入插件代码包: 使用插件之前开发者需要在manifest.json中的各平台对应的字段内声明使用的插件,具体配置参照所用插件的开发文档

// 支付宝小程序
  "mp-alipay": {
    "plugins": {
      "myPlugin": {
        "version": "*",
        "provider": "2019235609092837"
      }
    }
  }
  1. 在页面中使用

    {
    "path": "pages/index/index",
    "style": {
    "mp-alipay": {
     "usingComponents": {
       "hello-component": "plugin://myPlugin/hello-component"
     }
    }
    }
    }
    

    看起来好像没啥坑,然鹅….编译之后你会发现,他在页面基本确实是引入了插件,但是组件内部并没有引入插件,于是你在组件内使用插件就报错了~

    <component>
    <plugin>
    </component>
    

    解决办法

    解决方式倒也不复杂,既然在组件内部不给我使用插件,那么我就在组件内挖个 slot,然后在页面级别,给组件传递插件的 slot,完美解决这个问题~。

uniapp 中部分标签的使用(life-follow)

在 uniapp 中life-follow无法使用。

<life-follow
  a:if="&#123;&#123;show&#125;&#125;"
  sceneId="****"
  checkFollow="&#123;&#123;checkFollow&#125;&#125;"
  onCheckFollow="checkFollowCb"
  onClose="closeCb"
/>

被编译成

<life-follow
      vue-id="588c7fd8-1"
      sceneId=""
      checkFollow="{{checkLifeFlow}}"
      data-event-opts="{{[['^checkFollow',[['checkFollowCb']]],['close',[['closeCb']]]]}}"
      onCheckFollow="__e"
      onClose="__e"
      onVueInit="__l"
    ></life-follow>

添加 $event 参数后

<life-follow
  a:if="&#123;&#123;show&#125;&#125;"
  sceneId="****"
  checkFollow="&#123;&#123;checkFollow&#125;&#125;"
  @checkFollow="checkFollowCb($event)"
  @close="closeCb($event)"
/>

编译为:

<life-follow
      vue-id="588c7fd8-1"
      sceneId="***"
      checkFollow="&#123;&#123;checkLifeFlow&#125;&#125;"
      data-event-opts="&#123;&#123;[['^checkFollow',[['checkFollowCb',['$event']]]],['close',[['closeCb',['$event']]]]]&#125;&#125;"
      onCheckFollow="__e"
      onClose="__e"
      onVueInit="__l"
></life-follow>

为了区分自定义事件,uniapp, 添加 ^ 前缀,目前 uniapp 没有对 tag 进行区分,这部分最终走向了自定义事件,实际小程序中这里 js 报错。

解决办法

方法一:
使用小程序原生组件(非 uni 组件,mycomponents 这个目录下的小程序原生自定义组件),代码直接使用小程序原生的,不走 uniapp 的转换,规避这个问题。

方法二:
uniapp 给小程序特定标签加黑名单,这里不做自定义处理。我提了mr,官方觉得位置不好,换了个地方去写了:),好消息是你现在再用uniapp去新建一个项目,life-follow应该已经可以正常使用啦~

uniapp debugger模式

在项目根目录新建 .env 文件,输入下面内容,即可开启 uniapp 的 DEBUG 模式,从控制台能看到不少有意思的东西。

// .env
VUE_APP_DEBUG=true

uniapp 的编译

@dcloudio/uni-template-compiler:uniapp 模板编译器,事件等处理都是在这里编译
@dcloudio/uni-mp-alipay:uniapp 平台运行时,平台相关的处理,事件函数的处理,一般都在这个文件中。

uniapp 事件系统

<view @click="query" />

会被编译成:

<view data-event-opts="&#123;&#123;[['tap',[['query',['$event']]]]]&#125;&#125;"    onTap="__e" />

多个事件元素,它的onTap都是 __e,我们猜测,__euniapp 事件系统的管理分发的角色,通过 query,找到调用者,参数是$event

编译事件代码分析

在解析模板之后,拿到相关事件,对once、capture等事件,添加特定前缀。事件处理,统一添加 __e 方法。

function _processEvent(path, state, isComponent, isNativeOn = false, tagName, ret) &#123;
    const opts = []
    // remove invalid event
    path.node.value.properties = path.node.value.properties.filter(property => &#123;
        return property.key.value || property.key.name
    &#125;)
    const len = path.node.value.properties.length
    for (let i = 0; i < len; i++) &#123;
        //  .... 省略
        const getEventType = state.options.platform.getEventType

        let optType = isCustom ? customize(type) : getEventType(type) // 比如自定义组件使用了 click 自定义事件

        //  添加前缀
        // VUE_EVENT_MODIFIERS: &#123;
        //         capture: '!',
        //         once: '~',
        //         passive: '&',
        //         custom: '^'
        // &#125;,
        if (isOnce) &#123;
            optType = VUE_EVENT_MODIFIERS.once + optType
        &#125;
        if (isCustom) &#123;
            optType = VUE_EVENT_MODIFIERS.custom + optType
        &#125;
        opts.push(&#123;
            opt: t.arrayExpression([
                t.stringLiteral(optType),
                t.arrayExpression(methods)
            ]),
            params
        &#125;)

        keyPath.replaceWith(
            t.stringLiteral(
                state.options.platform.formatEventType(
                    isCustom ? customize(type) : getEventType(type), // 比如自定义组件使用了 click 自定义事件
                    isCatch,
                    isCapture,
                    isCustom
                )
            )
        )
        // INTERNAL_EVENT_PROXY === '__e' ,这里添加了 事件处理函数,'__e'
        valuePath.replaceWith(t.stringLiteral(INTERNAL_EVENT_PROXY))
    &#125;
    return opts
&#125;

事件调用代码分析

在页面初始化的时候,调用 parsePage,进行了事件绑定,__e指向了handleEvent

function parsePage (vuePageOptions) &#123;
  const [VueComponent, vueOptions] = initVueComponent(Vue, vuePageOptions)

  const pageOptions = &#123;
    mixins: initBehaviors(vueOptions),
    data: initData(vueOptions, Vue.prototype),
    onLoad (query) &#123;
      const properties = this.props

      const options = &#123;
        mpType: 'page',
        mpInstance: this,
        propsData: properties
      &#125;

      // 初始化 vue 实例
      this.$vm = new VueComponent(options)

      initSpecialMethods(this)

      // 触发首次 setData
      this.$vm.$mount()

      const copyQuery = Object.assign(&#123;&#125;, query)
      delete copyQuery.__id__

      this.$page = &#123;
        fullPath: '/' + this.route + stringifyQuery(copyQuery)
      &#125;

      this.options = query
      this.$vm.$mp.query = query // 兼容 mpvue
      this.$vm.__call_hook('onLoad', query)
    &#125;,
    onReady () &#123;
      initChildVues(this)
      this.$vm._isMounted = true
      this.$vm.__call_hook('mounted')
      this.$vm.__call_hook('onReady')
    &#125;,
    onUnload () &#123;
      this.$vm.__call_hook('onUnload')
      this.$vm.$destroy()
    &#125;,
    events: &#123;
      // 支付宝小程序有些页面事件只能放在events下
      onBack () &#123;
        this.$vm.__call_hook('onBackPress')
      &#125;
    &#125;,
    __r: handleRef,
    // 绑定事件
    __e: handleEvent,
    __l: handleLink$1,
    triggerEvent
  &#125;

  initHooks(pageOptions, hooks$1, vuePageOptions)

  if (Array.isArray(vueOptions.wxsCallMethods)) &#123;
    vueOptions.wxsCallMethods.forEach(callMethod => &#123;
      pageOptions[callMethod] = function (args) &#123;
        return this.$vm[callMethod](args)
      &#125;
    &#125;)
  &#125;

  return pageOptions
&#125;

handleEvent 方法:

function handleEvent (event) &#123;
  event = wrapper$1(event)

  // [['tap',[['handle',[1,2,a]],['handle1',[1,2,a]]]]]
  const dataset = (event.currentTarget || event.target).dataset
  if (!dataset) &#123;
    return console.warn('事件信息不存在')
  &#125;
  const eventOpts = dataset.eventOpts || dataset['event-opts'] // 支付宝 web-view 组件 dataset 非驼峰
  if (!eventOpts) &#123;
    return console.warn('事件信息不存在')
  &#125;

  // [['handle',[1,2,a]],['handle1',[1,2,a]]]
  const eventType = event.type

  const ret = []

  eventOpts.forEach(eventOpt => &#123;
    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)) &#123;
      eventsArray.forEach(eventArray => &#123;
        const methodName = eventArray[0]
        if (methodName) &#123;
          let handlerCtx = this.$vm
          if (handlerCtx.$options.generic) &#123; // mp-weixin,mp-toutiao 抽象节点模拟 scoped slots
            handlerCtx = getContextVm(handlerCtx) || handlerCtx
          &#125;
          if (methodName === '$emit') &#123;
            handlerCtx.$emit.apply(handlerCtx,
              processEventArgs(
                this.$vm,
                event,
                eventArray[1],
                eventArray[2],
                isCustom,
                methodName
              ))
            return
          &#125;
          const handler = handlerCtx[methodName]
          if (!isFn(handler)) &#123;
            throw new Error(` _vm.$&#123;methodName&#125; is not a function`)
          &#125;
          if (isOnce) &#123;
            if (handler.once) &#123;
              return
            &#125;
            handler.once = true
          &#125;
          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())) &#123;
            // eslint-disable-next-line no-sparse-arrays
            params = params.concat([, , , , , , , , , , event])
          &#125;
          ret.push(handler.apply(handlerCtx, params))
        &#125;
      &#125;)
    &#125;
  &#125;)

  if (
    eventType === 'input' &&
    ret.length === 1 &&
    typeof ret[0] !== 'undefined'
  ) &#123;
    return ret[0]
  &#125;
&#125;

代码有点长,但还是比较清晰的,函数对事件分类进行处理,最终事件函数被调用执行,也就是handler.apply(handlerCtx, params),关键词 apply,寻找这个词,基本就能够找到函数的调用。

参考