我們在書寫echarts組件的時候,往往會發現:
其打包的chunk包是全量的,比較大。
調用api麻煩,要注意dom的渲染時機,資源清除,大小自適應。
不支持css變量,無法動態換膚的。
本文就圍繞以上幾點進行提供一個解決方案。
Vue/React Echarts TS 本文以Vue為例,文末會貼上React實現方案
github倉庫地址: github.com/Freedom-FJ/…
首先為了解決echarts的全量引入問題,我們需要單獨書寫一個echarts依賴引入文件。
utils/echarts/index.ts
ts復制代碼/*
* @Author: mjh
* @Date: 2023-08-11 12:16:33
* @LastEditors: mjh
* @LastEditTime: 2023-08-15 10:20:14
* @Description:
*/
import * as echarts from 'echarts/core'
import { GraphicComponent, GridComponent, LegendComponent, PolarComponent, TitleComponent, TooltipComponent } from 'echarts/components'
import { BarChart, BoxplotChart, LineChart, PieChart, RadarChart } from 'echarts/charts'
import { UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
GraphicComponent,
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent,
LineChart,
BarChart,
PieChart,
BoxplotChart,
CanvasRenderer,
UniversalTransition,
RadarChart,
PolarComponent,
])
// import * as echarts from 'echarts'
// 初始化語法糖
const draw=(dom: HTMLElement, option: Record<string, any>)=> {
const chart=echarts.init(dom)
chart.clear()
chart.setOption(option)
return chart
}
export default {
...echarts,
draw
} as any
接下來我們要書寫一個 echarts 組件,只需要我們傳入option 就可以自動的繪制echarts節點。
我們先寫一個簡單的vue顯示echarts的組件,我們需要在初始化的時候獲取到dom節點,并且監聽option如參,動態的更新渲染echarts實例。 但是不要忘了在組件銷毀的時候清除echarts實例,防止占用內存。
vue復制代碼<template>
<div ref="domBox" :style="{ width, height }">
<div ref="domRef" :style="{ width, height }" />
</div>
</template>
<script lang="ts" setup>
import { watch, ref, onMounted, onUnmounted } from 'vue'
import type { ECharts } from 'echarts'
import echarts from './index'
const props=defineProps({
width: {
type: String,
default: '100%',
},
height: {
type: String,
default: '100%',
},
options: {
type: Object,
default: null,
},
})
const domRef=ref(null)
const domBox=ref(null)
let chartObj: null | ECharts=null
onMounted(()=> {
if (!domRef.value) return
init()
if (props.options)
drawOption()
})
onUnmounted(()=> {
if (chartObj) {
chartObj.dispose()
chartObj=null
}
})
watch(()=> props.options, ()=> drawOption())
// 初始化
const init=()=> {
chartObj=(echarts.init(domRef.value) as any)
}
const drawOption=()=> {
if(!chartObj) return
chartObj.setOption(props.options)
}
</script>
不要忘了我們在繪制echarts實例的時候,如果我們已經繪制完了,但是我們的外部盒子尺寸發生變化了怎么辦,這個時候我們就要監聽我們的外部盒子的尺寸變化來動態的 resize 我們的echarts實例。
MutationObserver 接口提供了監視對 DOM 樹所做更改的能力。創建并返回一個新的 MutationObserver 它會在指定的 DOM 發生變化時被調用。 其的 observe() 方法配置了 MutationObserver 對象的回調方法以開始接收與給定選項匹配的 DOM 變化的通知。 根據配置,觀察者會觀察 DOM 樹中的單個 Node,也可能會觀察被指定節點的部分或者所有的子孫節點。 要停止 MutationObserver(以便不再觸發它的回調方法),需要調用 MutationObserver.disconnect() 方法。
語法
js復制代碼var mutationObserver=new MutationObserver(callback);
mutationObserver.observe(target[, options])
對于其option下面我們需要用到以下幾個語法
獲取實例監聽變化
js復制代碼const observer=new MutationObserver((mutationsList)=> {
// 循環尋找我們的echarts dom實例,發現更新的內容包含則調用 resize 方法更新echarts的尺寸
for (const mutation of mutationsList)
if (mutation.target===echartDomBox.value) chartObj && chartObj.resize()
})
observer.observe(domBox.value, {
attributes: true,
childList: false,
characterData: true,
subtree: true
})
我們將其應用到組建內,同時為了增加交互的舒適我們還可以利用echarts api 的 showLoading 加上loading的動畫效果,(也可以使用 hideLoading 方法隱藏效果),為我們組件在未渲染實例時提升交互體驗。
vue復制代碼<template>
<div ref="domBox" :style="{ width, height }">
<div ref="domRef" :style="{ width, height }" />
</div>
</template>
<script lang="ts" setup>
import { watch, ref, onMounted, onUnmounted, nextTick } from 'vue'
import type { ECharts } from 'echarts'
import echarts from './index'
const props=defineProps({
width: {
type: String,
default: '100%',
},
height: {
type: String,
default: '100%',
},
options: {
type: Object,
default: null,
},
})
const domRef=ref(null)
const domBox=ref(null)
let chartObj: null | ECharts=null
let observer: null | MutationObserver=null // dom 監聽
onMounted(()=> {
if (!domRef.value) return
init()
!props.options && chartObj.showLoading({
text: '',
color: '#409eff',
textColor: '#000',
maskColor: 'rgba(255, 255, 255, .95)',
zlevel: 0,
lineWidth: 2,
})
if (props.options)
drawOption()
observer=new MutationObserver((mutationsList)=> {
for (const mutation of mutationsList)
if (mutation.target===domBox.value) chartObj && chartObj.resize()
})
// 注意: 要放在nextTick內,,因為初始化時我們已經進行了一次option的更新操作
nextTick(()=> {
domBox.value && (observer as MutationObserver).observe(domBox.value, {
attributes: true,
childList: false,
characterData: true,
subtree: true
})
})
setTimeout(()=> {
chartObj && chartObj.resize()
}, 1000)
})
onUnmounted(()=> {
if (chartObj) {
chartObj.dispose()
chartObj=null
}
// 注意銷毀監聽器
observer && observer.disconnect()
})
watch(()=> props.options, ()=> drawOption())
// 初始化
const init=()=> {
chartObj=(echarts.init(domRef.value) as any)
}
const drawOption=()=> {
if(!chartObj) return
chartObj.hideLoading()
chartObj.setOption(props.options)
}
</script>
主題切換現在比較主流的而且適配最好的是CSS變量的方案,也就是所有的顏色都采用css變量進行替換。我們只需要引入新的樣式表,不需要額外的js代碼,瀏覽器會自動更新CSSOM樹,重繪所有dom的顏色,實現主體切換。
但是echarts不支持皮膚切換,因為其顏色是通過option傳入的,使用canvas渲染實例,而且其傳入顏色變量的字符串是無法識別的,必須直接傳入顏色值,不支持css變量動態渲染,但是我們可以幫他做這個操作。
我們在echarts 傳入的opiton的所有顏色處,做一層代理,我們傳入所有的顏色都是變量的字符串例如:'var(--dv-color-1)' 在渲染的時候通過中間函數,將所有的顏色變量轉化成真正的顏色值例如: #fff 那么只需要監聽主題的切換然后重新在將傳入的option進行更新顏色值再更新echarts即可。
好了思路有了,那我們開干!。
首先書寫一個 replaceVarStrings 中間函數用于將option內的所有顏色變量字符串都轉化為真正的顏色值,匹配 var() 字符串并將其中間的顏色提取出來。 當然我們需要用到宏api getComputedStyle 實時動態計算獲取我們的dom實例的樣式屬性,其返回值的 getPropertyValue 方法傳入變量名稱字符串就會返回實時的變量顏色值。
但是getComputedStyle會導致瀏覽器的重排,都為求一個“即時性”和“準確性”。
ts復制代碼/**
* echarts樣式
*/
export const useThemeValue=(styleVariables: string)=> {
return getComputedStyle(document.documentElement).getPropertyValue(styleVariables)
}
export function replaceVarStrings(obj: Record<string, any>) {
const newObj: Record<string, any>=Array.isArray(obj) ? [] : {}
for (const key in obj) {
if (typeof obj[key]==='object') {
newObj[key]=replaceVarStrings(obj[key]) // 遞歸處理子對象
}
else if (typeof obj[key]==='string' && obj[key].startsWith('var(') && obj[key].endsWith(')')) {
const varContent=obj[key].slice(4, -1) // 提取括號內的內容
newObj[key]=useThemeValue(varContent) // 替換為括號內的內容
}
else {
newObj[key]=obj[key] // 其他情況直接復制值
}
}
return newObj
}
以我們的element-plus 組件庫主題切換為例,其切換為 dark 模式是在html 的標簽上增加一個 dark的class類
html復制代碼<!-- 正常 模式 -->
<html> </html>
<!-- dark 模式 -->
<html class="dark"> </html>
通過一套 dark 的變量樣式覆蓋原來的html下的樣式
css復制代碼html {
--dv-color-background-base: #000b1a;
--dv-color-background-overlay: #000b1a;
--dv-color-background-page: #000b1a;
}
/* dark 模式 */
html .dark {
--dv-color-background-base: #000b1a;
--dv-color-background-overlay: #000b1a;
--dv-color-background-page: #000b1a;
}
剩下只需要監聽html dom節點的class列表即可。 但是監聽 div span document這種很常見,可是怎么監聽 html啊? 其實html 標簽也是一個標簽,你可以將它當作一個dom盒子,只需要像獲取div一樣 直接 document.querySelector('html') 即可獲取到他的對象了。 監聽class類還是用上文講到的 MutationObserver ,只是不同的是 observer 方法的入參屬性加上了 attributes, attributeFilter 配置:
當然監聽dom的方法有很多 對于針對dom大小的監聽也可以使用其他方法來實現,文末的React實現方案就是用 ResizeObserver 來監聽的, 我們vue就以 MutationObserver 為例。
接下來寫一個簡單的hook,返回一個響應式的isDark變量,用于讓我們的echarts組件監聽即可,在此我定義為 isDark: boolean 的形式,如果涉及到多套主題的話,也可以對下面代碼進行修改,把返回值改成 theme: 'dark', 字符串的形式。
ts復制代碼/**
* @name: 判斷當前主題hook
* @desc:
* @return {*}
*/
export const useTheme=()=> {
const htmlDom=document.querySelector('html')
if (!htmlDom) return { isDark: ref(false) }
const isDark=ref(!!htmlDom.classList.contains('dark'))
// 創建 MutationObserver 實例
const observer=new MutationObserver((mutationsList)=> {
for (const mutation of mutationsList) {
if (mutation.type==='attributes' && mutation.attributeName==='class') {
const currentClass=(mutation.target as any).className
isDark.value=currentClass.includes('dark')
}
}
})
// 配置 MutationObserver 監聽的選項
const observerOptions={
attributes: true,
attributeFilter: ['class'],
}
// 開始監聽目標節點
observer.observe(htmlDom, observerOptions)
return {
isDark
}
}
有了這些前置條件,這不是咱們的組件就呼之欲出了,只需要修改 drawOption 方法進行中間顏色代理,并且增加一個監聽器監聽 isDark 變量即可。
vue復制代碼<template>
<div ref="domBox" :style="{ width, height }">
<div ref="domRef" :style="{ width, height }" />
</div>
</template>
<script lang="ts" setup>
import type { ECharts } from 'echarts'
import { watch, ref, onMounted, onUnmounted, nextTick } from 'vue'
import { replaceVarStrings, useTheme } from './utils'
import echarts from './index'
const props=defineProps({
width: {
type: String,
default: '100%',
},
height: {
type: String,
default: '100%',
},
options: {
type: Object,
default: null,
},
})
const { isDark }=useTheme()
const domRef=ref(null)
const domBox=ref(null)
let chartObj: null | ECharts=null
let observer: null | MutationObserver=null // dom 監聽
onMounted(()=> {
if (!domRef.value) return
init()
drawOption()
observer=new MutationObserver((mutationsList)=> {
for (const mutation of mutationsList)
if (mutation.target===domBox.value) chartObj && chartObj.resize()
})
nextTick(()=> {
domBox.value && (observer as MutationObserver).observe(domBox.value, {
attributes: true,
childList: false,
characterData: true,
subtree: true
})
})
setTimeout(()=> {
chartObj && chartObj.resize()
}, 1000)
})
onUnmounted(()=> {
if (chartObj) {
chartObj.dispose()
chartObj=null
}
observer && observer.disconnect()
})
watch(()=> props.options, ()=> drawOption())
watch(()=> isDark.value, ()=> drawOption())
// 初始化
const init=()=> {
chartObj=(echarts.init(domRef.value) as any)
}
const drawOption=()=> {
if(!chartObj) return
if(!props.options) {
chartObj.clear()
chartObj.showLoading({
text: '',
color: '#409eff',
textColor: '#000',
maskColor: 'rgba(255, 255, 255, .95)',
zlevel: 0,
lineWidth: 2,
})
}
else {
chartObj.hideLoading()
chartObj.setOption(replaceVarStrings(props.options))
}
}
</script>
為了提高組建的擴展性,我們也可以為組建增加一個非自動化的lazy模式。當我們開啟了lazy模式以后,組建將不會自動的收集option依賴更新dom,也不用傳入響應式的option,而是需要外部調用組建expose的方法控制更新時機。
vue復制代碼<template>
<div ref="domBox" :style="{ width, height }">
<div ref="domRef" :style="{ width, height }" />
</div>
</template>
<script lang="ts" setup>
import { watch, ref, onMounted, onUnmounted, nextTick } from 'vue'
import { replaceVarStrings, useTheme } from './utils'
import type { ECharts } from 'echarts'
import echarts from './index'
const props=defineProps({
width: {
type: String,
default: '100%',
},
height: {
type: String,
default: '100%',
},
lazy: {
type: Boolean,
default: false,
},
options: {
type: Object,
default: null,
},
})
const { isDark }=useTheme()
const domRef=ref(null)
const domBox=ref(null)
let chartObj: null | ECharts=null
let observer: null | MutationObserver=null // dom 監聽
onMounted(()=> {
if (!domRef.value) return
init()
if(props.lazy) return
drawOption()
observer=new MutationObserver((mutationsList)=> {
for (const mutation of mutationsList)
if (mutation.target===domBox.value) resize()
})
nextTick(()=> {
domBox.value && (observer as MutationObserver).observe(domBox.value, {
attributes: true,
childList: false,
characterData: true,
subtree: true
})
})
})
onUnmounted(()=> {
if (chartObj) {
chartObj.dispose()
chartObj=null
}
observer?.disconnect()
})
watch(()=> props.options, ()=> !props.lazy && drawOption())
watch(()=> isDark.value, ()=> !props.lazy && drawOption())
// 繪制方法
const drawOption=(options=props.options)=> {
if(!chartObj) return
if(!options) {
chartObj.clear()
chartObj.showLoading({
text: '',
color: '#409eff',
textColor: '#000',
maskColor: 'rgba(255, 255, 255, .95)',
zlevel: 0,
lineWidth: 2,
})
}
else {
chartObj.hideLoading()
chartObj.setOption(replaceVarStrings(options))
}
}
// 初始化
const init=()=> {
chartObj=(echarts.init(domRef.value) as any)
}
// 重繪 自適應尺寸
const resize=()=> {
chartObj?.resize()
}
defineExpose({
drawOption,
resize,
init
})
</script>
vue復制代碼<template>
<div style="height: 300px;width: 200px;">
<BaseECharts :options="options" />
</div>
</template>
<script setup lang="ts">
import BaseECharts from '@/utils/echarts/BaseECharts.vue'
const options=ref<any>({
xAxis: {
type: 'category',
data: [],
},
color: ['var(--dv-color-danger)'], // 顏色的變量字符串
yAxis: {
type: 'value',
},
series: [
{
data: [],
type: 'line',
},
],
})
setTimeout(()=> {
options.value={
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
color: ['var(--dv-color-danger)'], // 顏色的變量字符串
yAxis: {
type: 'value',
},
series: [
{
data: [150, 230, 224, 218, 135, 147, 260],
type: 'line',
},
],
}
}, 3000)
</script>
對于react方案,我們在實現useTheme 上會有一些不一樣,我們的變量需要用useState實現,注意 useState 避免放在條件return語句之后。 useTheme:
ts復制代碼/**
* @name: 判斷當前主題hook
* @desc:
* @return {*}
*/
export const useTheme=()=> {
const htmlDom=document.querySelector('html');
const [isDark, setIsDark]=useState(!!htmlDom?.classList.contains('dark'));
const observer=useRef<MutationObserver>();
if (!htmlDom) return { isDark: false };
// 創建 MutationObserver 實例
if (!observer.current) {
observer.current=new MutationObserver((mutationsList)=> {
for (const mutation of mutationsList) {
if (mutation.type==='attributes' && mutation.attributeName==='class') {
const currentClass=(mutation.target as HTMLElement).className;
setIsDark(currentClass.includes('dark'));
}
}
});
}
// 配置 MutationObserver 監聽的選項
const observerOptions={
attributes: true,
attributeFilter: ['class'],
};
// 開始監聽目標節點
observer.current.observe(htmlDom, observerOptions);
return {
isDark,
};
};
在組件實現這邊我們換用更為簡單的api ResizeObserver 接口可以監視 Element 內容盒或邊框盒或者 SVGElement 邊界尺寸的變化。
注意記得在 useEffect 的 return 語句中將我們的 ResizeObserver 監聽器和實例銷毀。
組件:
tsx復制代碼/*
* @Author: mjh
* @Date: 2023-11-25 16:04:13
* @LastEditors: mjh
* @LastEditTime: 2023-11-25 23:23:28
* @Description:
*/
import { useEffect, useRef } from 'react';
import echarts from './index';
import { replaceVarStrings, useTheme } from './utils';
export interface EchartControllerProps {
width?: string;
height?: string;
options?: Record<string, any> | null;
}
export default function EchartController(props: EchartControllerProps) {
const { height='100%', width='100%', options }=props;
const chartRef=useRef<any>();
const cInstance=useRef<any>();
const { isDark }=useTheme();
useEffect(()=> {
if (!chartRef.current) return;
if (!cInstance.current) {
cInstance.current=echarts.init(chartRef.current);
}
const observer=new ResizeObserver(()=> {
cInstance.current.resize();
});
observer.observe(chartRef.current);
return ()=> {
cInstance.current?.dispose()
observer.disconnect();
};
}, []);
useEffect(()=> {
if (!cInstance.current) return;
if (!options) {
cInstance.current.showLoading({
text: '',
color: '#409eff',
textColor: '#000',
maskColor: 'rgba(255, 255, 255, .95)',
zlevel: 0,
lineWidth: 2,
});
return;
}
cInstance.current.hideLoading();
cInstance.current.setOption(replaceVarStrings(options));
}, [options, isDark]);
return (
<div ref={chartRef} style={{ height, width }} />
);
}
developer.mozilla.org/zh-CN/docs/… developer.mozilla.org/zh-CN/docs/…
作者:Freedom風間 鏈接:https://juejin.cn/post/7304959484828844043 來源:稀土掘金 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
說一說有哪些系統封裝工具哪個好
最近在網上搜索系統封裝相關問題時,總是會遇到這樣的問題:系統封裝工具哪個好?我不明白究竟是
哪些人這么無聊。一個左手、一個右手、你來和我說說哪個好?想不通是人為的刷詞、還是真的有人提
先說下系統封裝工具有哪些
Nowprep 系統封裝工具(信念論壇-小兵老師的系統封裝工具)
JM一鍵封裝工具(IT姊妹論壇-非常經典的一款軟件)
一鍵系統封裝工具(雨林木風論壇-名氣足夠大了吧)
ES系統封裝工具(IT天空系統封裝工具、老牌系統封裝技術論壇)
SC系統封裝工具(系統總裁論壇,后起之秀。)
如果一一舉例,我估計沒有100也有80個,后面我就不進行詳細的舉例了。
下面回到主題:系統封裝工具哪個好?
不得不說的一個話題是系統封裝工具的目的是什么?將系統制作成可以重復安裝的一個狀態。
系統封裝工具的核心是什么?sysprep.exe(微軟官方提供的封裝平臺)
系統封裝工具的作用是什么?在sysprep.exe的基礎上調用接口(部署前、部署中、部署后...)
那么,以上我舉例的這些系統封裝工具哪一個做不到這3點?所以系統封裝工具在我看來沒有好壞之分,
適合自己的才是最好的系統封裝工具。