Skip to content

小程序自定义组件支持 ​

qinjs 在支持vue组件之外,也实现了对小程序自定义组件的兼容。

小程序组件不是vue组件,并且每家小程序都有自己的组件规范,比如微信小程序的组件是wxml格式。

小程序组件不是全端可用,支持度最广的微信小程序自定义组件,也只能支持微信小程序、app-vue、web,其他平台无法兼容。

如果需求上只需兼容有限平台,也可以使用小程序组件。否则仍然推荐使用vue组件。

平台差异说明

平台支持情况小程序组件存放目录
H5支持微信小程序组件(2.4.7+)wxcomponents
微信小程序支持微信小程序组件wxcomponents
支付宝小程序支持支付宝小程序组件mycomponents
百度小程序支持百度小程序组件swancomponents
抖音小程序、飞书小程序支持抖音小程序、飞书小程序组件ttcomponents
QQ小程序支持QQ小程序组件wxcomponents
快手小程序支持快手小程序组件kscomponents
京东小程序支持京东小程序组件jdcomponents

此文档要求开发者对各端小程序的自定义组件有一定了解,没接触过小程序自定义组件的可以参考:

目录结构

shell
┌─wxcomponents                  微信小程序自定义组件存放目录
│   └──custom                   微信小程序自定义组件
│    ├─index.js
│    ├─index.wxml
│    ├─index.json
│    └─index.wxss
├─mycomponents                  支付宝小程序自定义组件存放目录
│   └──custom                   支付宝小程序自定义组件
│    ├─index.js
│    ├─index.axml
│    ├─index.json
│    └─index.acss
├─swancomponents                百度小程序自定义组件存放目录
│   └──custom                   百度小程序自定义组件
│    ├─index.js
│    ├─index.swan
│    ├─index.json
│    └─index.css
├─pages
│  └─index
│    └─index.vue
│
├─static
├─main.js
├─App.vue
├─manifest.json
└─pages.json
┌─wxcomponents                  微信小程序自定义组件存放目录
│   └──custom                   微信小程序自定义组件
│    ├─index.js
│    ├─index.wxml
│    ├─index.json
│    └─index.wxss
├─mycomponents                  支付宝小程序自定义组件存放目录
│   └──custom                   支付宝小程序自定义组件
│    ├─index.js
│    ├─index.axml
│    ├─index.json
│    └─index.acss
├─swancomponents                百度小程序自定义组件存放目录
│   └──custom                   百度小程序自定义组件
│    ├─index.js
│    ├─index.swan
│    ├─index.json
│    └─index.css
├─pages
│  └─index
│    └─index.vue
│
├─static
├─main.js
├─App.vue
├─manifest.json
└─pages.json

使用方式

在 pages.json 对应页面的 style -> usingComponents 引入组件:

js
{
  "pages": [{
    "path": "index/index",
    "style": {
      // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-QQ
      "usingComponents": {
        "custom": "/wxcomponents/custom/index"
      },
      // #endif
      // #ifdef MP-BAIDU
      "usingComponents": {
        "custom": "/swancomponents/custom/index"
      },
      // #endif
      // #ifdef MP-ALIPAY
      "usingComponents": {
        "custom": "/mycomponents/custom/index"
      },
      // #endif
      "navigationBarTitleText": "QIN_JS"
    }
  }]
}
{
  "pages": [{
    "path": "index/index",
    "style": {
      // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-QQ
      "usingComponents": {
        "custom": "/wxcomponents/custom/index"
      },
      // #endif
      // #ifdef MP-BAIDU
      "usingComponents": {
        "custom": "/swancomponents/custom/index"
      },
      // #endif
      // #ifdef MP-ALIPAY
      "usingComponents": {
        "custom": "/mycomponents/custom/index"
      },
      // #endif
      "navigationBarTitleText": "QIN_JS"
    }
  }]
}

在页面中使用

html
<!-- 页面模板 (index.vue) -->
<view>
    <!-- 在页面中对自定义组件进行引用 -->
    <custom name="qin-js"></custom>
</view>
<!-- 页面模板 (index.vue) -->
<view>
    <!-- 在页面中对自定义组件进行引用 -->
    <custom name="qin-js"></custom>
</view>

代码示例

下面以微信小程序官方自定义组件示例 miniprogram-slide-view 为例演示小程序自定义组件的使用方式。 其他组件使用示例见GitHub:wxcomponents-template。 插件市场有一个完整的vant weapp 引用好的示例工程,详见https://ext.dcloud.net.cn/plugin?id=302。

目录结构

shell
┌─components
├─wxcomponents
│   └──miniprogram-slide-view
│    ├─index.js
│    ├─index.wxml
│    ├─index.json
│    └─index.wxss
│
├─pages
│  └─slide-view
│    └─slide-view.vue
│
├─static
├─main.js
├─App.vue
├─manifest.json
└─pages.json
┌─components
├─wxcomponents
│   └──miniprogram-slide-view
│    ├─index.js
│    ├─index.wxml
│    ├─index.json
│    └─index.wxss
│
├─pages
│  └─slide-view
│    └─slide-view.vue
│
├─static
├─main.js
├─App.vue
├─manifest.json
└─pages.json

pages.json

json
{
    "pages": [
        {
          "path": "slide-view/slide-view",
          "style": {
            "navigationBarTitleText": "slide-view",
            "usingComponents": {
              "slide-view": "/wxcomponents/miniprogram-slide-view/index"
            }
          }
        }
    ]
}
{
    "pages": [
        {
          "path": "slide-view/slide-view",
          "style": {
            "navigationBarTitleText": "slide-view",
            "usingComponents": {
              "slide-view": "/wxcomponents/miniprogram-slide-view/index"
            }
          }
        }
    ]
}

slide-view.vue

html
<template>
  <view class='slide'>
    <slide-view width="750" height="110" slide-width="500">
      <view slot="left" class="l">
        <image src="/static/file_transfer.jpg" class="img"></image>
        <view class='text'>
          <view class='title'>文件传输助手</view>
          <view class='time'>7:00 PM</view>
        </view>
      </view>
      <view slot="right" class="r">
        <view class='read'>标为已读</view>
        <view class='delete'>删除</view>
      </view>
    </slide-view>
  </view>
</template>
<script>
  export default {}
</script>
<style>
  .slide {
    border-bottom: 1px solid #DEDEDE;
  }
  .l {
    background-color: white;
    height: 110rpx;
    width: 750rpx;
    display: flex;
    flex-direction: row;
  }
  .r {
    height: 110rpx;
    display: flex;
    direction: row;
    text-align: center;
    vertical-align: middle;
    line-height: 110rpx;
  }
  .read {
    background-color: #ccc;
    color: #fff;
    width: 350rpx;
  }
  .delete {
    background-color: red;
    color: #fff;
    width: 150rpx;
  }
  .img {
    width: 90rpx;
    height: 90rpx;
    border-radius: 10rpx;
    margin: 10rpx 15rpx;
  }
  .text {
    display: flex;
    flex-direction: row;
  }

  .title {
    margin-top: 15rpx;
    font-size: 33rpx;
  }
  .time {
    margin-top: 15rpx;
    color: #ccc;
    font-size: 25rpx;
    margin-left: 330rpx;
  }
</style>
<template>
  <view class='slide'>
    <slide-view width="750" height="110" slide-width="500">
      <view slot="left" class="l">
        <image src="/static/file_transfer.jpg" class="img"></image>
        <view class='text'>
          <view class='title'>文件传输助手</view>
          <view class='time'>7:00 PM</view>
        </view>
      </view>
      <view slot="right" class="r">
        <view class='read'>标为已读</view>
        <view class='delete'>删除</view>
      </view>
    </slide-view>
  </view>
</template>
<script>
  export default {}
</script>
<style>
  .slide {
    border-bottom: 1px solid #DEDEDE;
  }
  .l {
    background-color: white;
    height: 110rpx;
    width: 750rpx;
    display: flex;
    flex-direction: row;
  }
  .r {
    height: 110rpx;
    display: flex;
    direction: row;
    text-align: center;
    vertical-align: middle;
    line-height: 110rpx;
  }
  .read {
    background-color: #ccc;
    color: #fff;
    width: 350rpx;
  }
  .delete {
    background-color: red;
    color: #fff;
    width: 150rpx;
  }
  .img {
    width: 90rpx;
    height: 90rpx;
    border-radius: 10rpx;
    margin: 10rpx 15rpx;
  }
  .text {
    display: flex;
    flex-direction: row;
  }

  .title {
    margin-top: 15rpx;
    font-size: 33rpx;
  }
  .time {
    margin-top: 15rpx;
    color: #ccc;
    font-size: 25rpx;
    margin-left: 330rpx;
  }
</style>

注意事项

  • 小程序组件需要放在项目特殊文件夹 wxcomponents(或 mycomponents、swancomponents)。HBuilderX 建立的工程 wxcomponents 文件夹在 项目根目录下。vue-cli 建立的工程 wxcomponents 文件夹在 src 目录下。可以在 vue.config.js 中自定义其他目录
  • 小程序组件的性能,不如vue组件。使用小程序组件,需要自己手动setData,很难自动管理差量数据更新。而使用vue组件会自动diff更新差量数据。所以如无明显必要,建议使用vue组件而不是小程序组件。比如某些小程序ui组件,完全可以用更高性能的 qin ui 替代。
  • 当需要在 vue 组件中使用小程序组件时,注意在 pages.json 的 globalStyle 中配置 usingComponents,而不是页面级配置。
  • 注意数据和事件绑定的差异,组件使用时应按照 vue 的数据和事件绑定方式
    • 属性绑定从 attr="",改为 :attr="a";从 title="复选框" 改为 :title="'复选框' + item"
    • 事件绑定从 bind:click="toggleActionSheet1" 改为 @click="toggleActionSheet1",目前支付宝小程序不支持 vue 的事件绑定方式,具体参考:支付宝小程序组件事件监听示例
    • 阻止事件冒泡 从 catch:tap="xx" 改为 @tap.native.stop="xx"
    • wx:if 改为 v-if
    • wx:for="" wx:key="" 改为v-for="(item,index) in list"

详细的小程序转 qinjs 语法差异可参考文档https://ask.dcloud.net.cn/article/35786。

WXS ​

WXS是一套运行在视图层的脚本语言,微信端的规范详见。

它的特点是运行在视图层。当需要避免逻辑层和渲染层交互通信折损时,可采用wxs。

qinjs 可以将wxs代码编译到微信小程序、QQ小程序、app-vue、H5

与wxs类似,百度小程序提供了Filter、阿里小程序提供了SJS,qinjs 也支持使用这些功能,并将它们编译到百度和阿里的小程序端。不过它们的功能还不如wxs强大。此外头条系小程序自身不支持类似功能。

平台差异说明

AppH5微信小程序支付宝小程序百度小程序抖音小程序、飞书小程序QQ小程序
√(不支持nvue)√√SJSFilterx√

App端nvue解决此类需求,不应该使用wxs,而是使用bindingx。

wxs示例

以下是一些使用 WXS 的简单示例,要完整了解 WXS 语法,请参考WXS 语法参考。本示例使用wxs响应touchmove事件,减少视图层与逻辑层通信,使滑动更加丝滑。

html
<template>
  <view>
    <view class="area">
      <view @touchstart="test.touchstart" @touchmove="test.touchmove" class="movable">{{test.msg}}</view>
    </view>
  </view>
</template>
<script module="test" lang="wxs">
  var startX = 0
  var startY = 0
  var lastLeft = 50; var lastTop = 50
  function touchstart(event, ins) {
    console.log("touchstart")
    var touch = event.touches[0] || event.changedTouches[0]
    startX = touch.pageX
    startY = touch.pageY
  }
  function touchmove(event, ins) {
    var touch = event.touches[0] || event.changedTouches[0]
    var pageX = touch.pageX
    var pageY = touch.pageY
    var left = pageX - startX + lastLeft
    var top = pageY - startY + lastTop
    startX = pageX
    startY = pageY
    lastLeft = left
    lastTop = top
    ins.selectComponent('.movable').setStyle({
      left: left + 'px',
      top: top + 'px'
    })
    return false
  }
  module.exports = {
    msg: 'Hello',
    touchstart: touchstart,
    touchmove: touchmove
  }
</script>

<script>
  export default {
    data() {
      return {
      }
    },
    methods: {
    }
  }
</script>

<style>
.area{
  position: absolute;
  width: 100%;
  height: 100%;
}
.movable{
  position: absolute;
  width: 100px;
  height: 100px;
  left: 50px;
  top: 50px;
  color: white;
  text-align: center;
  line-height: 100px;
  background-color: red;
}
</style>
<template>
  <view>
    <view class="area">
      <view @touchstart="test.touchstart" @touchmove="test.touchmove" class="movable">{{test.msg}}</view>
    </view>
  </view>
</template>
<script module="test" lang="wxs">
  var startX = 0
  var startY = 0
  var lastLeft = 50; var lastTop = 50
  function touchstart(event, ins) {
    console.log("touchstart")
    var touch = event.touches[0] || event.changedTouches[0]
    startX = touch.pageX
    startY = touch.pageY
  }
  function touchmove(event, ins) {
    var touch = event.touches[0] || event.changedTouches[0]
    var pageX = touch.pageX
    var pageY = touch.pageY
    var left = pageX - startX + lastLeft
    var top = pageY - startY + lastTop
    startX = pageX
    startY = pageY
    lastLeft = left
    lastTop = top
    ins.selectComponent('.movable').setStyle({
      left: left + 'px',
      top: top + 'px'
    })
    return false
  }
  module.exports = {
    msg: 'Hello',
    touchstart: touchstart,
    touchmove: touchmove
  }
</script>

<script>
  export default {
    data() {
      return {
      }
    },
    methods: {
    }
  }
</script>

<style>
.area{
  position: absolute;
  width: 100%;
  height: 100%;
}
.movable{
  position: absolute;
  width: 100px;
  height: 100px;
  left: 50px;
  top: 50px;
  color: white;
  text-align: center;
  line-height: 100px;
  background-color: red;
}
</style>

支付宝小程序,百度小程序官方暂未支持事件响应,不过也可以使用对应的SJS、Filter过滤器实现一些数据处理的操作,以下代码展示了一个时间格式化的小功能

index.vue

html
<template>
  <view>
    <view>
      {{timestr}} 是
    </view>
    <view>
      {{utils.friendlyDate(timestamp)}}
    </view>
  </view>
</template>
<script module="utils" lang="filter" src="./utils.filter.js"></script>
<script module="utils" lang="sjs" src="./utils.sjs"></script>

<script>
  export default {
    data() {
      return {
        timestr: '2019/08/22 10:10:10',
        timestamp: 0
      }
    },
    created() {
      this.timestamp = new Date(this.timestr).getTime()
    },
    methods: {
    }
  }
</script>
<template>
  <view>
    <view>
      {{timestr}} 是
    </view>
    <view>
      {{utils.friendlyDate(timestamp)}}
    </view>
  </view>
</template>
<script module="utils" lang="filter" src="./utils.filter.js"></script>
<script module="utils" lang="sjs" src="./utils.sjs"></script>

<script>
  export default {
    data() {
      return {
        timestr: '2019/08/22 10:10:10',
        timestamp: 0
      }
    },
    created() {
      this.timestamp = new Date(this.timestr).getTime()
    },
    methods: {
    }
  }
</script>

utils.sjs 与 utils.filter.js 内容一致

js
export default {
  friendlyDate: (timestamp) => {
    var formats = {
      'year': '%n% 年前',
      'month': '%n% 月前',
      'day': '%n% 天前',
      'hour': '%n% 小时前',
      'minute': '%n% 分钟前',
      'second': '%n% 秒前',
    };
    var now = Date.now();
    var seconds = Math.floor((now - parseInt(timestamp)) / 1000);
    var minutes = Math.floor(seconds / 60);
    var hours = Math.floor(minutes / 60);
    var days = Math.floor(hours / 24);
    var months = Math.floor(days / 30);
    var years = Math.floor(months / 12);

    var diffType = '';
    var diffValue = 0;
    if (years > 0) {
      diffType = 'year';
      diffValue = years;
    } else {
      if (months > 0) {
        diffType = 'month';
        diffValue = months;
      } else {
        if (days > 0) {
          diffType = 'day';
          diffValue = days;
        } else {
          if (hours > 0) {
            diffType = 'hour';
            diffValue = hours;
          } else {
            if (minutes > 0) {
              diffType = 'minute';
              diffValue = minutes;
            } else {
              diffType = 'second';
              diffValue = seconds === 0 ? (seconds = 1) : seconds;
            }
          }
        }
      }
    }
    return formats[diffType].replace('%n%', diffValue);
  }
}
export default {
  friendlyDate: (timestamp) => {
    var formats = {
      'year': '%n% 年前',
      'month': '%n% 月前',
      'day': '%n% 天前',
      'hour': '%n% 小时前',
      'minute': '%n% 分钟前',
      'second': '%n% 秒前',
    };
    var now = Date.now();
    var seconds = Math.floor((now - parseInt(timestamp)) / 1000);
    var minutes = Math.floor(seconds / 60);
    var hours = Math.floor(minutes / 60);
    var days = Math.floor(hours / 24);
    var months = Math.floor(days / 30);
    var years = Math.floor(months / 12);

    var diffType = '';
    var diffValue = 0;
    if (years > 0) {
      diffType = 'year';
      diffValue = years;
    } else {
      if (months > 0) {
        diffType = 'month';
        diffValue = months;
      } else {
        if (days > 0) {
          diffType = 'day';
          diffValue = days;
        } else {
          if (hours > 0) {
            diffType = 'hour';
            diffValue = hours;
          } else {
            if (minutes > 0) {
              diffType = 'minute';
              diffValue = minutes;
            } else {
              diffType = 'second';
              diffValue = seconds === 0 ? (seconds = 1) : seconds;
            }
          }
        }
      }
    }
    return formats[diffType].replace('%n%', diffValue);
  }
}

注意

引入方式

html
<!-- 内联 -->
<script module="test" lang="wxs">
  //...code
</script>
<script module="utils" lang="filter">
  //...code
</script>


<!-- 外部引入 -->
<script module="utils" lang="wxs" src="./utils.wxs"></script>
<script module="utils" lang="filter" src="./utils.filter.js"></script>
<script module="utils" lang="sjs" src="./utils.sjs"></script>
<!-- 内联 -->
<script module="test" lang="wxs">
  //...code
</script>
<script module="utils" lang="filter">
  //...code
</script>


<!-- 外部引入 -->
<script module="utils" lang="wxs" src="./utils.wxs"></script>
<script module="utils" lang="filter" src="./utils.filter.js"></script>
<script module="utils" lang="sjs" src="./utils.sjs"></script>
  • 【重要】 编写wxs、sjs、filter.js 内容时必须遵循相应语法规范
  • 【重要】 module所指定的模块名不可与data、methods、computed内的属性重名
  • vue3 项目不支持 setup script 用法
  • 目前各个小程序正在完善相关规范,可能会有较大改动,请务必仔细阅读相应平台的文档
  • 支付宝小程序请使用sjs规范,详见
  • 支付宝小程序sjs只能定义在.sjs 文件中,然后使用<script>标签引入
  • 支付宝小程序script的标签属性name、from被统一为了module、src以便后续实现多平台统一写法
  • 百度小程序中请使用Filter规范,详见
  • 百度小程序Filter只能导出function函数
  • 暂不支持在 wxs、sjs、filter.js 中调用其他同类型文件
  • wxs、filter.js既能内联使用又可以外部引入,sjs只能外部引入
  • QQ小程序目前对内联的 wxs 支持不好,部分写法可能会导致编译出错,尽量使用外部引入的方式
  • 在微信自定义组件中wxcomponents也可以使用wxs
  • nvue页面暂不支持wxs、sjs、filter.js
  • 各个script标签会分别被打包至对应支持平台,不需要额外写条件编译
  • H5 端,提供了wxs的升级版,更加强大,见下面的 renderjs 章节