分类 MiniProgram 下的文章

代码实现

  1. 微信开发者工具上可以通过「编译模式」下的「下次编译模拟更新」开关来调试
  2. 小程序开发版/体验版没有「版本」概念,所以无法在开发版/体验版上测试版本更新情况
// app.js
onLaunch: function() {
    this.checkForUpdate();
},
// 检查是否有新版本
checkForUpdate: function() {
    if (wx.canIUse('getUpdateManager')) { // 判断getUpdateManager在当前版本是否可用
        const updateManager = wx.getUpdateManager();
        updateManager.onCheckForUpdate(function(res) {
            // 请求完新版本信息的回调
            if (res.hasUpdate) { // 有新版本
                updateManager.onUpdateReady(function() {
                    wx.showModal({
                        title: '更新提示',
                        content: '新版本已经准备好,是否重启应用?',
                        success: function(res) {
                            if (res.confirm) {
                                // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
                                updateManager.applyUpdate();
                            }
                        }
                    })
                })
                updateManager.onUpdateFailed(function() {
                    // 新版本下载失败
                    wx.showModal({
                        title: '已经有新版本了哟~',
                        content: '新版本已经上线,请您删除当前小程序,重新搜索打开哟~'
                    })
                })
            }
        })
    } else {
        wx.showModal({
            title: '提示',
            content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'
        })
    }
},

本文转自:https://www.jinjin.link/2021/06/09/小程序强制更新全解析/

WXS

所有监听事件先在onload监听。
// pages/index/to_news/to_news.js  
var app = getApp();
var socketOpen = false;
var SocketTask = false;
var url = 'ws://192.168.0.120:7011';
Page({
  data: {
    inputValue: '',
    returnValue: '',
  },
  onLoad: function (options) {
  },
  onReady: function () {
    // 创建Socket
    SocketTask = wx.connectSocket({
      url: url,
      data: 'data',
      header: {
        'content-type': 'application/json'
      },
      method: 'post',
      success: function (res) {
        console.log('WebSocket连接创建', res)
      },
      fail: function (err) {
        wx.showToast({
          title: '网络异常!',
        })
        console.log(err)
      },
    })
    if (SocketTask) {
      SocketTask.onOpen(res => {
        console.log('监听 WebSocket 连接打开事件。', res)
      })
      SocketTask.onClose(onClose => {
        console.log('监听 WebSocket 连接关闭事件。', onClose)
      })
      SocketTask.onError(onError => {
        console.log('监听 WebSocket 错误。错误信息', onError)
      })
      SocketTask.onMessage(onMessage => {
        console.log('监听WebSocket接受到服务器的消息事件。服务器返回的消息', onMessage)
      })
    }
  },

  // 提交文字
  submitTo: function (e) {
    let that = this;
    that.data.allContentList.push({that.data.inputValue });
    that.setData({
      allContentList: that.data.allContentList
    })
    var data = {
      text: that.data.inputValue
    }
    if (socketOpen) {
      // 如果打开了socket就发送数据给服务器
      sendSocketMessage(data)
    }
  },
  bindKeyInput: function (e) {
    this.setData({
      inputValue: e.detail.value
    })
  },

  onHide: function () {
      SocketTask.close(function (close) {
        console.log('关闭 WebSocket 连接。', close)
      })
  },
})

//通过 WebSocket 连接发送数据,需要先 wx.connectSocket,并在 wx.onSocketOpen 回调之后才能发送。
function sendSocketMessage(data) {
  console.log('通过 WebSocket 连接发送数据')
  if (socketOpen) {
    SocketTask.send({data: JSON.stringify(data)
    }, function (res) {
      console.log('已发送', res)
    })
  } else {
    socketMsgQueue.push(msg)
  }
} 

WXML

 <input type="text" bindinput="bindKeyInput" value='{
   {inputValue}}' placeholder="" />
  <button bindtap="submitTo" class='user_input_text'>发送</button>

app.js

在app.js下添加三个函数openSocket(), closeSocket(),sendMessage(),在app初始化的onLunch函数里面调用openSocket(),这样子用户一进入小程序就会自动连接websocket
App({
      globalData: {
        socketStatus: 'closed',
      },
    onLaunch: function() {   
        var that = this;
        if (that.globalData.socketStatus === 'closed') {
              that.openSocket();
        }
    }
    openSocket() {
       //打开时的动作
        wx.onSocketOpen(() => {
          console.log('WebSocket 已连接')
          this.globalData.socketStatus = 'connected';
          this.sendMessage();
        })
        //断开时的动作
        wx.onSocketClose(() => {
          console.log('WebSocket 已断开')
          this.globalData.socketStatus = 'closed'
        })
        //报错时的动作
        wx.onSocketError(error => {
          console.error('socket error:', error)
        })
        // 监听服务器推送的消息
        wx.onSocketMessage(message => {
          //把JSONStr转为JSON
          message = message.data.replace(" ", "");
          if (typeof message != 'object') {
            message = message.replace(/\ufeff/g, ""); //重点
            var jj = JSON.parse(message);
            message = jj;
          }
          console.log("【websocket监听到消息】内容如下:");
          console.log(message);
        })
        // 打开信道
        wx.connectSocket({
          url: "ws://" + "localhost" + ":8888",
        })
      },
        
    //关闭信道
      closeSocket() {
        if (this.globalData.socketStatus === 'connected') {
          wx.closeSocket({
            success: () => {
              this.globalData.socketStatus = 'closed'
            }
          })
        }
      },
        
     //发送消息函数
      sendMessage() {
        if (this.globalData.socketStatus === 'connected') {
        //自定义的发给后台识别的参数 ,我这里发送的是name
          wx.sendSocketMessage({
            data: "{\"name\":\"" + wx.getStorageSync('openid') + "\"}"  
          })
        }
      },
})



websocket API

wx.connectSocket:创建一个 WebSocket 连接 ;
sotk.onOpen:监听 WebSocket 连接打开事件 ;
sotk.onClose:监听 WebSocket 连接关闭事件 ;
sotk.onError:监听 WebSocket 错误事件 ;
sotk.onMessage:监听
WebSocket 接受到服务器的消息事件 ;(重要:负责监听接收数据)
sotk.send:通过
WebSocket 连接发送数据 。(重要:负责发送数据)
var sotk = null;
var socketOpen = false;
var wsbasePath = "ws://开发者服务器 wss 接口地址/";

//开始webSocket
  webSocketStart(e){
    sotk = wx.connectSocket({
      url: wsbasePath,
      success: res => {
        console.log('小程序连接成功:', res);
      },
      fail: err => {
        console.log('出现错误啦!!' + err);
        wx.showToast({
          title: '网络异常!',
        })
      }
    })

    this.webSokcketMethods();

  },

//监听指令
  webSokcketMethods(e){
    let that = this;
    sotk.onOpen(res => {
      socketOpen = true;
      console.log('监听 WebSocket 连接打开事件。', res);
    })
    sotk.onClose(onClose => {
      console.log('监听 WebSocket 连接关闭事件。', onClose)
      socketOpen = false;
    })
    sotk.onError(onError => {
      console.log('监听 WebSocket 错误。错误信息', onError)
      socketOpen = false
    })

    sotk.onMessage(onMessage => {
      var data = JSON.parse(onMessage.data);
      console.log('监听WebSocket接受到服务器的消息事件。服务器返回的消息',data);
     
    })
   
  },

//发送消息
  sendSocketMessage(msg) {
    let that = this;
    if (socketOpen){
      console.log('通过 WebSocket 连接发送数据', JSON.stringify(msg))
      sotk.send({
        data: JSON.stringify(msg)
      }, function (res) {
        console.log('已发送', res)
      })
    }
    
  },
 //关闭连接
  closeWebsocket(str){
    if (socketOpen) {
      sotk.close(
        {
          code: "1000",
          reason: str,
          success: function () {
            console.log("成功关闭websocket连接");
          }
        }
      )
    }
  },

实现逻辑

1、通过onMessage监听收到的信息,接收到的信息会返回一个type值,通过type值判断接收到的数据是什么数据,之后分别进行处理、赋值即可;
2、通过sendSocketMessage发送数据,根据后端定义的字段发送请求,字段中也包含type值,告诉websocket是处理什么类型的值。
webSokcketMethods(e) {
    this.websocket.onMessage(onMessage => {
      console.log(this.data)
      var data = JSON.parse(onMessage.data);
      console.log('监听WebSocket接受到服务器的消息事件。服务器返回的消息', data);
      if (data.client_id) {
        this.setData({
          client_id: data.client_id
        })
      }
      if (data.msg_type === 'init') {
        this.joinLive()
      }
      if (data.msg_type === 'join') {
        wx.hideLoading()
        this.endtrain(data.msg_content)
        this.setData({
          is_join: true,
          lookNumber: data.online_num
        })
        if (data.message_list) {
          this.setData({
            commentList: data.message_list,
            toLast: 'item' + data.message_list.length,
            lookNumber: data.online_num,
            followNumber: data.subscribe_num
          })
        }

      }
      if (data.msg_type == 'message') {
        var commentItem = {
          name: data.from_name,
          content: data.content
        }
        var commentArr = this.data.commentList
        console.log(commentArr)
        commentArr.push(commentItem)
        this.setData({
          commentList: commentArr,
          toLast: 'item' + commentArr.length,
        })

        if (this.data.platform === 'android') {
          this.messageScroll()
        }
      }
      if (data.msg_type == 'subscribe') {
        this.setData({
          followNumber: data.subscribe_num
        })
        this.endtrain(data.msg_content)

      }

      if (data.msg_type == 'leave') {
        this.endtrain(data.msg_content)
      }

      if (data.msg_type == 'buy') {
        this.endtrain(data.msg_content)
      }
    })
  },
   // 加入直播间
  joinLive() {
    var data = {
      msg_type: 'join',
      room_id: this.data.liveRoomId,
      live_id: this.data.liveId,
      from_uid: this.data.anchorUid,
      client_id: this.data.client_id,
      is_anchor: true,
      from_name: this.data.anchorName
    }
    console.log('通过 WebSocket 连接发送数据', JSON.stringify(data))
    this.websocket.send({
      data: JSON.stringify(data),
      success: function (res) {
        console.log(res)
      },
      fail: function (err) {
        console.log(err)
      }
    })
  },
  // 商品上架
  goodsShelfOn(goodsId, goodsName, goodsPrice, defaultImage, status) {
    var data = {
      msg_type: 'on_off_shelf',
      room_id: this.data.liveRoomId,
      live_id: this.data.liveId,
      from_uid: this.data.anchorUid,
      client_id: this.data.client_id,
      from_name: this.data.anchorName,
      goods_id: goodsId,
      goods_name: goodsName,
      goods_price: goodsPrice,
      default_image: defaultImage,
      status: 'on'
    }
    console.log('通过 WebSocket 连接发送数据', JSON.stringify(data))
    this.websocket.send({
      data: JSON.stringify(data),
      success: function (res) {
        console.log(res)
      },
      fail: function (err) {
        console.log(err)
      }
    })
  }

心跳检测、断线重连

在实际应用中,需要不断判断websocket处于连接阶段。
wechat-websocket.js文件
export default class websocket {
  constructor({ heartCheck, isReconnection }) {
    // 是否连接
    this._isLogin = false;
    // 当前网络状态
    this._netWork = true;
    // 是否人为退出
    this._isClosed = false;
    // 心跳检测频率
    this._timeout = 3000;
    this._timeoutObj = null;
    // 当前重连次数
    this._connectNum = 0;
    // 心跳检测和断线重连开关,true为启用,false为关闭
    this._heartCheck = heartCheck;
    this._isReconnection = isReconnection;
    this._onSocketOpened();
  }
  // 心跳重置
  _reset() {
    clearTimeout(this._timeoutObj);
    return this;
  }
  // 心跳开始
  _start() {
    let _this = this;
    this._timeoutObj = setInterval(() => {
      wx.sendSocketMessage({
        // 心跳发送的信息应由前后端商量后决定
        data: JSON.stringify({
          "msg_type": 'ping'
        }),
        success(res) {
          console.log(res)
          console.log("发送心跳成功");
        },
        fail(err) {
          console.log(err)
          // wx.showToast({
          //   title: "websocket已断开",
          //   icon: 'none'
          // })
          _this._reset()
        }
      });
    }, this._timeout);
  }
  // 监听websocket连接关闭
  onSocketClosed(options) {
    wx.onSocketClose(err => {
      console.log('当前websocket连接已关闭,错误信息为:' + JSON.stringify(err));
      // 停止心跳连接
      if (this._heartCheck) {
        this._reset();
      }
      // 关闭已登录开关
      this._isLogin = false;
      // 检测是否是用户自己退出小程序
      console.log(this._isClosed)
      if (!this._isClosed) {
        // 进行重连
        if (this._isReconnection) {
          this._reConnect(options)
        }
      }
      
    })
  }
  // 检测网络变化
  onNetworkChange(options) {
    wx.onNetworkStatusChange(res => {
      console.log('当前网络状态:' + res.isConnected);
      if (!this._netWork) {
        this._isLogin = false;
        // 进行重连
        if (this._isReconnection) {
          this._reConnect(options)
        }
      }
    })
  }
  _onSocketOpened() {
    wx.onSocketOpen(res => {
      console.log('websocket已打开');
      // 打开已登录开关
      this._isLogin = true;
      // 发送心跳
      if (this._heartCheck) {
        this._reset()._start();
      }
      // 发送登录信息
      wx.sendSocketMessage({
        // 这里是第一次建立连接所发送的信息,应由前后端商量后决定
        data: JSON.stringify({
          "key": 'value'
        })
      })
      // 打开网络开关
      this._netWork = true;
    })
  }
  // 接收服务器返回的消息
  onReceivedMsg(callBack) {
    wx.onSocketMessage(msg => {
      if (typeof callBack == "function") {
        callBack(msg)
      } else {
        console.log('参数的类型必须为函数')
      }
    })
  }
 
  // 建立websocket连接
  initWebSocket(options) {
    let _this = this;
    if (this._isLogin) {
      console.log("您已经登录了");
    } else {
      console.log('未登录')
      // 检查网络
      wx.getNetworkType({
        success(result) {
          if (result.networkType != 'none') {
            console.log(result.networkType)
            // 开始建立连接
            wx.connectSocket({
              url: options.url,
              success(res) {
                if (typeof options.success == "function") {
                  options.success(res)
                } else {
                  console.log('参数的类型必须为函数')
                }
              },
              fail(err) {
                if (typeof options.fail == "function") {
                  options.fail(err)
                } else {
                  console.log('参数的类型必须为函数')
                }
              }
            })
          } else {
            console.log('网络已断开');
            _this._netWork = false;
            // 网络断开后显示model
            wx.showModal({
              title: '网络错误',
              content: '请重新打开网络',
              showCancel: false,
              success: function (res) {
                if (res.confirm) {
                  console.log('用户点击确定')
                }
              }
            })
          }
        }
      })
    }
  }
  // 发送websocket消息
  sendWebSocketMsg(options) {
    wx.sendSocketMessage({
      data: options.data,
      success(res) {
        if (typeof options.success == "function") {
          options.success(res)
        } else {
          console.log('参数的类型必须为函数')
        }
      },
      fail(err) {
        if (typeof options.fail == "function") {
          options.fail(err)
        } else {
          console.log('参数的类型必须为函数')
        }
      }
    })
  }
  // 重连方法,会根据时间频率越来越慢
  _reConnect(options) {
    let timer, _this = this;
    if (this._connectNum < 20) {
      timer = setTimeout(() => {
        this.initWebSocket(options)
      }, 3000)
      this._connectNum += 1;
    } else if (this._connectNum < 50) {
      timer = setTimeout(() => {
        this.initWebSocket(options)
      }, 10000)
      this._connectNum += 1;
    } else {
      timer = setTimeout(() => {
        this.initWebSocket(options)
      }, 450000)
      this._connectNum += 1;
    }
  }
  // 关闭websocket连接
  closeWebSocket(){
    wx.closeSocket();
    this._isClosed = true;
  }
}

微信官方文档

微信小程序心跳

微信websocket基础用法

转自:https://hehuiyun.github.io/2021/01/22/微信小程序websocket的用法/

webSocketInit () {
    const vm = this
    let token = ''
    let socketUrl = 'wss://websocket.test'
    // 建立 WebSocket 连接
    vm.socket = wx.connectSocket({
      url: socketUrl,
      header: {
        'content-type': 'application/json',
        'authorization': 'Bearer' + token,
        'x-wxapp-sockettype': 'ordertips'
      },
      success (res) {
        console.log('WebSocket 连接成功: ', res)
      },
      fail (err) {
        console.log('WebSocket 连接失败: ', err)
      }
    })
    // onOpen
    vm.socket.onOpen((ret) => {
      console.log('打开 WebSocket 连接')
    })
    // onMessage
    vm.socket.onMessage((ret) => {
      if (ret.data == 'pong') {
        return;
      }
      let data = JSON.parse(ret.data)
      wx.showToast({
        title: data.message,
        icon: 'none',
        duration: 3000
      })
    })
    // onError
    vm.socket.onError((err) => {
      console.log('WebSocket 连接失败:', err)
    })
    // onClose
    vm.socket.onClose((ret) => {
      console.log('断开 WebSocket 连接', ret)
    })
  },
  // send message
  send (msg) {
    const vm  = this
    vm.socket.send({
      data: msg,
      success (res) {
        console.log('WebSocket 消息发送成功', res)
      },
      fail (err) {
        console.log('WebSocket 消息发送失败', err)
      }
    })
  },
  // 心跳,由客户端发起
  ping () {
    const vm = this
    let times = 0
    // 每 10 秒钟由客户端发送一次心跳
    this.interval = setInterval(function () {
      if (vm.socket.readyState == 1) {
        vm.send('ping')
      } else if (vm.socket.readyState == 3) {
        times += 1
        // 超时重连,最多尝试 10 次
        if (times >= 10) {
          wx.showToast({
            title: 'WebSocket 连接已断开~',
            icon: 'none',
            duration: 3000
          })
          clearInterval(vm.interval)
        }
        vm.reconnect()
      }
    }, 10000)
  },
  // WebSocket 断线重连
  reconnect () {
    const vm = this
    vm.webSocketInit()
  },

HTML

<van-uploader file-list="{{ fileList }}" bind:after-read="uploadImage" accept="image" max-count="6" bind:delete="onDelImage" />

JS

  uploadImage(event) {
    const { file } = event.detail;
    // 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式
    wx.uploadFile({
      url: '后台真实upload地址', // 仅为示例,非真实的接口地址
      filePath: file.url,
      name: 'file',
      formData: { authorize: app.getToken() },
      success:(res) => { //这里要用箭头函数,否则报变量为定义的错
        // 上传完成需要更新 fileList
        const { fileList = [] } = this.data;
        const resData = JSON.parse(res.data);
        fileList.push({ ...file, url: resData.path });
        this.setData({ fileList });
      },
    });
  },

  //点击删除图片
  onDelImage(event) {
    let id = event.detail.index //能获取到对应的下标
    let fileList = this.data.fileList //这里是前端页面展示的数组
    fileList.splice(id, 1) //删除1张刚刚点击的那图
    this.setData({
      fileList: fileList, //在这里进行重新赋值  删除后 图片剩几张就相当于给后台传几张
    })
  },

var myDate = new Date();
myDate.getYear(); //获取当前年份(2位)
myDate.getFullYear(); //获取完整的年份(4位,1970-????)
myDate.getMonth(); //获取当前月份(0-11,0代表1月) // 所以获取当前月份是myDate.getMonth()+1;
myDate.getDate(); //获取当前日(1-31)
myDate.getDay(); //获取当前星期X(0-6,0代表星期天)
myDate.getTime(); //获取当前时间(从1970.1.1开始的毫秒数)
myDate.getHours(); //获取当前小时数(0-23)
myDate.getMinutes(); //获取当前分钟数(0-59)
myDate.getSeconds(); //获取当前秒数(0-59)
myDate.getMilliseconds(); //获取当前毫秒数(0-999)
myDate.toLocaleDateString(); //获取当前日期
var mytime=myDate.toLocaleTimeString(); //获取当前时间
myDate.toLocaleString( ); //获取日期与时间

将字符串形式的日期转换成日期对象

这里实现把一个日期字符串如“2021-12-23 12:31:45”转换为Date对象:

方法一:

 var strArray=str.split(" "); 
 var strDate=strArray[0].split("-"); 
 var strTime=strArray[1].split(":"); 
 var a=new Date(strDate[0],(strDate[1]-parseInt(1)),strDate[2],strTime[0],strTime[1],strTime[2])

方法二 :(超简单)

 var s = "2005-12-15 09:41:30"; 
 var d = new Date(Date.parse(s.replace(/-/g, "/")));
 或
 new Date(s);
 //'-'在js中可能会应为某些浏览器不兼容而出现问题,微信小程序没什么影响

new Date() 参数问题

  • new Date() ; //参数可以为整数; 也可以为字符串; 但格式必须正确

  • new Date(2009,1,1);

  • new Date(“2009/1/1”);

  • new Date(“2009-1-1”);

  • new Date( year, month, date, hrs, min, sec) 按给定的参数创建一日期对象

当前系统区域设置格式(toLocaleDateString和toLocaleTimeString) (locale本地) 例子:(new Date()).toLocaleDateString() + ” ” + (new Date()).toLocaleTimeString() 结果: xxxx年x月xx日 上午xx:xx:xx 比起正常的参数多了一个上、下午

转自:https://juejin.cn/post/7088203284174880799

第一步,在app.json 中加入 useExtendedLib 字段

  "useExtendedLib": {
    "weui": true
  }

app.json.png

第二步,在所需要的页面的 json 文件中 加入 usingComponents 字段

  "usingComponents": {
    "mp-uploader": "weui-miniprogram/uploader/uploader",
    "mp-cells": "weui-miniprogram/cells/cells",
    "mp-cell": "weui-miniprogram/cell/cell"
  }

index.json.png

第三步,在所需要的页面中调用该组件

<mp-cells>
  <mp-cell>
    <mp-uploader bindfail="uploadError" bindsuccess="uploadSuccess" select="{{selectFile}}" upload="{{uplaodFile}}" files="{{files}}" max-count="6" title="资料上传" tips="已加密请放心上传"></mp-uploader>
  </mp-cell>
</mp-cells>

最后,最后,看一下效果图

xiaoguo.png

感谢在最后:https://blog.csdn.net/Hero_rong/article/details/107063655

最近几个月因为公司的业务需求,一直在折腾小程序,从开始的完全不熟悉,到后面被各种坑折磨,是时候写一篇总结了,避免下一次遇到还找不到解决的方案。

配置项app.json和page.json

app.json

首先,需要确定的是,app.json 是小程序的全局配置,放在根目录下。

常用的一些字段如下:

  • pages: 这个用于设置页面的路径,通常访问的页面都放在pages文件夹里面,且必须在这个全局配置文件中设置好路径,否则访问的时候会报错找不到该路径, 写在 pages 字段的第一个页面就是这个小程序的首页 (打开小程序看到的第一个页面)
  • window: 全局样式的配置
    • 导航栏相关:可配置导航栏背景颜色、标题颜色,标题内容,
    • 可以隐藏导航栏(navigationStyle设置为custom,但需要注意, 自定义导航栏仍然保留右上角胶囊,且没有返回键,如果想要实现返回功能,需要自己自定义返回的样式和功能 ),==该设置对web-view 组件无效==
    • 窗口的背景色, backgroundColor,(即小程序下拉时露出的那一截),另外,仅ios支持的还有backgroundColorTop和backgroundColorBottom, 顶部和底部窗口的背景色
    • 下拉loading的样式,只有黑色和白色(dark / light)
    • enablePullDownRefresh ,小程序默认页面是没有下拉刷新功能的,如果在app.json中配置该项为true,则所有页面都可以生效。如果只想在单个页面使用下拉刷新,需要在对应的page.json中进行配置
    • onReachBottomDistance, 上拉加载更多生效时距页面底部距离,默认为50px
    • 屏幕旋转,pageOrientation, 暂时还没有用到过,支持 auto / portrait / landscape,默认为portrait竖屏显示,
  • tabbar: tab栏设置
    • 位置:支持顶部和底部(通常使用底部tab栏), position(bottom / top)
    • tab列表 list: 文字以及对应的路径
      • 最少2个,最多5个
      • pagePath,页面路径(必须是pages中定义的路径)
      • text, tab上的文字
      • iconPath, 图片路径,( 大小限制为 40kb,建议尺寸为 81px * 81px,不支持网络图片 )
      • selectedIconPath,选中时的图片路径。限制同上
    • 样式相关:color字体颜色,selectedColor选中文字的颜色,backgroundColor是tab栏背景色 ,borderStyle上边框颜色,
    • custom是否自定义,默认为否, 具体文档参考: 自定义tabBar
  • networkTimeout(设置网络请求的超时时间)
    • request
    • connectSocket
    • uploadFile
    • downloadFile
  • debug, 开启调试,会输出Page 的注册,页面路由,数据更新,事件触发等信息,方便查看生命周期的调试
  • navigateToMiniProgramAppIdList, 跳转到其他小程序时,需要先在app.json中进行声明
  • usingComponents,如果在app.json中声明该组件,则全局可以使用该自定义组件,不需要再在page的json中声明
  • permission, 用于授权相关,平时项目中是自己写的页面,调用button的open-type属性唤起授权,其实可以通过插件功能页来实现,比如授权昵称, 用户信息功能页

page.json

页面的样式配置优先级比全局配置中的window高。

  • 可以设置导航栏背景颜色、导航栏标题颜色,导航栏标题,
  • 可以隐藏导航栏,custom 自定义导航栏,只保留右上角胶囊按钮
  • 窗口的背景色 , 以及ios支持顶部和底部窗口的背景色
  • backgroundTextStyle,下拉 loading 的样式,仅支持 dark / light
  • enablePullDownRefresh开启下拉刷新,这个属性只控制当前的page页面可以刷新
  • onReachBottomDistance,上拉加载更多生效时距页面底部距离,默认为50px
  • pageOrientation,屏幕旋转设置
  • disableScroll ,为 true 则页面整体不能上下滚动。只在页面配置中有效,无法在 app.json 中设置
  • disableSwipeBack, 禁止页面右滑手势返回
  • usingComponents, 使用自定义组件,如果没有在页面的json中声明,直接使用组件会报错

sitemap.json文件(用于微信索引)

默认规则为都默认被索引:

{"action": "allow", "page": "*"}

语法区别

wxml文件跟html的区别:

  • 没有div, p, span, 使用view代替div, text代替span
  • wx:if的使用同vue中的v-if, 但使用上略有区别,不管在标签中还是文本上,都是使用双大括号来表示插值, wx:for="{{array}}" ,
  • 条件渲染有wx:if, wx:elif, wx:else, 使用方式: <view wx:if="{{view == 'WEBVIEW'}}"> WEBVIEW </view>
  • 尤其注意,如果是使用boolean类型的值,不要直接在双引号内直接写,也需要写在双大括号里面。如 <checkbox checked="{{false}}"> </checkbox>
  • 列表渲染 wx:for="{{array}}" , 可直接使用item和index,也可以使用 wx:for-item="idx" , 和 wx:for-item="itemName" 重新指定index和item的key值, 花括号和引号之间如果有空格,将最终被解析成为字符串
  • wxml文件中有一个标签是block, 仅仅是一个包装元素,不会在页面中做任何渲染,只接受控制属性
  • wx:key的值只能是字符串或者 "*this", wx:key="*this" 表示for 循环中的 item 本身, 但是要保证这个item本身是一个唯一的字符串或者数字, 当key绑定的是对象的时候会报错

问题思考?

wx:if 和 hidden 该怎么合理的判断使用场景?

  • wx:if 是惰性的,只有为true时才会局部渲染,当 wx:if 的条件值切换时,条件块会在切换时销毁或重新渲染。
  • 而hidden始终会被渲染,只是控制显示与隐藏
  • 所以,如果需要频繁切换的情景下,用 hidden 更好,如果在运行时条件不大可能改变则 wx:if 较好

组件

基础组件

  • 组件的公共属性:
    • id, class, style, hidden, data- , bind /catch*
  • 视图容器:
    • 可移动: movable-area, movable-view。 其中,movable-view必须在 movable-area 组件中,并且必须是直接子节点
    • 覆盖原生组件:cover-view, cover-image,主要用于覆盖 map、video、canvas、camera、live-player、live-pusher等级别高的原生组件。 注意,在cover-view中只能嵌套cover-view、cover-image和button
    • 可滚动的视图区域:scroll-view, 竖向滚动时,需要给scroll-view一个固定高度
    • 滑块视图容器: swiper, 其中只能放置swiper-item组件(常用于轮播图)
    • 视图容器view, 如果想使用hover的样式,可以指定按下去的样式类,跟hover相关的属性: hover-class,hover-stop-propagation, hover-start-time, hover-stay-time
  • 图标icon,在小程序中可以使用自带的一些icon图标,可以设置type、大小、颜色,详情查看, 小程序icon组件
  • 进度条,progress, 普通的进度条包括动画等可以直接使用该组件
  • 富文本,rich-txt, (可以使用该组件渲染html内容,传入html字符串)
  • 文本,text, (如果需要展示多个空格,可以设置space属性)
  • 按钮,button,(可以设置disabled属性进行禁用,防止多次点击触发事件)
  • 单选、多选框:radio、radio-goup、checkbox、checkbox-group
  • 富文本编辑器,editor
  • 表单form,支持switch input checkbox slider radio picker 的提交
  • 输入框input,支持文本、数字、身份证、带小数点的数字键盘等4个类型,password可以显示密码类型,(可以控制键盘右下角的文字,使用confirm-type属性控制,包括“发送”、“搜索”、“下一个”、“前往”、“完成”)注意:input无法设置 font-family,使用的是系统字体
  • 滚动选择器,picker,支持普通、多列、时间、日期、省市选择器,
  • 嵌入页面的滚动选择器, picker-view, picker-view-column
  • 滑动选择器,slider,类似进度条不过可以拖动
  • 开关,switch
  • 多行输入框,textarea
  • 导航,navigator,(类似于a标签,可以使用navigate、redirect、switchTab、reLaunch、navigateBack、exit(退出小程序)等类型)
  • 系统相机,camera,可以用于扫描二维码
  • 视频video,(注意,使用video组件在列表中的时候会引起小程序页面卡顿甚至崩溃,目前项目中的解决方案是使用阿里云上传视频的截帧图片来替换掉video组件,只有点击图片后才会播放视频,查看网上资料说是video的src默认为空,解决了viode自动下载的bug,使用custom-cache="{{false}}"可以解决视频缓存中卡住的问题,不过还没有试过)
  • 公众号关注组件,official-account
  • 承载网页的容器,web-view,(适用于第三方网站内容的嵌入,注意需要在公众平台配置业务域名),避免在链接中带有中文字符,在 iOS中会有打开白屏的问题,建议加一下encodeURIComponent

原生组件的使用限制

  • 原生组件包括:
    • camera
    • canvas
    • input(仅在focus时表现为原生组件)
    • live-player
    • live-pusher
    • map
    • textarea
    • video
  • 层级: 最高,其他组件无论设置 z-index 为多少,都无法盖在原生组件上
  • 原生组件不能设置动画、fixed布局、不能使用overflow: hidden来裁剪原生组件的显示区域,
  • 事件监听不能使用 bind:eventname 的写法,只支持 bindeventname,不支持 catch 和 capture 的事件绑定方式

事件系统

  • 事件处理函数,参数是event,event的事件内容包括:

    • type
    • timeStamp, 事件生成时的时间戳
    • target,触发事件的 源组件
    • currentTarget,事件绑定的 当前组件
    • detail, 自定义事件所携带的数据
    • touches,一个数组,每个元素为一个 Touch 对象
    • changedTouches, 有变化的触摸点
  • dataset:在target或currentTarget的事件对象中,可以通过dataset获取当前自定义的一些数据,(在JS文件中使用event.currentTarget.dataset时,dataset的属性会自动转换字符串, 连字符写法会转换成驼峰写法,而大写字符会自动转成小写字符 )

  • mark,类似于dataset,(如果存在同名的 mark ,父节点的 mark 会被子节点覆盖)

  • 页面的用户行为:

    • 下拉刷新 onPullDownRefresh
    • 上拉触底 onReachBottom
    • 页面滚动 onPageScroll
    • 用户转发 onShareAppMessage

思考:

  1. 小程序中bind事件和catch事件有什么区别?
  • bind事件绑定不会阻止冒泡事件向上冒泡,
  • catch事件绑定可以阻止冒泡事件向上冒泡
  1. target和currentTarget有什么不同?
<view id="outer" bindtap="handleTap1">
  outer view
  <view id="middle" catchtap="handleTap2">
    middle view
    <view id="inner" bindtap="handleTap3">
      inner view
    </view>
  </view>
</view>
复制代码

如上面这个示例中,点击inner view会触发handleTap3,同时会冒泡到handleTap2。 对于handleTap2这个组件来说,此时收到事件对象的target就是inner的元素,而currentTarget是middle的部分

  1. mark 和 dataset 有什么区别?
  • mark 会包含从触发事件的节点到根节点上所有的 mark: 属性值;
  • dataset 仅包含一个节点的 data- 属性值
  • 节点的 mark 不会做连字符和大小写转换

小程序相关机制

  • 小程序框架的逻辑层并非运行在浏览器中,因此 JavaScript 在 web 中一些能力都无法使用,如 window,document 等

小程序的运行机制:冷启动和热启动

  • 冷启动: 首次打开小程序;或小程序销毁后打开。(冷启动会触发app.js中的onLaunch生命周期函数)
  • 热启动: 点击右上角胶囊退出、或者home键离开微信、或者左右滑动返回到微信等动作,都是热启动,并没有销毁小程序,(所以热启动不会触发onLaunch函数,但会触发onShow函数)

启动场景

  • 小程序启动场景分为两种:
    • 从发现栏、另一个小程序返回、微信支付、首页下拉小程序栏等为A场景
    • B场景:打开某个特定页面:如转发分享的卡片链接
  • 热启动场景效果:
    1. 如果重新进入的场景和退出时的场景都是A场景,则保留原来状态;例如:停留在“个人中心”页面,退出后,下拉首页的小程序列表进入后还是会停留在“个人中心”页面,(触发“个人中心”Page的onShow函数,但是不会触发onLoad函数)
    2. 如果当前是A场景,上一个是B场景,则清空原来的页面栈,打开首页(即执行 wx.reLaunch 到首页)如分享出去的卡片是“学员列表”页面,点击卡片停留后退出,再从首页下拉的小程序进入,此时会跳转到首页
    3. 不管上一个场景是什么,如果当前是B场景,如点击小程序的分享卡片,此时会清空原来的页面栈,重新进入这个分享页,首先触发app中的onShow函数,然后触发这个分享页的onLoad函数,当然如果app中已经进行了跳转到其它页面,则不会再走这个分享页。
  • 冷启动场景效果:
    • 冷启动规则比较简单,如果冷启动时是A场景,则进入小程序首页
    • 如果冷启动时是B场景,则进入对应的特定页面

更新机制

  • 小程序首先会在后台有一个最新版本,一般来说会在24小时内下发新版本信息到用户,静默更新到新版本(此时用户是无感知态)
  • 在后台还没及时更新版本时,每次冷启动都会检测是否有更新版本,如果有,会 异步下载 ,同时启动当前的旧版本包,所以新版本需要下一次的冷启动才会应用上
  • 如果启动时就想立刻获取最新版本,需要使用 wx.getUpdateManager 的API,用wx.showModal提示用户是否重启应用更新

ES6的支持情况:

  • 微信小程序已经支持了绝大部分的 ES6 API,但部分API仍依赖于系统版本而不支持
  • String的normalize在ios8,ios9不支持
  • 数组的values在ios8和android不支持
  • 数组的includes在ios8不支持
  • Proxy不支持ios8、ios9和android

注册小程序

  • app.js中注册小程序,整个小程序只有一个 App 实例,是全部页面共享的。
  • 开发者可以通过 getApp 方法获取到全局唯一的 App 示例,获取App上的数据或调用开发者注册在 App 上的函数
  • 在app.js中可以直接使用this.globalData赋值,但在其他页面需通过getApp()方法获取到实例后才能使用全局变量globalData
App({
  onLaunch: function(options) {},
  onShow: function(options) {},
  onHide: function() {},
  onError: function(msg) {},
  globalData: 'I am global data'
})
复制代码

注册页面和自定义组件

页面

  • data 是页面第一次渲染使用的初始数据
  • onLoad页面加载时触发。一个页面只会调用一次;
  • onShow在页面显示/切入前台时触发
  • onPageScroll,监听用户滑动页面事件,( 请避免在 onPageScroll 中过于频繁的执行 setData 等引起逻辑层-渲染层通信的操作。尤其是每次传输大量数据,会影响通信耗时 )
  • onShareAppMessage监听页面内的转发,转发有button 组件 open-type="share"和右上角转发两个方式,(只有Page中定义了onShareAppMessage,右上角才会出现转发功能)
  • setData:
    • 将数据从逻辑层发送到视图层(异步),同时改变对应的 this.data 的值(同步)(跟react的setState类似,但又不一样)
    • 直接修改 this.data 而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致
    • 不要把 data 中任何一项的 value 设为 undefined
    • 如果需要视图层更新完毕后再处理事件,可以将事件放在setData的回调函数中

Page构造器:

Page({
  data: { text: "This is page data." },
  onLoad: function(options) { },
  onReady: function() { },
  onShow: function() { },
  onHide: function() { },
  onUnload: function() { },
  onPullDownRefresh: function() { },
  onReachBottom: function() { },
  onShareAppMessage: function () { },
  onPageScroll: function() { }
})
复制代码

自定义组件

  • 页面使用自定义组件时需要现在page的json文件中声明usingComponents定义段
  • 组件使用页面的生命周期方法时(即 on 开头的方法),应该写在methods定义段中
  • behaviors 提取所有页面中公用的代码段,例如, 在所有页面被创建和销毁时都要执行同一段代码,就可以把这段代码提取到 behaviors 中
// page-common-behavior.js
module.exports = Behavior({
  attached: function() {
    // 页面创建时执行
    console.info('Page loaded!')
  },
  detached: function() {
    // 页面销毁时执行
    console.info('Page unloaded!')
  }
})


// 页面 A
var pageCommonBehavior = require('./page-common-behavior')
Component({
  behaviors: [pageCommonBehavior],
  data: { /* ... */ },
  methods: { /* ... */ },
})


// 页面 B
var pageCommonBehavior = require('./page-common-behavior')
Component({
  behaviors: [pageCommonBehavior],
  data: { /* ... */ },
  methods: { /* ... */ },
})
复制代码
  • 在 properties 定义段中,属性名采用驼峰写法(propertyName);在 wxml 中,指定属性值时则对应使用连字符写法(component-tag-name property-name="attr value"),应用于数据绑定时采用驼峰写法(attr="")
  • observers数据监听器,(类似于vue中的watch),具体请参考 数据监听器
  • created,组件实例刚被创建,( 注意此时不能调用 setData )
  • definitionFilter,定义段过滤器,用于自定义组件扩展(如扩展自定义组件一个computed计算属性功能)

注意:

  • 使用 this.data 可以获取内部数据和属性值;但直接修改它不会将变更应用到界面上,应使用 setData 修改
  • 生命周期函数无法在组件方法中通过 this 访问到

组件间的通信与事件

  • 父传子:父组件绑定属性值,子组件通过properties获取,
  • 子组件向父组件传递数据:使用事件传递
  • 父组件访问子组件: 父组件可以通过 this.selectComponent 方法获取子组件实例对象,这样就可以直接访问组件的任意数据和方法

此处列举第二种自定义事件传递数据的方法:

// 1. 父组件中监听事件
<component-tag-name bindmyevent="onMyEvent" />

// 2. 子组件触发事件,触发wxml的onMyEvent事件,触发父组件的方法
<!--自定义组件中-->
<button bindtap="onTap">点击这个按钮将触发“myevent”事件</button>

<!--自定义组件js文件中-->
Component({
  properties: {},
  methods: {
    onTap: function(){
      var myEventDetail = {} // detail对象,提供给事件监听函数
      var myEventOption = {} // 触发事件的选项
      this.triggerEvent('myevent', myEventDetail, myEventOption)
    }
  }
})

复制代码

具体可参考文档: 组件间通信与事件

注意:

  • 组件和引用组件的页面不能使用id选择器(#a)、属性选择器([a])和标签名选择器,请改用class选择器
  • 避免使用后代选择器(.a .b)
  • 子元素选择器(.a>.b)只能用于 view 组件与其子节点之间,用于其他组件可能导致非预期的情况
  • app.wxss 中的样式、组件所在页面的的样式对自定义组件无效(除继承样式外,如 font 、 color会继承自组件外)

组件间关系

场景: 如果一个页面中需要使用到A组件和B组件,而A组件与B组件之间需要通信,此时可以在组件定义时加入relations定义段,如封装的ul和li组件后,之间需要存在关联的父子关系

  • 必须在两个组件定义中都加入relations定义,否则不会生效
  • type,与目标组件的相对关系,可选的值为 parent 、 child 、 ancestor 、 descendant
  • 另一种情况是关联一类组件(如form与input、checkbox等的关系),需要使用到behavior

具体使用方式参见 组件间关系

使用setData应注意的事项:

  • 不要频繁的去 setData,否则可能导致Android 下用户在滑动时会感觉到卡顿,或者渲染有出现延时
  • 不要每次 setData 都传递大量新数据
  • 不要在后台态页面进行 setData ,

小程序运行流程

  1. 微信打开小程序前,会把整个小程序的代码包下载到本地
  2. 通过 app.json 的 pages 字段就可以知道你当前小程序的所有页面路径
  3. 写在 pages 字段的第一个页面就是这个小程序的首页(打开小程序看到的第一个页面),所以微信会把首页的代码装载进来
  4. 小程序启动,触发app.js 定义的 App 实例的 onLaunch回调,启动时也会触发onShow函数,或切前台、从其他页面返回到这个页面,都会触发onShow;onHide监听小程序切后台,(如tab切换,navigateTo,离开微信等); onPageNotFound页面不存在监听函数
  5. 页面的渲染流程开始,微信客户端会先根据 page的json文件配置生成一个界面,
  6. 接着装载这个页面的 WXML 结构和 WXSS 样式
  7. 最后客户端会装载 page的js文件
  8. Page构造器会生成一个页面,生成页面的时候小程序框架会把 data 数据和 index.wxml 一起渲染出最终的结构,渲染完界面
  9. 渲染完界面之后,页面实例就会收到一个 onLoad 的回调

生命周期函数

小程序里面又三种生命周期:小程序运行的生命周期(在app.js中处理),页面的生命周期(在page的js文件中处理,或者组件的pageLifetimes触发的生命周期函数),组件的生命周期

小程序生命周期函数

  • onLaunch, 小程序初始化完成时触发,全局只触发一次
  • onShow,小程序启动,或从后台进入前台显示时触发
  • onHide,从前台进入后台时触发
  • onError,小程序发生脚本错误或 API 调用报错时触发
  • onPageNotFound,小程序要打开的页面不存在时触发

onLaunch,onShow参数:

  • path: 打开小程序的页面路径
  • query: 打开小程序的页面参数query
  • scene: 打开小程序的场景值,
  • shareTicket: 转发分享参数
  • referrerInfo:当场景为由从另一个小程序或公众号或App打开时,返回此字段
  • referrerInfo.appId: 来源小程序或公众号或App的 appId
  • referrerInfo.extraData: 来源小程序传过来的数据,scene=1037或1038时支持

页面生命周期函数

  • onLoad,监听页面加载
  • onShow, 监听页面显示(如tab切换或者离开微信、或navigateTo)
  • onReady, 监听页面初次渲染完成(一个页面只会调用一次,代表页面已经准备妥当)
  • onHide, 监听页面隐藏, (如 wx.navigateTo 或底部 tab 切换到其他页面,小程序切入后台等)
  • onUnload, 监听页面卸载(如wx.redirectTo或wx.navigateBack到其他页面时)

组件生命周期函数

  • created,在组件实例刚刚被创建时执行,注意此时不能调用 setData
  • attached,在组件实例进入页面节点树时执行
  • ready,在组件布局完成后执行
  • moved, 在组件实例被移动到节点树另一个位置时执行
  • detached, 在组件实例被从页面节点树移除时执行

注意:

组件中存在一个组件所在页面的生命周期,定义在pageLifetimes中,其中可用的生命周期包括:

  • show, 组件所在的页面被展示时执行
  • hide, 组件所在的页面被隐藏时执行
  • resize, 组件所在的页面尺寸变化时执行

网络

  1. 服务器域名配置,当使用体验版或者开发版报“服务器开小差”了的错误,大部分可能性都是因为没有打开调试,因为在调试模式下,小程序不会去校验域名的合法性。
  2. 小程序只可以跟指定的域名与进行网络通信,包括普通 HTTPS 请求(wx.request)、上传文件(wx.uploadFile)、下载文件(wx.downloadFile) 和 WebSocket 通信(wx.connectSocket)
  3. 如果接入第三方网页,使用web-view组件,需要配置业务域名
  4. 默认超时时间为60s,超时时间可以在 app.json 或 game.json 中通过 networktimeout 配置
  5. 并发限制:wx.request、wx.uploadFile、wx.downloadFile 的最大并发限制是 10 个;wx.connectSockt 的最大并发限制是 5 个
  6. 返回状态,只要成功接收到服务器返回, 无论 statusCode 是多少,都会进入 success 回调

自定义tabBar

用户可以通过配置app.json 中的 tabBar 项指定 custom 字段为true,来自定义tabBar,不过有些使用地方还需要注意:

  1. tabBar 的相关配置项仍然需完整声明,以兼容低版本以及区分哪些是tab页
  2. 需要用户自定义一个组件来渲染 tabBar,推荐用 fixed 在底部的 cover-view + cover-image 组件渲染样式,以保证 tabBar 层级相对较高
  3. 与 tabBar 样式相关的接口,如 wx.setTabBarItem 等将失效
  4. 可以在自定义组件下通过 getTabBar 接口,获取当前页面的自定义 tabBar 组件实例

小程序登录流程

小程序登录需要调用微信的开放接口,整体流程如下:

  1. 小程序通过wx.login()获取一个code
  2. 调用后端的api,发送code,后端通过这个code来校验接口,并返回一个自定义登录态
  3. 小程序将登录状态存入storage
  4. 之后再访问服务器发送业务请求就只用后端判断登录态查询openid和session_key返回业务数据

小程序优化操作

  1. 缩短白屏时间:
    • 首屏渲染的内容较多,需要集合多份数据进行渲染:此时可以把优先级高的内容做优先展示
    • 首屏内容依赖的数据从服务端请求的时间太长:分析数据返回的时间长的原因
    • 一次性渲染数据太大或依赖的计算过于复杂:减少渲染的数据量、优化渲染相关数据的算法
  2. 渲染界面的耗时过长,需要校验下是否同时渲染的区域太大(例如列表过长)
  3. 脚本执行时间过长:需要确认并优化脚本的逻辑
  4. setData调用频繁:避免无用的频繁调用,每秒调用setData的次数不超过 20 次,
// 不要频繁调用setData
    this.setData({ a: 1 })
    this.setData({ b: 2 })
// 绝大多数时候可优化为
    this.setData({ a: 1, b: 2 })
复制代码
  1. setData的数据太大,setData的数据在JSON.stringify后不超过 256KB
  2. setData一个未绑定的变量
// 不要设置不在界面渲染时使用的数据,并将界面无关的数据放在data外
    this.setData({
      myData: {
        a: '这个字符串在WXML中用到了',
        b: '这个字符串未在WXML中用到,而且它很长…………………………'
      }
    })
    // 可以优化为
    this.setData({
      'myData.a': '这个字符串在WXML中用到了'
    })
    this._myData = {
      b: '这个字符串未在WXML中用到,而且它很长…………………………'
    }

复制代码
  1. 开启 HTTP 缓存控制
  2. 控制WXML节点数
<view data-my-data="{{myData}}"> <!-- 这个 view 和下一行的 view 可以合并 -->
  <view class="my-class" data-my-data="{{myData}}" bindtap="onTap">
    <text> <!-- 这个 text 通常是没必要的 -->
      {{myText}}
    </text>
  </view>
</view>

<!-- 可以简化为 -->

<view class="my-class" data-my-data="{{myData}}" bindtap="onTap">
  {{myText}}
</view>
复制代码
  1. 控制图片大小,图片宽高都不超过实际显示宽高的3倍
  2. 合理控制网络请求数量
  3. 控制图片请求数
  4. 开启惯性滚动:wxss中带有overflow: scroll的元素,在 iOS 下需要设置-webkit-overflow-scrolling: touch样式
  5. 避免使用:active伪类来实现点击态
  6. 保持图片大小比例
  7. 可点击元素的宽高都不小于 20px
  8. iphoneX兼容,用以下wxss进行兼容:
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
复制代码
  1. 避免JS异常
  2. 不使用废弃接口
  3. 设置最低基础库版本
  4. 移除不可访问到的页面
  5. 移除大量未使用的样式
  6. 及时回收定时器:定时器是全局的,并不是跟页面绑定的,当小程序从一个页面路由到另一个页面之后,前一个页面定时器应注意手动回收


转载:https://www.eyoucms.com/wxmini/doc/course/25179.html