renderjs解读

3,253 阅读3分钟

1.前言

对于UNI APP端的开发而言,由于上并没有document,所以我们不能进行相关的DOM操作,同时有关DOM渲染的第三方库(echartopenlayer等)也无法有效的使用,因此官方推出了renderjs方案,来解决上述问题。

renderjs是一个运行在视图层的js,仅支持APP端和H5端,实际上H5端本身就支持DOM等,所以renderjs方案基本上只用于APP端的开发。

2.renderjs的细节解析

在这里我们以echarts为例子,进行组件的封装

根据官方提供的renderjs版echart例子,我们发现其示例的页面,有两个<script><script>,其中一个<script>设置了modulelang属性

<template>
    ...
</template>
<script>
    ...
</script>
<script module="echarts" lang="renderjs">
    ...
</script>

然后在<script module="echarts" lang="renderjs">中通过动态引入js文件的方式,实现echarts.js的注入。

<script module="echarts" lang="renderjs">
export default {
    mounted() {
        if (typeof window.echarts === 'function') 
        {
            this.initEcharts()
        } 
        else
        {
            // 动态引入较大类库避免影响页面展示
            const script = document.createElement('script')
            // view 层的页面运行在 www 根目录,其相对路径相对于 www 计算
            script.src = 'static/echarts.js'
            script.onload = this.initEcharts.bind(this)
            document.head.appendChild(script)
        }
    },
    methods: {
        initEcharts() {
            myChart = echarts.init(document.getElementById('echarts'))
            // 观测更新的数据在 view 层可以直接访问到
            myChart.setOption(this.option)
        },
        updateEcharts(newValue, oldValue, ownerInstance, instance) {
            // 监听 service 层数据变更
            myChart.setOption(newValue)
        },
        onClick(event, ownerInstance) {
            // 调用 service 层的方法
            ownerInstance.callMethod('onViewClick', {
                    test: 'test'
            })
        }
    }
}
</script>

image.png

2.1 路径

官方的示例项目中写道:

view 层的页面运行在 www 根目录,其相对路径相对于 www 计算

我们来看下www目录的文件结构是什么样的,我们打开手机的文件资源管理器找到如下路径 Android/data/io.dcloud.HBuilder/apps/HBubilder,后会发现有如下两个文件夹

  • doc
  • www

image.png

我们打开www文件夹,会发现如下文件

image.png

这些文件是不是很熟悉,其实在我们保存代码编译APP的时候,在/unpackage/dev/app-plus文件也是和我们在手机中的文件相互对应的

image.png

因此renderjs实际上是运行在app-view.js中的,因此在官方示例中的路径可以写为如下的类型

  • static/echarts.js
  • ./static/echarts.js 但是不能写为/static/echarts.js

2.2 直接引用库文件

官方文档中提到

  • 目前仅支持内联使用。
  • 不要直接引用大型类库,推荐通过动态创建 script 方式引用。 那么我们尝试下如何直接引用类库

在项目的根目录创建一个文件名为libs,并将static里的echarts.js复制一份到该目录。

image.png

然后修改示例文档的js代码

<script module="echarts" lang="renderjs">
let myChart
import * as echarts from '@/libs/echarts.js'
export default {
    mounted() {
        this.initEcharts()
    },
    methods: {
        initEcharts() {
            myChart = echarts.init(document.getElementById('echarts'))
            // 观测更新的数据在 view 层可以直接访问到
            myChart.setOption(this.option)
        },
        updateEcharts(newValue, oldValue, ownerInstance, instance) {
            // 监听 service 层数据变更
            myChart.setOption(newValue)
        },
        onClick(event, ownerInstance) {
            // 调用 service 层的方法
            ownerInstance.callMethod('onViewClick', {
                    test: 'test'
            })
        }
    }
}
</script>

image.png

可以看到直接引入库文件也是支持的

2.3 生命周期及执行顺序

官方文档中提到

  • 可以使用 vue 组件的生命周期不可以使用 App、Page 的生命周期

但是根据本人测试,renderjs不支持beforeCreate钩子,调用会报错,因此总结如下

钩子逻辑层视图层(renderjs)
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
beforeDestroy
destroyed

执行顺序如下

image.png

2.4 数据访问

官方文档提到

  • APP 端可以使用 dom、bom API,不可直接访问逻辑层数据,不可以使用 uni 相关接口(如:uni.request)
  • 观测更新的数据在视图层可以直接访问到
  • H5 端逻辑层和视图层实际运行在同一个环境中,相当于使用 mixin 方式,可以直接访问逻辑层数据。

代码测试如下

<template>
    <view>
        <view :prop="name"></view>
    </view>
</template>

<script>
export default {
    data() {
        return {
            name:"张三"
        };
    }
}
</script>
<script module="renderjs" lang="renderjs">
export default {
    created() {
        console.log(this.name);
    },
    mounted() {
        console.log(this.name);
    }
}
</script>	

结果如下

image.png

<template>
    <view>
        <view :prop="name" :change:prop="renderjs.update"></view>
    </view>
</template>

<script>
    export default {
        name:"k-eChart",
        data() {
            return {
                name:"张三",
                age:12
            };
        }
    }
</script>
<script module="renderjs" lang="renderjs">
    export default {
        created() {
            console.log(this.name);
        },
        mounted() {
            console.log(this.name);
        },
    }
</script>	

image.png

<template>
    <view>
        <!-- 这里change:prop不是rederjs而是abc -->
        <view :prop="name" :change:prop="abc.update"></view>
    </view>
</template>

<script>
    export default {
        name:"k-eChart",
        data() {
            return {
                name:"张三",
                age:12
            };
        }
    }
</script>
<script module="renderjs" lang="renderjs">
    export default {
        created() {
            console.log(this.name);
        },
        mounted() {
            console.log(this.name);
        },
    }
</script>	

image.png

<template>
    <view>
        <view 
            :nameqqq="name" 
            :change:nameqqq="renderjs.update"
            :age="age"
            :change:age="renderjs.update"
        ></view>
    </view>
</template>

<script>
    export default {
        name:"k-eChart",
        data() {
            return {
                name:"张三",
                age:12
            };
        }
    }
</script>
<script module="renderjs" lang="renderjs">
    export default {
        mounted() {
            console.log(this.name);
	    console.log(this.age);
        },
    }
</script>	

image.png

因此我们可以得出结论

  • renderjs无法直接访问逻辑层的任何数据,只能访问通过显式传递给视图层的数据
  • renderjs不能直接在模板上直接绑定字符串,必须绑定逻辑层的数据,否则无法监听
  • 通过绑定到视图的数据可以被renderjs访问,但是必须和change:参数名称成对出现才有效
  • scriptmodule的名称可以随便取,但是change:参数名称必须和module保持一致,虽然不会阻断renderjs的运行,但是会报错,也会导致无法捕获数据的变化
  • 模板传递的数据只能在视图层的mounted钩子之后访问

2.4.1 逻辑层传递的数据中包含函数对象

有同学肯定在renderjs的使用过程中遇到过对象的某个值是方法的时候,传递给逻辑层就变成了{}了,如下

<template>
	<view>
            <view :prop="option" :change:prop="renderjs"></view>
	</view>
</template>

<script>
export default {
    data() {
        return {
            option: {
                xAxis: {
                    type: 'category',
                    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
                },
                yAxis: {
                    type: 'value'
                },
                series: [{
                    data: [150, 230, 224, 218, 135, 147, 260],
                    type: 'line'
                }],
                tooltip: {
                    formatter: function() {
                        return "1"
                    }
                }
            }
        };
    },
}
</script>
<script module="renderjs" lang="renderjs">
export default {
    mounted() {
        console.log(this.option);
    }
}
</script>

image.png

这是因为逻辑层给视图层传递数据的时候,函数对象会自动进行一层处理(处理原理和原因未知),直接成为空对象。

解决办法

在逻辑层把方法写成字符串的形式,传递给视图层再用new Function()或者eval()的方式将其还原为方法对象

<template>
	<view>
            <view :prop="option" :change:prop="renderjs"></view>
	</view>
</template>

<script>
export default {
    data() {
        return {
            option: {
                xAxis: {
                    type: 'category',
                    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
                },
                yAxis: {
                    type: 'value'
                },
                series: [{
                    data: [150, 230, 224, 218, 135, 147, 260],
                    type: 'line'
                }],
                tooltip: {
                    formatter: `function() {
                        return "1"
                    }`
                }
            }
        };
    },
}
</script>
<script module="renderjs" lang="renderjs">
export default {
    mounted() {
        this.option.formatter = new Function(this.option.formatter)
        //或者 this.option.formatter = eval(this.option.formatter)
        console.log(this.option);
    }
}

image.png

3.实战:基于renderjsecharts组件封装(部分)

<template>
    <view>
        <view 
            class="chart"
            :option="option" 
            :change:option="renderjs.changeOption"
            :id="id"
            :change:id="renderjs"
            :style="chartStyle"
        ></view>
    </view>
</template>

<script>
export default {
    name: "k-eChart",
    props:{
        option:{
            type:Object,
            required:true
        },
        chartStyle:{
            type:Object
        }
    },
    data() {
            return {
                id:this.getGUID()
            };
        },
    methods:{
        /**
         * @description 生成唯一的一个id
         */
        getGUID() {
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
                var r = Math.random() * 16 | 0,
                        v = c == 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16);
            });
        }
    }
}
</script>
<script module="renderjs" lang="renderjs">
import * as echarts from "echarts"
let chartInstance
export default {
    mounted() {
        chartInstance = echarts.init(document.getElementById(this.id))
        this.sttringToFuncPrototype(this.option)
        chartInstance.setOption(this.option)
    },
    methods:{
        /**
         * @description 将字符串函数转换为函数
         * @param {Object} option
         */
        sttringToFuncPrototype(option){
            for (const key in option)
            {
                let prototype = option[key]
                if(typeof prototype === "object")
                {
                    //如果属性值是数组
                    //遍历数组
                    if(Array.isArray(prototype))
                    {
                        prototype.forEach(item=>{
                            if(typeof item === "object")
                                this.sttringToFuncPrototype(item)
                        })
                    }
                    //遍历此属性值
                    else
                        this.sttringToFuncPrototype(prototype)
                }
                //转换string为function
                if(typeof prototype === "string" && prototype.includes("function"))
                    prototype = eval(`(${prototype})`)
            }
        },
    }
}
</script>
<style>
.chart {
        height: 400px;
        width: 100%;
}
</style>

调用

<template>
    <view>
        <k-eChart :option="option"></k-eChart>
        <k-eChart :option="option2"></k-eChart>
    </view>
</template>

<script>
const colors = ['#5470C6', '#91CC75', '#EE6666'];
export default {
        data() {
            return {
                option: {
                    xAxis: {
                        type: 'category',
                        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
                    },
                    yAxis: {
                        type: 'value'
                    },
                    series: [{
                        data: [150, 230, 224, 218, 135, 147, 260],
                        type: 'line'
                    }],
                    tooltip: {
                        formatter: `function() {
                            return "1"
                        }`
                    }
                },
                option2: {
                    color: colors,
                    tooltip: {
                        trigger: 'axis',
                        axisPointer: {
                            type: 'cross'
                        }
                    },
                    grid: {
                        right: '20%'
                    },
                    toolbox: {
                        feature: {
                            dataView: {
                                show: true,
                                readOnly: false
                            },
                            restore: {
                                show: true
                            },
                            saveAsImage: {
                                show: true
                            }
                        }
                    },
                    legend: {
                        data: ['Evaporation', 'Precipitation', 'Temperature']
                    },
                    xAxis: [{
                        type: 'category',
                        axisTick: {
                            alignWithLabel: true
                        },
                        // prettier-ignore
                        data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
                    }],
                    yAxis: [{
                        type: 'value',
                        name: 'Evaporation',
                        position: 'right',
                        alignTicks: true,
                        axisLine: {
                            show: true,
                            lineStyle: {
                                color: colors[0]
                            }
                        },
                        axisLabel: {
                            formatter: '{value} ml'
                        }
                    },
                    {
                            type: 'value',
                            name: 'Precipitation',
                            position: 'right',
                            alignTicks: true,
                            offset: 80,
                            axisLine: {
                                show: true,
                                lineStyle: {
                                    color: colors[1]
                                }
                            },
                            axisLabel: {
                                formatter: '{value} ml'
                            }
                    },
                    {
                            type: 'value',
                            name: '温度',
                            position: 'left',
                            alignTicks: true,
                            axisLine: {
                                show: true,
                                lineStyle: {
                                    color: colors[2]
                                }
                            },
                            axisLabel: {
                                formatter: '{value} °C'
                            }
                    }
                    ],
                    series: [{
                        name: 'Evaporation',
                        type: 'bar',
                        data: [
                            2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 135.6, 162.2, 32.6, 20.0, 6.4, 3.3
                        ]
                        },
                        {
                            name: 'Precipitation',
                            type: 'bar',
                            yAxisIndex: 1,
                            data: [
                                    2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3
                            ]
                        },
                        {
                            name: 'Temperature',
                            type: 'line',
                            yAxisIndex: 2,
                            data: [2.0, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3, 23.4, 23.0, 16.5, 12.0, 6.2]
                        }
                    ]
            }
        }
    },
}
</script>

效果如下

image.png

参考文献

  1. renderjs有什么用?聊聊uniapp中用renderjs的一些细节
  2. renderjs