小程序框架   本章主要内容是小程序框架,包括逻辑层、视图层和flex模型。逻辑层是由JavaScript编写而成的,视图层由WXML和WXSS配合组件构成,flex布局可以确保页面需要适应不同屏幕尺寸及设备类型时元素在恰当的位置。 * 掌握注册程序和页面相关函数的用法; * 掌握页面路由的方式和模块化的用法; * 掌握WXML的数据绑定、列表/条件渲染、模板、事件和引用; * 掌握WXSS的尺寸单位、使用方式和选择器的用法; * 了解flex布局的基本概念; 掌握flex布局中的容器属性和项目属性。* 3.1 逻辑层   小程序开发框架的逻辑层又称为App Service,是由JavaScript编写和实现的。开发者 写的所有代码最后将被打包成一份JavaScript,并在小程序启动的时候运行,直到小程序被 销毁。   逻辑层的主要作用是处理数据后发送给视图层渲染以及接收视图层的事件反馈。   为了更方便地进行项目开发,小程序在JavaScript的基础上进行了一些优化,例如:   (1)新增App()和Page()方法,分别用于整个应用程序和单独页面的注册。   (2)新增getApp()和getCurrentPages()方法,分别用于获取整个应用实例和当前页面 实例。   (3)提供丰富的微信原生API,例如可以方便地获取微信用户信息、本地存储、扫一扫、微信支付、微信运动等特殊功能。   (4)每个页面具有独立的作用域,并提供模块化功能。   需要注意的是,由于框架不在浏览器中运行,所以JavaScript与浏览器相关的一些功能无法使用,例如document、window等。 3.1.1 注册程序   App()方法   小程序通过使用App(OBJECT)方法进行应用注册,用其指定小程序的生命周期函数。   OBJECT参数如表3-1所示。 表3-1 App()方法的OBJECT参数 属 性 类 型 描 述 触 发 时 机 备 注 onLaunch() Function 生命周期函数——监听小程序的初始化 当小程序初始化完成时触发 全局只触发1次 onShow() Function 生命周期函数——监听小程序的显示 当小程序启动或从后台进入前台显示时触发 onHide() Function 生命周期函数——监听小程序的隐藏 当小程序从前台进入后台隐藏时触发 onError() Function 错误监听函数 当小程序发送脚本错误或API调用失败时触发 onPageNotFound() Function 页面不存在函数 当小程序需要打开的页面不存在时触发 其他自定义参数 Any 开发者可以添加自定义名称的函数或数据到OBJECT参数中 用this可以访问      注意:App()方法只能写在小程序根目录下的app.js文件中,并且只能注册1个。      用户可以使用微信web开发者工具在空白app.js文件中直接输入关键词app,此时会自动出现提示列表,如图3-1所示。 图3-1 app.js的代码提示列表   默认选择提示列表中的第一项,直接按回车键就可以自动生成带有生命周期全套函数的代码结构,如图3-2所示。   事实上,App()中的这些函数均为可选函数,开发者可以根据实际需要删除其中的部分函数,或保留这些函数但空着不填充内容。   第12、19行注释语句均提到了小程序后台和前台的概念,具体说明如下。 * 小程序后台:指的是小程序没有在手机当前画面显示,但是并没有被销毁。当用户 单击左上角的按钮关闭小程序或者按设备的Home键离开微信时会进入后台运行 状态。 * 小程序前台:指的是小程序在手机当前画面被使用。当用户再次打开处于后台运行状态的小程序时会重新进入前台运行状态。      注意:只有当小程序进入后台一定时间或者系统资源占用过高时才会被真正地销毁。 图3-2 app.js自动生成App()代码   由图3-2中的代码可见,onLaunch()、onShow()和onError()方法在触发时均会返回参数,用户可以利用这些参数进行状态的判断与处理。其中,onLaunch()与onShow()方法返回的参数名称完全相同,具体如表3-2所示。 表3-2 onLaunch()和onShow()方法返回的参数 字 段 类 型 说 明  path String  打开小程序的路径  query Object  打开小程序的query  scene Number  打开小程序的场景值  shareTicket String  小程序被转发时会生成一个shareTicket,打开被转发的小程序页面可以获取该参数  referrerInfo Object  当场景为从另一个小程序/公众号/App打开时返回此字段  referrerInfo.appId String  跳转前的小程序/公众号/App的appId  referrerInfo.extraData Object  跳转前的小程序传来的数据,当scene=1037或1038时才支持      支持返回referrerInfo.appId的场景值如表3-3所示。 表3-3 支持返回referrerInfo.appId的场景值 场 景 值 场 景 appId信息的含义 1020   公众号profile页的相关小程序列表 返回来源公众号appId 1035   公众号自定义菜单 返回来源公众号appId 1036   App 分享消息卡片 返回来源应用appId 1037   小程序打开小程序 返回来源小程序appId 1038   从另一个小程序返回 返回来源小程序appId 1043   公众号模板消息 返回来源公众号appId   说明:关于场景值的更多介绍,见附录。   除了函数外,App()也支持添加自定义的全局变量,示例代码如下:    1. App({ 2. globalData: { 3. userInfo: null //这是一个全局变量 4. } 5. })      这里,全局变量的名称、取值和数量都可以由开发者自定义。   onPageNotFound()方法   当需要打开的页面不存在时,微信客户端会有一个原生模板页面提示。如果开发者不希望跳转到此页面,想自行处理,则需用到onPageNotFound()方法。   该方法从基础库1.9.90开始支持,低版本需要做兼容处理。当要打开的页面并不存在时,会回调这个方法并带有3个参数,具体参数内容如表3-4所示。 表3-4 onPageNotFound()方法的参数 字 段 类 型 说 明 path String 不存在页面的路径 query Object 打开不存在页面的query isEntryPage Boolean 是否为本次启动的首个页面(例如从分享等入口进来,首个页面是开发者配置的分享页面)      onPageNotFound()方法的示例代码如下:    1. App({ 2. onPageNotFound: function(res) { 3. //小程序打开的页面不存在时需要执行的代码 4. wx.redirectTo({ 5. url: 'pages/test/test' 6. }) //如果是tabBar页面,请使用wx.switchTab 7. } 8. })      上述代码可以用指定的页面代替原生模板页面。需要注意的是,如果 onPageNotFound() 回调中又重定向到另一个不存在的页面,将重定向到微信自带的原生模板页面提示页面不存在,并且不再触发onPageNotFound()监听。   getApp()方法   在小程序的其他JS文件中均可以使用全局的getApp()方法获取小程序实例。   例如,test.js,示例代码如下:    1. var app=getApp() 2. console.log(app.globalData.userInfo)      此时,就可以在test.js页面获得app.js中保存的公共数据,并在console控制台打印输出。   需要注意的是,用户不可以在app.js的App()函数内部调用getApp()方法,可以直接使用关键字this代替。例如:    1. App({ 2. globalData: { 3. userInfo: null //这是一个全局变量 4. } 5. onLoad:function(options){ 6. console.log(this.globalData.userInfo) //使用this关键字获得全局变量 7. } 8. })      上述代码就在onLoad()方法中直接使用了this关键字获得全局变量。 3.1.2 注册页面   小程序在每个页面JS文件中通过使用Page(OBJECT)方法进行页面注册,该方法可以用于指定小程序页面的生命周期函数。Page()方法的OBJECT参数如表3-5所示。 表3-5 Page()方法的OBJECT参数 属 性 类 型 说 明 data Object 页面的初始数据 onLoad() Function 生命周期函数——监听页面的加载 onReady() Function 生命周期函数——监听页面初次渲染完成 onShow() Function 生命周期函数——监听页面的显示 onHide() Function 生命周期函数——监听页面的隐藏 onUnload() Function 生命周期函数——监听页面的卸载 onPullDownRefresh() Function 页面相关事件处理函数——监听用户下拉动作 onReachBottom() Function 页面上拉触底事件的处理函数 onShareAppMessage() Function 用户单击右上角转发 onPageScroll() Function 页面滚动触发事件的处理函数 onTabItemTap() Function 若当前是tab页,单击tab时触发 其他 Any 开发者可以添加任意函数或数据到OBJECT参数中,在页面的函数中用?this?可以访问      注意:Page()方法只能写在小程序每个页面对应的JS文件中,并且每个页面只能注册1个。      在微信web开发者工具中新建页面时会自动生成页面JS文件的Page()方法。这里以test页面为例,创建完成后test.js的代码如下:    1. // pages/test/test.js 2. Page({ 3. /** 4. * 页面的初始数据 5. */ 6. data: { 7. 8. }, 9. /** 10. * 生命周期函数——监听页面的加载 11. */ 12. onLoad: function(options) { 13. 14. }, 15. /** 16. * 生命周期函数——监听页面初次渲染完成 17. */ 18. onReady: function() { 19. 20. }, 21. /** 22. * 生命周期函数——监听页面的显示 23. */ 24. onShow: function() { 25. 26. }, 27. /** 28. * 生命周期函数——监听页面的隐藏 29. */ 30. onHide: function() { 31. 32. }, 33. /** 34. * 生命周期函数——监听页面的卸载 35. */ 36. onUnload: function() { 37. 38. }, 39. /** 40. * 页面相关事件处理函数——监听用户下拉动作 41. */ 42. onPullDownRefresh: function() { 43. 44. }, 45. /** 46. * 页面上拉触底事件的处理函数 47. */ 48. onReachBottom: function() { 49. 50. }, 51. /** 52. * 用户单击右上角分享 53. */ 54. onShareAppMessage: function() { 55. 56. } 57.})      与App()方法的函数情况类似,开发者同样可以根据实际情况删除Page()中不需要的函数,或者保留该函数内部为空白。   除了函数外,Page()同样也支持添加自定义的页面变量,示例代码如下:    1. Page({ 2. myData: '123', //定义页面变量 3. onLoad: function(options) { 4. console.log(this.myData) //使用this关键字调用页面变量 5. }, 6. })      这里,变量的名称、取值和数量也都可以由开发者自定义。   初始数据   在Page()方法中默认生成的第一项就是data属性,该属性是页面第一次渲染使用的初始数据。当页面加载时,data将会以JSON字符串的形式由逻辑层传至渲染层,因此data中的数据必须是可以转成JSON的类型,例如字符串、数字、布尔值、对象、数组。渲染层可以通过WXML对数据进行绑定。   例如在data中放置两个自定义数据,页面JS文件的示例代码如下:    1. Page({ 2. data:{ 3. msg01: 'Hello', 4. msg02: 2018 5. } 6. })      对应WXML的示例代码如下:    {{msg01}} {{msg02}}      此时{{msg01}}和{{msg02}}不会显示字面内容,而是会查找data中的初始数据,然后显示出“Hello 2018”字样。   生命周期回调函数   Page()函数中默认生成的onLoad()、onShow()、onReady()、onHide()以及onUnload()均属于页面的生命周期回调函数,具体说明如下。 * onLoad():格式为onLoad(Object query),只在页面加载时触发一次,可以在onLoad()的参数中获取打开当前页面路径附带的参数。 * onShow():当页面显示或从小程序后台切入前台时触发。 * onReady():当页面初次渲染完成时触发。注意,一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互。 * onHide():当页面隐藏/切入后台时触发。例如navigateTo或底部tab切换到其他页面,小程序切入后台等。 * onUnload():当页面卸载时触发。例如redirectTo或navigateBack到其他页面时。   页面事件处理函数   Page()方法中默认生成的onPullDownRefresh()、onReachBottom()、onShareAppMessage()以及未自动生成的onPageScroll()、onTabItemTap()均属于页面事件处理函数,具体说明如下。 * onPullDownRefresh():监听用户下拉刷新事件,需要在app.json的window选项中或页面配置中开启enablePullDownRefresh。 * onReachBottom():监听用户上拉触底事件,可以在app.json的window选项中或页面配置中设置触发距离onReachBottomDistance。在触发距离内滑动期间,本事件只会被触发一次。 * onPageScroll(OBJECT):监听用户滑动页面事件。其参数OBJECT具有唯一属性scrollTop,该属性为Number类型,表示页面在垂直方向已滚动的距离(单位为px)。 * onShareAppMessage(OBJECT):监听用户单击页面内“转发”按钮(      在JS的Page()方法中相关代码如下:    1. Page({ 2. btnTap: function() { 3. console.log('按钮被单击。') 4. } 5. })      除了bindtap可以绑定单击事件外,还有很多事件,详见3.2.1节“WXML”。   route   在Page()方法中可以使用this.route查看当前页面的路径地址。例如:    1. Page({ 2. onShow: function() { 3. console.log(this.route) 4. } 5. })      setData()   在Page()方法中,setData()可以用来同步更新data属性中的数据值,也能异步更新相关数据到WXML页面上。其参数说明如表3-9所示。 表3-9 setData()参数 字 段 类 型 必填 说 明 最 低 版 本 data Object 是 要更新的一个或多个数据,格式为{key1:value1, key2:value2,…, keyN:valueN} callback Function 否 setData引起的界面更新渲染完毕后的回调函数 1.5.0      例如在Page()的data中定义初始数据,JS文件代码如下:    1. Page({ 2. data:{ 3. today: '2018-01-01' 4. } 5. })      此时WXML页面的{{today}}初始值为2018-01-01。   为组件追加自定义单击事件changeData,WXML页面代码如下:    {{today}}      在Page()中追加自定义函数changeData()的具体内容,JS文件代码如下:    1. Page({ 2. changeData:function{ 3. this.setData({today: '2018-09-09'}) 4. } 5. })      如果用户触发了该组件的单击事件,WXML中的{{today}}值将立刻更新成2018-09-09。   setData()方法在使用时不是必须事先在Page()方法的data中定义初始值,可以在data数据空白的情况下直接用该方法设置一些新定义的变量。   如果想读取data中的数值,可以使用this.data的形式。例如,上述代码如果只是想获得当前today值,可以用this.data.today表示。   生命周期   小程序应用与页面有各自的生命周期函数,它们在使用过程中也会互相影响。   小程序应用生命周期如图3-3所示。 图3-3 小程序应用生命周期   小程序在被打开时会首先触发onLaunch()进行程序启动,完成后调用onShow()准备展示页面,如果被切换进入后台会调用onHide(),直到下次程序在销毁前重新被唤起会再次调用onShow()。   小程序页面生命周期如图3-4所示。 图3-4 小程序页面生命周期   在小程序应用生命周期调用完onShow()以后就准备触发小程序页面生命周期了。页面初次打开会依次触发onLoad()、onShow()、onReady()这3个函数。同样,如果被切换到后台,会调用页面onHide(),从后台被唤醒会调用页面onShow()。直到页面关闭会调用onUnload(),下次打开还会依次触发onLoad()、onShow()、onReady()这3个函数。   需要注意的是,tab页面的互相切换以及在当前页面上打开一个新页面都不算页面关闭,只是起到了隐藏的作用。这几种情况只会触发onHide(),回到此页面只触发onShow(),具体情况见3.1.3节“页面路由”。 3.1.3 页面路由   页面栈   在小程序中页面之间的切换路由均由框架统一进行管理,框架以栈的形式维护了当前的所有页面。当发生路由切换的时候,页面栈的表现如表3-10所示。 表3-10 路由方式与页面栈的表现 路 由 方 式 页面栈的表现 初始化     新页面入栈 打开新页面     新页面入栈 页面重定向     当前页面出栈,新页面入栈 页面返回     页面不断出栈,直到目标返回页面 tab切换     页面全部出栈,只留下新的tab页面 重加载     页面全部出栈,只留下新的页面      获取页面栈   小程序使用getCurrentPages()方法获取当前页面栈的实例,实例将以数组形式按栈的顺序给出。其中,第一个元素为首页,最后一个元素为当前页面。   路由方式   路由方式及页面生命周期函数如表3-11所示。 表3-11 路由方式与页面生命周期函数 路 由 方 式 触 发 时 机 路由前页面 路由后页面 初始化 小程序打开的第一个页面   onLoad()、onShow() 打开新页面 调用API?wx.navigateTo?或使用组件? onHide() onLoad()、onShow() 页面重定向 调用API?wx.redirectTo?或使用组件? onUnload() onLoad()、onShow() 页面返回 调用API?wx.navigateBack?或使用组件或用户单击左上角的“返回”按钮 onUnload() onShow() tab切换 调用API wx.switchTab?或使用组件?或用户切换tab   请参考表3-12 重启动 调用API?wx.reLaunch或使用组件 onUnload() onLoad()、onShow()      的用法见第4章“小程序组件”,API的用法见第11章“界面API”。   由于tab页面的切换情况比较复杂,这里用A、B、C、D几个页面举例说明。假设其中A、B为tabBar页面,C是从A打开的页面,D是从C打开的页面,tab切换对应的生命周期如表3-12所示。 表3-12 tab切换对应的生命周期函数 当 前 页 面 路由后页面 触发的生命周期(按顺序)  A A  不触发任何内容  A B  A.onHide()、B.onLoad()、B.onShow()  A B(再次打开)  A.onHide()、B.onShow()  C A  C.onUnload()、A.onShow()  C B  C.onUnload()、B.onLoad()、B.onShow()  D B  D.onUnload()、C.onUnload()、B.onLoad()、B.onShow()  D(从转发进入) A  D.onUnload()、A.onLoad()、A.onShow()  D(从转发进入) B  D.onUnload()、B.onLoad()、B.onShow() 3.1.4 模块化   文件的作用域   在小程序的任意JS文件中声明的变量和函数只在该文件中有效,不同的JS文件中可以声明相同名字的变量和函数,不会互相影响。   如果需要跨页面进行数据共享,可以在app.js中定义全局变量,然后在其他JS文件中使用getApp()获取和更新。例如在app.js中设置全局变量msg,代码如下:    1. App({ 2. globalData: { 3. msg: ' Goodbye 2018' //这是一个全局变量 4. } 5. })      假设在test.js文件中希望修改全局变量msg的值,代码如下:    1. var app=getApp() 2. app.globalData.msg='Hello 2019' //全局变量被更新      此时在任意其他JS文件中再读取msg的值都会是更新后的内容。   模块的调用   小程序支持将一些公共JavaScript代码放在一个单独的JS文件中,作为一个公共模块,可以被其他JS文件调用。注意,模块只能通过module.exports或者exports对外提供接口。   例如在根目录下新建utils文件夹并创建公共JS文件common.js,代码如下:    1. function sayHello(name) { 2. console.log('Hello ${name} ! ') 3. } 4. function sayGoodbye(name) { 5. console.log('Goodbye ${name} ! ') 6. } 7. module.exports.sayHello=sayHello //推荐使用这种语法提供接口 8. exports.sayGoodbye=sayGoodbye      上述代码创建了两个自定义函数,即sayHello()和sayGoodbye(),且都带有参数name。需要注意的是,exports是module.exports的一个引用,因此在模块中随意更改exports的指向会造成未知错误。   在页面JS中使用require引用common.js文件,此时可以调用其中的函数,代码如下:    1. var common=require('../../utils/common.js') //目前暂时不支持绝对路径地址 2. Page({ 3. hello: function() { 4. common.sayHello('2019') 5. }, 6. goodbye: function() { 7. common.sayGoodbye('2018') 8. } 9. }) 3.1.5 API 小程序开发框架提供丰富的微信原生API接口,可以方便地调用微信提供的功能,例如获取用户信息、本地存储、地理定位等。常用的API如下。 * 网络:实现小程序与服务器端的网络交互。 * 媒体:实现图片、录音、音/视频和相机管理。 * 文件:实现临时文件和本地文件的管理。 * 数据:实现小程序本地数据的缓存。 * 位置:使用小程序获取地理位置和控制地图组件。 * 设备:获得手机内存、网络、传感器、扫码、剪贴板、振动等功能。 * 界面:实现交互反馈、导航条设置、页面导航、动画、绘图等功能。   这些API及其相关标签会在后续章节陆续介绍。 3.2 视图层   视图层主要负责视图的显示,WXML页面、WXSS样式表和组件都是视图层的内容。 3.2.1 WXML   WXML的全称是WeiXin Markup Language(微信标记语言),类似于HTML,也是一种使用<标签>和构建页面结构的语言。   WXML具有数据绑定、列表渲染、条件渲染、模板、事件和引用的功能。   数据绑定   1)简单绑定   在WXML页面中可以使用{{变量名}}的形式表示动态数据。例如:    {{msg}}      此时WXML页面上不会显示msg这个词,而是会显示这个变量对应的值。动态数据的值都来自JS文件的data属性中的同名变量。例如:    1. Page({ 2. data: { 3. msg: '你好!' 4. } 5. })      上述代码会把“你好!”渲染到WXML页面上{{msg}}出现的地方。   2)组件属性绑定   组件的属性也可以使用动态数据,例如组件的id、class等属性值。   WXML页面的相关代码如下:    测试      JS文件的相关代码如下:    1. Page({ 2. data: { 3. id: 'myView' 4. } 5. })      3)控制属性绑定   控制属性也可以使用动态数据,但必须在引号内。   WXML页面的相关代码如下:    测试      JS文件的相关代码如下:    1. Page({ 2. data: { 3. condition: false 4. } 5. })      上述代码表示测试组件不被显示出来。   4)关键字绑定   如果直接在引号内写布尔值也必须用双花括号括起来,例如:    1. 测试1 2. 测试2      注意:不可以去掉双花括号直接写成wx:if='false',此时false会被认为是一个字符串,转换为布尔值后表示true。      5)运算绑定   在双花括号内部还可以进行简单的运算,其支持的运算有三元运算、算术运算、逻辑判断、字符串运算和数据路径运算。   三元运算的示例代码如下:    1. 2. 1. //JS文件代码 2. Page({ 3. data: { 4. result: false 5. } 6. })      算术运算的示例代码如下:    1. 2. {{a + b}} + {{c}} + d 1. //JS文件代码 2. Page({ 3. data: { 4. a:1, b:2, c:3 5. } 6. })      双花括号内的a+b会进行算术运算,但是注意括号外面的+会原封不动地显示出来,不起任何算术运算作用。   逻辑判断的示例代码如下:    1. 2. 该组件将被显示 1. //JS文件代码 2. Page({ 3. data: { 4. x: 99 5. } 6. })      此时,判断x>5返回true,因此wx:if条件成立。   字符串运算的示例代码如下:    1. 2. {{'你好'+name}} 1. //JS文件代码 2. Page({ 3. data: { 4. name: '小程序' 5. } 6. })      此时,双花括号中的“+”号起到了连接前后字符串的作用。   数据路径运算的示例代码如下:    1. 2. {{object.key1}} {{array[1]}} 1. //JS文件代码 2. Page({ 3. data: { 4. object: { 5. key1: 'Hello ', 6. key2: 'Goodbye ' 7. }, 8. array: ['2018', '2019', '2020'] 9. } 10.})      6)组合绑定   用户还可以在双花括号内直接进行变量和值的组合,构成新的对象或者数组。   数组组合的示例代码如下:    1. 2. {{item}} 1. //JS文件代码 2. Page({ 3. data: { 4. x:3 5. } 6. })      上述代码中的x会被替换成数字3,从而形成数组[1,2,3,4]。   对象组合的示例代码如下:    1. 2. 1. //JS文件代码 2. Page({ 3. data: { 4. value1 : 'admin', 5. value2 : '123456' 6. } 7. })      上述代码最终会组合出对象{username: 'admin', password: '123456'}。其中,WXML代码部分使用了