技术博客——微信小程序的架构与原理

时间:2024-01-18 09:21:26

技术博客——微信小程序的架构与原理

  • 在两个月的微信小程序开发过程中,我曾走了不少弯路,也曾被很多现在看来十分可笑的问题所困扰。这些弯路与困扰,基本上都是由于当时对小程序的架构理解不够充分,对小程序的原理学习不够深入。我在解决这些问题的过程中,不仅学到了很多有意义的、对开发有直接帮助的知识点,更在微信小程序的架构与原理上补了不少课,对于我在微信小程序的设计上大有裨益。在这篇博客中,我将平常学习到的关于微信小程序的架构与原理的知识记录下来,同时记录我在一些功能上的代码实现,这些功能的实现曾经困扰过我、让我走了一些弯路。

    微信小程序的推出,使得开发者的技术门槛降低、减少了推广与开发成本,也节约了用户的手机存储空间、节省了使用时间成本。它作为一种全新的连接用户与服务的方式,与传统意义上的应用程序有很多区别,同时在开发过程中也有一些共性。

    小程序的前身是微信的JS API,最初这类API并不是对外暴露的,仅仅是提供给腾讯内部的一些业务使用。后来JS API进一步地引入了更多类别的接口,功能愈加丰富,包括拍摄、录音、语音识别以及二维码等重要功能,这些API整合成为一套完善的、公开的网页开发工具包,称为JS-SDK。JS-SDK在Web技术之上,做了很多方面的优化,使得Web开发者具有更多的能力。例如JS-SDK中的“微信Web资源离线存储”功能,该功能通过使用微信离线存储,使得Web开发者可以借助微信提供的存储能力,直接从微信本地加载Web资源,减少网页加载时间。

  • 双线程机制

    • 小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。网页开发中的渲染线程和脚本线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应。而在小程序中,二者是分开的,分别运行在不同的线程中。其中渲染层(对应渲染线程)在webview中渲染,一个页面对应一个webview,所以渲染层会存在多个webview线程;逻辑层(对应脚本线程)运行在JSCore线程中。渲染层和逻辑层之间通过微信客户端(Native)做中转,而Native又与第三方服务器之间进行网络通信。
  • 程序与页面

    • 整个小程序只有一个App实例,可以通过getApp()函数获取。App实例全局共享,其中app.js文件中定义了全局变量,绑定了全局生命周期函数、错误监听函数等回调函数;app.json文件中定义了所有页面路径(pages字段)、顶部导航栏样式、底部tabBar设置等全局信息;app.wxss中定义了全局组件样式,所有页面的wxml可以直接使用app.wxss所定义的样式。
    • 以开发者的视角看页面生命周期,对于页面生命周期的理解主要体现在回调函数中。当一个页面被初次加载,会调用onLoad()函数。一个页面加载完毕之后,从其他页面跳再次转回到该页面,或是在tabBar中切换回到该页面,onLoad()不再被执行。所以onLoad()函数适合做一些初始化的工作,例如获取用户信息、静态配置数据等。onLoad()函数执行完毕后,紧接着会调用onShow()函数。onShow()函数的调用时机是页面显示之时,包括页面初次启动、页面从隐藏到显示、从后台切换到前台等。onShow()函数适合做一些与实时更新的数据相关联,但又对实时性要求不太高的工作,比如推荐热搜关键词,尽管关键词搜索频度可能每时每刻都在变化,但是用户在点开热搜页面后并不希望看到关键词疯狂变化。如果页面被关闭,或是小程序切换到后台,此时会调用onHide()函数。onHide()函数适合做一些与数据保存相关的工作,比如要将用户在页面上做某种操作的次数保存到数据库,这种保存操作适合在用户离开页面后执行,如果实时地与数据库进行交互则会造成较大的开销。当一个页面被销毁,比如彻底退出小程序,此时会调用onUnload()函数。开发者可能并不经常用到onUnload()函数,但是如果一个功能与用户使用小程序期间的所有数据相关联,包括小程序挂载到后台时产生的数据,那么可能会在onUnload()执行某些操作。
    • 页面中的其他类型的回调函数,比如onPullDownRefresh()、onReachBottom()也经常被开发者使用。前者是监听用户的下拉动作,后者是监听用户的上拉触底动作。若要利用onPullDownRefresh()实现下拉刷新,首先要在页面的json文件里设置window属性:
"window":{
"enablePullDownRefresh":true
}

之后在onPullDownRefresh()中添加监听到下拉动作所要执行的操作,比如在标题栏中显示加载3秒钟:

onPullDownRefresh(){
console.log('--------下拉刷新-------')
wx.showNavigationBarLoading() //在标题栏中显示加载
setTimeout(function () {
wx.hideNavigationBarLoading() //完成停止加载
wx.stopPullDownRefresh() //停止下拉刷新
}, 3000)
}

若要利用onReachBottom()函数实现上拉加载,可以在页面的json文件里设置:

"onReachBottomDistance":10,

表示距离底部还有多少px的距离时监听到上拉动作,当然也可以不设置,采用默认值0。之后在onReachBottom()函数中实现动作,以下是我实现的上拉加载更多表情的函数:

onReachBottom:function() {
var judge = 1
if (this.data.isLoading == 1) return;
//上拉加载方式:获取数据,拼接数组
var loadTime = this.data.globalShowIndex
console.log("loadTime:",loadTime)
//每次加载一行表情(3个)
var init = 9 + loadTime*3
var globalList = this.data.showListCache
console.log("globalLenth:",globalList.length)
if (init >= globalList.length) {
wx.showToast({
title: '抱歉,没有更多了',
duration:2000
})
}
else {
var temp3 = []
for (var i = init;i < init+3;i++) {
var path = globalList[i]
temp3.push({'file_id':path})
}
this.data.showPicList.push(temp3)
this.setData({
isLoading:0,
showPicList:this.data.showPicList
})
this.data.globalShowIndex++
}
}

此外,还可以wxml文件中做一些与上拉下拉动作相配合的设置,优化视觉体验,比如上拉加载时页面底部的提示:

<view wx:if='{{!isRefreshing}}' class="weui-loadmore">
<view wx:if='{{isLoading}}'>
<view class="weui-loading"></view>
<view class="weui-loadmore-tips">正在加载</view>
</view>
<view wx:elif='{{more}}'>
<!--bindtap为onReachBottom()回调函数,点击此处同样执行上拉加载动作-->
<view class="weui-loadmore-tips" bindtap='onReachBottom'>加载更多 </view>
</view>
<view wx:else>
<view class="weui-loadmore-tips">抱歉,没有更多了</view>
</view>
</view>

其中isRefreshing等指示变量需要在onReachBottom()函数中根据执行过程更改。

  • 小程序的云开发
    • 微信小程序开发与Web开发的一个重要的不同之处,在于微信小程序开发者可以使用云开发功能,无需搭建服务器,即可使用云端能力。云开发为开发者提供完整的原生云端支持和微信服务支持,弱化后端和运维概念,无需搭建服务器,使用平台提供的 API 进行核心业务开发,即可实现快速上线和迭代。云开发提供了以下几大基础能力支持:1.云函数,开发者无需自建服务器,在云端运行代码。2.数据库,开发者无需自建数据库,既可以在开发工具中直接操作,又能在云函数、js逻辑函数中读写。3.存储,开发者无需自建存储和CDN,既可以在开发工具中直接上传、下载文件,又能在程序中通过微信提供的API进行上传下载。4.云调用, 使用小程序开放接口,包括服务端调用、获取开放数据等。
    • 使用云开发的难点主要在于数据库的使用,因为对于数据库的查询只能依靠小程序提供的统一接口,并没有sql语句那么强的灵活性。例如嵌套查询操作,如果只用统一接口的话操作就比较复杂。另外,微信云存储的增删查改接口种类较为繁多,若要灵活掌握可能需要较长时间的学习周期。所以在实际开发中有时会采用多级操作的方式,暂存中间操作结果,进行下一级操作。这样可能会造成内存上的额外开销,但是对于初学者来说可以方便地实现一些数据库的复杂操作。
for (var i = 0;i < batchTimes;i++) {
var res = await db.collection("expression_visit_times").where({
id:fileid
}).skip(i*100).get()
if ((res.data[0] == null) && (i == batchTimes-1)) {
await db.collection('expression_visit_times').add({
data: {
id:fileid,
tag:filetag,
times:1
}
}).then(res=>{
console.log("第一次访问表情")
})
}
else if(res.data[0] != null){
visits = res.data[0].times
visits++
_id = res.data[0]._id
try{
await db.collection('expression_visit_times').doc(_id).
update({
data:{
times:visits
}
}).then(res=>{
console.log("更新成功")
})
}
catch(e){
console.log(e)
}
}
}

以上是我实现的对于expression_visit_times集合的一个比较复杂的操作:如果集合中存在file_id对应的表情的记录,那么就把它的访问次数加1;如果不存在,则新增一条该表情的访问记录。我将查询的结果保存下来进行判断,之后做进一步的操作。在我们的开发过程中,存在大量复杂的数据库操作,如果对于繁多的增删查改接口了解不够深入,可能会出现许多意想不到的问题。比如where().update()操作,在页面的逻辑函数中会报错,但是在云函数中可以执行。如果开发者不能在期限内非常熟练地掌握数据库操作,那么可以出于稳妥起见,可以将一些基础的、不容易出错的数据库操作进行嵌套、组合,保证正确性。

  • 同步与异步
    • 微信小程序开发,甚至Web开发中困扰初学者的普遍问题就是程序运行过程中的同步与异步问题。我们知道,JavaScript是单进程执行的,同步操作会对程序的执行进行阻塞处理。比如在浏览器页面程序中,如果一段同步的代码需要执行很长时间(比如一个很大的循环操作),则页面会产生卡死的现象。所以,在JavaScript中,提供了一些异步特性,为程序提供了性能和体验上的益处,比如利用setTimeout()进行回调处理,或是将时间开销较大的数据请求做异步处理,使之不阻塞当前页面的主进程。
    • 异步处理尽管会优化程序的使用体验,但也对开发者提出了新的问题:如果一个数据请求与之后的函数存在数据关联,后者需要前者请求到的数据,按照JavaScript的异步执行,后面的函数不被数据请求阻塞,会被优先执行。这样的问题在开发中普遍存在,如果处理不好二者之间的关系,将会对程序的正确性造成很大的威胁。
    • 上述问题可以用回调函数的方式解决,只需把后面的函数放到success:function()之中,当前面的操作完成,回调success函数,便可以执行后面的操作。但是这又引入了新的问题:如果现在有一系列的同步操作,记作operation1-operation5,后面的操作均需要等待前面的操作,那么我们的代码将会变成下面的样子:
operation1({
success:function(res){
operation2({
success:function(res){
operation3({
success:function(res){
operaion4({
success:function(res){
operation5({})
}
})
}
})
}
})
}
})

看起来非常不舒服,维护起来也比较困难。就算我们能够忍受这种代码风格,那么还有一个致命的问题无从解决:for循环中的异步问题。如果一个异步操作被放在了for循环中,那它就会变成薛定谔的猫:你不知道它会在什么时候被执行,可能它的几次执行并不是严格按照for循环的遍历次序。

  • 在云开发中,我们经常在云函数中使用Async-await方法,将异步请求变为同步请求。也就是说把让一个操作阻塞后面的操作,直到该操作完成。在云函数中使用Async-await比较简单,只需要以promise风格声明函数,之后加上async关键字,就可以在函数中使用await,将异步操作变成同步操作了:
exports.main = async (event, context) => {
const db = cloud.database()
const request = event.request
if (request == 1) {
//后续操作等待统计'expression_visit_times'集合记录数目
const countResult = await db.collection('expression_visit_times').count()
const total = countResult.total
// 计算需分几次取
var batchTimes = Math.ceil(total / 100)
if (batchTimes==0) {
batchTimes = 1
}
var resultArray = []
for (var i = 0;i < batchTimes;i++) {
var temp = await db.collection("expression_visit_times").skip(i*100).get()
resultArray = resultArray.concat(temp.data)
}
console.log("resultArray:",resultArray)
return {
data:resultArray
}
}
  • 在小程序端,目前是不支持Async-await方法的。但是可以通过手动添加API的方式,进行小程序端的配置。参考链接如下:https://www.jb51.net/article/158648.htm.