一.微信小程序的由来
小程序是一种全新的连接用户与服务的方式,它可以在微信内被便捷地获取和传播,同时具有出色的使用体验。
小程序并非凭空冒出来的一个概念。当微信中的 WebView 逐渐成为移动 Web 的一个重要入口时,微信就有相关的 JS API 了。
代码清单1-1 使用 WeixinJSBridge 预览图片
WeixinJSBridge.invoke('imagePreview', {
current: 'http://inews.gtimg.com/newsapp_bt/0/1693121381/641',
urls: [ // 所有图片的URL列表,数组格式
'https://img1.gtimg.com/10/1048/104857/10485731_980x1200_0.jpg',
'https://img1.gtimg.com/10/1048/104857/10485726_980x1200_0.jpg',
'https://img1.gtimg.com/10/1048/104857/10485729_980x1200_0.jpg'
]
}, function(res) {
console.log(res.err_msg)
})
代码1-1是一个调用微信原生组件浏览图片的JS API,相比于额外引入一个JS图片预览组件库,这种调用方式显得非常简洁和高效。
实际上,微信官方是没有对外暴露过如此调用的,此类 API 最初是提供给腾讯内部一些业务使用,很多外部开发者发现了之后,依葫芦画瓢地使用了,逐渐成为微信中网页的事实标准。2015年初,微信发布了一整套网页开发工具包,称之为 JS-SDK,开放了拍摄、录音、语音识别、二维码、地图、支付、分享、卡券等几十个API。给所有的 Web 开发者打开了一扇全新的窗户,让所有开发者都可以使用到微信的原生能力,去完成一些之前做不到或者难以做到的事情。
同样是调用原生的浏览图片,调用方式如代码清单1-2所示。
代码清单1-2 使用 JS-SDK 调用图片预览组件
wx.previewImage({
current: 'https://img1.gtimg.com/10/1048/104857/10485726_980x1200_0.jpg',
urls: [ // 所有图片的URL列表,数组格式
'https://img1.gtimg.com/10/1048/104857/10485731_980x1200_0.jpg',
'https://img1.gtimg.com/10/1048/104857/10485726_980x1200_0.jpg',
'https://img1.gtimg.com/10/1048/104857/10485729_980x1200_0.jpg'
],
success: function(res) {
console.log(res)
}
})
JS-SDK是对之前的 WeixinJSBridge 的一个包装,以及新能力的释放,并且由对内开放转为了对所有开发者开放,在很短的时间内获得了极大的关注。从数据监控来看,绝大部分在微信内传播的移动网页都使用到了相关的接口。
JS-SDK 解决了移动网页能力不足的问题,通过暴露微信的接口使得 Web 开发者能够拥有更多的能力,然而在更多的能力之外,JS-SDK 的模式并没有解决使用移动网页遇到的体验不良的问题。用户在访问网页的时候,在浏览器开始显示之前都会有一个白屏的过程,在移动端,受限于设备性能和网络速度,白屏会更加明显。我们团队把很多技术精力放置在如何帮助平台上的Web开发者解决这个问题。因此我们设计了一个 JS-SDK 的增强版本,其中有一个重要的功能,称之为“微信 Web 资源离线存储”。
以下文字引用自内部的文档(没有最终对外开放):
微信 Web 资源离线存储是面向 Web 开发者提供的基于微信内的 Web 加速方案。通过使用微信离线存储,Web 开发者可借助微信提供的资源存储能力,直接从微信本地加载 Web 资源而不需要再从服务端拉取,从而减少网页加载时间,为微信用户提供更优质的网页浏览体验。每个公众号下所有 Web App 累计最多可缓存 5M 的资源。
这个设计有点类似 HTML5 的 Application Cache,但在设计上规避了一些 Application Cache的不足。
在内部测试中,我们发现 离线存储 能够解决一些问题,但对于一些复杂的页面依然会有白屏问题,例如页面加载了大量的 CSS 或者是 JavaScript 文件。除了白屏,影响 Web 体验的问题还有缺少操作的反馈,主要表现在两个方面:页面切换的生硬和点击的迟滞感。
微信面临的问题是如何设计一个比较好的系统,使得所有开发者在微信中都能获得比较好的体验。这个问题是之前的 JS-SDK 所处理不了的,需要一个全新的系统来完成,它需要使得所有的开发者都能做到:
- 快速的加载
- 更强大的能力
- 原生的体验
- 易用且安全的微信数据开放
- 高效和简单的开发
这就是小程序的由来。
二、小程序架构
微信小程序的框架包含两部分View视图层(可能存在多个)、App Service逻辑层(一个),View层用来渲染页面结构,AppService层用来逻辑处理、数据请求、接口调用,它们在两个线程里运行。
视图层使用WebView渲染,逻辑层使用JSCore运行。
视图层和逻辑层通过系统层的WeixinJsBridage进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。
重点讲一下wxs :
由于view 与 App Service是不同线程,之前是传递数据,当遇到一些数据需要在view中处理时,就可以用wxs来处理,如下所示定义
index.js
//获取应用实例
const app = getApp()
Page({
data: {
motto: 'Hello World',
userInfo: {},
hasUserInfo: false
},
//事件处理函数
bindViewTap: function() {
},
onLoad: function() {
}
})
{{tools.bar(motto)}}
{{tools.foo}}
var foo = "'hello world' from comm.wxs";
var bar = function(d) {
return '啥子玩意'+d;
}
module.exports = {
foo: foo,
bar: bar
};
三、小程序启动加载
运行机制
小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。 假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台态的小程序切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动。
更新机制
小程序冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。如果需要马上应用最新版本,可以使用 wx.getUpdateManager API 进行处理。
运行机制
- 小程序没有重启的概念
- 当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁
- 当短时间内(5s)连续收到两次以上收到系统内存告警,会进行小程序的销毁
四、View (页面视图)
视图层由 WXML 与 WXSS 编写,由组件来进行展示。
将逻辑层的数据反应成视图,同时将视图层的事件发送给逻辑层。
1、View - WXML
wxml编译器:wcc 把wxml文件 转为 js 执行方式:wcc index.wxml
2、View - WXSS
- WXSS(WeiXin Style Sheets)
- wxss编译器:wcsc 把wxss文件转化为 js 执行方式: wcsc index.wxss
3、View - Component
- 小程序的组件基于Web Component标准
- 使用Polymer框架实现Web Component
4、View - Native Component
- 目前Native实现的组件有
- Native组件层在WebView层之上
五、WebView预加载
每次小程序进入除了当前页面,Native预先额外加载一个WebView
当打开指定页面时,用默认数据直接渲染,请求数据回来时局部更新
返回显示历史View
退出小程序,View状态不销毁
六、App Service(逻辑层)
逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈
1、App( ) 小程序的入口;Page( ) 页面的入口
3、提供丰富的 API,如微信用户数据,扫一扫,支付等微信特有能力。
4、每个页面有独立的作用域,并提供模块化能力。
5、数据绑定、事件分发、生命周期管理、路由管理
1、App Service - Binding
- 数据绑定使用 Mustache 语法(双大括号)将变量包起来,动态数据均来自对应 Page 的 data,可以通过setData方法修改数据。
- 事件绑定的写法同组件的属性,以 key、value 的形式,key 以bind或catch开头,然后跟上事件的类型,如bindtap, catchtouchstart,value 是一个字符串,需要在对应的 Page 中定义同名的函数。
2、App Service - Life Cylce
3、App Service - API
API通过WeixinJSBridge和Native 进行通信
4、App Service - Router
- navigateTo(OBJECT)
保留当前页面,跳转到应用内的某个页面,使用navigateBack可以返回到原页面。页面路径只能是五层
- redirectTo(OBJECT)
关闭当前页面,跳转到应用内的某个页面。
- navigateBack(OBJECT)
关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages()) 获取当前的页面栈,决定需要返回几层。
五、小程序开发经验
1、小程序存在的问题
小程序仍然使用WebView渲染,并非原生渲染
需要独立开发,不能在非微信环境运行 。
开发者不可以扩展新组件。
依赖浏览器环境的js库不能使用,因为是JSCore执行的,没有window、document对象。
WXSS中无法使用本地(图片、字体等)。
WXSS转化成js 而不是css。
WXSS不支持级联选择器。
小程序无法打开页面,无法拉起APP。
2、小程序的优点
提前新建WebView,准备新页面渲染。
View层和逻辑层分离,通过数据驱动,不直接操作DOM。
使用Virtual DOM,进行局部更新。
全部使用https,确保传输中安全。
加入rpx单位,隔离设备尺寸,方便开发。
rpx(responsive pixel):
可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。
如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,
1rpx = 0.5px = 1物理像素。
设备 rpx换算px (屏幕宽度/750) px换算rpx (750/屏幕宽度)
iPhone5 1rpx = 0.42px 1px = 2.34rpx
iPhone6 1rpx = 0.5px 1px = 2rpx
iPhone6Plus 1rpx = 0.552px 1px = 1.81rpx复制代码
七、代码运行
运行时,外面包裹define,代码作为回到,当调用回调时,只传入前面三个值,由于后面的变量都是局部定义的变量,就屏蔽了(window,document等这些变量.
其中O就是上面define('app.js',callback),的回调,回调值传入了三个参数,屏蔽了其他属性
八、优化建议(官方建议)
setData 工作原理
小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript
所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。
而 evaluateJavascript
的执行会受很多方面的影响,数据到达视图层并不是实时的。
常见的 setData 操作错误
1. 频繁的去 setData
在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去setData
,其导致了两个后果:
- Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;
- 渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;
2. 每次 setData 都传递大量新数据
由setData
的底层实现可知,我们的数据传输实际是一次 evaluateJavascript
脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程,
3. 后台态页面进行 setData
当页面进入后台态(用户不可见),不应该继续去进行setData
,后台态页面的渲染用户是无法感受的,另外后台态页面去setData
也会抢占前台页面的执行。
图片资源
- 目前图片资源的主要性能问题在于大图片和长列表图片上,这两种情况都有可能导致 iOS 客户端内存占用上升,从而触发系统回收小程序页面。
- 在 iOS 上,小程序的页面是由多个 WKWebView 组成的,在系统内存紧张时,会回收掉一部分 WKWebView。从过去我们分析的案例来看,大图片和长列表图片的使用会引起 WKWebView 的回收。
代码包大小的优化
开发者在实现业务逻辑同时也有必要尽量减少代码包的大小,因为代码包大小直接影响到下载速度,从而影响用户的首次打开体验。除了代码自身的重构优化外,还可以从这两方面着手优化代码大小:
- **分包加载
**对小程序进行分包,可以优化小程序首次启动的下载时间 - 清理没有使用到的代码和资源
目前小程序打包是会将工程下所有文件都打入代码包内,也就是说,这些没有被实际使用到的库文件和资源也会被打入到代码包里,从而影响到整体代码包的大小。
预先加载数据
原理
小程序在启动时,会直接加载所有页面逻辑代码进内存,即便 page2 可能都不会被使用。在 page1 跳转至 page2 时,page1 的逻辑代码 Javascript 数据也不会从内存中消失。page2 甚至可以直接访问 page1 中的数据。
小程序的这种机制差异正好可以更好的实现预加载。通常情况下,我们习惯将数据拉取写在 onLoad 事件中。但是小程序的 page1 跳转到 page2,到 page2 的 onLoad 是存在一个 300ms ~ 400ms 的延时的。如下图:
因为小程序的特性,完全可以在 page1 中预先拿取数据,然后在 page2 中直接使用数据,这样就可以避开 redirecting 的 300ms ~ 400ms了。如下图:
渲染view线程和AppServcie是相互独立的,对于AppServcie中js运行不会阻塞view的渲染
官方的示例也是采用这种方式: 先App中请求数据,在index.js使用数据
九、小程序与普通网页开发(H5)的区别
小程序的主要开发语言是 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 的微信客户端,以及用于辅助开发的小程序开发者工具,小程序中三大运行环境也是有所区别的,如表1-1所示。
表1-1 小程序的运行环境
运行环境 | 逻辑层 | 渲染层 |
---|---|---|
iOS | JavaScriptCore | WKWebView |
安卓 | V8 | chromium定制内核 |
小程序开发者工具 | NWJS | Chrome WebView |
网页开发者在开发网页的时候,只需要使用到浏览器,并且搭配上一些辅助工具或者编辑器即可。小程序的开发则有所不同,需要经过申请小程序帐号、安装小程序开发者工具、配置项目等等过程方可完成。