微信小程序-wepy学习笔记

zhaidpjava 2019-07-01

wepy文档:https://tencent.github.io/wepy/

全局安装wepy: npm i -g wepy-cli

初始化项目: wepy init standard myproject

切换到项目目录: cd myproject

安装依赖: npm install

wepy项目目录

├── dist // 打包后的文件
├── src // 开发文件目录
│ ├── components // 组件目录
│ ├── images // 图片文件目录
│ ├── mixins // 通用函数
│ ├── mocks // 模拟数据目录
│ ├── pages // 小程序单个页面目录
│ ├── store // redux存储数据(页面传值)
│ ├── app.wpy // 入口文件(配置页面的跳转)
│ ├── index.html // 模版文件
├── wepy.config.js // 配置文件

重要提醒

  1. 使用微信开发者工具-->添加项目,项目目录请选择dist目录。
  2. 微信开发者工具-->项目-->关闭ES6转ES5。 重要:漏掉此项会运行报错。
  3. 微信开发者工具-->项目-->关闭上传代码时样式自动补全。 重要:某些情况下漏掉此项也会运行报错。
  4. 微信开发者工具-->项目-->关闭代码压缩上传。 重要:开启后,会导致真机computed, props.sync 等等属性失效。(注:压缩功能可使用WePY提供的build指令代替,详见后文相关介绍以及Demo项目根目录中的wepy.config.js和package.json文件。)
  5. 本地项目根目录运行npm run dev(wepy build --watch),开启实时编译。(注:如果同时在微信开发者工具-->设置-->编辑器中勾选了文件保存时自动编译小程序,将可以实时预览,非常方便。)

开发介绍

index.template.html: 为模块文件

其中通过wepy.app创建入口文件,wepy.page创建页面文件,wepy.component创建组件文件。

app.wpy:入口文件,包含config,globalData,constructor和生命周期。在config中配置路由

  1. config:配置pages,window,tabBar。分别为页面路由配置,页面导航栏配置,页面底部栏配置。详细配置在小程序框架配置
  2. globalData: 全局参数配置,用于多个页面的公用参数。
  3. constructor: 拦截器的配置。
  4. 生命周期使用
其中如果在页面开发中需要用到async/await的话,需要在app.wpy中使用import 'wepy-async-function'加载模块,不然在编译后页面会报错,导致async/await无法使用。
// 设置路由,导航栏,底部栏
config = {
    pages: [
        'pages/main',
        'pages/admin-center'
    ],
    window: {
        backgroundTextStyle: 'light',
        navigationBarBackgroundColor: '#fff',
        navigationBarTitleText: 'WeChat',
        navigationBarTextStyle: 'black'
    },
    tabBar = {
        color: '#AEADAD',
        selectedColor: '#049BFF',
        backgroundColor: '#fff',
        borderStyle: 'black',
        list: [{
            pagePath: 'pages/index',
            text: '首页',
            "iconPath": "images/ico-home.png",
            "selectedIconPath": "images/ico-home-d.png"
        }, {
            pagePath: 'pages/admin-center',
            text: '借阅',
            "iconPath": "images/ico-setting.png",
            "selectedIconPath": "images/ico-setting-d.png"
        }]
    }
}
// 全局参数(方便后期各页面中的使用)
globalData = {
    prizeList: [],                  // 领取的奖品列表
}
// 设置拦截器,intercept为拦截器函数
constructor () {
    super()
    intercept(this)
}
// 页面加载
onLaunch(res) {
    console.log(res)
}

wpy模块中的属性

export default class Index @extends wepy.page{
    customData = {}  // 自定义数据

    customFunction () {}  //自定义方法

    onLoad () {}  // 在Page和Component共用的生命周期函数

    onShow () {}  // 只在Page中存在的页面生命周期函数

    config = {};  // 只在Page实例中存在的配置数据,对应于原生的page.json文件

    data = {};  // 页面所需数据均需在这里声明,可用于模板数据绑定

    components = {};  // 声明页面中所引用的组件,或声明组件中所引用的子组件

    mixins = [];  // 声明页面所引用的Mixin实例

    computed = {};  // 声明计算属性(详见后文介绍)

    watch = {};  // 声明数据watcher(详见后文介绍)

    methods = {};  // 声明页面wxml中标签的事件处理函数。注意,此处只用于声明页面wxml中标签的bind、catch事件,自定义方法需以自定义方法的方式声明

    events = {};  // 声明组件之间的事件处理函数
}
属性说明
config页面配置对象,对应于原生的page.json文件,类似于app.wpy中的config
components页面组件列表对象,声明页面所引入的组件列表
data页面渲染数据对象,存放可用于页面模板绑定的渲染数据
methodswxml事件处理函数对象,存放响应wxml中所捕获到的事件的函数,如bindtap、bindchange,不能用于声明自定义方法。
eventsWePY组件事件处理函数对象,存放响应组件之间通过$broadcast、$emit、$invoke所传递的事件的函数
其它小程序页面生命周期函数,如onLoad、onReady等,以及其它自定义的方法与属性
wpy中自定义的函数应当写在与methods平级的位置,不用写在methods中。

页面的跳转

页面的跳转需要先在app.wpyconfig中的pages中设置页面的路由,在页面中通过navigateTo跳转到相应页面。在wepy.page的脚本中可以通过this.$navigate({url:""})实现页面的跳转。而在wepy.component的组件中可以通过this.$parent.$navigate({url:""})wepy.navigateTo实现。

wepy.navigateTo({
  url: '/pages/info'
})

wepy中的生命周期

wepy中的生命周期的钩子函数有:onLoad,onReady,onShow,onPrefetch等,其中onReady,onShow,onPrefetch只有wepy.page中才有用。wepy.component只支持onLoad,其他都不会触发。

  1. onLoad: 页面加载完成时调用,一个页面只会调用一次。(在路由跳转的时候通过navigateTo跳转的话onload会重新执行,通过navigateBack跳转的话onload不会重新执行)
  2. onShow:页面显示的时候调用。
  3. onReady: 页面中的所有资源加载完成时调用。
  4. onPrefetch:在页面跳转时触发,用于预加载和预查询数据。
  5. onUnload: 在页面卸载时触发(通过redirectTo,switchTab,navigateBack,reLaunch会触发当前页面中的onUnload,但navigateTo不会触发)。

生命周期顺序:onPrefetch > onLoad > onShow > onReady。

onPrefetch这个生命周期是wepy中扩展的,它的触发需要通过this.$navigate及其他wepy封装的跳转方式才能实现。当设置onPrefetch后,可以在onload中设置变量来获取onPrefetch中返回的值。

案例:

onLoad (params, data) {
  data.prefetch.then((list) => {
    this.adminMath = list.chasucccnt.data.succcnt
    this.recordInfo = list.adminCenter.data.challengeRecList
    this.heightScore = list.adminCenter.data.hs
    this.hadMath = list.adminCenter.data.cc
    this.$apply()
  })
}
// chasucccnt,getAdminCenter为请求后台的函数,返回的数据结构是{data:{succcnt:0}。
async onPrefetch () {
  let chasucccnt = await this.chasucccnt()
  let adminCenter = await this.getAdminCenter()
  return new Promise((resolve, reject) => {
    resolve({
      chasucccnt: chasucccnt,
      adminCenter: adminCenter
    })
  })
}

props实现父子组件之间的传值

官方案例:

// parent.wpy
<child :title = 'parentTitle' :syncTitle.sync = 'parentTitle' :twoWayTitle = 'parentTitle'></child>
在script中的设置
data = {
    parentTitle: 'p-title'
}

// child.wpy
props = {
    // 静态传值
    title: String,

    // 父向子单向动态传值
    syncTitle: {
        type: String,
        default: 'null'
    },

    twoWayTitle: {
        type: Number,
        default: 'nothing',
        twoWay: true
    }
};

onLoad () {
    console.log(this.title); // p-title
    console.log(this.syncTitle); // p-title
    console.log(this.twoWayTitle); // p-title

    this.title = 'c-title';
    console.log(this.$parent.parentTitle); // p-title.
    this.twoWayTitle = 'two-way-title';
    this.$apply();
    console.log(this.$parent.parentTitle); // two-way-title.  --- twoWay为true时,子组件props中的属性值改变时,会同时改变父组件对应的值
    this.$parent.parentTitle = 'p-title-changed';
    this.$parent.$apply();
    console.log(this.title); // 'c-title';
    console.log(this.syncTitle); // 'p-title-changed' --- 有.sync修饰符的props属性值,当在父组件中改变时,会同时改变子组件对应的值。
}
有上案例可以知道:title为静态传值,即只有第一次有效,后面改变值后子组件中的title不会发生改变,当在属性后添加.sync后,即该属性发生改变会导致子组件中相应的值发生改变,当在子组件中的props中设置twoWay: true后,可以实现父子组件的双向绑定。

组件之间的数据通信

wepy中的通信主要采用三种方法:$broadcast, $emit, $invoke;

  1. $broadcast:父组件触发所有子组件(包含子孙级组件)事件。
  2. $emit:子组件触发所有父组件(包含祖先级组件)事件。当在父组件属性中使用.user设置自定义事件后,$emit可以用于触发自定义事件而events中声明的函数将不会再执行。(与vue中的用法不同,vue中需要在父组件中设置子组件的属性,用于在子组件中触发。而wepy中则不需要,只要在events中设置方法就可以在子组件中触发。)
  3. $invoke:页面或子组件触发另一个子组件事件。
parent.wpy
<template>
    <view>
        <children @childFun.user = 'someEvent'></children>
    </view>
</template>
<script>
export default class Parent extends wepy.page{
    data = {
        name: 'parent'
    }
    events = {
        'some-event': (p1, p2, p3, $event) => {
            //  输出为'parent receive some-event children',$event.source指向子组件。
            console.log(`${this.name} receive ${$event.name} from ${$event.source.name}`)
        }
    }
    onLoad () {
            this.$broadcast('getIndex', 1, 4)
    }
    methods = {
        someEvent (...p) {
            // 输出[1, 2, 3, _class]。
            console.log(p)
        }
    }
}
</script>

children.wpy
<script>
export default class Parent extends wepy.page{
    data = {
        name: 'children'
    }
    onLoad () {
        // this.$emit('some-event', 1, 2, 3)
        // 触发组件中的自定义事件
        this.$emit('childFun', 1, 2, 3)
    }
    events = {
        'getIndex': (...p) => {
            console.log(p)        // 输出[1, 4]
        }
    }
}
在父组件中给子组件添加属性@childFun.user = 'someEvent'后,在子组件中修改触发条件this.$emit('childFun', 1, 2, 3)


//$invoke
父组件向子组件发送事件:
使用import导入子组件后,在使用时可以直接通过
this.$invoke('子组件,必须要单引号括起来', '子组件方法名称',  param1,param2,param3.......);
子组件间发送事件:
this.$invoke('子组件的相对路径', '子组件方法名称',  param1,param2,param3.......);
子组件的相对路径的理解: 当设置'./'即当前组件,'../'为父组件,以此类推。它可以指定向哪一个组件分发内容,但只适用于简单的组件树结构,复杂的结构考虑使用redux。
在子组件中使用$emit会触发父组件及祖先组件中的相同事件。在父组件中使用$broadcast会触发子组件及子孙组件中的相同事件。其中$emit和$broadcast触发的事件设置在组件中的events中,而$invoke触发的函数与events是平级的,当在组件中已经通过components导入组件的话,$invoke中的第一个参数可以直接设置为components中的值,当设置相对路径时,即根据当前组件在整个组件树中的位置来确定路径,它可以指定向特定的组件传值,但当结构复杂时,需要嵌套的路径会比较复杂,不好维护,考虑用redux实现。

Mixins混合

  1. 默认式混合(模块中的data数据,components,events,自定义事件)

当在wepy.mixin中设置了和页面中相同的函数或变量时,以当前页面的函数和变量为主,mixin中的函数和变量会失效。官方案例:

// mixin.js
export default class TestMixin extends wepy.mixin {
    data = {
        foo: 'foo defined by page',
        bar: 'bar defined by testMix'
    };
    methods: {
    tap () {
      console.log('mix tap');
    }
  }
}
....
import wepy from 'wepy';
import TestMixin from './mixins/test';
export default class Index extends wepy.page {
    data = {
        foo: 'foo defined by index'
    };
    mixins = [TestMixin ];
    onShow() {
        console.log(this.foo); // foo defined by index
        console.log(this.bar); // bar defined by testMix
    }
}
在mixin中申明的函数可以调用引入mixin后的页面的data数据和函数。mixin中的this指向当前页面组件。
  1. 兼容式混合(生命周期)

当在mixin中设置生命周期一类的钩子函数时,会优先执行mixin中的生命周期,再执行页面中的函数。

// mixin.js
export default class TestMixin extends wepy.mixin {
    onLoad () {
        console.log(2222)
    }
}
....
import wepy from 'wepy';
import TestMixin from './mixins/test';
export default class Index extends wepy.page {
    data = {
        foo: 'foo defined by index'
    };
    mixins = [TestMixin ];
    onLoad() {
        console.log(11111); 
    }
}
结果打印为: 
2222
11111

wxs的使用,实现过滤器

在项目目录下新建wxs文件夹,在该文件夹中新增wxs文件。新增内容如下:

// 设置一个过滤器对超过10000的数字进行转化
module.exports = {
  filter: function (num) {
    if (num < 10000) {
      return num
    } else {
      var reNum = (num / 10000).toFixed(1) 
      return reNum + '万'
    }
  }
}

在页面中通过import导入该过滤器:

// template中使用过滤器,mywxs对应下方wxs中设置的key值
<view>{{mywxs.filter(mItem.playerCount)}}人</view>
.....
import mywxs from '@/wxs/fixed.wxs'
export default class Index extends wepy.page{
......
wxs = {
    mywxs: mywxs
}
.....
}
导入的wxs文件只能在template中使用,不能在js中使用

Promise和async/await的使用(脏数据的检测)

// 录音
async endVideo (event) {
  let endTime = event.timeStamp
  let startTime = this.startTime
  this.videoInfo = '2131'
  let video = await new Promise((resolve, reject) => {
    recorderManager.onStop((res) => {
      console.log('recorder stop', res)
      const { tempFilePath } = res
      resolve(tempFilePath)
    })
    recorderManager.stop()
  })
  if ((endTime - startTime) > 1000) {
    this.videoInfo = video
    this.$apply()
  }
}
当使用异步函数的时候,我们如果需要重新渲染页面的时候,需要手动调用$apply()方法,才能触发脏数据检查流程的运行。

群转发

用于实现微信小程序的分享功能,获取每个群的独立ID。在上弹窗中默认有转发按钮,可以通过使用hideShareMenu来隐藏弹窗中的转发按钮.

用法
在wpy类型文件的onload中设置
// 用于显示分享的群列表
wepy.showShareMenu({
    withShareTicket: true
})

在onShareAppMessage中设置分享的界面,在分享成功后的回调函数中通过wepy.getShareInfo获取分享群的信息。群的信息是经过加密的,需要通过后台来进行解密操作。
onShareAppMessage (res) {
    if (res.from === 'button') {
        console.log(res.target)
    }
    return {
        title: '自定义转发标题',
        path: '/pages/main',
        success: function(res) {
            let shareId = res.shareTickets[0]
            // 转发成功
            wepy.getShareInfo({
                shareTicket: shareId,
                success: (data) => {
                  var appId = '小程序的appID'
                  var encryptedData = data.encryptedData
                  var iv = data.iv
                  wepy.request({
                    url: 'http://localhost:3000/api/decode',
                    method: 'post',
                    data: {
                      appId: appId,
                      encryptedData: encryptedData,
                      iv: iv
                    },
                    success: (info) => {
                      console.log('info:' + info)
                    },
                    fail: (info) => {
                      console.log(info)
                    }
                  })
                  console.log(data)
                },
                fail: (data) => {
                  console.log(data)
                }
            })
            console.log(res)
        },
        fail: function(res) {
            // 转发失败
            console.log(res)
        }
    }
}
其中onShareAppMessage需要在wepy.page中设置才有效果,在wepy.component中设置无效果。在onShareAppMessage中的path设置的参数可以跟随?ie=值,然后在对应的页面中设置onload来获取跟随的值

微信小程序和本地接口对接,模拟请求数据

后台采用express,代码如下:

var express = require('express');
var app = express();
app.listen(3000, function () {
    console.log('listen:3000');
})

设置前后台的对接接口:

var router = express.Router();
var bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
router.get('/info', function (req, res) {
    console.log(req.query);
    console.log(req.body);
}
app.use('/api', router)     // 前台通过ajax向'/api/info'发送请求

加载请求模块(因为前台无法直接向https://api.weixin.qq.com/sns...,该域名无法添加到小程序的服务器域名中):

var request = require('request');
// 在'/info'配置的接口中使用,获取session_key。
request.get({
    uri: 'https://api.weixin.qq.com/sns/jscode2session',
    json: true,
    qs: {
        grant_type: 'authorization_code',
        appid: '小程序的appID',
        secret: '小程序的密钥',
        js_code: code
    }
}, (err, response, data) => {
    if (response.statusCode === 200) {
        console.log("[openid]", data.openid)
        console.log("[session_key]", data.session_key)
        session_key = data.session_key
        //TODO: 生成一个唯一字符串sessionid作为键,将openid和session_key作为值,存入redis,超时时间设置为2小时
        //伪代码: redisStore.set(sessionid, openid + session_key, 7200)
        res.json({
            openid: data.openid,
            session_key: data.session_key
        });
    } else {
        console.log("[error]", err)
        res.json(err)
    }
})

解密

在官方案例中下载解密需要的文件WXBizDataCrypt.js。
从案例中知道需要的参数有4个:

1. AppID(微信小程序的id,在微信小程序的后台设置中有),

2. session_key(用户登录的时候可以获取到),

3. encryptedData(需要解密的字符串,在获取的群信息中加密的字段),

4. iv(算法初始向量,在获取的群信息中有返回)。

在app.js中引入WXBizDataCrypt.js:

// 解密模块
var WXBizDataCrypt = require('./WXBizDataCrypt')

// 解密数据
router.post('/decode', function (req, res) {
    var appId = req.body.appId;
    var sessionKey = session_key;
    var encryptedData = req.body.encryptedData;
    var iv = req.body.iv;
    var pc = new WXBizDataCrypt(appId, sessionKey);
    var data = pc.decryptData(encryptedData , iv);
    res.json({data: data});
})

相关推荐