这篇博客灵感也是源于公司的项目,因为我自己之前是写 React 比较多,虽然一直记得使用了框架,开发者就不要原生的操作 DOM,但 React 主流脚手架 CRA 在将虚拟 DOM 转换成真实 DOM 的时候,也用的 document.getElementById
加上 React 写起来就很像原生的 JavaScript,所以可能对这条告诫也没有很大的敬畏之心,直到前几天写公司 Vue 栈项目需求的时候出问题了...
1 问题描述
我在项目里复用了一个叫 <lay-bar-graphs>
的组件,该组件里面用到了 echarts。,大致如下:
<div class="template-bewteen">
<lay-bar-graphs :num="1"></lay-bar-graphs>
<lay-bar-graphs :num="2"></lay-bar-graphs>
</div>
复制代码
组件里面大致是这样的(就一个容器 div
和包含一个 echarts 渲染的 div
):
<template>
<div class="layGraphs-container">
<div
id="layBarGraphs"
ref="layBarGraphs"
style="width:100%;height:100%;"
>
</div>
</div>
</template>
复制代码
我想要的效果是这样的(内网截图传不到外网,小糊):
但是我得到的却是这样的(看似只有一个 <lay-bar-graphs>
组件成功渲染)
打开浏览器开发者工具一查发现,<lay-bar-graphs>
组件渲染了两次,但 DOM 树结构里面只有一个 <canvas>
(我们都知道 echarts 会把元素渲染成 canvas),真是奇了怪~
2 解决问题
2.1 聚焦问题
接着我们意识到问题出在渲染这里,也通过 console
等手段确定,echarts 确实渲染了两次。
用过 echarts 的小伙伴都会知道,echarts 需要获取 DOM,大致如下,具体可以去看官网示例
import * as echarts from 'echarts';//引入
var chartDom = document.getElementById('main');//获取DOM
var myChart = echarts.init(chartDom);//初始化
var option = {...};//配置
option && myChart.setOption(option);//渲染
复制代码
所以我们马上把问题聚焦到获取 DOM 上,同时也是瞬间反应过来。
CSS 是全局的,Vue 和 React 等库虽然做到了组件化,但只是通过闭包等手段取巧的模拟组件效果,事实上 组件里面的id
,class
仍然是全局的。这也是为什么现在脚手架会使用 less/sass/css module 等做局部样式隔离的原因。
这个时候问题又来了,项目中是使用了 less
的,像这样 <style scoped lang="less">
,那为什么获取的 DOM 还是同一个呢?我猜测哈!less
只是将选择器隔离在一个组件内,你复用的时候,还是这个组件,所以选择器选择的结果还是一样的。
顺带一提,浏览器自带的 web component
最近经常听到吧,不用框架,浏览器原生的支持组件化,也不需要考虑样式冲突,真不错,期待后续的普及。
2.2 解决方案
在框架中获取 DOM,肯定能想到使用 ref
。那为什么使用 ref
就不会有问题呢?我来回顾一下。
被 ref
标记的元素会成为组件实例的一个属性,组件的每次使用都会创建一个新的实例,这样即使属性名一样,但是他们能够区分谁是谁,这样 echarts 的渲染也就不是同一个元素了。
3 拓展
那么原生 HTML 、JS 是如何复用 echarts 组件的呢?经过和同事以及群里老哥的讨论,我们写了一些代码,大伙可以粘贴自己去试一下~
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div{
border: black solid 1px;
width: 400px;
height: 400px;
}
.bule {
background-color: #00f;
width: 3oopx;
height: 300px;
}
.green {
background-color: #0f0;
width: 3oopx;
height: 300px;
}
</style>
</head>
<body>
<div id="t1"></div><div id="t2"></div>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
<script>
function fn (color) {
let obj = document.createElement('p');
obj.className = color//这种不起作用,因为echarts已经画完了,你才给个高度,晚了
obj.style.width = '300px'//需要及时给
obj.style.height = '300px'
var myChart = echarts.init(obj);
var option;
option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line',
smooth: true
}
]
};
option && myChart.setOption(option);
return obj;
}
document.getElementById('t1').appendChild( fn('green') );
document.getElementById('t2').appendChild( fn('bule') );
</script>
</body>
</html>
复制代码
这种方式比较取巧的是,通过 createElement
封装成一个函数,每次执行都新建元素,不存在渲染同一个元素,另外在 Vue 中也可以通过 document.getElementsByClassName
来获取 DOM,因为是一个类数组嘛!复用组件的时候传递一个参数进去,指定渲染哪个 DOM (但肯定用 ref 来的简单)。