uni-app <script module="echarts" lang="reanderjs"> 怎么与组件通讯?
uni-app 中如何合理正确地封装一个可复用echarts图像组件?
问题描述
- uni-app 提供了实用 echarts 的方式如下,由于 renderjs 是一个运行在视图层的js,这就意味着在 APP 环境下(非浏览器环境)是无法获取到组件代码中的组件实例信息的
- 因此项目中出现了这样的画面
(不知大家工作项目中有没有见到过这样的目录画面,我见到这种大吃一惊,本来以为只是命名不规范,细看代码后才了解到问题还不小,这份工程几年了都没人探究如何解决这问题,就很奇怪,我探索一番后找到了方案,是uniapp提供的唯一方案)
|-components/
|-echarts/
|--bar-chart-data01.vue
|--bar-chart-data02.vue
|--bar-chart-data03.vue
|--bar-chart-data04.vue
|--bar-chart-data05.vue
|--bar-chart-data06.vue
...
- 其中除了canvas容器的id不同,其余代码完全一样,组件目的就是为了能够在一个页面中绘制多个统计图,若以目前的方式一个页面共用同一个组件创建多个图像,则会由于document.getElementById('chartId')获取的id相同,导致后绘制的覆盖前一个的echarts图像(因为获取的容器id是同一个)
<template>
<view id="chartId" ...></view>
</template>
<script>
// 组件代码省略
export default {...}
</script>
<script module="echarts" lang="renderjs">
export default {
methods: {
initEcharts() {
let that = this
// 项目原始代码中一直这么写的原因是,在 APP 环境下,renderjs属于视图层,无法直接通过 this.boxid 的方式来获取上面组件代码实例中的信息(若上方组件代码中写的 data 或 props 中有 boxid,这里都无法直接获取到,但是web环境下是可以的)
myChart = echarts.init(document.getElementById('chartId'))
// 其他代码省略...
}
}
}
</script>
解决方案
- 通过uniapp官方提供的 :change:boxid="renderJsModuleA.changeBoxid" 方案,可以实现组件与renderjs模块之间的通讯
-
<template>
<!-- 这里的 :boxid="boxid" :change:boxid="echarts.changeBoxid" 是uniapp官方提供的与renderjs通讯方案,:boxid="boxid" 用于监听相应的响应式变量,:change:boxid="echarts.changeBoxid" 而此处则是与之对应的,是把监听到的响应式变量的值通过自定义的changeBoxid事件来传递到renderjs中去(至于其中的echarts是下方renderjs的模块名,也是自定义的) -->
<view class="echartsCanvasBox" :id="boxid" :boxid="boxid" :change:boxid="echarts.changeBoxid" ></view>
</template>
<script>
/*组件实例 script*/
export default {
props: {
/**
* 容器id(为了避免绘制出现意外情况,此id一定要由父组件提供)
*/
boxid: {
type: String,
required: true
}
}
}
</script>
<script module="echarts" lang="renderjs">
/* 这里的 module="echarts" 是自定义的,给当前 <script> 模块的一个名称,就是上方 :change:boxid="echarts.changeBoxid" 中调用 echarts 就是要与这里的 module="echarts" 名称对应 */
export default {
methods: {
/**
* 此方法对应的时上方 :change:boxid="echarts.changeBoxid" 中调用的 changeBoxid 函数,凡是上方组件中绑定的 boxid 响应式变量发生变化包括初始值,此处函数都会被调用
* @param [any] data 即绑定的 boxid 的值
* @param [any] oldData 绑定的 boxid 上一次的值(若是第一次,则这里是undefined)
* @param [Object] ownderInstance 对应上方的组件实例,借助此实例对象可以与上方组件实现相互通讯
*/
changeBoxid(data,oldData,ownderInstance) {
// renderjs 中与上方组件实例进行通讯的方式如下(我还没有尝试过,只是记录在此处,似乎这种方式若上方组件没有监听和绑定renderjs的change事件的话,这里也是无法实现通讯的):
// this.$ownerInstance.$vm.xxx
// this.$ownerInstance.callServiceMessage(msg)
// ownderInstance.$vm.xxx
// ownderInstance.callServiceMessage(msg)
}
}
}
</script>
- 此方案需要父组件提供 boxid(canvas容器id),若只靠组件自身创建boxid的话,还是会出现echarts图像绘制识失败的情况
- 源码请看下方
源码在此!
<template>
<view class="content">
<view @click="echarts.onClick"
:id="boxid"
:boxid="boxid"
:change:boxid="echarts.changeBoxid"
:prop="option"
:change:prop="echarts.updateEcharts" style="width: 100%;height: 260px;">
</view>
</view>
</template>
<script>
export default {
props: {
/**
* 创建 canvas 的容器 id,在同一个画面中,这个 id 必须唯一
* 若此 id 相同,那多个echarts组件会共用一个容器,导致后绘制的 echarts 实例会覆盖前一个
*/
boxid: {
type: String,
required: true,
default: ''
},
chartData: {
type: Object
},
},
data() {
return {
option: {}
};
},
mounted() {
this.$nextTick(() => {
if (Object.keys(this.chartData).length > 0) {
this.initChart()
}
})
},
methods: {
initChart() {
const seriesArr = [{
name: this.chartData.chartName,
type: "pie",
color: '#9fe080',
center: ["50%", "50%"],
radius: ["50%", "60%"],
hoverAnimation: false,
clockWise: false,
itemStyle: {
normal: {
borderWidth: 1,
borderColor: "rgba(204, 221, 247,0.6)",
color: "rgba(204, 221, 247,0.6)"
}
},
label: {
show: false
},
data: [100]
},];
const colors = [
["#389af4", "#dfeaff"],
["#ff8c37", "#ffdcc3"],
["#ffc257", "#ffedcc"],
["#fd6f97", "#fed4e0"],
["#a181fc", "#e3d9fe"],
];
seriesArr.push({
name: this.chartData.factoryName,
type: "pie",
clockWise: false,
radius: ["25%", "50%"],
center: ["50%", "50%"],
itemStyle: {
normal: {
color: colors[0][0],
shadowColor: colors[0][0],
shadowBlur: 0,
label: {
show: false,
},
labelLine: {
show: false,
},
},
},
hoverAnimation: false,
data: [
{
value: this.chartData.finishedOrder,
label: {
normal: {
show: true,
formatter: '{d}%',
position:'center',
textStyle: {
fontSize: "15",
fontWeight: "bold",
color: '#6f99ee',
},
},
},
},
{
value: this.chartData.totalOrder - this.chartData.finishedOrder,
name: "invisible",
itemStyle: {
normal: {
color: '#afc6f0',
},
emphasis: {
color: '#afc6f0',
},
},
},
],
});
this.option = {
tooltip: {
trigger: 'item',
formatter: (param) => {
let result = ''
result = `数量:${this.chartData.count}<br/>总数量:${this.chartData.total}`
return result
},
},
backgroundColor: "#fff",
series: seriesArr,
};
},
onViewClick(options) {
this.$emit('pieParams', options.series[0].name)
}
}
};
</script>
<script module="echarts" lang="renderjs">
let myChart
export default {
data() {
return {
cvboxid: '',
}
},
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: {
changeBoxid(data, _old, _ownerInstance) {
this.cvboxid = data
setTimeout(() => {this.initEcharts()}, 0)
},
initEcharts() {
if(!this.cvboxid) return
console.log(1)
myChart = echarts.init(document.getElementById(this.cvboxid))
myChart.setOption(this.option)
// 观测更新的数据在 view 层可以直接访问到
},
updateEcharts(newValue, oldValue, ownerInstance, instance) {
if(myChart){
myChart.setOption(newValue)
}
// 监听 service 层数据变更
},
onClick(event, ownerInstance) {
ownerInstance.callMethod('onViewClick', this.option)
}
}
}
</script>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.echarts01 {
width: 100%;
height: 300px;
}
</style>
- test.vue -- uni-app 的页面组件的关键代码
<template>
<view v-for="(item, index) in list" :key="item.boxid" class="card">
<RingChart :boxid="item.boxid" :chart-data="item.chartData"></RingChart>
</view>
</template>
<script>
import RingChart from './ring-chart.vue';
export default {
components: {
RingChart
},
data() {
return {
list: []
};
},
onLoad(options) {
this.getData();
},
methods: {
getData() {
// 这里是一个模拟的api,实际请求数据根据自己项目的实际接口而定
this.http({
url: '/api/request/data',
method: 'GET',
success: res => {
// 假设接口反馈类似这样的数据:res.data 是个二维数组,包含多个提供给echarts的绘图数据,
// 这块根据自己项目实际情况而定,解析成绘制图像所需的数据格式即可,这里简化
// 如[[{name:'kate', value:100},{name:'nice', value:200}],...]
// 这里是个关键:boxid一定要现在
const newTestList = res.data.map((d,i)=>({boxid: `ring-chart-${i}`,chartData:d}))
this.dataList = newTestList
}
});
}
},
};
</script>