小程序开发学习笔记

参考文档:https://developers.weixin.qq.com/ebook?action=get_post_info&docid=000846df9a03909b0086a50025180a

WXML

  1. WXML中的属性是大小写敏感的

  2. 变量名是大小写敏感的

  3. 没有被定义的变量的或者是被设置为 undefined 的变量不会被同步到 wxml 中

  4. 变量支持三元语法,例如{{a === 10? "变量 a 等于10": "变量 a 不等于10"}}

  5. 变量支持算数运算,例如{{a + b}}

  6. 变量支持字符串拼接,例如{{"hello " + name}}

  7. 变量中还可以直接放置数字、字符串或者是数组

  8. 支持 wx:if="{{condition}}"wx:elifwx:else

  9. 如果要一次性判断多个组件标签,可以使用一个 <block/> 标签将多个组件包装起来,并在上边使用 wx:if 控制属性。

  10. 列表渲染使用wx:for

    <view wx:for="{{array}}">
      {{index}}: {{item.message}}
    </view>
    
  11. 使用 wx:for-item 指定数组当前元素的变量名,使用 wx:for-index 指定数组当前下标的变量名

    <view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName">
      {{idx}}: {{itemName.message}}
    </view>
    
  12. 类似 block wx:if ,也可以将 wx:for 用在 <block/> 标签上,以渲染一个包含多节点的结构块。例如:

    <block wx:for="{{[1, 2, 3]}}">
      <view> {{index}}: </view>
      <view> {{item}} </view>
    </block>
    
  13. 如果列表中项目的位置会动态改变或者有新的项目添加到列表中,并且希望列表中的项目保持自己的特征和状态(如 <input/> 中的输入内容, <switch/> 的选中状态),需要使用 wx:key 来指定列表中项目的唯一的标识符。

    <switch wx:for="{{objectArray}}" wx:key="unique" > {{item.id}} </switch>
    <switch wx:for="{{numberArray}}" wx:key="*this" > {{item}} </switch>
    

    当数据改变触发渲染层重新渲染的时候,会校正带有 key 的组件,框架会确保他们被重新排序,而不是重新创建,以确保使组件保持自身的状态,并且提高列表渲染时的效率。

  14. WXML提供模板(template),可以在模板中定义代码片段,然后在不同的地方调用。使用 name 属性,作为模板的名字。然后在 <template/> 内定义代码片段。

    <template name="msgItem">
      <view>
        <text> {{index}}: {{msg}} </text>
        <text> Time: {{time}} </text>
      </view>
    </template>
    
  15. 使用 is 属性,声明需要的使用的模板,然后将模板所需要的 data 传入。

    <template name="msgItem">
      <view>
        <text> {{index}}: {{msg}} </text>
        <text> Time: {{time}} </text>
      </view>
    </template>
    
    <template is="msgItem" data="{{...item}}"/>
    
  16. is可以动态决定具体需要渲染哪个模板。

    <template name="odd">
      <view> odd </view>
    </template>
    
    <template name="even">
      <view> even </view>
    </template>
    
    <block wx:for="{{[1, 2, 3, 4, 5]}}">
      <template is="{{item % 2 == 0 ? 'even' : 'odd'}}"/>
    </block>
    
  17. import 可以在该文件中使用目标文件定义的 template,如:

    在 item.wxml 中定义了一个叫 item的 template :

    <!-- item.wxml -->
    <template name="item">
      <text>{{text}}</text>
    </template>
    

    在 index.wxml 中引用了 item.wxml,就可以使用 item模板:

    <import src="item.wxml"/>
    
    <template is="item" data="{{text: 'forbar'}}"/>
    
  18. 需要注意的是 import 有作用域的概念,即只会 import 目标文件中定义的 template,而不会 import 目标文件中 import 的 template,简言之就是 import 不具有递归的特性。

    例如:C 引用 B,B 引用A,在C中可以使用B定义的 template,在B中可以使用A定义的 template ,但是C不能使用A定义的template。

  19. include 可以将目标文件中除了 <template/> <wxs/> 外的整个代码引入,相当于是拷贝到 include 位置

    代码清单2-22 index.wxml

    <!-- index.wxml -->
    <include src="header.wxml"/>
    
    <view> body </view>
    
    <include src="footer.wxml"/>
    

    代码清单2-23 header.wxml

    <!-- header.wxml -->
    <view> header </view>
    

    代码清单2-24 footer.wxml

    <!-- footer.wxml -->
    <view> footer </view>
    
  20. 所有wxml 标签都支持的属性称之为共同属性,如表2-1所示。

    表2-1 共同属性

    属性名类型描述注解
    idString组件的唯一标识整个页面唯一
    classString组件的样式类在对应的 WXSS 中定义的样式类
    styleString组件的内联样式可以动态设置的内联样式
    hiddenBoolean组件是否显示所有组件默认显示
    data-*Any自定义属性组件上触发的事件时,会发送给事件处理函数
    bind*/catch*EventHandler组件的事件

WXSS

  1. 根目录中的app.wxss为项目公共样式,它会被注入到小程序的每个页面。

  2. 与app.json注册过的页面同名且位置同级的WXSS文件是页面样式。

  3. 在CSS中,开发者可以这样引用另一个样式文件:@import url('./test_0.css'),这种方法在请求上不会把test_0.css合并到index.css中,也就是请求index.css的时候,会多一个test_0.css的请求。

  4. 在小程序中,我们依然可以实现样式的引用,样式引用是这样写:

    @import './test_0.wxss'
    

    由于WXSS最终会被编译打包到目标文件中,用户只需要下载一次,在使用过程中不会因为样式的引用而产生多余的文件请求。

  5. WXSS内联样式与Web开发一致:

    <!--index.wxml-->
    
    <!--内联样式-->
    <view style="color: red; font-size: 48rpx"></view>
    

    小程序支持动态更新内联样式:

    <!--index.wxml-->
    
    <!--可动态变化的内联样式-->
    <!--
    {
      eleColor: 'red',
      eleFontsize: '48rpx'
    }
    -->
    <view style="color: {{eleColor}}; font-size: {{eleFontsize}}"></view>
    
  6. 目前支持的选择器如表2-2所示。

    表2-2 小程序WXSS支持的选择器

    类型选择器样例样例描述
    类选择器.class.intro选择所有拥有 class="intro" 的组件
    id选择器#id#firstname选择拥有 id="firstname" 的组件
    元素选择器elementview checkbox选择所有文档的 view 组件和所有的 checkbox 组件
    伪元素选择器::afterview::after在 view 组件后边插入内容
    伪元素选择器::beforeview::before在 view 组件前边插入内容
  7. WXSS优先级与CSS类似,权重判断如下:

    element <.class<#id<style=""<!important

    view{ // 权重为 1
      color: blue
    }
    
    .ele{ // 权重为 10
      color: red
    }
    
    #ele{ // 权重为 100
      color: pink
    }
    
    view#ele{ // 权重为 1 + 100 = 101,优先级最高,元素颜色为orange
      color: orange
    }
    
    view.ele{ // 权重为 1 + 10 = 11
      color: green
    }
    
  8. 可以使用官方源生样式库,保证和微信源生视觉体验一致。https://github.com/Tencent/weui-wxss

Javascript

  1. 小程序中无法加载原生库,也无法直接使用大部分的 NPM 包。

  2. 不同的平台的小程序的脚本执行环境也是有所区别的。

  3. 小程序中, 不同的平台的小程序的脚本执行环境也是有所区别的。例如,iOS9和iOS10 所使用的运行环境并没有完全的兼容到 ECMAScript 6 标准,有些代码在旧的手机操作系统上出现一些语法错误。为了帮助开发者解决这类问题,小程序IDE提供语法转码工具帮助开发者,将 ECMAScript 6代码转为 ECMAScript 5代码。开发者需要在项目设置中,勾选 ES6 转 ES5 开启此功能。

  4. 浏览器中,所有 JavaScript 是在运行在同一个作用域下的,定义的参数或者方法可以被后续加载的脚本访问或者改写。同浏览器不同,小程序中可以将任何一个JavaScript 文件作为一个模块,通过module.exports 或者 exports 对外暴露接口。

    请看是一个简单模块示例,B.js 引用模块A,并使用A暴露的multiplyBy2方法完成一个变量乘以 2 的操作。

    代码清单2-26 模块示例

    // moduleA.js
    module.exports = function( value ){
      return value * 2;
    }
    

    代码清单2-27 引用模块A

    // B.js
    
    // 在B.js中引用模块A
    var multiplyBy2 = require('./moduleA')
    var result = multiplyBy2(4)
    

    代码清单2-28 在需要使用这些模块的文件中,使用 require(path) 将公共代码引入

    var common = require('common.js')
    Page({
      helloMINA: function() {
        common.sayHello('MINA')
      },
      goodbyeMINA: function() {
        common.sayGoodbye('MINA')
      }
    })
    
  5. 浏览器中,脚本严格按照加载的顺序执行,而在小程序中的脚本执行顺序有所不同。小程序的执行的入口文件是 app.js 。并且会根据其中 require 的模块顺序决定文件的运行顺序。当 app.js 执行结束后,小程序会按照开发者在 app.json 中定义的 pages 的顺序,逐一执行。

  6. 作用域问题

    同浏览器中运行的脚本文件有所不同,小程序的脚本的作用域同 NodeJS 更为相似。

    在文件中声明的变量和函数只在该文件中有效,不同的文件中可以声明相同名字的变量和函数,不会互相影响,如代码2-36、代码2-37所示。

    代码清单2-36 在脚本 a.js 中定义局部变量

    // a.js
    // 定义局部变量
    var localValue = 'a'
    

    代码清单2-37 在脚本 b.js 中无法访问 a.js 定义的变量

    // b.js
    // 定义局部变量
    console.log(localValue) // 触发一个错误 b.js中无法访问 a.js 中定义的变量
    

    当需要使用全局变量的时,通过使用全局函数 getApp() 获取全局的实例,并设置相关属性值,来达到设置全局变量的目的,如代码2-38、代码2-39所示。

    代码清单2-38 在脚本 a.js 中设置全局变量

    // a.js
    // 获取全局变量
    var global = getApp()
    global.globalValue = 'globalValue'
    

    代码清单2-39 在脚本 b.js 中访问 a.js 定义的全局变量

    // b.js
    // 访问全局变量
    var global = getApp()
    console.log(global.globalValue) // 输出 globalValue
    

    需要注意的是,上述示例只有在 a.js 比 b.js 先执行才有效,当需要保证全局的数据可以在任何文件中安全的被使用到,那么可以在 App() 中进行设置,如代码2-40、代码2-41、代码2-42所示。

    代码清单2-40 定义全局变量

    // app.js
    App({
      globalData: 1
    })
    

    代码清单2-41 获取以及修改 global 变量的方法

    // a.js
    // 局部变量
    var localValue = 'a'
    
    // 获取 global 变量
    var app = getApp()
    
    // 修改 global 变量
    app.globalData++  // 执行后 globalData 数值为 2
    

    代码清单2-42 获取 global 变量

    // b.js
    // 定义另外的局部变量,并不会影响 a.js 中文件变量
    var localValue = 'b'
    
    // 如果先执行了 a.js 这里的输出应该是 2
    console.log(getApp().globalData)
    

小程序的宿主环境

  1. 小程序是通过数据驱动的,和vue类似。

  2. WXML结构实际上等价于一棵Dom树,WXML可以先转成JS对象,然后再渲染出真正的Dom树。

  3. 小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面,

  4. 宿主环境提供了 App() 构造器用来注册一个程序App,需要留意的是App() 构造器必须写在项目根目录的app.js里,App实例是单例对象,在其他JS脚本中可以使用宿主环境提供的 getApp() 来获取程序实例。

    代码清单3-3 getApp() 获取App实例

    // other.js
    var appInstance = getApp()
    

    App() 的调用方式如代码清单3-4所示,App构造器接受一个Object参数,参数说明如表3-1所示,其中onLaunch / onShow / onHide 三个回调是App实例的生命周期函数,我们会在后文展开;onError我们暂时不在本章展开,我们会在第8章里详细讨论;App的其他参数我们也放在后文进行展开。
    代码清单3-4 App构造器

    App({
      onLaunch: function(options) {},
      onShow: function(options) {},
      onHide: function() {},
      onError: function(msg) {},
      globalData: 'I am global data'
    })
    

    表3-1 App构造器的参数

    参数属性类型描述
    onLaunchFunction当小程序初始化完成时,会触发 onLaunch(全局只触发一次)
    onShowFunction当小程序启动,或从后台进入前台显示,会触发 onShow
    onHideFunction当小程序从前台进入后台,会触发 onHide
    onErrorFunction当小程序发生脚本错误,或者 API 调用失败时,会触发 onError 并带上错误信息
    其他字段任意可以添加任意的函数或数据到 Object 参数中,在App实例回调用 this 可以访问
  5. 为了避免程序上的混乱,我们不应该从其他代码里主动调用App实例的生命周期函数。

  6. 在微信客户端中打开小程序有很多途径:从群聊会话里打开,从小程序列表中打开,通过微信扫一扫二维码打开,从另外一个小程序打开当前小程序等,针对不同途径的打开方式,小程序有时需要做不同的业务处理,所以微信客户端会把打开方式带给onLaunch和onShow的调用参数options.

    代码清单3-5 onLaunch和onShow带参数

    App({
      onLaunch: function(options) { console.log(options) },
      onShow: function(options) { console.log(options) }
    })
    

    详细文档:https://mp.weixin.qq.com/debug/wxadoc/dev/framework/app-service/app.html

    表3-2 onLaunch,onShow参数

    字段类型描述
    pathString打开小程序的页面路径
    queryObject打开小程序的页面参数query
    sceneNumber打开小程序的场景值,详细场景值请参考小程序官方文档
    shareTicketStringshareTicket,详见小程序官方文档
    referrerInfoObject当场景为由从另一个小程序或公众号或App打开时,返回此字段
    referrerInfo.appIdString来源小程序或公众号或App的 appId,详见下方说明
    referrerInfo.extraDataObject来源小程序传过来的数据,scene=1037或1038时支持

    表3-3 以下场景支持返回 referrerInfo.appId

    场景值场景appId信息含义
    1020公众号 profile页相关小程序列表 返回来源公众号 appId
    1035公众号自定义菜单返回来源公众号 appId
    1036App 分享消息卡片返回来源应用 appId
    1037小程序打开小程序返回来源小程序 appId
    1038从另一个小程序返回返回来源小程序 appId
    1043公众号模板消息返回来源公众号 appId
  7. 在小程序切换页面时,小程序逻辑层的JS脚本运行上下文依旧在同一个JsCore线程中。

  8. 不同页面直接可以通过App实例下的属性来共享数据。App构造器可以传递其他参数作为全局属性以达到全局共享数据的目的。

    代码清单3-6 小程序全局共享数据

    // app.js
    App({
      globalData: 'I am global data' // 全局共享数据
    })
    // 其他页面脚本other.js
    var appInstance = getApp()
    console.log(appInstance.globalData) // 输出: I am global data
    
  9. 要特别留意一点,所有页面的脚本逻辑都跑在同一个JsCore线程,页面使用setTimeout或者setInterval的定时器,然后跳转到其他页面时,这些定时器并没有被清除,需要开发者自己在页面离开的时候进行清理。

  10. 一个页面是分三部分组成:界面、配置和逻辑。界面由WXML文件和WXSS文件来负责描述,配置由JSON文件进行描述,页面逻辑则是由JS脚本文件负责。

  11. 一个页面的文件需要放置在同一个目录下,其中WXML文件和JS文件是必须存在的,JSON和WXSS文件是可选的。

  12. 页面路径需要在小程序代码根目录app.json中的pages字段声明,否则这个页面不会被注册到宿主环境中。

  13. 页面路径需要在小程序代码根目录app.json中的pages字段声明,否则这个页面不会被注册到宿主环境中。

    代码清单3-7 app.json声明页面路径

    {
      "pages":[
        "pages/index/page", // 第一项默认为首页
        "pages/other/other"
      ]
    }
    
  14. 宿主环境提供了 Page() 构造器用来注册一个小程序页面,Page()在页面脚本page.js中调用,Page() 的调用方式如代码清单3-8所示。Page构造器接受一个Object参数,参数说明如表3-4所示,其中data属性是当前页面WXML模板中可以用来做数据绑定的初始数据。

    代码清单3-8 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() { }
    })
    

    onLoad / onReady / onShow / onHide /onUnload 5个回调是Page实例的生命周期函数;

    onPullDownRefresh / onReachBottom / onShareAppMessage / onPageScroll 4个回调是页面的用户行为。

    表3-4 Page构造器的参数

    参数属性类型描述
    dataObject页面的初始数据
    onLoadFunction生命周期函数--监听页面加载,触发时机早于onShow和onReady
    onReadyFunction生命周期函数--监听页面初次渲染完成
    onShowFunction生命周期函数--监听页面显示,触发事件早于onReady
    onHideFunction生命周期函数--监听页面隐藏
    onUnloadFunction生命周期函数--监听页面卸载
    onPullDownRefreshFunction页面相关事件处理函数--监听用户下拉动作
    onReachBottomFunction页面上拉触底事件的处理函数
    onShareAppMessageFunction用户点击右上角转发
    onPageScrollFunction页面滚动触发事件的处理函数
    其他Any可以添加任意的函数或数据,在Page实例的其他函数中用 this 可以访问
  15. 页面的生命周期和打开参数:

    • 页面初次加载的时候,微信客户端就会给Page实例派发onLoad事件,Page构造器参数所定义的onLoad方法会被调用,onLoad在页面没被销毁之前只会触发1次,在onLoad的回调中,可以获取当前页面所调用的打开参数option,关于打开参数我们放在这一节的最后再展开阐述。
    • 页面显示之后,Page构造器参数所定义的onShow方法会被调用,一般从别的页面返回到当前页面时,当前页的onShow方法都会被调用。
    • 在页面初次渲染完成时,Page构造器参数所定义的onReady方法会被调用,onReady在页面没被销毁前只会触发1次,onReady触发时,表示页面已经准备妥当,在逻辑层就可以和视图层进行交互了。
    • 以上三个事件触发的时机是onLoad早于 onShow,onShow早于onReady。
    • 页面不可见时,Page构造器参数所定义的onHide方法会被调用,这种情况会在使用wx.navigateTo切换到其他页面、底部tab切换时触发。
    • 当前页面使用wx.redirectTo或wx.navigateBack返回到其他页时,当前页面会被微信客户端销毁回收,此时Page构造器参数所定义的onUnload方法会被调用。
    • 我们可以看到,Page的生命周期是由微信客户端根据用户操作主动触发的。为了避免程序上的混乱,我们不应该在其他代码中主动调用Page实例的生命周期函数。
  16. 页面之间传递参数

    代码清单3-9 页面的打开参数Page构造器

    // pages/list/list.js
    // 列表页使用navigateTo跳转到详情页
    wx.navigateTo({ url: 'pages/detail/detail?id=1&other=abc' })
    
    // pages/detail/detail.js
    Page({
      onLoad: function(option) {
            console.log(option.id)
            console.log(option.other)
      }
    })
    

    小程序把页面的打开路径定义成页面URL,其组成格式和网页的URL类似,在页面路径后使用英文 ? 分隔path和query部分,query部分的多个参数使用 & 进行分隔,参数的名字和值使用 key=value 的形式声明。在页面Page构造器里onLoad的option可以拿到当前页面的打开参数,其类型是一个Object,其键值对与页面URL上query键值对一一对应。和网页URL一样,页面URL上的value如果涉及特殊字符(例如:&字符、?字符、中文字符等,详情参考URI的RFC3986说明 ),需要采用UrlEncode后再拼接到页面URL上

  17. setData传递数据是一个异步的过程,setData的第二个参数是一个callback回调,在这次setData对界面渲染完毕后触发。

    // page.js
    Page({
      onLoad: function(){
        this.setData({
          text: 'change data'
        }, function(){
          // 在这次setData对界面渲染完毕后触发
        })
      }
    })
    
  18. 实际在开发的时候,页面的data数据会涉及相当多的字段,你并不需要每次都将整个data字段重新设置一遍,你只需要把改变的值进行设置即可,宿主环境会自动把新改动的字段合并到渲染层对应的字段中,我们只要保持一个原则就可以提高小程序的渲染性能:每次只设置需要改变的最小单位数据。

    // page.js
    Page({
      data: {
        a: 1, b: 2, c: 3,
        d: [1, {text: 'Hello'}, 3, 4]
      }
      onLoad: function(){
           // a需要变化时,只需要setData设置a字段即可
        this.setData({a : 2})
      }
    })
    
  19. 直接修改 Page实例的this.data 而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致。

  20. 由于setData是需要两个线程的一些通信消耗,为了提高性能,每次设置的数据不应超过1024kB。

  21. 不要把data中的任意一项的value设为undefined,否则可能会有引起一些不可预料的bug。

  22. 页面用户行为

    小程序宿主环境提供了四个和页面相关的用户行为回调:

    1. 下拉刷新 onPullDownRefresh
      监听用户下拉刷新事件,需要在app.json的window选项中或页面配置page.json中设置enablePullDownRefresh为true。当处理完数据刷新后,wx.stopPullDownRefresh可以停止当前页面的下拉刷新。

    2. 上拉触底 onReachBottom
      监听用户上拉触底事件。可以在app.json的window选项中或页面配置page.json中设置触发距离onReachBottomDistance。在触发距离内滑动期间,本事件只会被触发一次。

    3. 页面滚动 onPageScroll
      监听用户滑动页面事件,参数为 Object,包含 scrollTop 字段,表示页面在垂直方向已滚动的距离(单位px)。

    4. 用户转发 onShareAppMessage
      只有定义了此事件处理函数,右上角菜单才会显示“转发”按钮,在用户点击转发按钮的时候会调用,此事件需要return一个Object,包含title和path两个字段,用于自定义转发内容,如代码清单3-13所示。

      代码清单3-13 使用onShareAppMessage自定义转发字段

      // page.js
      Page({
      onShareAppMessage: function () {
       return {
         title: '自定义转发标题',
         path: '/page/user?id=123'
       }
      }
      })
      
  23. 一个小程序拥有多个页面,我们可以通过wx.navigateTo推入一个新的页面,小程序宿主环境限制了这个页面栈的最大层级为10层 ,也就是当页面栈到达10层之后就没有办法再推入新的页面了。后续为了表述方便,我们采用这样的方式进行描述页面栈:[ pageA, pageB, pageC ],其中pageA在最底下,pageC在最顶上。

    使用 wx.navigateTo({ url: 'pageD' }) 可以往当前页面栈多推入一个 pageD,此时页面栈变成 [ pageA, pageB, pageC, pageD ]。
    使用 wx.navigateBack() 可以退出当前页面栈的最顶上页面,此时页面栈变成 [ pageA, pageB, pageC ]。
    使用wx.redirectTo({ url: 'pageE' }) 是替换当前页变成pageE,此时页面栈变成 [ pageA, pageB, pageE ],当页面栈到达10层没法再新增的时候,往往就是使用redirectTo这个API进行页面跳转

  24. 小程序提供了原生的Tabbar支持,我们可以在app.json声明tabBar字段来定义Tabbar页。

    代码清单3-14 app.json定义小程序底部tab

    {
      "tabBar": {
        "list": [
          { "text": "Tab1", "pagePath": "pageA" },
          { "text": "Tab1", "pagePath": "pageF" },
          { "text": "Tab1", "pagePath": "pageG" }
        ]
      }
    }
    
  25. 我们可以在刚刚的例子所在的页面栈中使用wx.switchTab({ url: 'pageF' }),此时原来的页面栈会被清空(除了已经声明为Tabbar页pageA外其他页面会被销毁),然后会切到pageF所在的tab页面,页面栈变成 [ pageF ],此时点击Tab1切回到pageA时,pageA不会再触发onLoad,因为pageA没有被销毁。

  26. wx.navigateTo和wx.redirectTo只能打开非TabBar页面,wx.switchTab只能打开Tabbar页面。

  27. 我们还可以使用 wx. reLaunch({ url: 'pageH' }) 重启小程序,并且打开pageH,此时页面栈为 [ pageH ]。

  28. 表3-5 页面路由触发方式及页面生命周期函数的对应关系

    路由方式触发时机路由前页面生命周期路由后页面生命周期
    初始化小程序打开的第一个页面 onLoad, onShow
    打开新页面 调用API wx.navigateToonHideonLoad, onShow
    页面重定向 调用API wx.redirectToonUnloadonLoad, onShow
    页面返回 调用API wx.navigateBackonUnloadonShow
    Tab切换 调用 API wx.switchTab请参考表3-6请参考表3-6
    重启动调用 API wx.reLaunchonUnloadonLoad, onShow

    Tab 切换对应的生命周期(以 A、B 页面为 Tabbar 页面,C 是从 A 页面打开的页面,D 页面是从 C 页面打开的页面为例)如表3-6所示,注意Tabbar页面初始化之后不会被销毁。

    表3-6 页面路由触发方式及页面生命周期函数的对应关系

    当前页面路由后页面触发的生命周期(按顺序)
    AA
    ABA.onHide(), B.onLoad(), B.onShow()
    AB(再次打开)A.onHide(), B.onShow()
    CAC.onUnload(), A.onShow()
    CBC.onUnload(), B.onLoad(), B.onShow()
    DBD.onUnload(), C.onUnload(), B.onLoad(), B.onShow()
    D(从转发进入)AD.onUnload(), A.onLoad(), A.onShow()
    D(从转发进入)BD.onUnload(), B.onLoad(), B.onShow()
  29. 小程序使用标签名来引用一个组件,通常包含开始标签和结束标签,该标签的属性用来描述该组件,需要注意,所有组件名和属性都是小写,多个单词会以英文横杠 "-" 进行连接

    <!-- page.wxml -->
    <image mode="scaleToFill" src="img.png"></image>
    
  30. 对于一些容器组件,其内容可以声明在其开始标签和结束标签之间。
    代码清单3-16 容器组件嵌套其他组件

    <!-- page.wxml -->
    <view>
      <image mode="scaleToFill" src="img.png"></image>
      <view>
        <view>1</view>
        <view>2</view>
        <view>3</view>
      </view>
    </view>
    

    所有组件属性说明:https://mp.weixin.qq.com/debug/wxadoc/dev/component/

  31. API一般调用的约定:

    1. wx.on* 开头的 API 是监听某个事件发生的API接口,接受一个 Callback 函数作为参数。当该事件触发时,会调用 Callback 函数。
    2. 如未特殊约定,多数 API 接口为异步接口 ,都接受一个Object作为参数
    3. API的Object参数一般由success、fail、complete三个回调来接收接口调用结果,示例代码如代码清单3-17所示,详细说明如表3-9所示。
    4. wx.get* 开头的API是获取宿主环境数据的接口。
    5. wx.set* 开头的API是写入数据到宿主环境的接口。
  32. 通过wx.request发起网络请求

    wx.request({
    url: 'test.php',
    data: {},
    header: { 'content-type': 'application/json' },
    success: function(res) {
     // 收到https服务成功后返回
     console.log(res.data)
    },
    fail: function() {
     // 发生网络错误等情况触发
    },
    complete: function() {
     // 成功或者失败后触发
    }
    })
    
  33. 有部分API会拉起微信的原生界面,此时会触发Page的onHide方法,当用户从原生界面返回到小程序时,会触发Page的onShow方法。官方API文档地址:https://mp.weixin.qq.com/debug/wxadoc/dev/api/

  34. 表3-10 常见的事件类型

    类型触发条件
    touchstart手指触摸动作开始
    touchmove手指触摸后移动
    touchcancel手指触摸动作被打断,如来电提醒,弹窗
    touchend手指触摸动作结束
    tap手指触摸后马上离开
    longpress手指触摸后,超过350ms再离开,如果指定了事件回调函数并触发了这个事件,tap事件将不被触发
    longtap手指触摸后,超过350ms再离开(推荐使用longpress事件代替)
    transitionend会在 WXSS transition 或 wx.createAnimation 动画结束后触发
    animationstart会在一个 WXSS animation 动画开始时触发
    animationiteration会在一个 WXSS animation 一次迭代结束时触发
    animationend会在一个 WXSS animation 动画完成时触发
  35. 当事件回调触发的时候,会收到一个事件对象,对象的详细属性如下表所示。

    表3-11 事件对象属性

    属性类型说明
    typeString事件类型
    timeStampInteger页面打开到触发事件所经过的毫秒数
    targetObject触发事件的组件的一些属性值集合
    currentTargetObject当前组件的一些属性值集合
    detailObject额外的信息
    touchesArray触摸事件,当前停留在屏幕中的触摸点信息的数组
    changedTouchesArray触摸事件,当前变化的触摸点信息的数组
  36. target和currentTarget的区别,currentTarget为当前事件所绑定的组件,而target则是触发该事件的源头组件。

    代码清单3-19 事件对象示例

    <!-- page.wxml -->
    <view id="outer" catchtap="handleTap">
      <view id="inner">点击我</view>
    </view>
    // page.js
    Page({
      handleTap: function(evt) {
           // 当点击inner节点时
           // evt.target 是inner view组件
           // evt.currentTarget 是绑定了handleTap的outer view组件
           // evt.type == “tap”
           // evt.timeStamp == 1542
           // evt.detail == {x: 270, y: 63}
           // evt.touches == [{identifier: 0, pageX: 270, pageY: 63, clientX: 270, clientY: 63}]
           // evt.changedTouches == [{identifier: 0, pageX: 270, pageY: 63, clientX: 270, clientY: 63}]
      }
    })
    
  37. 关于target和currentTarget对象的详细参数如表3-12所示。

    表3-12 target和currentTarget事件对象属性

    属性类型说明
    idString当前组件的id
    tagNameString当前组件的类型
    datasetObject当前组件上由data-开头的自定义属性组成的集合
  38. 关于touch和changedTouches对象的详细参数如表3-13所示。

    表3-13 touch和changedTouches对象属性

    属性类型说明
    identifierNumber触摸点的标识符
    pageX, pageYNumber距离文档左上角的距离,文档的左上角为原点 ,横向为X轴,纵向为Y轴
    clientX, clientYNumber距离页面可显示区域(屏幕除去导航条)左上角距离,横向为X轴,纵向为Y轴
  39. 事件绑定的写法和组件属性一致,以key="value"的形式,其中:

    1. key以bind或者catch开头,然后跟上事件的类型,如bindtap、catchtouchstart。自基础库版本1.5.0起,bind和catch后可以紧跟一个冒号,其含义不变,如bind:tap、catch:touchstart。同时bind和catch前还可以加上capture-来表示捕获阶段。

    2. value是一个字符串,需要在对应的页面Page构造器中定义同名的函数,否则触发事件时在控制台会有报错信息。

      bind和capture-bind的含义分别代表事件的冒泡阶段和捕获阶段,其触发的顺序如图3-8所示。

      img

  40. 以下示例中,点击 inner view 会先后调用handleTap2、handleTap4、handleTap3、handleTap1。

    代码清单3-20 事件的冒泡和捕获

    <view
      id="outer"
      bind:touchstart="handleTap1"
      capture-bind:touchstart="handleTap2"
    >
       outer view
      <view
        id="inner"
        bind:touchstart="handleTap3"
        capture-bind:touchstart="handleTap4"
      >
       inner view
      </view>
    </view>
    

    bind事件绑定不会阻止冒泡事件向上冒泡,catch事件绑定可以阻止冒泡事件向上冒泡。如果将上面代码中的第一个capture-bind改为capture-catch,将只触发handleTap2(capture-catch将中断捕获阶段和取消冒泡阶段)

    代码清单3-21 使用catch阻止事件的传递

    <view
      id="outer"
      bind:touchstart="handleTap1"
      capture-catch:touchstart="handleTap2"
    >
      outer view
      <view
        id="inner"
        bind:touchstart="handleTap3"
        capture-bind:touchstart="handleTap4"
      >
        inner view
      </view>
    </view>
    

    注意,除表3-10列举的事件类型之外的其他组件自定义事件,如无特殊声明都是非冒泡事件,如

    的submit事件,的input事件,的scroll事件。

  41. 可以使用 wx.getSystemInfo 或者 wx.getSystemInfoSync 来获取手机品牌、操作系统版本号、微信版本号以及小程序基础库版本号等,通过这个信息,我们可以针对不同平台做差异化的服务。

    wx.getSystemInfoSync()
    /*
      {
        brand: "iPhone",      // 手机品牌
        model: "iPhone 6",    // 手机型号
        platform: "ios",      // 客户端平台
        system: "iOS 9.3.4",  // 操作系统版本
        version: "6.5.23",    // 微信版本号
        SDKVersion: "1.7.0",  // 小程序基础库版本
        language: "zh_CN",    // 微信设置的语言
        pixelRatio: 2,        // 设备像素比
        screenWidth: 667,    // 屏幕宽度
        screenHeight: 375,     // 屏幕高度
        windowWidth: 667,    // 可使用窗口宽度
        windowHeight: 375,     // 可使用窗口高度
        fontSizeSetting: 16   // 用户字体大小设置
      }
     */
    
  42. 随着宿主环境的更新,新版本的宿主环境会提供一些新的API,你可以通过判断此API是否存在来做程序上的兼容。

    代码清单3-23 通过判断API是否存在做兼容

    if (wx.openBluetoothAdapter) {
      wx.openBluetoothAdapter()
    } else {
      // 如果希望用户在最新版本的客户端上体验您的小程序,可以这样子提示
      wx.showModal({
        title: '提示',
        content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'
      })
    }
    
  43. 小程序还提供了wx.canIUse这个API,用于判断接口或者组件在当前宿主环境是否可用,其参数格式为: ${API}.${method}.${param}.${options}或者${component}.${attribute}.${option}
    各个段的含义如下:

    • $ 代表 API 名字

    • $ 代表调用方式,有效值为return, success, object, callback

    • $ 代表参数或者返回值

    • $ 代表参数的可选值

    • $ 代表组件名字

    • $ 代表组件属性

    • $ 代表组件属性的可选值
      调用的示例代码如下。

      码清单3-24 wx.canIUse调用示例

      // 判断接口及其参数在宿主环境是否可用
      wx.canIUse('openBluetoothAdapter')
      wx.canIUse('getSystemInfoSync.return.screenWidth')
      wx.canIUse('getSystemInfo.success.screenWidth')
      wx.canIUse('showToast.object.image')
      wx.canIUse('onCompassChange.callback.direction')
      wx.canIUse('request.object.method.GET')
      
       // 判断组件及其属性在宿主环境是否可用
      wx.canIUse('contact-button')
      wx.canIUse('text.selectable')
      wx.canIUse('button.open-type.contact')
      

      我们可以选择合适的判断方法来做小程序的向前兼容,以保证我们的小程序在旧版本的微信客户端也能工作正常。在不得已的情况下(小程序强依赖某个新的API或者组件时),还可以通过在小程序管理后台设置“基础库最低版本设置”来达到不向前兼容的目的。例如你选择设置你的小程序只支持1.5.0版本以上的宿主环境,那么当运行着1.4.0版本宿主环境的微信用户打开你的小程序的时候,微信客户端会显示当前小程序不可用,并且提示用户应该去升级微信客户端。

布局

  1. flex不单是一个属性,它包含了一套新的属性集。属性集包括用于设置容器,和用于设置项目两部分。

    img

  2. 设置容器的属性有:

    display:flex;
    
    flex-direction:row(默认值) | row-reverse | column |column-reverse
    
    flex-wrap:nowrap(默认值) | wrap | wrap-reverse
    
    justify-content:flex-start(默认值) | flex-end | center |space-between | space-around | space-evenly
    
    align-items:stretch(默认值) | center  | flex-end | baseline | flex-start
    
    align-content:stretch(默认值) | flex-start | center |flex-end | space-between | space-around | space-evenly
    
  3. 设置项目的属性有:

    order:0(默认值) | <integer>
    
    flex-shrink:1(默认值) | <number>
    
    flex-grow:0(默认值) | <number>
    
    flex-basis:auto(默认值) | <length>
    
    flex:none | auto | @flex-grow @flex-shrink @flex-basis
    
    align-self:auto(默认值) | flex-start | flex-end |center | baseline| stretch
    
  4. flex-direction 属性

    通过设置坐标轴,来设置项目排列方向。

    .container{
      flex-direction: row(默认值) | row-reverse | column | column-reverse
    }
    

    row(默认值):主轴横向,方向为从左指向右。项目沿主轴排列,从左到右排列。

    row-reverse:row的反方向。主轴横向,方向为从右指向左。项目沿主轴排列,从右到左排列。

    column:主轴纵向,方向从上指向下。项目沿主轴排列,从上到下排列。

    column-reverse:column的反方向。主轴纵向,方向从下指向上。项目沿主轴排列,从下到上排列。

  5. flex-wrap 属性

    设置是否允许项目多行排列,以及多行排列时换行的方向。

    .container{
      flex-wrap: nowrap(默认值) | wrap | wrap-reverse
    }
    

    nowrap(默认值):不换行。如果单行内容过多,则溢出容器。
    wrap:容器单行容不下所有项目时,换行排列。
    wrap-reverse:容器单行容不下所有项目时,换行排列。换行方向为wrap时的反方向。

  6. justify-content 属性

    设置项目在主轴方向上对齐方式,以及分配项目之间及其周围多余的空间。

    .container{
    
      justify-content: flex-start(默认值) | flex-end | center | space-between | space-around| space-evenly
    
    }
    

    flex-start(默认值):项目对齐主轴起点,项目间不留空隙。

    center:项目在主轴上居中排列,项目间不留空隙。主轴上第一个项目离主轴起点距离等于最后一个项目离主轴终点距离。

    flex-end:项目对齐主轴终点,项目间不留空隙。
    space-between:项目间间距相等,第一个项目离主轴起点和最后一个项目离主轴终点距离为0。
    space-around:与space-between相似。不同点为,第一个项目离主轴起点和最后一个项目离主轴终点距离为中间项目间间距的一半。

    space-evenly:项目间间距、第一个项目离主轴起点和最后一个项目离主轴终点距离等于项目间间距。

  7. align-items 属性

    设置项目在行中的对齐方式。

    .container{
    
      align-items:stretch(默认值) | flex-start | center | flex-end | baseline
    
    }
    

    stretch(默认值):项目拉伸至填满行高。
    flex-start:项目顶部与行起点对齐。
    center:项目在行中居中对齐。
    flex-end:项目底部与行终点对齐。
    baseline:项目的第一行文字的基线对齐

  8. align-content 属性

    多行排列时,设置行在交叉轴方向上的对齐方式,以及分配行之间及其周围多余的空间。

    .container{
    
      align-content: stretch(默认值) | flex-start | center | flex-end | space-between |space-around | space-evenly
    
    }
    

    stretch(默认值):当未设置项目尺寸,将各行中的项目拉伸至填满交叉轴。当设置了项目尺寸,项目尺寸不变,项目行拉伸至填满交叉轴。

    flex-start:首行在交叉轴起点开始排列,行间不留间距。

    center:行在交叉轴中点排列,行间不留间距,首行离交叉轴起点和尾行离交叉轴终点距离相等。

    flex-end:尾行在交叉轴终点开始排列,行间不留间距。
    space-between:行与行间距相等,首行离交叉轴起点和尾行离交叉轴终点距离为0。
    space-around:行与行间距相等,首行离交叉轴起点和尾行离交叉轴终点距离为行与行间间距的一半。

    space-evenly:行间间距、以及首行离交叉轴起点和尾行离交叉轴终点距离相等。

  9. order 属性

    设置项目沿主轴方向上的排列顺序,数值越小,排列越靠前。属性值为整数。

    .item{
    
      order: 0(默认值) | <integer>
    
    
    }
    
  10. flex-shrink 属性

    当项目在主轴方向上溢出时,通过设置项目收缩因子来压缩项目适应容器。属性值为项目的收缩因子,属性值取非负数。

    .item{
    
      flex-shrink: 1(默认值) | <number>
    
    }
    
    .item1{
    
      width: 120px;
    
      flex-shrink: 2;
    
    }
    
    .item2{
    
      width: 150px;
    
      flex-shrink: 3;
    
    }
    
    .item3{// 项目3未设置flex-shrink,默认flex-shrink值为1
    
      width: 180px;
    
    }
    
  11. flex-grow 属性

    当项目在主轴方向上还有剩余空间时,通过设置项目扩张因子进行剩余空间的分配。属性值为项目的扩张因子,属性值取非负数。

    .item{
    
      flex-grow: 0(默认值) | <number>
    
    }
    
  12. flex-basis 属性

    当容器设置flex-direction为row或row-reverse时,flex-basis和width同时存在,flex-basis优先级高于width,也就是此时flex-basis代替项目的width属性。

    当容器设置flex-direction为column或column-reverse时,flex-basis和height同时存在,flex-basis优先级高于height,也就是此时flex-basis代替项目的height属性。

    需要注意的是,当flex-basis和width(或height),其中一个属性值为auto时,非auto的优先级更高。

    .item{
    
      flex-basis: auto(默认值) | <number>px
    
    }
    
  13. flex 属性

    是flex-grow,flex-shrink,flex-basis的简写方式。值设置为none,等价于00 auto。值设置为auto,等价于1 1 auto。

    .item{
    
      flex: none | auto | @flex-grow @flex-shrink@flex-basis
    
    }
    
  14. align-self 属性

    设置项目在行中交叉轴方向上的对齐方式,用于覆盖容器的align-items,这么做可以对项目的对齐方式做特殊处理。默认属性值为auto,继承容器的align-items值,当容器没有设置align-items时,属性值为stretch。

    .item{
    
      align-self: auto(默认值) | flex-start | center | flex-end | baseline |stretch
    
    }
    

交互反馈

  1. 小程序的view容器组件和button组件提供了hover-class属性,触摸时会往该组件加上对应的class改变组件的样式。

    代码清单4-1 通过hover-class属性改变触摸时的样式

    /*page.wxss */
    
    .hover{
    
      background-color: gray;
    
    }
    
    
    
    <!--page.wxml -->
    
    <button hover-class="hover"> 点击button </button>
    
    <view hover-class="hover"> 点击view</view>
    
  2. 对于用户的操作及时响应是非常优秀的体验,有时候在点击button按钮处理更耗时的操作时,我们也会使用button组件的loading属性,在按钮的文字前边出现一个Loading,让用户明确的感觉到,这个操作会比较耗时,需要等待一小段时间。

    代码清单4-2 设置button的loading属性

    <!--page.wxml -->
    
    <button loading="{{loading}}" bindtap="tap">操作</button>
    
    
    
    //page.js
    
    Page({
    
      data: { loading: false },
    
      tap: function() {
    
        // 把按钮的loading状态显示出来
    
        this.setData({
    
          loading: true
    
        })
    
        // 接着做耗时的操作
    
      }
    
    })
    
  3. 在完成某个操作成功之后,我们希望告诉用户这次操作成功并且不打断用户接下来的操作。弹出式提示Toast就是用在这样的场景上,Toast提示默认1.5秒后自动消失

    小程序提供了显示隐藏Toast的接口,代码示例如下所示。

    代码清单4-3 显示/隐藏Toast

    Page({
    
      onLoad: function() {
    
        wx.showToast({ // 显示Toast
    
          title: '已发送',
    
          icon: 'success',
    
          duration: 1500
    
        })
    
        // wx.hideToast() // 隐藏Toast
    
      }
    
    })
    
  4. 一般需要用户明确知晓操作结果状态的话,会使用模态对话框来提示,同时附带下一步操作的指引。

    代码清单4-4 显示模态对话框

    Page({
    
      onLoad: function() {
    
        wx.showModal({
    
          title: '标题',
    
          content: '告知当前状态,信息和解决方法',
    
          confirmText: '主操作',
    
          cancelText: '次要操作',
    
          success: function(res) {
    
            if (res.confirm) {
    
              console.log('用户点击主操作')
    
            } else if (res.cancel) {
    
              console.log('用户点击次要操作')
    
            }
    
          }
    
        })
    
      }
    
    })
    
  5. 宿主环境提供了统一的下拉刷新交互,开发者只需要通过配置开启当前页面的下拉刷新,用户往下拉动界面触发下拉刷新操作时,Page构造器的onPullDownRefresh回调会被触发,此时开发者重新拉取新数据进行渲染,实例代码如下所示。

    代码清单4-5 页面下拉刷新

    //page.json
    
    {"enablePullDownRefresh": true }
    
    
    
    //page.js
    
    Page({
    
      onPullDownRefresh: function() {
    
        // 用户触发了下拉刷新操作
    
        // 拉取新数据重新渲染界面
    
        // wx.stopPullDownRefresh() // 可以停止当前页面的下拉刷新。
    
      }
    
    })
    
  6. 多数的购物小程序会在首页展示一个商品列表,用户滚动到底部的时候,会加载下一页的商品列表渲染到列表的下方,我们把这个交互操作叫为上拉触底。宿主环境提供了上拉的配置和操作触发的回调,如下代码所示。

    代码清单4-6 页面上拉触底

    //page.json
    
    // 界面的下方距离页面底部距离小于onReachBottomDistance像素时触发onReachBottom回调
    
    {"onReachBottomDistance": 100 }
    
    
    
    //page.js
    
    Page({
    
      onReachBottom: function() {
    
        // 当界面的下方距离页面底部距离小于100像素时触发回调
    
      }
    
    })
    

    当然我们有些时候并不想整个页面进行滚动,而是页面中某一小块区域需要可滚动,此时就要用到宿主环境所提供的scroll-view可滚动视图组件。可以通过组件的scroll-x和scroll-y属性决定滚动区域是否可以横向或者纵向滚动,scroll-view组件也提供了丰富的滚动回调触发事件,这部分我们就不再展开细节,读者可以通过scroll-view组件的官方文档了解到细节[1]

发起HTTPS网络通信

  1. 如果我们需要从 https://test.com/getinfo 接口拉取用户信息,其代码示例如下所示,详细参数如表4-1所示。

    代码清单4-7 wx.request调用示例

    wx.request({
    
      url: 'https://test.com/getinfo',
    
      success: function(res) {
    
        console.log(res)// 服务器回包信息
    
      }
    
    })
    
  2. 表4-1 wx.request详细参数

    参数名类型必填默认值描述
    urlString 开发者服务器接口地址
    dataObject/String 请求的参数
    headerObject 设置请求的 header,header 中不能设置 Referer,默认header['content-type'] = 'application/json'
    methodStringGET(需大写)有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT
    dataTypeStringjson回包的内容格式,如果设为json,会尝试对返回的数据做一次 JSON解析
    successFunction 收到开发者服务成功返回的回调函数,其参数是一个Object,见表4-2。
    failFunction 接口调用失败的回调函数
    completeFunction 接口调用结束的回调函数(调用成功、失败都会执行)
  3. url参数是当前发起请求的服务器接口地址,小程序宿主环境要求request发起的网络请求必须是https协议请求,因此开发者服务器必须提供HTTPS服务的接口,同时为了保证小程序不乱用任意域名的服务,wx.request请求的域名需要在小程序管理平台进行配置[2]如果小程序正式版使用wx.request请求未配置的域名,在控制台会有相应的报错

  4. 一般我们在开发阶段时,处于开发阶段的服务器接口还没部署到现网的域名下,经常会通过另一个域名来进行开发调试,考虑到这一点,为了方便开发者进行开发调试,开发者工具、小程序的开发版和小程序的体验版在某些情况下[3]允许wx.request请求任意域名。

  5. 通过wx.request这个API,有两种方法把数据传递到服务器:通过url上的参数以及通过data参数。举个例子:我们需要向服务器拿id为1的用户的信息,同时我们把当前小程序的版本带给服务器,让服务器可以做新旧版逻辑兼容,两种方法的代码示例如代码4-8所示。

    代码清单4-8 wx.request调用示例

    // 通过url参数传递数据
    
    wx.request({
    
      url:'https://test.com/getinfo?id=1&version=1.0.0',
    
      success: function(res) {
    
        console.log(res)// 服务器回包信息
    
      }
    
    })
    
       // 通过data参数传递数据
    
    wx.request({
    
      url: 'https://test.com/getinfo',
    
         data: { id:1, version:'1.0.0' },
    
      success: function(res) {
    
        console.log(res)// 服务器回包信息
    
      }
    
    })
    

    两种实现方式在HTTP GET请求的情况下表现几乎是一样的,需要留意的是url是有长度限制的,其最大长度是1024字节,同时url上的参数需要拼接到字符串里,参数的值还需要做一次urlEncode向服务端发送的数据超过1024字节时,就要采用HTTPPOST的形式此时传递的数据就必须要使用data参数,基于这个情况,一般建议需要传递数据时,使用data参数来传递。

  6. 我们再来单独看看POST请求的情况,并不是所有请求都是按照键值对key=value的形式传递到后台服务器,有时候需要传一些比较复杂的数据结构到后台的时候,用JSON格式会更加合适。此时我们可以在wx.request的header参数设置content-type头部为application/json,小程序发起的请求的包体内容就是data参数对应的JSON字符串,代码示例如下。

    代码清单4-9 wx.request发起POST请求包体使用json格式

    // 请求的包体为 {"a":{"b":[1,2,3],"c":{"d":"test"}}}
    
    wx.request({
    
      url: 'https://test.com/postdata',
    
      method: 'POST',
    
      header: { 'content-type': 'application/json'},
    
      data: {
    
        a: {
    
          b: [1, 2, 3],
    
          c: { d: "test" }
    
        }
    
      },
    
      success: function(res) {
    
        console.log(res)// 服务器回包信息
    
      }
    
    })
    
  7. 通过wx.request发送请求后,服务器处理请求并返回HTTP包,小程序端收到回包后会触发success回调,同时回调会带上一个Object信息,详细参数表4-2所示。

    表4-2 wx.request的success返回参数

    参数名类型描述
    dataObject/String开发者服务器返回的数据
    statusCodeNumber开发者服务器返回的 HTTP 状态码
    headerObject开发者服务器返回的 HTTP Response Header

    尤其注意,只要成功收到服务器返回,无论HTTP状态码是多少都会进入success回调。因此开发者自己通过对回包的返回码进行判断后再执行后续的业务逻辑

    success回调的参数data字段类型是根据header['content-type']决定的,默认header['content-type']是'application/json',在触发success回调前,小程序宿主环境会对data字段的值做JSON解析,如果解析成功,那么data字段的值会被设置成解析后的Object对象,其他情况data字段都是String类型,其值为HTTP回包包体。

  8. 小程序发出一个HTTPS网络请求,有时网络存在一些异常或者服务器存在问题,在经过一段时间后仍然没有收到网络回包,我们把这一段等待的最长时间称为请求超时时间。小程序request默认超时时间是60秒,一般来说,我们不需要这么长的一个等待时间才收到回包,可能在等待3秒后还没收到回包就需要给用户一个明确的服务不可用的提示。在小程序项目根目录里边的app.json可以指定request的超时时间。

    代码清单4-10 app.json指定wx.requset超时时间为3000毫秒

    {
    
      "networkTimeout": {
    
        "request": 3000
    
      }
    
    }
    
  9. 大部分场景可能是这样的,用户点击一个按钮,界面出现“加载中...”的Loading界面,然后发送一个请求到后台,后台返回成功直接进入下一个业务逻辑处理,后台返回失败或者网络异常等情况则显示一个“系统错误”的Toast,同时一开始的Loading界面会消失。我们给出一个常见的wx.request的示例代码,如下所示。

    代码清单4-11 wx.request常见的示例代码

    var hasClick = false;
    
    Page({
    
      tap: function() {
    
        if (hasClick) {
    
          return
    
        }
    
        hasClick = true
    
        wx.showLoading()
    
    
    
        wx.request({
    
          url: 'https://test.com/getinfo',
    
          method: 'POST',
    
          header: { 'content-type':'application/json' },
    
          data: { },
    
          success: function (res) {
    
            if (res.statusCode === 200) {
    
              console.log(res.data)// 服务器回包内容
    
            }
    
          },
    
          fail: function (res) {
    
            wx.showToast({ title: '系统错误' })
    
          },
    
          complete: function (res) {
    
            wx.hideLoading()
    
            hasClick = false
    
          }
    
        })
    
      }
    
    })
    

    为了防止用户极快速度触发两次tap回调,我们还加了一个hasClick的“锁”,在开始请求前检查是否已经发起过请求,如果没有才发起这次请求,等到请求返回之后再把锁的状态恢复回去。

  10. 在使用wx.request接口我们会经常遇到无法发起请求或者服务器无法收到请求的情况,我们罗列排查这个问题的一般方法:

    1. 检查手机网络状态以及wifi连接点是否工作正常。
    2. 检查小程序是否为开发版或者体验版,因为开发版和体验版的小程序不会校验域名。
    3. 检查对应请求的HTTPS证书是否有效,同时TLS的版本必须支持1.2及以上版本,可以在开发者工具的console面板输入showRequestInfo()查看相关信息。
    4. 域名不要使用IP地址或者localhost,并且不能带端口号,同时域名需要经过ICP备案。
    5. 检查app.json配置的超时时间配置是否太短,超时时间太短会导致还没收到回报就触发fail回调。
    6. 检查发出去的请求是否302到其他域名的接口,这种302的情况会被视为请求别的域名接口导致无法发起请求。

微信登录

  1. 文档地址:https://developers.weixin.qq.com/ebook?action=get_post_info&docid=000cc48f96c5989b0086ddc7e56c0a
  2. 首先小程序登录获取code
  3. 发送code,业务侧账户密码到服务端
  4. 服务端访问https://api.weixin.qq.com/sns/jscode2session?appid=%3CAppId%3E&secret=%3CAppSecret%3E&js_code=%3Ccode%3E&grant_type=authorization_code,拿到openid和session_key,
  5. 绑定openid和业务侧账户密码,加上session_key存到服务器端,把session_key返回小程序存储,后续用于获取用户资料等。
  6. 账户密码可以使用绑定手机号的方式,更加快捷。
  7. 后续登录,拿code传到服务器,获取openid,即可匹配业务内id,无需输入账户密码,操作便捷。

本地数据缓存

  1. 小程序提供了读写本地数据缓存的接口,通过wx.getStorage/wx.getStorageSync读取本地缓存,通过wx.setStorage/wx.setStorageSync写数据到缓存,其中Sync后缀的接口表示是同步接口[9],执行完毕之后会立马返回,示例代码和参数说明如下所示。

    代码清单4-13 wx.getStorage/wx.getStorageSync读取本地数据缓存

    wx.getStorage({
    
      key: 'key1',
    
      success: function(res) {
    
        // 异步接口在success回调才能拿到返回值
    
        var value1 = res.data
    
      },
    
      fail: function() {
    
        console.log('读取key1发生错误')
    
      }
    
    })
    
    
    
    try{
    
      // 同步接口立即返回值
    
      var value2 = wx.getStorageSync('key2')
    
    }catch (e) {
    
      console.log('读取key2发生错误')
    
    }
    
  2. 表4-4 wx.getStorage/wx.getStorageSync详细参数

    参数名类型必填描述
    keyString本地缓存中指定的 key
    successFunction异步接口调用成功的回调函数,回调参数格式: {data: key对应的内容}
    failFunction异步接口调用失败的回调函数
    completeFunction异步接口调用结束的回调函数(调用成功、失败都会执行)
  3. 代码清单4-14 wx.setStorage/wx.setStorageSync写入本地数据缓存

    // 异步接口在success/fail回调才知道写入成功与否
    
    wx.setStorage({
    
      key:"key",
    
      data:"value1"
    
      success: function() {
    
        console.log('写入value1成功')
    
      },
    
      fail: function() {
    
        console.log('写入value1发生错误')
    
      }
    
    })
    
    
    
    try{
    
      // 同步接口立即写入
    
      wx.setStorageSync('key', 'value2')
    
      console.log('写入value2成功')
    
    }catch (e) {
    
      console.log('写入value2发生错误')
    
    }
    
  4. 表4-5 wx.setStorage/wx.setStorageSync详细参数

    参数名类型必填描述
    keyString本地缓存中指定的 key
    dataObject/String需要存储的内容
    successFunction异步接口调用成功的回调函数
    failFunction异步接口调用失败的回调函数
    completeFunction异步接口调用结束的回调函数(调用成功、失败都会执行)
  5. 小程序宿主环境会管理不同小程序的数据缓存,不同小程序的本地缓存空间是分开的,每个小程序的缓存空间上限为10MB,如果当前缓存已经达到10MB,再通过wx.setStorage写入缓存会触发fail回调。

  6. 小程序的本地缓存不仅仅通过小程序这个维度来隔离空间,考虑到同一个设备可以登录不同微信用户,宿主环境还对不同用户的缓存进行了隔离,避免用户间的数据隐私泄露。

  7. 由于本地缓存是存放在当前设备,用户换设备之后无法从另一个设备读取到当前设备数据,因此用户的关键信息不建议只存在本地缓存,应该把数据放到服务器端进行持久化存储。

  8. 可以利用本地缓存提前渲染界面,这种做法可以让用户体验你的小程序时感觉加载非常快,但是你还要留意这个做法的缺点,如果小程序对渲染的数据实时性要求非常高的话,用户看到一个旧数据的界面会非常困惑。因此一般在对数据实时性/一致性要求不高的页面采用这个方法来做提前渲染,用以优化小程序体验。

  9. 可以缓存用户登录态SessionId,在重新打开小程序的时候,我们把上一次存储的SessionId内容取出来,恢复到内存。

设备能力

  1. 为了让用户减少输入,我们可以把复杂的信息编码成一个二维码,利用宿主环境wx.scanCode这个API调起微信扫一扫,用户扫码之后,wx.scanCode的success回调会收到这个二维码所对应的字符串信息。

  2. 代码清单4-19 利用wx.scanCode获取二维码的数据

    //page.js
    
    Page({
    
      // 点击“扫码订餐”的按钮,触发tapScan回调
    
      tapScan: function() {
    
        // 调用wx.login获取微信登录凭证
    
        wx.scanCode({
    
          success: function(res) {
    
            var num = res.result // 获取到的num就是餐桌的编号
    
          }
    
        })
    
      }
    
    })
    
  3. 代码清单4-20 利用wx.getNetworkType获取网络状态

    //page.js
    
    Page({
    
      // 点击“预览文档”的按钮,触发tap回调
    
      tap: function() {
    
        wx.getNetworkType({
    
          success: function(res) {
    
            // networkType字段的有效值:
    
            // wifi/2g/3g/4g/unknown(Android下不常见的网络类型)/none(无网络)
    
            if (res.networkType == 'wifi') {
    
              // 从网络上下载pdf文档
    
              wx.downloadFile({
    
                url:'http://test.com/somefile.pdf',
    
                success: function (res) {
    
                  // 下载成功之后进行预览文档
    
                  wx.openDocument({
    
                    filePath: res.tempFilePath
    
                  })
    
                }
    
              })
    
            } else {
    
              wx.showToast({ title: '当前为非Wifi环境' })
    
            }
    
          }
    
        })
    
      }
    
    })
    
  4. 某些情况下,我们的手机连接到网络的方式会动态变化,例如手机设备连接到一个信号不稳定的Wifi热点,导致手机会经常从Wifi切换到移动数据网络。小程序宿主环境也提供了一个可以动态监听网络状态变化的接口wx.onNetworkStatusChange,让开发者可以及时根据网络状况去调整小程序的体验

  5. next