此贴作为小程序采坑帖,记录小程序开发中遇到的采坑记录

# wx:key

学习小程序列表渲染的时候,涉及到几个属性wx:forwx:for-indexwx:for-itemwx:key,前三个都非常好理解绑定要渲染的数组、指定数组当前下标的变量名、指定数组当前元素的变量名,今天重点讲讲较难理解的wx:key

# 官方定义

如果列表中项目的位置会动态改变或者有新的项目添加到列表中,并且希望列表中的项目保持自己的特征和状态(如 input 中的输入内容,switch 的选中状态),需要使用 wx:key 来指定列表中项目的唯一的标识符。

乍一看有点难理解啊2333,那我们就动手做做看看列表渲染加和不加wx:key会出现什么?

# wx:key 警告

js文件中的数据

data: {
    switchArray: [
          {
            id: 0,
            value: '朋友圈'
          },
          {
            id: 1,
            value: '扫一扫'
          },
          {
            id: 2,
            value: '摇一摇'
          }
        ]
}

源码

<switch wx:for="{{switchArray}}" wx:for-index="myIndex" wx:for-item="myItem">{{myIndex}}: {{myItem.id}}</switch>

此时控制台警告如下:

// 现在你可以为`wx:for`提供attr`wx:key`来提高性能。
Now you can provide attr `wx:key` for a `wx:for` to improve performance.

加上wx:key警告消失

// 值为被循环对象的某个属性,且该属性值为字符串或数字
wx:key="id"

# wx:key 作用

列表中项目的位置会变化(比如排序),或者会插入新的项目(前后插入新项目),而此时要保持已有项目的特征和状态(比如input的输入值或者checkbox的勾选状态),就要使用wx:key来指定列表中项目的唯一标识符。

# wx:key 形式

  • 字符串或数字:item的某个property,且不能重复。(比如上例中的id)
  • 保留关键字*this,需要item是一个唯一的字符串或数字。

# cover-view

cover-view是小程序中的一个视图容器,可覆盖在原生组件上(map/video等组件层级很高,自己设置z-index不生效的,所以cover-view就出现了)。

# 局限性

  • cover-view容器只能包含cover-view/cover-image/button。(对,你没看错,就支持嵌套这几种控件2333)
  • 诸多bug,比如不支持嵌套其他组件(会直接被忽略),不支持设置border等样式。

# 我妄想在map上覆盖一个input输入框怎么办?

  • 直接在map上写一个input,虽被忽略但是能获取焦点但是无法设置样式。
  • 写一个cover-view悬浮在input相同的位置,给cover-view设置样式,假装我是一个input。
  • input焦点有了、样式有了,假装完美,但是问题多多,慎用。
  • 参考代码

# map

微信小程序地图组件

# 坑汇总

  • 写在map中的元素,视觉上是浮在map之上,实质上设置事件无效,因此建议写在cover-view中。

# 自定义组件

直接给子组件绑定事件不生效,需要设置事件触发并设置事件监听

# 流程总结如下

  • triggerEvent:在组件中用来触发事件
// 假如component名称为 my-component
Component({
  properties: {},
  methods: {
    myEvent: function(){
      var myEventDetail = {} // detail对象,提供给事件监听函数
      var myEventOption = {
          bubbles: false, // 是否事件冒泡(事件冒泡你懂得,不过真有这样的需求?)
          composed: true, // 设置为true会触发组件内部的事件,也就是假如有一个自定义组件也定义了一个事件,那么这个事件也会被触发
          capturePhase: true // 事件是否拥有捕获阶段
      } // 触发事件的选项
      this.triggerEvent('myEvent', myEventDetail, myEventOption)
    }
  }
})
  • 给组件添加事件
// 在my-component中添加事件
<button bind:tap="myEvent">点我点我</button>
  • 页面中调用组件并绑定事件
// 页面中引入组件
<view>
    <my-component bind:myEvent="toDo"></my-component>
</view>
// 父亲中定义事件
toDo: function(){
    console.log('你造吗,我是你爸爸。')
}

# wx:for获取某个item的值

  • 可直接使用data-绑定属性获取,比如data-info=""给每个元素添加值;直接在相应元素的事件function(e)中,通过e可以获取到整个item的值。

# 引入weui.wxss

  • 拷贝dist-style文件夹下的weui.wxss到小程序的最外层目录中
  • 在app.js中导入weui.wxss文件(@import './weui.wxss')
  • 直接在page中使用weui.wxss的样式即可,具体可参考example文件夹的例子

# 注意

在Components中使用weui.wxss样式不生效,需要单独按需把样式文件拷贝进组件文件夹中

# 其他UI库

小程序UI库

# 登录

用流程图总结一下,如有误请指正。

小程序登录流程记录参考文档

# 授权

# 获取手机号

手机号和用户信息必须用按钮来获取了,说是这样更友好。

# 微信支付

小程序支付交互流程

预留参考文章

# echarts使用

在微信小程序中使用图表插件,这里总结一下echarts的使用,另外还有其他图标库供选择antv-f2小-程序示例(antv-f2是静态的图表,点击无交互)。

我选择echarts是因为是大厂出品且关注度相对较高,采坑的人相对较多,经验帖或者issues丰富,且是动态交互更友好。(虽然它已经八个月没更新了o(╯□╰)o)

# 引入echarts库

  • 直接下载echarts库,并拷贝ec-canvas文件夹到你的工程中。
  • 在第一步下载的工程中,pages文件夹为小程序使用示例,可多做参考。

# 使用

  • 注册组件"ec-canvas": "/ec-canvas/ec-canvas"
  • wxml中使用组件<ec-canvas id="mychart-dom-pie" canvas-id="mychart-pie" ec="" params=""></ec-canvas>
  • js文件中实例化图表,主要方法使用echarts中的initChart(canvas, width, height)方法。(强烈建议参考官方demo)
  • 至此,page中使用echarts已经结束了,图像可以正常展示出来。但我们的目标是做成组件,方便在任何页面引用,那就继续。

# 封装组件

  • 传参:思考下哪些数据需要动态传入,将它们提取出来,比如饼图中,我的提取如下:
// series中的data参数需要作为参数动态传入
params: {
    type: Array,
    value: [{
        value: 55,
        name: '苹果'
        }, {
        value: 20,
        name: '香蕉'
        }, {
        value: 10,
        name: '橘子'
        }]
}
  • 改写initChart(canvas, width, height)方法为initChart(canvas, width, height, params),因为params参数势必要动态传入。
  • 改写ec-canvas.js文件,加入params参数,并修改onInit方法,加入参数params,至此完成组件封装。
// ec-canvas.js文件中加入我们的参数
properties: {
    params: {
      type: Object,
      observer: function (newVal, oldVal) {
        if (newVal.length == 0) {
          return;
        }
        this.init();
      }
    }
  }
// ec-canvas.js文件中,onInit方法中加入参数this.data.params
this.chart = this.data.ec.onInit(canvas, res.width, res.height, this.data.params);

# 采坑和总结

# 封装echarts组件为什么改写源码?

一开始没改写源码,而是在组件的methods中对 initChart(canvas, width, height, params) 方法做了封装initPie(为了获取父组件传的参数),然后在wxml中使用bind:init="initPie",但是这样渲染出来的图是静态无交互的,达不到效果。最终使用上面修改源码的方法得到的是正常的图。 如果你有更好的解决办法请告诉我,非常感谢(#^.^#)

/**
   * 组件的方法列表
   */
  methods: {
    initPie: function(e) {
      console.log('--initPie--', e)
      return initChart(e.detail.canvas, e.detail.width, e.detail.height, e.target.dataset.params)
    }
  }

wxml文件中调用方法initPie

<ec-canvas id="mychart-dom-pie" canvas-id="mychart-pie" ec="{{ec}}" bind:init="initPie" data-params="{{params}}"></ec-canvas>
# 参考文章 小程序引入多个e-charts图表

# 圆环图点击后中间部位渲染出颜色?

点击圆环图,莫名其妙圆圈中间白色部分也有相应颜色,官方没有给出解决办法,在issues中看到非官方解决办法1非官方解决办法2

按照上面的解决办法,发现点击图例后又出现了圆环渲染内径的情况,所以取消掉了图例的点击事件selectedMode: false

# 一个页面中加载多个echarts图表,滚动后图表卡顿或显示为空白

这个问题官方也没解决,请参考issues解决思路echarts图表空白

# wx.navigateTo

这里想特别说一下小程序跳转新加的事件events和eventChannel,这两个东西可以完成跳转页面之间的通讯。熟悉android或者java的小伙伴一定不陌生,对照handle、eventBus去理解,甚至js中的dispatchEvent事件。

应用场景:待支付订单列表A页面进入订单详情B页面并完成支付,此时通知订单列表A页面刷新数据,诸如此类。

# 概念

  • eventChannel: 用于页面间事件通讯
  • events: 监听被打开页面传递给当前页面的数据
  • eventChannel.emit: 触发一个事件
  • eventChannel.on: 监听一个事件

# 使用

当前页触发跳转页面事件,跳转到test页面(非tabbar页面)

wx.navigateTo({
    url: '/pages/test',
    events: {
        // 监听事件
        myEvent: function(data) {
          // do something
          console.log('被打开的页面给我传数据了', data)
        },
        testEvent: function(data) {
            console.log(data) // 打印:test
        }
    },
    success: function(res) {
        // 触发myEvent事件
      res.eventChannel.emit('myEvent', {data: 'now'})
    }
})

test页面接收/传递数据

Page({
    onLoad: function(option) {
      const eventChannel = this.getOpenerEventChannel()
      // 监听当前页面触发的事件
      eventChannel.on('myEvent', function(data) {
        // do something
        console.log(data) // 打印:now
      })
      eventChannel.emit('testEvent', {data: 'test'})
    }
})

# vant-weapp使用

在微信小程序中使用vant-weapp组件库,可以直接用npm安装使用,具体请戳这里

# wxs应用

wxs是小程序的一套脚本语言(和js非常非常像,但是对es6支持的不够友好),无法与js互相调用,它可以运行在wxml中。

// 定义一个wxs文件 util.wxs
// 格式化价格
function formatPrice(num) {
  return `${num}`
}
module.exports.formatPrice = formatPrice

wxml文件中使用

// 引入
<wxs src="./util.wxs" module="util"></wxs>
<view>商品价格:{{util.formatPrice(num)}}</view>

# input标签

注意一个坑,原生input标签不支持readonly属性,目前没找到好的解决方式。

# 自定义组件slot

和vue组件插槽用法相似,需要注意的一点是,组件中有多个插槽需要在组件中声明一下启用多插槽,否则无效。

Component({
  options: {
    multipleSlots: true // 在组件定义时的选项中启用多slot支持
  }
})

# 自定义组件外部样式定义

自定义组件可以开启接收外部样式的声明,这样别人用你的组件时可自定义外观。

// 组件card中定义外部样式类名
Component({
  externalClasses: ['custom-class']
})

组件card中使用class

// 接收外部定义的样式
<view class="origin-class custom-class">测试</view>

外部页面使用组件card

<view>
  <card custom-class="my-custom-class"></card>
</view>

# 自定义nav

思路其实挺简单,取消掉自带的nav,把自己写的nav放在页面最顶部即可。

# 配置navigationStyle

navigationStyle取值default则默认使用原生的导航;取值custom则只保留导航栏的胶囊按钮,页面自动从屏幕顶部开始渲染页面,我们把自定义的导航栏放在最顶部即可替代原来的导航栏位置。

# 全局配置

顾名思义即一键配置全部页面的navigationStyle样式,可在app.json中配置。

{
    "window": {
        "navigationStyle": "custom"
    }
}

注意微信客户端最低版本要求6.6.0

# 单页面配置

如果只有一个或少量页面需自定义导航栏,可以给单个页面配置导航样式。比如home页面,只需配置home.json即可。

{
    "navigationStyle": "custom"
}

注意微信客户端最低版本要求7.0.0

# 自定义导航栏

和其他组件一样的实现方式,我使用的是cover-view,保证用于最高的层级,防止在地图组件等层级较高的页面无法渲染出来。

思路是利用flex布局,简单粗暴地分成左中右三块,右侧是胶囊占位。最下方留一块空白是防止下拉页面的时候导航栏页跟着下拉。

<!--自定义标题栏-->
<cover-view class="container" style="height:{{statusBarHeight + navBarHeight}}px;line-height:{{statusBarHeight + navBarHeight}}px;">
  <!--左侧自定义图标-->
	<cover-view class="container-item container-item-left">
		<cover-image bindtap="_toMain" class="container-item-left-img" src="/assets/images/icon-nav-main.png"/>
	</cover-view>

  <!--中间标题-->
	<cover-view 
    class="container-item container-item-mid container-item-mid-text" 
    style="height:{{statusBarHeight + navBarHeight}}px;line-height:{{statusBarHeight + navBarHeight + statusBarHeight}}px;">
		  {{title}}
	</cover-view>

  <!--右侧胶囊占位-->
	<cover-view class="container-item ">
	</cover-view>
</cover-view>

<cover-view style="height:{{statusBarHeight + navBarHeight}}px;"></cover-view>

wxss文件

.container {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  background: #fff;
  display: flex;
  justify-content: space-between;
}

.container-item {
  width: 33.33%;
  display: flex;
  align-items: flex-end;
}

.container-item-left {
  justify-content: flex-start;
  padding-left: 1rem;
  padding-bottom: 0.5rem;
}

.container-item-left-img {
  display: inline-block;
  height: 25px;
  width: 25px;
}

.container-item-mid {
  justify-content: center;
  padding-top: 0.5rem;
}

.container-item-mid-text {
  font-size: 18px;
  text-align: center;
  color: #333;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

js文件

// 这是工具函数,to是对跳转页面的封装
import { to } from '../../utils/util'
// 获取全局应用实例
const app = getApp()
Component({
  /**
   * 组件的属性列表
   */
  properties: {
    title: {
      type: String,
      value: '???'
    }
  },

  /**
   * 组件的初始数据
   */
  data: {
    navBarHeight: 44,       
    statusBarHeight: 20
  },
  ready: function () {
    if (app.globalData) {
      this.setData({
        statusBarHeight: app.globalData.statusBarHeight,
        navBarHeight: app.globalData.navBarHeight
      })
    }
  },
  /**
   * 组件的方法列表
   */
  methods: {
    /**
     * 跳转到mine个人信息页
     * @param {Object} e 
     */
    _toMine: function (e) {
      to('/pages/mine/mine')
    }
  }
})

app.js

App({
  onLaunch: function () {
    this.initData()
  },
  globalData: { // 定义全局数据
    statusBarHeight: 20,
    navBarHeight: 44
  },
  /**
   * 获取手机系统信息
   */
  initData: function() {
    wx.getSystemInfo({
      success: res => {
        // 获取手机系统数据
        this.globalData.statusBarHeight = res.statusBarHeight
        if (res.model.toLowerCase().includes('iphone')) {
          this.globalData.navBarHeight = 44
        } else {
          this.globalData.navBarHeight = 48
        }
      }
    })
  }
})

# 兼容低版本

可设置一个全局变量判断是否渲染自定义导航栏。详见兼容问题参考文档

// app.js中
//app.js
App({
  onLaunch: function () {
    this.initData()
  },
  globalData: {
    canUse: false,
    lowVersion: '7.0.0',
    lowSdkVersion: '2.4.3'
  },
  /**
   * 初始化系统参数
   */
  initData: function() {
    wx.getSystemInfo({
      success: res => {
          // 微信版本号和sdk版本号都符合要求才会渲染
        if(
            this.compareVersion(res.version, this.globalData.lowVersion) >= 0 && 
            this.compareVersion(res.SDKVersion, this.globalData.lowSdkVersion) >= 0) {
            this.globalData.canUse = true
        }
      }
    })
  },
  compareVersion(v1, v2) {
    v1 = v1.split('.')
    v2 = v2.split('.')
    const len = Math.max(v1.length, v2.length)
    while(v1.length < len) {
      v1.push('0')
    }
    while(v2.length < len) {
      v2.push('0')
    }
    for(let i=0; i< len; i++) {
      const num1 = parseInt(v1[i])
      const num2 = parseInt(v2[i])
      if(num1 > num2) {
        return 1
      } else if(num1 < num2) {
        return -1
      }
    }
    return 0
  }
})