1.前言
对于UNI APP端的开发而言,由于上并没有document
,所以我们不能进行相关的DOM操作,同时有关DOM渲染的第三方库(echart
、openlayer
等)也无法有效的使用,因此官方推出了renderjs
方案,来解决上述问题。
renderjs
是一个运行在视图层的js,仅支持APP端和H5端,实际上H5端本身就支持DOM等,所以renderjs
方案基本上只用于APP端的开发。
2.renderjs的细节解析
在这里我们以echarts为例子,进行组件的封装
根据官方提供的renderjs版echart例子,我们发现其示例的页面,有两个<script><script>
,其中一个<script>
设置了module
和lang
属性
<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>
2.1 路径
官方的示例项目中写道:
view 层的页面运行在 www 根目录,其相对路径相对于 www 计算
我们来看下www目录的文件结构是什么样的,我们打开手机的文件资源管理器找到如下路径
Android/data/io.dcloud.HBuilder/apps/HBubilder
,后会发现有如下两个文件夹
- doc
- www
我们打开www文件夹,会发现如下文件
这些文件是不是很熟悉,其实在我们保存代码编译APP的时候,在/unpackage/dev/app-plus
文件也是和我们在手机中的文件相互对应的
因此renderjs
实际上是运行在app-view.js
中的,因此在官方示例中的路径可以写为如下的类型
static/echarts.js
./static/echarts.js
但是不能写为/static/echarts.js
2.2 直接引用库文件
官方文档中提到
- 目前仅支持内联使用。
- 不要直接引用大型类库,推荐通过动态创建
script
方式引用。 那么我们尝试下如何直接引用类库
在项目的根目录创建一个文件名为libs
,并将static
里的echarts.js
复制一份到该目录。
然后修改示例文档的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>
可以看到直接引入库文件也是支持的
2.3 生命周期及执行顺序
官方文档中提到
- 可以使用 vue 组件的生命周期不可以使用 App、Page 的生命周期
但是根据本人测试,renderjs不支持beforeCreate
钩子,调用会报错,因此总结如下
钩子 | 逻辑层 | 视图层(renderjs) |
---|---|---|
beforeCreate | ✔ | ❌ |
created | ✔ | ✔ |
beforeMount | ✔ | ✔ |
mounted | ✔ | ✔ |
beforeUpdate | ✔ | ✔ |
updated | ✔ | ✔ |
beforeDestroy | ✔ | ✔ |
destroyed | ✔ | ✔ |
执行顺序如下
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>
结果如下
<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>
<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>
<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>
因此我们可以得出结论
renderjs
无法直接访问逻辑层的任何数据,只能访问通过显式传递给视图层的数据renderjs
不能直接在模板上直接绑定字符串,必须绑定逻辑层的数据,否则无法监听- 通过绑定到视图的数据可以被
renderjs
访问,但是必须和change:参数名称
成对出现才有效 script
的module
的名称可以随便取,但是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>
这是因为逻辑层给视图层传递数据的时候,函数对象会自动进行一层处理(处理原理和原因未知),直接成为空对象。
解决办法
在逻辑层把方法写成字符串的形式,传递给视图层再用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);
}
}
3.实战:基于renderjs
的echarts
组件封装(部分)
<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>
效果如下