uni-app的echarts lang=“reanderjs”这种方式如何公用一个组件动态绘制多个图像

89 阅读4分钟

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图像绘制识失败的情况
  • 源码请看下方
源码在此!
  • ring-chart.vue 组件代码
<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>