微信小程序开发总结

最近做了一个投票的微信小程序,开发过程主要还是参考官方文档:https://mp.weixin.qq.com/debug/wxadoc/dev/ 由于一直是做 Android 开发的,所以写小程序界面时还需要大概看一下前端的东西,对于这些东西微信自己也做了一些封装,但总体来说差别不大,这里进行一下总结。

注册并创建项目

登陆 https://mp.weixin.qq.com 进行注册,获取 AppID并在设置中进行一些配置(如服务器域名),下载小程序的开发工具,扫描二维码登陆,创建新项目。在创建项目时可以选择创建一个 quick start 项目,这样会自动生成一个简单的 demo,有助于我们了解项目的结构和组成。

但是请注意,坑爹的小程序是不支持个人用户注册的,所以如果不是公司项目,而只是个人想体验一下,要么在创建项目时选择无 AppID,但这样就只能在开发工具的模拟器上运行,不能在手机上运行也不能发布,要么参考 https://zhuanlan.zhihu.com/p/24810538 这篇文章钻个漏洞获取一个AppID,当然还是没办法发布,但至少能在手机上运行了。

框架

小程序的框架分为视图层(View)和逻辑层(App Service),它提供了自己的视图层描述语言 WXML 和 WXSS,以及基于 JavaScript 的逻辑层框架,并在视图层与逻辑层间提供了数据传输和事件系统。

这里的 WXML 和 WXSS 类似于前端的 HTML 和 CSS,但是 WXML 只能使用微信自己定义的组件而不能使用 HTML 里面的标签,WXSS 则和 CSS 无太大差别。

框架的核心是一个响应的数据绑定系统,也就是说当做数据修改的时候,只需要在逻辑层修改数据,视图层就会做相应的更新。

代码结构

图片.png

上图是我做的投票小程序里面的代码结构:
1、一个小程序主体部分由 app.js、app.json、app.wxss 三个文件组成,必须放在项目的根目录,分别是整个程序的逻辑、全局配置及样式。

(1) app.js 是小程序的脚本代码。通过App()函数用来注册一个小程序,接受一个 Object 参数,指定小程序的生命周期函数等,如下图所示。

图片.png

类似于 Android 中 的 Application,我们可以在这个文件中监听并处理小程序的生命周期函数、声明全局变量。
其他地方使用时通过var app = getApp()即可获取其实例,并调用其中定义的方法和变量,但不要调用生命周期的方法。在App()的外面还可以另外定义 function 和变量,但只能在本文件内使用。

(2) app.json 是对整个小程序的全局配置。我们可以在这个文件中配置小程序是由哪些页面组成,配置小程序的窗口背景色,配置导航条样式,配置默认标题。

图片.png

pages 指定了小程序的组成页面,第一个代表小程序的初始页面。
window 用于设置小程序的状态栏、导航条、标题、窗口背景色。
tabBar 用于配置客户端窗口的底部或顶部 tab 栏的样式以及 tab 切换时显示的对应页面。
另外还可以配置各种网络请求的超时时间networkTimeout和是否开启调试模式debug

(3) app.wxss 是整个小程序的公共样式表。可以配置一些通用的样式。

图片.png

2、pages 里面则是小程序的各个页面,其中 index 一般作为主界面(当然这并不是由名字决定的,而是在app.json里面配置的第一个page),可以看到,一个界面由 wxml、wxss、js、json 等四个文件组成,分别是页面的逻辑、界面结构,样式以及配置。小程序规定这四个文件必须具有相同的路径和名字。

(1) js 是页面的脚本代码。通过Page()函数用来注册一个页面。接受一个 Object 参数,其指定页面的初始数据、生命周期函数、事件处理函数等,如下图所示。

图片.png

其中data定义了页面的初始数据,会以 JSON 的形式由逻辑层传至渲染层,所以其数据必须是可以转成 JSON 的格式:字符串,数字,布尔值,对象,数组。渲染层可以通过 WXML 对数据进行绑定。
onLoad、onShow、onReady、onHide、onUnload 是页面的生命周期函数,分别在页面加载、显示、初次渲染完成、隐藏和卸载时调用。其中onLoadonReady只会在页面加载时调用一次,onShow则每次显示页面都会调用一次。
onPullDownRefresh用于监听用户下拉刷新事件,需要在 json 配置文件中开启enablePullDownRefresh。当处理完数据刷新后,wx.stopPullDownRefresh可以停止当前页面的下拉刷新。
onShareAppMessage只有定义了该方法才会在微信的右上角菜单显示分享按钮,需要 return 一个 Object,用于自定义分享内容,包括title标题和path分享的页面的完整路径。
viewTap是事件处理函数,函数名是自己取的,在渲染层可以在组件中加入事件绑定<view bindtap="viewTap"> click me </view>,当达到触发事件时,就会执行 Page 中定义的事件处理函数。
当需要改变data中的数据时,不能直接修改this.data,而需要调用this.setData()方法进行修改。
Page()的外面同样可以另外定义 function 和变量,也只能在本文件内使用。

(2) wxml 是页面的布局文件,只能使用微信自己定义的组件。https://mp.weixin.qq.com/debug/wxadoc/dev/component/ 这里是微信提供的所有组件的列表和属性,其中使用得最多的是 view 以及一些表单组件如 button 等等。具体的布局方式跟HTML差不多,这里不再多说。

(3) wxss 是样式表,具有 CSS 大部分特性,并进行了特性扩展,主要包括:
① 尺寸单位:rpx(responsive pixel)可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。
② 样式导入:使用@import语句可以导入外联样式表,@import后跟需要导入的外联样式表的相对路径,用;表示语句结束。例如:

/** common.wxss **/
.small-p {
  padding:5px;
}

/** app.wxss **/
@import "common.wxss";
.middle-p {
  padding:15px;
}

定义在 app.wxss 中的样式为全局样式,作用于每一个页面。在 page 的 wxss 文件中定义的样式为局部样式,只作用在对应的页面,并会覆盖 app.wxss 中相同的选择器。

(4) json 是页面的配置文件,页面的配置比app.json全局配置简单得多,只是设置 app.json 中的 window 配置项的内容,页面中配置项会覆盖 app.json 的 window 中相同的配置项,无需写 window 这个键。上面说的开启下拉刷新功能就需要在这个文件里面进行配置:

图片.png

由于是 json 格式的文件,即使不需要配置任何东西也需要写{},否则会报错。

3、utils 里面包含了一些将公共的代码抽离出来的 js 文件,作为一个模块可以方便被任何地方使用。模块只有通过 module.exports 才能对外暴露接口。下图即为utils/util.js文件中的内容,包含了一个formatDate的方法,并将方法 exports 给外部使用。

图片.png

在其他地方使用时需要通过var utils = require('../../utils/util.js');进行引用,之后就可以通过变量 utils 调用 util.js 文件中定义的方法。

4、images 里面则放了一些图片资源。

数据绑定

WXML 中的动态数据均来自对应 Page 的 data。数据绑定使用双大括号将变量包起来,可以作用于内容、组件属性(需要在双引号之内)、控制属性(需要在双引号之内)、关键字(需要在双引号之内)。
例如:

Page({
    data: {
        message: "Hello",
        id: 0,
        condition: true
    }
})
<view> {{message}} </view>
<view id="item-{{id}}"> </view>
<view wx:if="{{condition}}"> </view>
<checkbox checked="{{condition}}"> </checkbox>

还可以在 {{}} 内进行简单的运算,如:

Page({
  data: {
    flag: true,
    a: 1, 
    b: 2,
    c: 3
    length: 6,
    name: 'MINA',
    object: {
      key: 'Hello '
    },
    array: ['MINA']
  }
})
<view hidden="{{flag ? true : false}}"> Hidden </view>
<view> {{a + b}} + {{c}} + d </view> // 结果为3 + 3 + d
<view wx:if="{{length > 5}}"> </view>
<view>{{"hello" + name}}</view>
<view>{{object.key}} {{array[0]}}</view>

条件渲染

在框架中,我们用 wx:if="{{condition}}" 来判断是否需要渲染该代码块:

<view wx:if="{{condition}}"> True </view>

也可以用 wx:elif 和 wx:else 来添加一个 else 块:

<view wx:if="{{length > 5}}"> 1 </view>
<view wx:elif="{{length > 2}}"> 2 </view>
<view wx:else> 3 </view>

因为 wx:if 是一个控制属性,需要将它添加到一个标签上。但是如果我们想一次性判断多个组件标签,我们可以使用一个 <block/> 标签将多个组件包装起来,并在上边使用 wx:if 控制属性。

<block wx:if="{{true}}">
  <view> view1 </view>
  <view> view2 </view>
</block>

注意: <block/> 并不是一个组件,它仅仅是一个包装元素,不会在页面中做任何渲染,只接受控制属性。

一般来说,wx:if 有更高的切换消耗而 hidden 有更高的初始渲染消耗。因此,如果需要频繁切换的情景下,用 hidden 更好,如果在运行时条件不大可能改变则 wx:if 较好。

列表渲染

在组件上使用wx:for控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件。
默认数组的当前项的下标变量名默认为index,数组当前项的变量名默认为item

<view wx:for="{{array}}">
  {{index}}: {{item.message}}
</view>

Page({
  data: {
    array: [{
      message: 'foo',
    }, {
      message: 'bar'
    }]
  }
})

使用 wx:for-item 可以指定数组当前元素的变量名,使用 wx:for-index 可以指定数组当前下标的变量名:

<view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName">
  {{idx}}: {{itemName.message}}
</view>

wx:for也可以嵌套,下边是一个九九乘法表

<view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="i">
  <view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="j">
    <view wx:if="{{i <= j}}">
      {{i}} * {{j}} = {{i * j}}
    </view>
  </view>
</view>

类似block wx:if,也可以将wx:for用在<block/>标签上,以渲染一个包含多节点的结构块。例如:

<block wx:for="{{[1, 2, 3]}}">
  <view> {{index}}: </view>
  <view> {{item}} </view>
</block>

如果列表中项目的位置会动态改变或者有新的项目添加到列表中,并且希望列表中的项目保持自己的特征和状态(如 <input/> 中的输入内容,<switch/> 的选中状态),需要使用 wx:key 来指定列表中项目的唯一的标识符。wx:key 的值以两种形式提供:
① 字符串,代表在 for 循环的 array 中 item 的某个 property,该 property 的值需要是列表中唯一的字符串或数字,且不能动态改变。
② 保留关键字 *this 代表在 for 循环中的 item 本身,这种表示需要 item 本身是一个唯一的字符串或者数字。

Page({
  data: {
    objectArray: [
      {id: 0, unique: 'unique_0'},
      {id: 1, unique: 'unique_1'},
      {id: 2, unique: 'unique_2'},
    ],
    numberArray: [1, 2, 3, 4]
  }
})
<switch wx:for="{{objectArray}}" wx:key="unique" style="display: block;"> {{item.id}} </switch>
<switch wx:for="{{numberArray}}" wx:key="*this" style="display: block;"> {{item}} </switch>

事件

上面简单提到过 <view> 的事件绑定,通常在组件中绑定一个事件处理函数,在相应的Page定义中写上相应的事件处理函数,参数是event。

事件分为冒泡事件(当一个组件上的事件被触发后,该事件会向父节点传递)和非冒泡事件(当一个组件上的事件被触发后,该事件不会向父节点传递)。
冒泡事件有:touchstart(手指触摸动作开始)、touchmove(手指触摸后移动)、touchcancel(手指触摸动作被打断,如来电提醒,弹窗)、touchend(手指触摸动作结束)、tap(手指触摸后马上离开)、longtap(手指触摸后,超过350ms再离开)。
除此之外其他组件自定义事件如无特殊申明都是非冒泡事件,如<form/>的submit事件,<input/>的input事件,<scroll-view/>的scroll事件。

事件绑定的写法同组件的属性,以 key、value 的形式。key 以 bind 或 catch 开头事件的类型结尾,如bindtap, catchtouchstart,value 是一个字符串,需要在对应的 Page 中定义同名的函数。
bind 事件绑定不会阻止冒泡事件向上冒泡,catch 事件绑定可以阻止冒泡事件向上冒泡。

当组件触发事件时,逻辑层绑定该事件的处理函数会收到一个事件对象。
BaseEvent 基础事件对象的属性包括type(事件类型)、timeStamp(事件生成时的时间戳)、target(触发事件的组件的一些属性值集合)、currentTarget(当前组件的一些属性值集合)。
CustomEvent 自定义事件对象继承自BaseEvent,并增加了 detail(额外的信息) 属性。
TouchEvent 触摸事件对象继承自BaseEvent,并增加了 touches(触摸事件,当前停留在屏幕中的触摸点信息的数组)和 changedTouches(触摸事件,当前变化的触摸点信息的数组) 属性。

dataset 在组件中可以定义数据,以data-开头,多个单词由连字符-连接,不能有大写(大写会自动转成小写) 如data-element-type,最终在 event.target.dataset 中会将连字符转成驼峰elementType。

登陆

小程序的登录流程如下图所示:

login.png

以下是我的登陆方法,放在 util.js 中,并传入了一个 function 的参数 cb 作为登陆成功时的回调。如果不需要回调则不传即可。通常会在程序启动时调用一次(可以在 app.js 的 onLaunch 中,也可以在 index.js 的onLoad 中),之后在任何请求中碰到 access_token 过期或无效时再调用。

function login(cb) {
  wx.login({
    success: function (res) {
      var code = res.code;
      if (code) {
        wx.getUserInfo({
          success: function (res) {
            var userInfo = res.userInfo;
            wx.setStorageSync('user', userInfo);
            wx.request({
              url: 'https://server-host/login',
              data: { 'code': code, 'user_info': userInfo },
              method: 'POST',
              success: function (res) {
                if (res.data.data && res.data.data.access_token) {
                  wx.setStorageSync('accessToken', res.data.data.access_token);
                  typeof cb == 'function' && cb();
                } else {
                  showFailedToast('服务器登陆失败,请退出后重新登录');
                }
              },
              fail: function (e) {
                showFailedToast('服务器登陆失败,请退出后重新登录');
              }
            })
          },
          fail: function (e) {
            showFailedToast('获取用户信息失败,请退出后重新登录');
          }
        })
      } else {
        showFailedToast('获取微信登录状态失败,请退出后重新登录');
      }
    },
    fail: function (e) {
      showFailedToast('微信登陆失败,请退出后重新登录');
    }
  })
}

1、微信小程序需要调用 wx.login() 进行登录,成功后会返回一个 code。
2、通过 wx.getUserInfo() 获取用户信息,虽然这个 API 不需要用到 code,但也只能在 wx.login 成功后才能调用。获取 userInfo 后,可以通过 wx.setStorageSync() 将用户信息保存到本地缓存中。
3、通过 wx.request() 请求服务器的登录api,将 code 和 userInfo 传过去,服务器会生成一个access_token(即上图中的3rd_session,命名可以按照各自的习惯) 返回,之后通过 wx.setStorageSync() 将这个 access_token 保存到本地缓存中。
4、之后在任何地方进行请求服务器的操作,都可以通过 wx.getStorageSync() 将本地保存的 access_token 取出来并作为参数带上。
5、由于 access_token 存在过期的问题,因此可以与服务器约定一个特殊的 sta,比如 -500,作为 access_token 过期的标识,在任何请求中,碰到返回的 sta 为 -500 时,就可以重新调用 login 方法,传入一个登陆成功后的回调 function 作为参数,获取新的 access_token 保存,并在登陆成功后的回调 function 中再次发送请求。代码如下:

function loadDetail(self) {
  wx.request({
    url: 'https://server-host/detail',
    data: { 'vid': self.data.vid, 'access_token': wx.getStorageSync('accessToken') },
    method: 'GET',
    success: function (res) {
      if (res.data.sta == -500) {
        utils.login(function () {
          loadDetail(self);
        })
        return;
      }
      ...
    },
    fail: function () {
      ...
  })
}

API

以上登陆过程中提到了几个比较常用的微信API,如 wx.login()、wx.getUserInfo()、wx.setStorageSync()、wx.getStorageSync()、wx.request() 等,另外还有很多的API,在 https://mp.weixin.qq.com/debug/wxadoc/dev/api/ 中写的很清楚,这里就不一一说明了,大家用的时候查看一下文档即可。

推荐阅读更多精彩内容