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页面进行了一个层次划分:

  1. 视图层:说简单一点这里就是<template></template>之中的内容,对应编译之后的HTML。
  2. 逻辑层:这里就是我们编写的VUE对象代码,能够访问底层的uni API并且最终与基座通讯。
  3. 基座:这里就是编译出来的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。

<!-- 定义逻辑层的组件的值、函数,这里面的代码基本上就是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的应用而已:

<view id="map" class="mapbox" :prop="centor" :change:prop="mapbox.getMap"></view>

这里就是讲逻辑层VUE的值centor绑定到视图上去,一旦这个值发生变化则视图调用视图层VUE对象之中

getMap函数进行实践的处理,由于视图层对象使用的值全部来自逻辑层所以实现了通讯,有点麻烦但是确实可行,就这样了。

注意事项

H5与APP打包使用的就是HTML DOM所以可以这样做,但是小程序不行因为对UniAPP而言使用的是小程序的组件所以无法采用这种方式进行库集成。

还有一个很烦的事情,由于VSCode不支持renderjs的语法高亮也没有对应的插件可以干这个事情,所以只有将就一下了。

UniAPP使用原生DOM API》有10条留言

    1. 大概率是由于定位到webview组件的src时候如果使用的是本地的html需要做一些特殊配置,具体记不清了逻辑是这样

  1. 你好,uni-app会把我的css中elemet选择器比如button转换为uni-button,但dom中button没有转为uni-button标签,这种有办法解决吗?万分感谢

  2. 楼主 你好 最近刚好使用renderjs来操作dom 关于使用prop于renderjs通讯这块我遇到一点问题 希望可以交流一下 方便的话加微信 13388350783

    1. 如果需要交流的话就在这里吧,也让别人看看参考一下,单纯通讯是肯定可以的,具体看你要干什么事情。

      1. 你好 这两天太忙了 没关注你的回复 我想的是 在视图层获取到逻辑层的数据 比如 我在逻辑层的data里面定义了一个Content为一个数组用来保存页面的本地数据 然后我在视图层使用了 html2canvas.js来生成页面的预览图 然后我想在 html2canvas.js 生成图片之后获取到逻辑层的Content数据来保存成一个本地历史的功能

        1. 不就是我给出的示例代码吗?逻辑层data之中的Content通过prop传到视图,监听data中Content的变化,一旦发生改变触发视图层的函数然后在触发函数之中便可以获取Content数据干你想干的事。