小程序架构介绍
我们都知道小程序本质上是运行在安卓端,苹果端的混合APP,只是微信提供了一套JSBridge,方便用户对一些原生功能和微信相关的功能的进行调用。而微信为了安全和性能的需要,一改以往网络架构中的单线程架构,改为小程序的双线程架构。分别是AppServie 和 Webview 两个线程,我们在小程序中编写的JS代码就是运行在AppService线程的JSCore引擎(类似V8 引擎,一个Js解释器)中,而我们的Wxml和Wxss则会依赖WebView线程进行渲染。
目前架构存在的问题
这样的架构虽然已经极大了提高了webview的渲染性能,但是依然会存在一些问题比如:
- 当页面节点数目过多,很容易发生卡顿
- 当我们新建一个页面,就要新建一个Webview进行渲染
- 页面之间共享资源,需要使用Native进行通信,就会消耗更多性能
- 当AppService(逻辑层)与Webview(视图层)通信也需要依赖Native
所以为了解决这些问题小程序推出Skyline渲染引擎
Skyline引擎介绍
在Skyline环境中,Skyline 会创建了一条渲染线程来负责 Layout, Composite 和 Paint 等渲染任务,并在 AppService 中划出一个独立的上下文,来运行之前 WebView 承担的 JS 逻辑、DOM 树创建等逻辑。说白了就是之前的样式计算是放到渲染线程来处理,现在把和样式相关的逻辑也放到AppService线程中处理,个人猜测这个渲染线程很有可能很有可能就是flutter,这样的架构就极大减少内存的消耗,和线程上通信时间的消耗。原本wxs中的逻辑,也可以移到Appservice线程中运行
使用Skyline引擎的使用步骤
- 在app.json 文件添加
"lazyCodeLoading": "requiredComponents"
属性,这是因为Skyline 依赖按需注入的特性。
{
"pages": [
"pages/index/index",
"pages/logs/logs",
"pages/test/test"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "Weixin",
"navigationBarTextStyle": "black"
},
"sitemapLocation": "sitemap.json",
// 在 app.json 文件添加
"lazyCodeLoading": "requiredComponents"
}
- 在全局或页面配置中声明为 Skyline 渲染,即 app.json 或 page.json 配上
"renderer": "skyline"
- Skyline 不支持页面全局滚动,需在页面配置项加上
"disableScroll": true
,在需要滚动的区域使用scroll-view 实现 - Skyline 不支持原生导航栏,需在页面配置项加上
"navigationStyle": "custom"
,并自行实现自定义导航栏
{
"usingComponents": {},
// 在 app.json 文件添加或者page页面的json中添加
"disableScroll": true,
"navigationStyle": "custom"
//也可以放在App.json文件中
"renderer": "skyline"
}
- 组件适配,参考 Skyline 基础组件支持与差异
- WXSS 适配,参考 Skyline WXSS 样式支持与差异
- 在本地设置中勾选Skyline渲染调试,如果看不到这个选项框,看一下是否在app.json中配置了
"renderer": "skyline"
Skyline的 worklet 动画介绍
小程序采用双线程架构,渲染线程(UI 线程)和逻辑线程(JS 线程)分离。JS
线程不会影响 UI
线程的动画表现,如滚动效果。但引入的问题是,UI
线程的事件发生后,需跨线程传递到 JS
线程,进而触发开发者回调,当做交互动画(如拖动元素)时,这种异步性会带来较大的延迟和不稳定,worklet
动画正是为解决这类问题而诞生的,使得小程序可以做到类原生动画般的体验
worklet函数定义
function helloWorklet() {
'worklet'; //'worklet'声明该函数为work函数,可以在js线程和UI线程中调用
console.log('hello worklet');
}
Page({
onLoad(options) {
helloWorklet('hello') // print: hello
wx.worklet.runOnUI(helloWorklet)()
},
})
在小程序控制台可以看到如下输出
如果看见SkylineGlobal is not defined错误看看是否__开启了Skyline渲染调试__
worklet函数间的相互调用
function slave() {
'worklet';
return "I am slave"
}
function master() {
'worklet';
const value = slave()
console.log(value);
}
从 UI 线程调回到 JS 线程
const {runOnUI ,runOnJS} = wx.worklet
function jsFun(message) {
// 普通函数不需要声明为worklet
console.log(message)
}
function uiFun() {
'worklet';
runOnJS(jsFun)('I am from UI')
}
使用shared共享数据
由worklet函数捕获的静态变量,会在编译期间序列化后生成在UI线程的拷贝环境之中,这就导致我们在JS线程中后续更新了变量,但是在UI线程中时得不到最新的数值的。
const obj = { name: 'skyline'}
function someWorklet() {
'worklet'
console.log(obj.name) // 输出的仍旧是 skyline
}
obj.name = 'change name'
wx.worklet.runOnUI(someWorklet)()
因此shyline使用shared来实现线程之间数据的共享
const { shared, runOnUI } = wx.worklet
const offset = shared(0)
function someWorklet() {
'worklet'
console.log(offset.value) // 输出的是新值 1
}
offset.value = 1
runOnUI(someWorklet)()
简单案例–实现探探的卡片功能
注意:编辑器版本:1.06.2303162 基础库版本:2.30.2
先看效果
代码如下 <br>
wxml 代码
<navigation-bar title="探探" />
<view class="page">
<block wx:for="{{containers}}" wx:key="*this">
<pan-gesture-handler data-id="container-{{index}}" onGestureEvent="handlePan">
<view id="container-{{index}}" class="container" style="z-index: {{zIdnexes[index]}};background-image: url({{partContentList[index]}});">
</view>
</pan-gesture-handler>
</block>
</view>
scss代码
.page{
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
position: relative;
.container{
height: 80vh;
width: 95vw;
background-color: burlywood;
position: absolute;
border-radius: 16rpx;
display: flex;
justify-content: center;
align-items: center;
background-size: cover;
.image{
display: block;
height: 1067rpx;
width: 712rpx;
margin: 0 0;
}
}
}
核心逻辑
import { useAnimation, setAni, Animation, GestureState } from "./method"
Page<{ pos: Animation }, any>({
/**
* 页面的初始数据
*/
data: {
containers: [
"burlywood",
"blue",
"cyan",
"black"
],
zIdnexes:[],
current:0,
partContentList:[]
},
/**
* 生命周期函数--监听页面加载
*/
onLoad() {
this.initNode()
// 当前node的下标
this.active = wx.worklet.shared(0)
// 当前contentList的下标
this.current = wx.worklet.shared(0)
this.zIndex = 100000
},
initNode() {
// 用与保存shared值
this.Nodes = {}
// 图片文件
this.contentList = [
"https://i.hexuexiao.cn/up/ca/63/4a/a32912fc26b8445797c8095ab74a63ca.jpg",
"https://th.bing.com/th/id/OIP.kSrrRGx6nqOgWzbaEvVD9AHaNK?pid=ImgDet&rs=1",
"https://img.zmtc.com/2019/0806/20190806061552744.jpg",
"https://img.zmtc.com/2019/0806/20190806061000600.jpg",
"https://img.ratoo.net/uploads/allimg/190523/7-1Z5231J058.jpg",
"https://th.bing.com/th/id/R.47de9dfcc25d579d84850d4575d24a6a?rik=%2fGkmrewzIEY4Iw&riu=http%3a%2f%2fimg3.redocn.com%2ftupian%2f20150930%2fqizhimeinvlisheyingtu_5034226.jpg&ehk=rG9Ks2QRzj81mZl38gVGmWVAgCHVLWppoDezpfwdxjo%3d&risl=&pid=ImgRaw&r=0",
"https://th.bing.com/th/id/R.95f8e6f6bd5b660ae3ad4f3e0d712276?rik=ELKcha%2bE5ryuiw&riu=http%3a%2f%2f222.186.12.239%3a10010%2fwlp_180123%2f003.jpg&ehk=mVN7AzIRR%2fmVPJYWrWOFbEiher3QWtwSdH%2f%2fe4lE7n8%3d&risl=&pid=ImgRaw&r=0"
]
this.data.containers.forEach((_: string, index: number) => {
if (index == 0) {
this.Nodes[`#container-${index}`] = useAnimation(`#container-${index}`, { x: 0, y: 0 }, this)
this.setData({
[`zIdnexes[${index}]`]:100000-index,
[`partContentList[${index}]`]:this.contentList[index]
})
} else {
console.log("10123")
this.Nodes[`#container-${index}`] = useAnimation(`#container-${index}`, { x: 0, y: 20, scale: 0.95 }, this)
this.setData({
[`zIdnexes[${index}]`]:100000-index,
[`partContentList[${index}]`]:this.contentList[index]
})
}
});
},
handlePan(evt: any) {
"worklet";
console.log(evt)
const now = this.Nodes[`#container-${this.active.value}`] as Animation
const next = this.Nodes[`#container-${(this.active.value+1)%4}`] as Animation
if (evt.state == GestureState.ACTIVE) {
// 滑动激活状态
// 设置当前的滑动块
now.x.value += evt.deltaX
now.y.value += evt.deltaY
now.rotate.value = now.x.value * 10 / 360
// 设置下一个滑动块
let rate = Math.abs(now.x.value) / 150
rate = rate > 1 ? 1 : rate
next.y.value = (20 - rate * 20) < 0 ? 0 : (20 - rate * 20)
next.scale.value = 0.95 + rate * 0.05
}
if (evt.state == GestureState.END) {
// 滑动结束
if (Math.abs(now.x.value) < 150) {
// 判断是否超过界限值
setAni(now.x, 0)
setAni(now.y, 0)
setAni(now.rotate, 0)
} else if (now.x.value < 0) {
// 判断判断左划还是右划
setAni(now.x, -2000)
setAni(now.y, -2000)
setAni(now.rotate, 0)
// 通知js线程进行数据的更新
wx.worklet.runOnJS(this.toNext.bind(this))()
} else if (now.x.value > 0) {
setAni(now.x, 2000)
setAni(now.y, -2000)
setAni(now.rotate, 0)
wx.worklet.runOnJS(this.toNext.bind(this))()
}
}
},
// 将当前序号的跳转到下一个
toNext(){
const current = this.current.value+1
this.active.value = current%4
this.current.value = current
this.setData({
current
})
if(current-2>=0){
wx.worklet.runOnUI(this.toReset)((current-2)%4)
this.setData({
[`zIdnexes[${(current-2)%4}]`]:99998-current,
[`partContentList[${(current-2)%4}]`]:this.contentList[current+2]
})
}
},
// 将动画归位
toReset(index:number){
"worklet";
const reset = this.Nodes[`#container-${index}`] as Animation
setAni(reset.x, 0,0)
setAni(reset.y, 20,0)
setAni(reset.rotate, 0,0)
setAni(reset.scale, 0.95,0)
}
})