微信小程序的开发与原理
1, 小程序与普通网⻚开发的区别
小程序的主要开发语言是 JavaScript ,小程序的开发同普通的网⻚开发相比有很大的相似性。对于前端开发者而言,从网⻚开发迁移到小程序的开发成本并不高,但是二者还是有些许区别的。
网⻚开发渲染线程和脚本线程是互斥的,这也是为什么⻓时间的脚本运行可能会导致⻚面失去响应,而在小程序中,二者是分开的,分别运行在不同的线程中。网⻚开发者可以使用到各种浏览器暴露出来的 DOM API,进行 DOM 选中和操作。而如上文所述,
小程序的逻辑层和渲染层是分开的,逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM API
。这一区别导致了前端开发非常熟悉的一些库,例如 jQuery、 Zepto 等,在小程序中是无法运行的。同时 JSCore 的环境同 NodeJS 环境也是不尽相同,所以一些 NPM 的包在小程序中也是无法运行的。网⻚开发者需要面对的环境是各式各样的浏览器,PC 端需要面对 IE、Chrome、QQ浏览器等,在移动端需要面对Safari、Chrome以及 iOS、Android 系统中的各式 WebView 。而小程序开发过程中需要面对的是两大操作系统 iOS 和 Android 的微信客户端,以及用于辅助开发的小程序开发者工具,小程序中三大运行环境也是有所区别的
2, 小程序结构中的 JSON 语法
小程序里JSON配置的一些注意事项。
JSON文件都是被包裹在一个大括号中 {},通过key-value的方式来表达数据。JSON的Key必须包裹在一个双引号中,在实践中,编写 JSON 的时候,忘了给 Key 值加双引号或者是把双引号写成单引号是常⻅错误。
JSON的值只能是以下几种数据格式,其他任何格式都会触发报错,例如 JavaScript 中的 undefined。
- 数字,包含浮点数和整数
- 字符串,需要包裹在双引号中
- Bool值,true 或者 false
- 数组,需要包裹在方括号中 []
- 对象,需要包裹在大括号中 {}
- Null
还需要注意的是 JSON 文件中无法使用注释,试图添加注释将会引发报错。
3,小程序的运行时
渲染层和逻辑层
小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层
,JS 脚本工作在逻辑层
。
小程序的渲染层和逻辑层分别由2个线程管理: 渲染层的界面使用了WebView 进行渲染
, 逻辑层采用 JsCore 线程运行JS脚本
。一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端做中转,逻辑层发送网络请求 也经由微信客户端转发。
逻辑层 App Service
小程序开发框架的逻辑层使用 JavaScript 引擎为小程序提供开发者 JavaScript 代码的运行环境以及微信小程序的特有功能。
逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈。
开发者写的所有代码最终将会打包成一份 JavaScript 文件,并在小程序启动的时候运行,直到小程序销毁
。这一行为类似 ServiceWorker,所以逻辑层也称之为 App Service
4, 小程序运行机制
前台/后台状态, 小程序启动后,界面被展示给用户,此时小程序处于前台状态
,
当用户点击右上⻆胶囊按钮关闭小程序,或者按了设备 Home 键离开微信时,小程序并没有完全终止运行,而是进入了后台状态,小程序还可以运行一小段时间, 当用户再次进入微信或再次打开小程序,小程序又会从后台进入前台。但如果用户很久没有再进入小程序,或者系统资源紧张,小程序可能被销毁,即完全终止运行
。
小程序启动可以分为两种情况: 一种是冷启动,一种是热启动
。
- 冷启动: 如果用户首次打开,或小程序销毁后被用户再次打开,此时小程序需要重新加载启动,即冷启动。
- 热启动: 如果用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时小程序并未被销毁,只是从后台状态进入前台状态,这个过程就是热启动。
小程序销毁时机:
通常,只有当小程序进入后台一定时间,或者系统资源占用过高,才会被销毁。具体而言包括以下几种情形:
- 当小程序进入后台,会维持一小段时间的运行状态,如果这段时间内都未进入前台,小程序会被销毁。
- 当小程序占用系统资源过高,可能会被系统销毁或被微信客户端主动回收。
- 在 iOS 上,当微信客户端在一定时间间隔内(目前是 5 秒)连续收到两次及以上系统内存告警时,会主动进行小程序的销毁,并提示用户
该小程序可能导致微信响应变慢被终止
- 建议小程序在必要时使用 wx.onMemoryWarning 监听内存告警事件,进行必要的内存清理。
- 在 iOS 上,当微信客户端在一定时间间隔内(目前是 5 秒)连续收到两次及以上系统内存告警时,会主动进行小程序的销毁,并提示用户
退出状态
每当小程序可能被销毁之前,⻚面回调函数 onSaveExitState 会被调用。如果想保留⻚面中的状态,可以在这个回调函数中“保存”一些数据,下次启动时可以通过 exitState 获得这些已保存数据。
5, 小程序的开发
在 JavaScript 的基础上,我们增加了一些功能,以方便小程序的开发:
- 增加 App 和 Page 方法,进行程序注册和⻚面注册。
- 增加 getApp 和 getCurrentPages 方法,分别用来获取 App 实例和当前⻚面栈。 - - 提供丰富的 API,如微信用户数据,扫一扫,支付等微信特有能力。 提供模块化能力,每个⻚面有独立的作用域。
注意: 小程序框架的逻辑层并非运行在浏览器中,因此 JavaScript 在 web 中一些能力都无法使用,如 window,document 等。
app.js
微信客户端在打开小程序之前,会把整个小程序的代码包下载到本地。紧接着通过 app.json 的 pages 字段就可以知道你当前小程序的所有⻚面路径。
写在app.json中 pages 字段的第一个⻚面就是这个小程序的首⻚
于是微信客户端就把首⻚的代码装载进来,通过小程序底层的一些机制,就可以渲染出这个页面
小程序启动之后,在 app.js 定义的 App 实例的 onLaunch 回调会被执行,整个小程序只有一个 App 实例,是全部⻚面共享的
var api = require('./utils/request.js').default
App({
onLaunch: function () {},
onShow() {},
onHide() {},
onError(msg) {console.log(msg)},
globalData: 'I am global data'
})
可以通过 getApp 方法获取到全局唯一的 App 示例,获取App上的数据或调用开发者注册在 App 上的函数。
const appInstance = getApp()
this.setData({
globalData: appInstance.globalData
})
<view>
{{globalData}} // I am global data
</view>
Page
小程序里边包含了不同类型的文件:
- .json 后缀的 JSON 配置文件
- .wxml 后缀的 WXML 模板文件
- .wxss 后缀的 WXSS 样式文件
- .js 后缀的 JS 脚本逻辑文件
在js文件中,Page 是一个⻚面构造器,这个构造器就生成了一个⻚面。在生成⻚面的时候,小程序框架会把 data 数据和 index.wxml 一起渲染出最终的结构,于是就得到了你看到的小程序的样子。
在渲染完界面之后,⻚面实例就会收到一个 onLoad 的回调,可以在这个回调处理对应的逻辑。
Page({
data: {
text: "This is page data."
},
onLoad: function(options) {
// ⻚面创建时执行
},
onShow: function() {
// ⻚面出现在前台时执行
},
onReady: function() {
// ⻚面首次渲染完毕时执行
},
onHide: function() {
// ⻚面从前台变为后台时执行
},
onUnload: function() {
// ⻚面销毁时执行
},
onPullDownRefresh: function() {
// 触发下拉刷新时执行
},
onReachBottom: function() {
// ⻚面触底时执行
},
onShareAppMessage: function () {
// ⻚面被用户分享时执行
},
onPageScroll: function() {
// ⻚面滚动时执行
},
onResize: function() {
// ⻚面尺寸变化时执行
},
onTabItemTap(item) {
// tab 点击时执行
console.log(item.index)
console.log(item.pagePath)
console.log(item.text)
},
// 事件响应函数
viewTap: function() {
this.setData({
text: 'Set some data for updating view.'
}, function() {
// this is setData callback
})
},
// 自由数据
customData: {
hi: 'MINA'
}
})
框架的核心是一个响应的数据绑定系统,可以让数据与视图非常简单地保持同步。数据修改时
, 只需要在逻辑层修改数据,视图层就会做相应的更新。
<view>
Hello {{name}}
</view>
<button bindtap="changeName">click me!</button>
data: {
name: 'xiaowang'
},
changeName: function (e) {
this.setData({
name: 'Jerry'
})
},
组件
小程序提供了丰富的基础组件,开发者可以像搭积木一样,组合各种组件拼合成自己的小程
序。
就像 HTML 的 div、p 等标签一样,在小程序里边,你只需要在 WXML 写上对应的组件标签名字就可以把该组件显示在界面上,例如,你需要在界面上显示地图,你只需要这样写即可:
<map></map>
使用组件的时候,还可以通过属性传递值给组件
,让组件可以以不同的状态去展现,例如,我们希望地图一开始的中心的经纬度是广州,那么你需要声明地图的 longitude(中心经度) 和 latitude(中心 纬度)两个属性:
<map longitude="广州经度" latitude="广州纬度"></map>
api
为了让开发者可以很方便的调起微信提供的能力,例如获取用户信息、微信支付等等,小程序提供了很多 API 给开发者使用。需要注意的是: 多数 API 的回调都是异步,需要处理好代码逻辑的异步问题。例如
自定义组件
开发者可以将⻚面内的功能模块抽象成自定义组件,以便在不同的⻚面中重复使用,也可以将复杂的⻚面拆分成多个低耦合的模块,有助于代码维护。
自定义组件在使用时与基础组件非常相似。开发自定义组件,类似于⻚面,一个自定义组件由 json、wxml、wxss、js 4个文件组成:
// json文件
{
"component": true // 设置为 自定义组件
}
在自定义组件的 js 文件中,需要使用 Component() 来注册组件,并提供组件的属性定义、内部数据和 自定义方法。组件的属性值和内部数据将被用于组件 wxml 的渲染,其中,属性值是可由组件外部传入的。
Component({
properties: {
// 这里定义了innerText属性,属性值可以在组件使用时指定
innerText: {
type: String,
value: 'default value',
}
},
data: {
// 这里是一些组件内部的数据
someData: {
},
methods: {
// 这里是一个自定义方法
customMethod: function () {}
},
}
})
使用自定义组件
使用已注册的自定义组件前,首先要在⻚面的 json 文件中进行引用声明。此时需要提供每个自定义组件的标签名和对应的自定义组件文件路径:
// 使用足定义组件的页面的 json 文件
{
"usingComponents": {
"component-tag-name": "../../components/HelloWorld/index"
}
}
// 使用足定义组件的页面的 wxml 文件
<component-tag-name />
6, 常⻅的 setData 操作错误
1. 频繁的去setData
频繁(毫秒级)的去setData,会导致了:
Android下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层,渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到⻚面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时
每次setData都传递大量新数据
由setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程,
后台⻚面进行setData
当⻚面进入后台态(用户不可⻅),不应该继续去进行setData,后台态⻚面的渲染用户是无法感知的,另外后台态⻚面去setData也会抢占前台⻚面的执行。
7,分包
某些情况下,开发者需要将小程序划分成不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载。
在构建小程序分包项目时,构建会输出一个或多个分包。每个使用分包小程序必定含有一个主包。所谓的主包,即放置默认启动⻚面/TabBar⻚面,以及一些所有分包都需用到公共资源/JS 脚本,而分包则是根据开发者的配置进行划分。
在小程序启动时,默认会下载主包并启动主包内⻚面,当用户进入分包内某个⻚面时,客户端会把对应分包下载下来,下载完成后再进行展示。
目前小程序分包大小有以下限制:
- 整个小程序所有分包大小不超过 8M
- 单个分包/主包大小不能超过 2M