小程序入门02 - 宿主环境

359 阅读9分钟

写在前面:

  1. 本文是单纯的个人学习总结和课堂笔记整理,大部分知识点来自“开课吧”线上直播课程,以及官方资料。
  2. 由于目前仍处于前端学习、入门阶段,因此对于知识的掌握和理解难免会有偏差,因此该文章仅仅是个人记录,请查阅者不要以此作为资料依据。

简介

宿主指的是微信客户端,也就是官方API里的 wx 对象。宿主环境就是微信客户端给小程序提供的一种环境。

宿主环境的主要作用:

  • 宿主环境会把所有文件(wxml、wxss、js、json)统统编译到一个js文件里面,然后通过宿主环境的解析,最终才会在微信APP里面展示出我们日常见到的视图。
  • 宿主环境可以喂小程序提供微信客户端的一些能力,比如微信扫码,这是普通网页不具备的。

当我们点开一个小程序的时候,宿主环境其实进行了如下操作:

  1. 我们点开微信小程序
  2. 微信客户端检索本地缓存中是否有代码包的缓存
    • 如果有,则直接加载
    • 如果没有,则通过网络请求下载代码包
  3. 加载好代码包之后,所有代码会被编译成一个js文件
  4. 微信客户端会根据代码包根目录下的 app.js 内的 APP() 构造器创建APP实例
  5. 在创建APP实例的时候,APP()构造器内定义的onLaunch方法会被微信客户端调用,然后进行登录相关的逻辑
  6. 这些登录相关的逻辑执行之后,就进入到了微信小程序界面
  7. 微信小程序根据我们定义好的索引配置,展示对应的页面

接下来,我们一起看一下宿主环境是如何工作的,以及为我们提供了哪些方便的API和开发方式。

渲染层和逻辑层

首先需要知道上述各类文件的运行环境,其中WXML文件和WXSS文件是运行在渲染层上面,其中的JS文件是运行在逻辑层上面。

微信小程序分别使用两个线程单独管理渲染层和逻辑层,也就是说逻辑层和渲染层是异步的,页面渲染和脚本逻辑二者之间不会阻塞。

渲染层使用webview进行渲染,逻辑层使用jscore进行渲染。这两类线程会经过微信客户端(也就是图中的native)进行通信和中转,此外,逻辑层的网络请求也会通过native转发。

因为个小程序会存在多个页面(page),因此渲染层会存在多个webview线程。

小程序中关于渲染层和逻辑层的详细解释,请移步官方文档

APP构造器

APP构造器是宿主环境提供的一个用来注册程序的APP。

  • App()构造器必须是写在根目录内的 app.js 文件里面。
  • App是单实例对象。整个项目中只能有一个全局对象。
  • App是一个全局对象,就像网页端的window对象。
    • 因此,宿主环境提供了 getApp() 方法让我们在全局获取到app实例,可以通过该方法,在其他 js 文件中获取到 app 内的数据。

前台状态和后台状态

  • 后台状态
    • 用户点击小程序右上角关闭按钮,或手机的 home 键时,会离开小程序,但小程序并不会被销毁,而是进入后台状态。此时,APP构造器参数里的 onHide方法会被调用。
  • 前台服务
    • •前台状态:用户再次小程序时,微信用户端会唤醒后台状态的微信小程序,微信小程序就进入了前台状态,onShow 方法会被调用。

备注:onShowonHide 方法必须是由用户操作手动触发的,不可以在代码内通过方法调用。

如下,是App构造器示例:

App({
  onLaunch() {
    console.log('小程序初始化完成');
    // 展示本地存储能力
    const logs = wx.getStorageSync('logs') || []
    logs.unshift(Date.now())
    wx.setStorageSync('logs', logs)
    // 登录
    wx.login({
      success: res => {
        // 发送 res.code 到后台换取 openId, sessionKey, unionId
      }
    })
    // 获取用户信息
    wx.getSetting({
      success: res => {
        if (res.authSetting['scope.userInfo']) {
          // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
          wx.getUserInfo({
            success: res => {
              // 可以将 res 发送给后台解码出 unionId
              this.globalData.userInfo = res.userInfo
              // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
              // 所以此处加入 callback 以防止这种情况
              if (this.userInfoReadyCallback) {
                this.userInfoReadyCallback(res)
              }
            }
          })
        }
      }
    })
  },
  onHide(){
    console.log('进入后台状态');
    
  },
  onShow(){
    console.log('进入前台状态');
  },
})

页面构造器

每一个页面的js逻辑都是写在对应的page()构造器里面的。page构造器接收一个object参数,在object里面可以绑定数据,监听页面事件以及生命周期函数。

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() { }
})

上述每一个参数我们一一来讲。

数据绑定

数据绑定已经在上一篇文章中写过了,这里不再赘述。

但是要注意的一点是:

  • 用数据驱动视图渲染,也就是修改data时,要用 this.setData(),不可以直接使用 this.data 修改数据,这样不仅无法实现数据驱动视图更新,还会产生数据不一致的错误。
  • 由于 setData 是两个线程间的通信,为了提高性能,每次设置的数据不应超过 1024KB
  • 不要把 data 中的任意一项的 value 设为 undefined,否则可能会有引起一些不可预料的 bug

生命周期

主要有如下生命周期:

  • 页面初次加载时:onLoad。在页面没被销毁之前只会触发1次。
  • 页面显示时:onShow。从别的页面返回到当前页面时,都会被调用。
  • 页面初次渲染完成时:onReady。在页面没被销毁前只会触发1次,在逻辑层可以和视图层进行交互。

此外,当用户操作小程序时,还会触发如下生命周期:

  • 页面不可见时:onHide。wx.navigateTo切换到其他页面、底部tab切换时触发。

  • 返回到其它页时:onUnload。wx.redirectTo或wx.navigateBack使当前页面会被微信客户端销毁回收时触发。

页面的用户行为

  • 下拉刷新 onPullDownRefresh:监听用户下拉刷新事件,需要在全局或具体页面的json 文件中配置 enablePullDownRefreshtrue

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

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

  • 用户转发 onShareAppMessage:只有定义了此事件处理函数,右上角菜单才会显示“转发”按钮,在用户点击转发按钮的时候会调用,此事件需要return一个Object,包含titlepath两个字段,用于自定义转发内容

页面用户行为有很多,这里就简单罗列了几个常见,更详细的请移步官方文档查阅。

页面跳转

页面间跳转主要有三类方式:

  • 在 app.json 里面用 tabBar 属性设置跳转方式

  • 在 wxml 页使用导航组件 <navigator> 跳转页面

  • 在 js 中使用 路由API 跳转

tabBar

tabBar 一般是用来设置页面上的导航栏,最多可以设置五个。

  "tabBar": {
    "selectedColor":"#00acec",
    "list": [
      {
        "selectedIconPath": "images/homeSel.png",
        "iconPath": "images/home.png",
        "pagePath": "pages/index/index",
        "text": "首页"
      },
      {
        "selectedIconPath": "images/heartSel.png",
        "iconPath": "images/heart.png",
        "pagePath": "pages/logs/logs",
        "text": "日志"
      }
    ]
  },

navigator

navigator 标签里面,通过 url 标签声明跳转路由的相对路径即可。

<navigator url="../03-secPage/03-secPage">去二级页</navigator>

路由API

在page.js文件里面声明好事件方法:

toSecPage(){
    wx.navigateTo({
      url:'../03-secPage/03-secPage'
    })
  }

然后在 wxml 文件里面通过行为事件触发:

<button bindtap="toSecPage">js跳转到二级页</button>

自定义组件

一个页面(page)是由很多个标签组成的,而这些标签其实也可以被称为组件(与React Vue一样),宿主环境自带的组件如 text view,有时候并不能满足我们的项目实际需求,因此需要我们自己定义组件。

自定义组件需要按照如下的步骤创建:

  1. 在主项目下建立components 文件夹,在其中建立floatball 文件夹,在此文件夹上右击“新建 Component”,这样就可以建立出json、wxml、wxss、js 四个文件。
  2. 组件的 json 中设置 "component": true
  3. 组件的 wxmlwxss 可以正常写。
  4. 组件的 js 中的 properties 可以接受父组件属性。
  5. 父组件在调用子组件时要在其 json 文件中设置 usingComponents,如:
"usingComponents": {

 "floatball":"/components/floatball/floatball"

}

组件间通信

小程序的组件间通信没有规定必须是像React 那样单项传递数据,因此,有如下两类数据传递方式:

  • 父组件向子组件传递数据:属性

    • 子组件可以使用 Mustache 语法预设一些模板,当父组件没有传递属性时显示默认值,如果父组件传来了属性值,就显示父组件传来的。

    • // child.js
      properties: {
        text:{type:'string',value:'悬浮球'}
      },
      
    • <view class="ball">{{text}}</view>
      
    • 如上述声明,默认会显示 ‘’悬浮球‘’ 三个字

    • 如果父组件传递进来属性:

      <floatball text="组件间通信"></floatball>
      
    • 那么就会显示 “组件间通信”五个字。

  • 子组件向父组件传递数据:事件

    • 父组件通过bind语法绑定上事件后,该事件名会传递到子组件(备注,下面代码里为了方便解释,把事件名和事件值写的不一样,但是实际项目中最好还是写成一样的,省的记错)

      // fater.wxml
      
      <floatball text="组件间通信" bind:onClickBall="doClickBall"></floatball>
      <!-- 
      onClickBall 事件名称,会传递到子组件 
      doClickBall 该事件的处理函数,可以在对应的 js 文件里接收到子组件传递来的数据 -->
      
    • 当子组件接收到该该事件之后,将数据传递到父组件

      // child.wxml
      <view class="ball" bind:handleClickBall="handleClickBall">{{text}}</view>
      
      // child.js
      methods: {
          handleClickBall(){
            // 第二个object参数就是传递子组件传递给父组件的数据
            this.triggerEvent('onClickBall',{tap:false})
          }
        }
      
    • 父组件通过该事件的回调函数接收数据。

      // father.js
      doClickBall(param){
        console.log(param);
      }
      

事件捕捉和事件冒泡

  • 事件冒泡:上面我们写的其实就是事件冒泡。

    • 书写方式:bind:clickBall
    • 执行顺序:当多层组件嵌套时,会先执行外层的组件,然后一层一层的执行进去,直到要我们需要的那一层
  • 事件捕捉:在 bind 前面加上前缀 capture 即为事件捕捉。

    • 书写方式;capture-bind:clickBall
    • 执行顺序:优先执行我们想要的那一层,那一层事件触发之后,再去执行外层组件的事件。