UniAPP使用原生DOM API
原始问题
首先是为什么要搞这个东西,本质原因在与UniAPP团队的SB设计,为了兼容APP、各种小程序与H5多端打包所以全平台采用去DOM化的设计思路(主要是兼容小程序),限制了开发者直接操作DOM对象,他使用的VUE在打包之后也只能作用于框架的组件。
这样导致的问题就是无法对DOM对象进行自定义结果是所以的DOM API全部无法使用。如ThreeJS、ECharts、地图等等直接DOM绑定的JS库全部无法使用,但是不同于RN这种框架打包之后采用的是原生组件没有DOM对象,但是UniAPP在打包APP时候使用还TM是WebView所以不存在任何性能与兼容性问题,同时他的社区力量和闹着玩一样比较弱,所以我们需要在APP之中对DOM进行操作以兼容第三方JS框架。
原理
原理上来讲比较简单,由于本质上在运行APP时依旧是WebView模式所以说对DOM的操作是绝对可行的,因为他的框架就是基于HTML Dom进行实现的,所以我们只需要一个在它的框架之中找到一个层级能够直接对DOM进行操作即可,然后是在这个层级上实现逻辑层之间的通讯,这样一来便可以正常使用DOM API。
首先UniAPP对一个VUE页面进行了一个层次划分:
- 视图层:说简单一点这里就是
<template></template>
之中的内容,对应编译之后的HTML。 - 逻辑层:这里就是我们编写的VUE对象代码,能够访问底层的uni API并且最终与基座通讯。
- 基座:这里就是编译出来的APP的壳,他提供了webview进行视图显示与dom逻辑,同时提供了底层API给JS实现部分原生功能。
由于它的逻辑层是直接依赖基座提供的原始API运行的,算是一个小型的JS翻译机,但是这个东西没有DOM的概念,他只是将JS代码编译为JS逻辑代码而已,然后通过基座内部的中间件去通讯控制视图层所以说直接编写的JS的代码只能做这些事,并且这就是为什么逻辑层对视图层进行帧更新时性能极低的原因。
所以我们最终的思路也很清晰最终在运行时无非是视图层实现原始DOM,而逻辑层实现框架沟通,因此我们只需要将我们的DOM API代码直接运行与视图层上即可,然后通过VUE的机制让逻辑层基于VUE绑定的方式绕过他的编译框架通讯到我们视图层里面的代码就可以了。
说了这么多原理但是也需要一个手段绕过他的编译进行对DOM对象的操作,所幸UniAPP官方也不是真的SB他们还是提供了一个手段叫做renderjs,这个玩意是在编译器里面的,告知编译器这块代码是直接运行在webview里面的不要动他。它这个东西主要解决的问题就是上面说的逻辑层高频次操作视图层时性能低的问题所以提供一个手段让高频次操作全部在Dom里面去跑不要通过基座,这样性能就会很好(简直是搬起石头砸自己的脚)。
实现步骤
首先是官方文档里面的参考,我上面写的东西也是对官方文档的补充,也是非要吹自己的框架,将renderjs这种补救措施描述的模棱两可还TM藏的很深找了很久,地址为:官方文档。
后续补充:最近我重新去看了一下文档,发现官方把renderJS的说明更新了,补全了使用说明和示例。
晓得原理之后整个东西就不神秘了,在一个VUE页面之中搞两个VUE对象一个是基于逻辑层给编译器用的也就是uniapp框架里面的标准vue,另一个对象就是直接作用于视图层的具备DOM API。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!-- 定义逻辑层的组件的值、函数,这里面的代码基本上就是uniapp标准 -->
<script>
export default {
data() {
return {
//定义数据
centor: [101.7, 30.5]
};
},
onLoad() {
},
methods: {
getLocation(){
//获取经纬度
uni.getLocation({
type: 'wgs84',
success: (res)=> {
console.log('当前位置的经度:' + res.longitude);
console.log('当前位置的纬度:' + res.latitude);
//将经纬度的值付给逻辑层数据之中
this.centor = [res.longitude,res.latitude];
}
});
}
}
};
</script>
<!-- 定义视图层组件的逻辑与函数,renderjs模式说明此脚本用于视图层,这里面的代码基本上就是html标准 -->
<script module="mapbox" lang="renderjs">
//引入mapbox-gl
import {Map} from "mapbox-gl"
export default {
//挂载完成之后初始化地图
mounted() {
this.InitMap();
},
methods: {
//定义构造地图的函数
InitMap() {
//视图层之中可以直接获取dom对象,基本上任何js类库都能搞进来运行
let container = document.getElementById('map');
//构造地图
let map = new Map({
container: container,
minZoom: 0,
maxZoom: 30,
style: "http://119.3.153.63:32460/styles/dark02/style.json",
center: [104.75323, 31.45444],
zoom: 15
});
//搞一个全局的地图出来
this.map = map;
},
//定义设置地图位置的函数
getMap() {
this.map.flyTo({
//这里的this是视图层对象,不能直接通过this去操作逻辑层里面的东西
//但是逻辑层之中的值与视图层是双向绑定的,所以可以直接获取值
center: this.centor
});
}
}
}
</script>
值得注意的是在视图层vue对象之中有一个getMap
函数,这里函数之中使用了this.centor
,这个值他虽然确实来自逻辑层vue对象,但是不是直接在JS里面完成的,流程是逻辑层将值绑定到视图视图之中,然后视图层VUE对象再从视图之中获取。还有就是<script module="mapbox" lang="renderjs">
会自动将这个module作为逻辑层VUE的一个对象,名称就是mapbox,编译为H5可以直接获取(没有基座这个SB东西了),但是编译为APP之后是无法获取的一定要注意。
通讯方式就很简单了一个VUE的prop的应用而已:
1
<view id="map" class="mapbox" :prop="centor" :change:prop="mapbox.getMap"></view>
这里就是讲逻辑层VUE的值centor绑定到视图上去,一旦这个值发生变化则视图调用视图层VUE对象之中
getMap函数进行实践的处理,由于视图层对象使用的值全部来自逻辑层所以实现了通讯,有点麻烦但是确实可行,就这样了。
注意事项
H5与APP打包使用的就是HTML DOM所以可以这样做,但是小程序不行因为对UniAPP而言使用的是小程序的组件所以无法采用这种方式进行库集成。
还有一个很烦的事情,由于VSCode不支持renderjs的语法高亮也没有对应的插件可以干这个事情,所以只有将就一下了。