第2章 小程序基础知识

第1章为小程序入门的各项事宜,接下来本章将介绍微信小程序开发的基础知识:项目有哪些配置文件,微信小程序的各种配置,WXSS样式语言,逻辑层.js脚本,WXML视图层开发等。

2.1 项目配置文件

可以在项目根目录使用project.config.json文件(参见1.3.1节)对项目进行配置,项目配置文件的内容参见表2-1。

表2-1 项目配置文件

其中,compileType的有效值如下。

❏ miniprogram:当前为普通小程序项目。

❏ plugin:当前为小程序插件项目。

setting中可以指定的内容如下:

scripts中指定自定义预处理的命令如下。

❏ beforeCompile:编译前预处理命令。

❏ beforePreview:预览前预处理命令。

❏ beforeUpload:上传前预处理命令。

packOptions用于配置项目在打包过程中的选项。打包是预览、上传时对项目进行的必须步骤。目前可以指定packOptions.ignore字段,忽略配置打包时符合指定规则的文件或文件夹,以跳过打包过程,这些文件或文件夹将不会出现在预览或上传的结果内。

packOptions.ignore为一对象数组,对象元素类型如下:

其中,type可以取值为folder、file、suffix、prefix、regexp、glob,分别对应文件夹、文件、后缀、前缀、正则表达式、Glob规则。所有规则值都会自动忽略大小写。

提示

value字段的值若表示文件或文件夹路径,以小程序目录(miniprogramRoot)为根目录。regexp、glob仅1.02.1809260及以上版本工具支持。

配置示例代码如下:

        {
          "packOptions": {
            "ignore": [{
              "type": "file",
              "value": "test/test.js"
            }, {
              "type": "folder",
              "value": "test"
            }, {
              "type": "suffix",
              "value": ".webp"
            }, {
              "type": "prefix",
              "value": "test-"
            }, {
              "type": "glob",
              "value": "test/**/*.js"
            }, {
              "type": "regexp",
              "value": "\\.jsx$"
            }]
          }
        }

注意

这部分设置的更改可能需要重新打开项目才能生效。

debugOptions用于配置在对项目代码进行调试时的选项。目前可以指定debugOptions. hidedInDevtools字段,用于配置是否显示调试器的源代码。

hidedInDevtools的配置规则和packOptions.ignore是一致的。当某个.js文件符合此规则时,调试器Sources面板中此文件源代码正文内容将被隐藏,显示代码示例如下:

        // xxx.js has been hided by project.config.json
        注:配置此规则后,可能需要关闭并重新打开项目才能看到效果。

项目配置代码示例如下:

        {
          "miniprogramRoot": "./src",
          "qcloudRoot": "./svr",
          "setting": {
            "postcss": true,
            "es6": true,
            "minified": true,
            "urlCheck": false
          },
          "packOptions": {
            "ignore": []
          },
          "debugOptions": {}
        }

2.2 全局配置和页面配置

每个微信小程序项目都有一个全局配置文件和多个页面配置文件。全局配置文件针对整个微信小程序项目的相关配置信息;页面配置文件只针对对应的页面,每个微信小程序都有一个对应的页面配置文件。全局配置文件和页面配置文件如果有相同的配置项目,页面配置文件的优先级高于全局配置文件,也就是以页面配置文件的效果为主。

2.2.1 全局配置

我们利用小程序根目录下的app.json文件对微信小程序进行全局配置,决定页面文件的路径、窗口表现、设置网络超时时间、设置多tab等。

每个微信小程序项目只有一个全局配置文件。下面是一个包含了部分常用配置选项的app.json:

        {
          "pages": [
            "pages/index/index",
            "pages/logs/index"
          ],
          "window": {
            "navigationBarTitleText": "Demo"
          },
          "tabBar": {
            "list": [{
              "pagePath": "pages/index/index",
              "text": "首页"
            }, {
              "pagePath": "pages/logs/logs",
              "text": "日志"
            }]
          },
          "networkTimeout": {
            "request": 10000,
            "downloadFile": 10000
          },
          "debug": true,
          "navigateToMiniProgramAppIdList": [
            "wxe5f52902cf4de896"
          ]
        }

app.json配置项列表参见表2-2。

表2-2 app.json配置项列表

(1)pages

pages用于指定小程序由哪些页面组成,是一个数组,数组中每一项都对应一个页面的“路径+文件名”信息。文件名不需要写文件后缀,开发框架会自动去寻找对应位置的.json、.js、.wxml、.wxss四个文件进行处理。数组的第一项代表小程序的初始页面(首页)。小程序中新增/减少页面,都需要对pages数组进行修改。例如,如下开发目录中:

        ├—— app.js
        ├—— app.json
        ├—— app.wxss
        ├—— pages
        |   |—— index
        |   |   ├—— index.wxml
        |   |   ├—— index.js
        |   |   ├—— index.json
        |   |   └—— index.wxss
        |   └—— logs
        |       ├—— logs.wxml
        |       └—— logs.js
        └—— utils

要在app.json中编写页面,则需要配置pages数组,代码如下所示。

        {
          "pages":[
            "pages/index/index",
            "pages/logs/logs"
          ]
        }

(2)window

window用于设置小程序的状态栏、导航条、标题、窗口背景色等,属性参见表2-3。

表2-3 window属性

其中,HexColor为十六进制颜色值,#ffffff表示白色,#000000表示黑色。

注意

navigationStyle只在app.json中生效。开启custom后,低版本客户端需要做好兼容。开发者工具基础库版本切到客户端6.7.2版本开始,navigationStyle: custom对<web-view>组件无效。

app.json示例代码如下,界面示例如图2-1所示:

图2-1 app.json示例界面

        {
          "window":{
            "navigationBarBackgroundColor": "#ffffff",
            "navigationBarTextStyle": "black",
            "navigationBarTitleText": "微信接口功能演示",
            "backgroundColor": "#eeeeee",
            "backgroundTextStyle": "light"
          }
        }

(3)tabBar

tabBar用于设置小程序多tab。例如,客户端窗口的底部或顶部有tab栏可以切换页面,可以通过tabBar配置项指定tab栏的表现,以及tab切换时显示的对应页面。tabBar属性参见表2-4。

表2-4 tabBar属性

其中,list接受一个数组,只能配置最少2个、最多5个tab。tab按数组的顺序排序,每个项都是一个对象,其属性值如下:

当属性iconPath和selectedIconPath的postion为top时,不显示icon。list属性如图2-2所示。

图2-2 list属性示例

(4)networkTimeout

networkTimeout用于指定各类网络请求的超时时间,单位均为毫秒,networkTimeout属性参见表2-5。

表2-5 networkTimeout属性

(5)debug

可以在开发者工具中开启debug模式,在开发者工具的控制台面板,调试信息以info的形式给出,其信息有Page的注册、页面路由、数据更新、事件触发等,可以帮助开发者快速定位一些常见的问题。

(6)functionalPages

基础库2.1.0开始支持,低版本需做兼容处理。启用插件功能页时,插件所有者小程序需要将functionalPages设置为true。

(7)subpackages

微信客户端6.6.0、基础库1.7.3及以上版本支持。启用分包加载时,声明项目分包结构。写成subPackages也支持。

(8)workers

使用Worker处理多线程任务时,设置Worker代码放置的目录。

(9)requiredBackgroundModes

微信客户端6.7.2及以上版本支持。声明需要后台运行的能力,类型为数组。目前支持以下项目:Audio后台音乐播放,代码示例如下:

        {
          "pages": ["pages/index/index"],
          "requiredBackgroundModes": ["audio"]
        }

注意

此处声明了后台运行的接口,开发版和体验版上可以直接生效,正式版还需通过审核。

(10)plugins

基础库1.9.6开始支持,低版本需做兼容处理。声明小程序需要使用的插件。

(11)preloadRule

基础库2.3.0开始支持,低版本需做兼容处理。声明分包预下载的规则。

(12)resizable

基础库2.3.0开始支持,低版本需做兼容处理。在iPad上运行的小程序可以设置支持屏幕旋转。

(13)navigateToMiniProgramAppIdList

基础库2.4.0开始支持,低版本需做兼容处理。当小程序需要使用wx.navigateToMini-Program接口跳转到其他小程序时,需要先在配置文件中声明需要跳转的小程序的AppID列表,最多允许填写10个。

2.2.2 页面配置

每一个小程序的页面可以使用.json文件对本页面的窗口表现进行配置。页面配置只能设置app.json中部分window配置项的内容,页面中配置项会覆盖app.json的window中相同的配置项,页面配置属性参见表2-6。

表2-6 页面配置属性

配置样例代码如下:

        {
          "navigationBarBackgroundColor": "#ffffff",
          "navigationBarTextStyle": "black",
          "navigationBarTitleText": "微信接口功能演示",
          "backgroundColor": "#eeeeee",
          "backgroundTextStyle": "light"
        }

提示

页面的.json只能设置window相关的配置项,以决定本页面的窗口表现,所以无须写window这个键。

2.3 WXSS样式语言

WXSS(WeiXin Style Sheets)是一套样式语言,用于描述WXML的组件样式。WXSS用来决定WXML的组件应该怎么显示。为了适应广大的前端开发者,WXSS具有CSS大部分特性。同时,为了更适合开发微信小程序,WXSS对CSS进行了修改和扩充。

与CSS相比,在微信小程序WXSS扩展的特性有:

❏ 尺寸单位。

❏ 样式导入。

❏ 全局样式和局部样式。

内联样式和选择器沿用了CSS的功能写法。

1.尺寸单位

尺寸单位为rpx(responsive pixel),可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在iPhone6上,屏幕宽度为375px,共有750个物理像素,则750rpx=375px=750物理像素,1rpx=0.5px=1物理像素,不同设备的换算方式如下:

在开发微信小程序时,建议设计师使用iPhone6作为视觉稿的标准。

注意

在较小的屏幕上不可避免会有一些毛刺,在开发时尽量避免这种情况。

2.样式导入

可以使用@import语句导入外联样式表,@import后跟需要导入的外联样式表的相对路径,用分号“; ”表示语句结束。代码示例如下:

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

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

3.全局样式与局部样式

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

4.内联样式

框架组件上支持使用style和class属性来控制组件的样式,说明如下。

❏ style:静态的样式统一写到class中,style接收动态的样式,在运行时会进行解析,尽量不要将静态的样式写进style,以免影响渲染速度,代码示例如下:

          <view style="color:{{color}}; " />

❏ class:用于指定样式规则,其属性值是样式规则中类选择器名(样式类名)的集合,样式类名不需要带上.,样式类名之间用空格分隔,代码示例如下:

          <view class="normal_view" />

5.选择器

目前支持的选择器有:

还有很多支持的选择器,不在这里一一列出,读者可以自行尝试,我们在后面第3章会讲解常用选择器在微信小程序中的使用。

2.4 逻辑层.js脚本

小程序开发框架的逻辑层使用JavaScript引擎,向小程序开发者提供JavaScript代码的运行环境以及微信小程序的特有功能。逻辑层对数据进行处理并发送给视图层,同时接受视图层的事件反馈。开发者写的所有代码最终将打包成一份JavaScript文件,并在小程序启动的时候运行,直到小程序销毁。这一行为类似于ServiceWorker,所以逻辑层也称为App Service。

在JavaScript的基础上,为了方便小程序开发增加了以下功能:

❏ 增加App和Page方法,进行程序和页面的注册。

❏ 增加getApp和getCurrentPages方法,用来获取App实例和当前页面栈。

❏ 提供丰富的API,如微信用户数据、扫一扫、支付等微信特有能力。

❏ 每个页面有独立的作用域,并提供模块化能力。

注意

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

2.4.1 App方法

小程序的App方法包含一系列函数,例如:App(Object)、onLaunch(Object)、on-Show(Object)、onHide()、onError(String error)、onPageNotFound(Object)、getApp(Object)等。

1. App(Object)

App()函数用来注册一个小程序。接受一个Object参数,指定小程序的生命周期回调等。App()必须在app.js中调用且只能调用一次,否则会出现无法预期的后果。Object参数说明参见表2-7。

表2-7 App()函数的Object参数表

前台、后台的含义是,当用户点击左上角关闭,或者按了设备Home键离开微信,小程序并没有直接销毁,而是进入了后台;当再次进入微信或再次打开小程序,小程序又会从后台进入前台。需要注意的是,只有当小程序进入后台一定时间,或者系统资源占用过高,小程序才会被真正销毁。

关闭小程序(基础库版本1.1.0开始支持)是指,当用户从扫一扫、转发等入口(场景值为1007, 1008, 1011, 1025)进入小程序,且没有置顶小程序的情况下退出,小程序会被销毁。

小程序运行机制在基础库版本1.4.0有所改变:上面“关闭小程序”逻辑在新版本已不适用。

代码示例如下:

        App({
          onLaunch: function(options) {
            // Do something initial when launch.
          },
          onShow: function(options) {
            // Do something when show.
          },
          onHide: function() {
            // Do something when hide.
          },
          onError: function(msg) {
            console.log(msg)
          },
          globalData: 'I am global data'
        })

2. onLaunch(Object)

onLaunch()函数在小程序初始化完成时触发,全局只触发一次,其中,Object参数说明参见表2-8。

表2-8 onLaunch()函数的Object参数表

其中,referrerInfo.appId场景值参见表2-9。

表2-9 referrerInfo.appId场景值

3. onShow(Object)

小程序启动时或从后台进入前台显示时触发onShow()函数。Object参数说明与on-Launch()函数一致。

4. onHide()

小程序从前台进入后台时触发onHide()函数。

5. onError(String error)

小程序发生脚本错误或者API调用失败时触发onError()函数。参数说明如下:

6. onPageNotFound(Object)

基础库1.9.90开始支持,低版本需做兼容处理。小程序要打开的页面不存在时触发onPageNotFound()函数。Object参数说明如下:

开发者可以在onPageNotFound回调中进行重定向,但必须在回调中同步处理,异步处理(例如setTimeout异步执行)无效。代码示例如下:

        App({
          onPageNotFound(res) {
            wx.redirectTo({
              url: 'pages/...'
            })     //如果是tabBar页面,请使用wx.switchTab
          }
        })

注意

● 如果开发者没有添加onPageNotFound监听,当跳转页面不存在时,将推入“微信客户端原生的页面不存在”提示页面。

● 如果onPageNotFound回调中又重定向到另一个不存在的页面,将推入“微信客户端原生的页面不存在”提示页面,并且不再回调onPageNotFound。

7. getApp(Object)

getApp()函数是全局函数,可以用来获取小程序App实例。Object参数说明如下:

代码示例如下:

        // other.js
        var appInstance = getApp()
        console.log(appInstance.globalData)      // I am global data

注意

❏ 不要在定义于App()内的函数中调用getApp(),使用this就可以获取App实例。

❏ 通过getApp()获取实例之后,不要私自调用生命周期函数。

2.4.2 运行机制

小程序启动会有两种情况,一种是“冷启动”,一种是“热启动”。假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无须重新启动,只需将后台状态的小程序切换到前台,这个过程就是“热启动”。“冷启动”指的是用户首次打开小程序,或小程序被微信主动销毁后再次打开,此时小程序需要重新加载启动。

小程序冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用。如果需要马上应用最新版本,可以使用wx.getUpdateManager API进行处理。

小程序没有重启的概念。当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁。当短时间内(5分钟)连续收到两次以上系统内存告警,会对小程序进行销毁。

再次打开逻辑:基础库1.4.0开始支持,低版本需做兼容处理。

打开小程序有A和B两类场景。

A场景,打开首页。场景值有以下几项:

B场景,打开小程序指定的某个页面。场景值为除上面场景以外的其他内容,再次打开一个小程序的逻辑如下:

2.4.3 场景值

当前支持的场景值参数见表2-10。基础库1.1.0开始支持,低版本需做兼容处理。

表2-10 场景值参数

(续)

(续)

可以在App的onLaunch和onShow中获取上述场景值,部分场景值下还可以获取来源应用、公众号或小程序的AppID。

提示

由于Android系统限制,目前还无法获取到按Home键退出到桌面,然后从桌面再次进小程序的场景值,对于这种情况,会保留上一次的场景值。

2.4.4 Page方法

小程序的Page方法用于页面的注册和配置。

1.页面Page()函数

Page()函数用来注册一个页面。接受一个Object类型参数,指定页面的初始数据、生命周期回调、事件处理函数等。Object参数说明参见表2-11。

表2-11 Page()函数的Object参数

代码示例如下:

    //index.js
    Page({
      data: {
        text: "This is page data."
      },
      onLoad: function(options) {
        // Do some initialize when page load.
      },
      onReady: function() {
        // Do something when page ready.
      },
      onShow: function() {
        // Do something when page show.
      },
      onHide: function() {
        // Do something when page hide.
      },
      onUnload: function() {
        // Do something when page close.
      },
      onPullDownRefresh: function() {
        // Do something when pull down.
      },
      onReachBottom: function() {
        // Do something when page reach bottom.
      },
      onShareAppMessage: function () {
        // return custom share data when user share.
      },
      onPageScroll: function() {
      // Do something when page scroll
      },
      onResize: function() {
      // Do something when page resize
      },
      onTabItemTap(item) {
            console.log(item.index)
            console.log(item.pagePath)
            console.log(item.text)
          },
          // Event handler.
          viewTap: function() {
            this.setData({
              text: 'Set some data for updating view.'
            }, function() {
              // this is setData callback
            })
          },
          customData: {
            hi: 'MINA'
          }
        })

页面可以像自定义组件一样使用Component来创建,这样就可以使用自定义组件的特性。

2.初始数据data

data是页面第一次渲染使用的初始数据。页面加载时,data将会以JSON字符串的形式由逻辑层传至渲染层,因此data中的数据必须可以转成JSON的类型,如字符串、数字、布尔值、对象、数组等。

渲染层可以通过WXML对数据进行绑定。代码示例如下:

        <view>{{text}}</view>
        <view>{{array[0].msg}}</view>
        Page({
          data: {
            text: 'init data',
            array: [{msg: '1'}, {msg: '2'}]
          }
        })

3.生命周期回调函数

注册页面时的生命周期回调函数包括onLoad()、onShow()、onReady()、onHide()、on-Unload()。

onLoad(Object query)页面加载时触发。一个页面只会调用一次,可以在onLoad的参数中获取打开当前页面路径中的参数。

参数说明如下:

onShow()页面显示/切入前台时触发。

onReady()页面初次渲染完成时触发。一个页面只会调用一次,该函数执行完毕代表页面已经准备妥当,可以和视图层进行交互。

注意

对界面内容进行设置的API如wx.setNavigationBarTitle,请在onReady之后进行。

onHide()页面隐藏/切入后台时触发,如navigateTo或底部tab切换到其他页面,小程序切入后台等。

onUnload()页面卸载时触发,如redirectTo或navigateBack到其他页面时。

4.页面事件处理函数

(1)onPullDownRefresh()

监听用户下拉刷新事件。需要在app.json的window选项中或页面配置中开启enable-PullDownRefresh。可以通过wx.startPullDownRefresh触发下拉刷新,调用后触发下拉刷新动画,效果与用户手动下拉刷新一致。当处理完数据刷新后,wx.stopPullDownRefresh可以停止当前页面的下拉刷新。

(2)onReachBottom()

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

(3)onPageScroll(Object)

监听用户滑动页面事件。Object参数如下:

(4)onShareAppMessage(Object)

监听用户点击页面内转发按钮(<button>组件open-type="share")或右上角菜单“转发”按钮的行为,并自定义转发内容。

注意

只有定义了此事件处理函数,右上角菜单才会显示“转发”按钮。

Object参数如下:

此事件需要返回一个Object,用于自定义转发内容,返回内容如下:

图片路径可以是本地文件路径、代码包文件路径或者网络图片路径。支持PNG及JPG。显示图片长宽比是5∶4。

代码示例如下:

        Page({
          onShareAppMessage: function (res) {
            if (res.from === 'button') {
              //来自页面内转发按钮
              console.log(res.target)
            }
            return {
              title: ’自定义转发标题’,
              path: '/page/user? id=123'
            }
          }
        })

(5)onTabItemTap(Object)

基础库1.9.0开始支持,低版本需做兼容处理。点击tab时触发,Object参数如下:

代码示例如下:

        Page({
          onTabItemTap(item) {
            console.log(item.index)
            console.log(item.pagePath)
            console.log(item.text)
          }
        })

5.组件事件处理函数

Page中还可以定义组件事件处理函数。在渲染层的组件中加入事件绑定,当事件被触发时,执行Page中定义的事件处理函数。

代码示例如下:

        <view bindtap="viewTap"> click me </view>

        Page({
          viewTap: function() {
            console.log('view tap')
          }
        })

6. route

Page.route表示到当前页面的路径,类型为String,基础库1.2.0开始支持,低版本需做兼容处理。代码示例如下:

        Page({
          onShow: function() {
            console.log(this.route)
          }
        })

7. setData

Page.prototype.setData(Object data, Function callback)函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的this.data的值(同步)。参数说明如下:

Object以key: value的形式表示,将this.data中key对应的值改变成value。

其中key可以用数据路径的形式给出,支持改变数组中的某一项或对象的某个属性,如array[2].message, a.b.c.d,并且不需要在this.data中预先定义。

注意

● 直接修改this.data而不调用this.setData是无法改变页面的状态的,并会造成数据不一致。

● 仅支持设置可JSON化的数据。单次设置的数据不能超过1024kb,请尽量避免一次设置过多的数据。请不要把data中任何一项的value设为undefined,否则这一项将不被设置并可能遗留一些潜在问题。

代码示例如下:

    <! --index.wxml-->
    <view>{{text}}</view>
    <button bindtap="changeText"> Change normal data </button>
    <view>{{num}}</view>
    <button bindtap="changeNum"> Change normal num </button>
    <view>{{array[0].text}}</view>
    <button bindtap="changeItemInArray"> Change Array data </button>
    <view>{{object.text}}</view>
    <button bindtap="changeItemInObject"> Change Object data </button>
    <view>{{newField.text}}</view>
    <button bindtap="addNewField"> Add new data </button>

    // index.js
    Page({
      data: {
        text: 'init data',
        num: 0,
        array: [{text: 'init data'}],
        object: {
          text: 'init data'
        }
      },
      changeText: function() {
        // this.data.text = 'changed data' //不要直接修改this.data
        //应该使用setData
        this.setData({
          text: 'changed data'
        })
      },
      changeNum: function() {
        //或者,可以修改this.data之后马上用setData设置一下修改了的字段
        this.data.num = 1
        this.setData({
          num: this.data.num
        })
      },
      changeItemInArray: function() {
        //对于对象或数组字段,可以直接修改一个其下的子字段,这样做通常比修改整个对象或数组更好
        this.setData({
          'array[0].text':'changed data'
        })
      },
      changeItemInObject: function(){
        this.setData({
          'object.text': 'changed data'
        });
      },
      addNewField: function() {
        this.setData({
          'newField.text': 'new data'
        })
      }
    })

8.生命周期

理解生命周期的含义,将会帮助你理解开发。图2-3为Page实例的生命周期。

图2-3 Page实例的生命周期

2.4.5 路由

在小程序中,所有页面的路由全部由框架进行管理。框架以栈的形式维护了当前的所有页面。当发生路由切换的时候,页面栈的表现如表2-12所示。

表2-12 当路由切换时页面栈的表现

getCurrentPages()函数用于获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个元素为首页,最后一个元素为当前页面。

注意

● 不要尝试修改页面栈,会导致路由以及页面状态错误。

● 不要在App.onLaunch的时候调用getCurrentPages(),此时page还没有生成。

路由的触发方式以及页面生命周期函数参见表2-13。

表2-13 路由的触发方式以及页面生命周期函数

例如,A、B页面为TabBar页面,C是从A页面打开的页面,D页面是从C页面打开的页面,Tab切换对应的生命周期如下:

提示

● navigateTo或redirectTo只能打开非tabBar页面。

● switchTab只能打开tabBar页面。

● reLaunch可以打开任意页面。

● 页面底部的tabBar由页面决定,即只要是定义为tabBar的页面,底部都有tabBar。

● 调用页面路由所带的参数可以在目标页面的onLoad中获取。

2.4.6 模块化

在JavaScript文件中声明的变量和函数只在该文件中有效;不同的文件中可以声明相同名字的变量和函数,不会互相影响。通过全局函数getApp()可以获取全局的应用实例,如果需要全局的数据可以在App()中设置,代码示例如下:

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

        // a.js
        // The localValue can only be used in file a.js.
        var localValue = 'a'
        // Get the app instance.
        var app = getApp()
        // Get the global data and change it.
        app.globalData++

        // b.js
        // You can redefine localValue in file b.js, without interference with the local-
            Value in a.js.
        var localValue = 'b'
        // If a.js it run before b.js, now the globalData shoule be 2.
        console.log(getApp().globalData)

可以将一些公共的代码抽离成一个单独的.js文件,作为一个模块。模块只有通过module.exports或exports才能对外暴露接口。

注意

exports是module.exports的一个引用,在模块里面随意更改exports的指向会造成未知的错误。所以推荐开发者采用module.exports来暴露模块接口,除非你已经清晰知道这两者的关系。小程序目前不支持直接引入node_modules,开发者需要使用到node_modules时建议拷贝相关的代码到小程序的目录中,或者使用小程序支持的npm功能。

代码示例如下:

        // common.js
        function sayHello(name) {
          console.log(`Hello ${name} ! `)
        }
        function sayGoodbye(name) {
          console.log(`Goodbye ${name} ! `)
        }
        module.exports.sayHello = sayHello
        exports.sayGoodbye = sayGoodbye

在需要使用这些模块的文件中,使用require(path)将公共代码引入,代码示例如下:

        var common = require('common.js')
        Page({
          helloMINA: function() {
            common.sayHello('MINA')
          },
          goodbyeMINA: function() {
            common.sayGoodbye('MINA')
          }
        })

提示

require暂时不支持绝对路径。

2.4.7 API

小程序开发框架提供丰富的微信原生API,可以方便地调用微信提供的能力,如获取用户信息、本地存储、支付功能等。通常,小程序API有以下几种类型:事件监听API,同步API,异步API。

1.事件监听API

以on开头的API为事件监听API,用来监听某个事件是否触发,如wx.onSocketOpen、wx.onCompassChange等。这类API接受一个回调函数作为参数,当事件触发时会调用这个回调函数,并将相关数据以参数形式传入。代码示例如下:

        wx.onCompassChange(function (res) {
          console.log(res.direction)
        })

2.同步API

以Sync结尾的API都是同步API,如wx.setStorageSync、wx.getSystemInfoSync等。此外,也有一些其他的同步API,如wx.createWorker、wx.getBackgroundAudioManager等。同步API的执行结果可以通过函数返回值直接获取,如果执行出错会抛出异常。代码示例如下:

        try {
          wx.setStorageSync('key', 'value')
        } catch (e) {
          console.error(e)
        }

3.异步API

大多数API都是异步的,如wx.request、wx.login等。这类API接口通常都接受一个Object类型的参数,这个参数用于指定如何接收接口调用结果。

异步API的Object参数说明如下:

API通常的回调函数有三个:success、fail和complete。回调函数调用时会传入一个Object类型参数,包含以下字段:

异步API的执行结果需要通过Object类型的参数中传入的对应回调函数获取。部分异步API也会有返回值,可以用来实现更丰富的功能,如wx.request、wx.connectSockets等。代码示例如下:

        wx.login({
          success(res) {
            console.log(res.code)
          }
        })

2.5 WXML视图层开发

WXML(WeiXin Markup Language)是框架设计的类似于HTML的标签语言,结合基础组件、事件系统就可以构建出页面的结构,组成.wxml文件。WXML中的动态数据均来自对应Page的data。本节主要讲解视图层开发中.wxml文件常用的语法,包含数据绑定、列表渲染、条件渲染、模板、事件、引用等处理。

2.5.1 数据绑定

数据绑定是指在小程序的.js文件里,将data定义的各类数据显示在.wxml页面中。当然data里面定义的各类数据可以通过其他方式进行变更。

1.简单绑定

数据简单绑定是指使用Mustache语法(双大括号)将变量包起来。

.wxml文件代码示例如下:

    <view> {{ message }} </view>

.js文件代码示例如下:

    Page({
      data: {
        message: 'Hello MINA! '
      }
    })

组件属性需要在双引号之内,.wxml文件代码示例如下:

    <view id="item-{{id}}"> </view>

.js文件代码示例如下:

    Page({
      data: {
        id: 0
      }
    })

控制属性需要在双引号之内,.wxml文件代码示例如下:

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

.js文件代码示例如下:

    Page({
      data: {
        condition: true
      }
    })

关键字(需要在双引号之内)包括。

❏ true:boolean类型的true,代表真值。

❏ false:boolean类型的false,代表假值。

.wxml文件代码示例如下:

    <checkbox checked="{{false}}"> </checkbox>

注意

不要直接写checked="false",其计算结果是一个字符串,转成boolean类型后代表真值。

2.运算

可以在{{}}内进行简单的运算,支持如下几种运算:

三元运算,.wxml文件代码示例如下:

    <view hidden="{{flag ? true : false}}"> Hidden </view>

算数运算,.wxml文件代码示例如下:

    <view> {{a + b}} + {{c}} + d </view>

.js文件代码示例如下:

    Page({
      data: {
        a: 1,
        b: 2,
        c: 3
      }
    })

view中的内容为3+3+d。

逻辑判断,.wxml文件代码示例如下:

    <view wx:if="{{length > 5}}"> </view>

字符串运算,.wxml文件代码示例如下:

    <view>{{"hello" + name}}</view>

.js文件代码示例如下:

    Page({
      data:{
        name: 'MINA'
      }
    })

数据路径运算,.wxml文件代码示例如下:

    <view>{{object.key}} {{array[0]}}</view>

.js文件代码示例如下:

    Page({
      data: {
        object: {
          key: 'Hello '
        },
        array: ['MINA']
      }
    })

3.组合

也可以在Mustache内直接进行组合,构成新的数组或者对象。

(1)数组

.wxml文件代码示例如下:

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

.js文件代码示例如下:

    Page({
      data: {
        zero: 0
      }
    })

最终组合成数组[0, 1, 2, 3, 4]。

(2)对象

.wxml文件代码示例如下:

    <template is="objectCombine" data="{{for: a, bar: b}}"></template>

.js文件代码示例如下:

    Page({
      data: {
        a: 1,
        b: 2
      }
    })

最终组合成对象{for: 1, bar: 2}。

也可以用扩展运算符...将一个对象展开。.wxml文件代码示例如下:

    <template is="objectCombine" data="{{...obj1, ...obj2, e: 5}}"></template>

.js文件代码示例如下:

    Page({
      data: {
        obj1: {
          a: 1,
          b: 2
        },
        obj2: {
          c: 3,
          d: 4
        }
      }
    })

最终组合成对象{a: 1, b: 2, c: 3, d: 4, e: 5}。

如果对象的key和value相同,也可以间接地表达。.wxml文件代码示例如下:

    <template is="objectCombine" data="{{foo, bar}}"></template>

.js文件代码示例如下:

        Page({
          data: {
            foo: 'my-foo',
            bar: 'my-bar'
          }
        })

最终组合成对象{foo: 'my-foo', bar:'my-bar'}。

注意

上述方式可以随意组合,但如存在变量名相同的情况,后边的会覆盖前面的,如:

          <template is="objectCombine" data="{{...obj1, ...obj2, a, c: 6}}"></template>
          Page({
            data: {
              obj1: {
                a: 1,
                b: 2
              },
              obj2: {
                b: 3,
                c: 4
              },
              a: 5
            }
          })

最终组合成的对象是{a: 5, b: 3, c: 6}。花括号和引号之间如果有空格,将最终被解析成字符串。

.wxml文件代码示例如下:

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

等同于代码:

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

2.5.2 列表渲染

列表渲染是指,在小程序.js文件里,把data定义的数组数据通过for循环语句显示在.wxml页面中。当然data里面定义的各类数据可以通过其他方式进行变更。在实际的应用中,通过列表渲染可以输出产品列表、新闻列表等。

1. wx:for

在组件上使用wx:for控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件。

数组的当前项的下标变量名默认为index,数组当前项的变量名默认为item, .wxml文件代码示例如下:

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

.js文件代码示例如下:

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

使用wx:for-item可以指定数组当前元素的变量名,使用wx:for-index可以指定数组当前下标的变量名,.wxml文件代码示例如下:

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

wx:for也可以嵌套,下面是一个九九乘法表的.wxml文件代码示例:

        <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>

2. block wx:for

可以将wx:for用在<block/>标签上,以渲染一个包含多节点的结构块。.wxml文件代码示例如下:

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

注意

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

3. wx:key

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

wx:key的值有以下两种形式。

❏ 字符串:代表在for循环的array中item的某个property,该property的值需要是列表中唯一的字符串或数字,且不能动态改变。

❏ 保留关键字(*this):代表在for循环中的item本身,这种表示需要item本身是一个唯一的字符串或者数字,例如,当数据改变触发渲染层重新渲染的时候,会校正带有key的组件,框架会将它们重新排序,而不是重新创建,以确保组件保持自身的状态,并且提高列表渲染时的效率。如不提供wx:key,会报错;如果明确知道该列表是静态的,或者不必关注其顺序,可以选择忽略。

.wxml文件代码示例如下:

        <switch wx:for="{{objectArray}}" wx:key="unique" style="display: block; "> {{item.
            id}} </switch>
        <button bindtap="switch"> Switch </button>
        <button bindtap="addToFront"> Add to the front </button>
        <switch wx:for="{{numberArray}}" wx:key="*this" style="display: block; "> {{item}}
            </switch>
        <button bindtap="addNumberToFront"> Add to the front </button>

.js文件代码示例如下:

        Page({
          data: {
            objectArray: [
              {id: 5, unique: 'unique_5'},
              {id: 4, unique: 'unique_4'},
              {id: 3, unique: 'unique_3'},
              {id: 2, unique: 'unique_2'},
              {id: 1, unique: 'unique_1'},
              {id: 0, unique: 'unique_0'},
            ],
            numberArray: [1, 2, 3, 4]
          },
          switch: function(e) {
            const length = this.data.objectArray.length
            for (let i = 0; i < length; ++i) {
              const x = Math.floor(Math.random() * length)
              const y = Math.floor(Math.random() * length)
              const temp = this.data.objectArray[x]
              this.data.objectArray[x] = this.data.objectArray[y]
              this.data.objectArray[y] = temp
            }
            this.setData({
              objectArray: this.data.objectArray
            })
          },
          addToFront: function(e) {
            const length = this.data.objectArray.length
            this.data.objectArray  =  [{id:  length,  unique:  'unique_'  +  length}].concat
                (this.data.objectArray)
            this.setData({
              objectArray: this.data.objectArray
            })
          },
          addNumberToFront: function(e){
            this.data.numberArray  =  [  this.data.numberArray.length  +  1  ].concat(this.
                data.numberArray)
            this.setData({
              numberArray: this.data.numberArray
            })
          }
        })

注意,当wx:for的值为字符串时,会将字符串解析成字符串数组,.wxml文件代码示例如下:

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

等同于:

        <view wx:for="{{['a', 'r', 'r', 'a', 'y']}}">
          {{item}}
        </view>

花括号和引号之间如果有空格,将最终被解析成字符串,.wxml文件代码示例如下:

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

等同于:

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

2.5.3 条件渲染

条件渲染是指,根据if语句中的条件来决定if语句所在区块是显示还是隐藏,如果if条件语句为True,则渲染显示;如果if条件语句为False,则隐藏不做渲染显示。在实际开发中,可以通过按钮来改变if条件语句的变量状态(True和False之间转换)来实现某个区块的显示或隐藏。

1. wx:if

在框架中,使用wx:if="{{condition}}"来判断是否需要渲染该代码块,.wxml文件代码示例如下:

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

也可以用wx:elif和wx:else来添加一个else块,.wxml文件代码示例如下:

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

2. block wx:if

因为wx:if是一个控制属性,需要将它添加到一个标签上。如果要一次性判断多个组件标签,可以使用一个<block/>标签将多个组件包装起来,并在上边使用wx:if控制属性,.wxml文件代码示例如下:

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

3. wx:if vs hidden

因为wx:if之中的模板也可能包含数据绑定,所以当wx:if的条件值切换时,框架有一个局部渲染的过程,以确保条件块在切换时销毁或重新渲染。

同时,wx:if也是惰性的,如果初始渲染条件为False,框架什么也不做,在条件第一次变成真的时候才开始局部渲染。相比之下,hidden就简单多了,组件始终会被渲染,只是简单地控制显示与隐藏。一般来说,wx:if有更高的切换消耗,而hidden有更高的初始渲染消耗。因此,在需要频繁切换的情景下,用hidden更好;而在运行时条件不大可能改变时,则用wx:if较好。

2.5.4 模板

模板用于将一些公用的WXML代码单独整理成一个.wxml文件,然后在有需要的地方直接引入即可。可以在模板中定义代码片段,然后在不同的地方调用。

1.定义模板

使用name属性作为模板的名字,然后在<template/>内定义代码片段。.wxml文件代码示例如下:

        <! --
          index: int
          msg: string
          time: string
        -->
        <template name="msgItem">
          <view>
            <text> {{index}}: {{msg}} </text>
            <text> Time: {{time}} </text>
          </view>
        </template>

2.使用模板

使用is属性声明需要使用的模板,然后将模板所需要的data传入。.wxml文件代码示例如下:

        <template is="msgItem" data="{{...item}}"/>

.js文件代码示例如下:

        Page({
          data: {
          item: {
            index: 0,
            msg: 'this is a template',
            time: '2016-09-15'
            }
          }
        })

is属性可以使用Mustache语法,动态决定具体需要渲染哪个模板。.wxml文件代码示例如下:

        <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>

模板拥有自己的作用域,只能使用data传入的数据以及模板定义文件中定义的<wxs />模块。

2.5.5 事件

事件是控件可以识别的操作,如按下某个按钮,选择某个复选框。每一种控件有自己可以识别的事件,如小程序页面的加载、按钮的单击、表单的提交等事件,也包括编辑框(文本框)的文本改变事件等。

事件有如下特征:

❏ 事件是视图层到逻辑层的通信方式。

❏ 事件可以将用户的行为反馈到逻辑层进行处理。

❏ 事件可以绑定在组件上,当达到触发事件,就会执行逻辑层中对应的事件处理函数。

❏ 事件对象可以携带额外信息,如id、dataset、touches。

1.事件的使用方式

在组件中绑定一个事件处理函数,如bindtap,当用户点击该组件的时候,会在该页面对应的Page中找到相应的事件处理函数,.wxml文件代码示例如下:

        <view id="tapTest" data-hi="WeChat" bindtap="tapName"> Click me! </view>

在相应的Page定义中写入相应的事件处理函数,参数是event, .js文件代码示例如下:

        Page({
          tapName: function(event) {
            console.log(event)
          }
        })

可以看到,log出来的信息大致如下:

        {
          "type":"tap",
          "timeStamp":895,
          "target": {
            "id": "tapTest",
            "dataset":  {
              "hi":"WeChat"
            }
          },
          "currentTarget":  {
            "id": "tapTest",
            "dataset": {
              "hi":"WeChat"
            }
          },
          "detail": {
            "x":53,
            "y":14
          },
        "touches":[{
          "identifier":0,
          "pageX":53,
          "pageY":14,
          "clientX":53,
          "clientY":14
        }],
        "changedTouches":[{
          "identifier":0,
          "pageX":53,
          "pageY":14,
          "clientX":53,
          "clientY":14
        }]
      }

2.事件分类

事件分为冒泡事件和非冒泡事件。

❏ 冒泡事件:当一个组件上的事件被触发后,该事件会向父节点传递。

❏ 非冒泡事件:当一个组件上的事件被触发后,该事件不会向父节点传递。

WXML的冒泡事件参见表2-14。

表2-14 WXML的冒泡事件列表

注意

除上表之外的其他组件自定义事件如无特殊声明都是非冒泡事件,如<form/>的submit事件,<input/>的input事件,<scroll-view/>的scroll事件。

3.事件绑定和冒泡

事件绑定的写法与组件的属性相关,分为key和value两种形式:

❏ key以bind或catch开头,后跟事件的类型,如bindtap、catchtouchstart。自基础库版本1.5.0起,在非原生组件中,bind和catch后可以紧跟一个冒号,其含义不变,如bind:tap、catch:touchstart。

❏ value是一个字符串,需要在对应的Page中定义同名的函数,否则当触发事件的时候会报错。

提示

bind事件绑定不会阻止冒泡事件向上冒泡,catch事件绑定可以阻止冒泡事件向上冒泡。

例如,在下面这个例子中,点击inner view会依次调用handleTap3和handleTap2(因为tap事件会冒泡到middle view,而middle view阻止了tap事件冒泡,不再向父节点传递),点击middle view会触发handleTap2,点击outer view会触发handleTap1, .wxml文件代码示例如下:

        <view id="outer" bindtap="handleTap1">
          outer view
          <view id="middle" catchtap="handleTap2">
            middle view
            <view id="inner" bindtap="handleTap3">
              inner view
            </view>
          </view>
        </view>

4.事件的捕获阶段

自基础库版本1.5.0起,触摸类事件支持捕获阶段。捕获阶段位于冒泡阶段之前,在捕获阶段中,事件到达节点的顺序与冒泡阶段恰好相反。需要在捕获阶段监听事件时,可以采用capture-bind、capture-catch关键字,后者将中断捕获阶段和取消冒泡阶段。

在下面的.wxml文件代码中,点击inner view会依次调用handleTap2、handleTap4、handleTap3、handleTap1:

        <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>

如果将上面代码中的第一个capture-bind改为capture-catch,将只触发handleTap2。.wxml文件代码示例如下:

        <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>

5.事件对象

如无特殊说明,当组件触发事件时,逻辑层绑定该事件的处理函数会收到一个事件对象。

BaseEvent基础事件对象属性如下:

其中,type代表事件的类型。

timeStamp页面打开到触发事件所经过的毫秒数。

target触发事件的源组件,属性如下:

currentTarget事件绑定的当前组件,属性如下:

说明:target和currentTarget可以参考上例,点击inner view时,handleTap3收到的事件对象target和currentTarget都是inner,而handleTap2收到的事件对象target就是inner, currentTarget就是middle。

CustomEvent自定义事件对象属性(继承BaseEvent)如下:

TouchEvent触摸事件对象属性(继承BaseEvent)如下:

特殊事件:<canvas/>中的触摸事件不可冒泡,所以没有currentTarget。

6. dataset

在组件中可以定义数据,这些数据将会通过事件传递给SERVICE。书写方式:以data-开头,多个单词由连字符-链接,不能有大写(大写会自动转成小写),如data-element-type,最终在event.currentTarget.dataset中会将连字符转成驼峰形式,如elementType。

.wxml文件代码示例如下:

        <view data-alpha-beta="1" data-alphaBeta="2" bindtap="bindViewTap"> DataSet Test
            </view>

.js文件代码示例如下:

        Page({
          bindViewTap:function(event){
            event.currentTarget.dataset.alphaBeta === 1 // -会转为驼峰写法
            event.currentTarget.dataset.alphabeta === 2 //大写会转为小写
          }
        })

7. touches

touches是一个数组,每个元素为一个Touch对象(canvas触摸事件中携带的touches是CanvasTouch数组)。表示当前停留在屏幕上的触摸点。

Touch对象属性如下:

8. changedTouches

changedTouches数据格式同touches,表示有变化的触摸点,如从无变有(touchstart),位置变化(touchmove),从有变无(touchend、touchcancel)。

9. detail

自定义事件所携带的数据,如表单组件的提交事件会携带用户的输入,媒体的错误事件会携带错误信息,详见组件定义中各个事件的定义。

点击事件的detail带有的x和y,同pageX和pageY,代表到文档左上角的距离。

2.5.6 引用

WXML提供两种文件引用方式:import和include。

1. import

import可以在该文件中使用目标文件定义的template,例如,在item.wxml中定义了一个名为item的template, .wxml文件代码示例如下:

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

在index.wxml中引用了item.wxml,就可以使用item模板,.wxml文件代码示例如下:

        <import src="item.wxml"/>
        <template is="item" data="{{text: 'forbar'}}"/>

import有作用域的概念,即只输入目标文件中定义的模板,而不输入目标文件自己输入的模板。例如,C import B, B import A,在C中可以使用B定义的template,在B中可以使用A定义的template,但是C不能使用A定义的template。

.wxml文件代码示例如下:

        <! -- A.wxml -->
        <template name="A">
          <text> A template </text>
        </template>

.wxml文件代码示例如下:

        <! -- B.wxml -->
        <import src="a.wxml"/>
        <template name="B">
          <text> B template </text>
        </template>

.wxml文件代码示例如下:

        <! -- C.wxml -->
        <import src="b.wxml"/>
        <template is="A"/>  <! -- Error! Can not use tempalte when not import A. -->
        <template is="B"/>

2. include

include可以将目标文件除<template/> <wxs/>之外的整个代码引入,相当于拷贝到include位置,.wxml文件代码示例如下:

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

.wxml文件代码示例如下:

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

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