Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
一系列文章介紹如何用Python寫一個發(fā)票管理小工具。
發(fā)票管理小工具要支持B/S和C/S兩種部署模式,因?yàn)樯婕暗桨l(fā)票這種隱私數(shù)據(jù),能夠安裝到自己電腦運(yùn)行可能是大部分人更能接受的方式。
先看一下最終的頁面效果。
發(fā)票夾頁面
設(shè)置頁面
添加抬頭頁面
這個工具我不用通常的Python可視化編程如tkinter或Qt來開發(fā)PC客戶端,給大家介紹一個不太一樣的套路,采用前后端分離的模式來實(shí)現(xiàn)。
使用FastAPI做服務(wù)端,Vue做前端頁面。
B/S模式將程序部署到服務(wù)器,用戶使用瀏覽器訪問即可;C/S模式用python自動打開瀏覽器頁面的方式來運(yùn)行,打包成exe下載安裝。
首先簡單地用思維導(dǎo)圖將頁面需求整理一下。主要分為兩個功能模塊:發(fā)票管理(取名發(fā)票夾)和設(shè)置。發(fā)票夾功能為發(fā)票的增刪改查以及導(dǎo)入導(dǎo)出。設(shè)置目前包括抬頭管理和自定義費(fèi)用類型管理。
對Vue熟悉的朋友看下面的內(nèi)容就相當(dāng)簡單了,用Vue3和ElementPlus開發(fā)網(wǎng)頁。對于網(wǎng)頁前端或Vue不太熟悉的朋友可以先看一下Vue的文檔和ElementPlus的文檔,Vue學(xué)習(xí)起來還是很簡單的。
因?yàn)楣δ芎芎唵芜@里我直接使用一個單頁面來開發(fā)這個頁面,這樣用Vue就相當(dāng)于Jquery一樣。不需要nodejs,不需要腳手架,使用起來相當(dāng)簡單。但是這種用法僅限于類似的簡單項(xiàng)目,稍微多幾個頁面還是需要模塊化開發(fā),便于代碼復(fù)用、代碼閱讀和代碼管理。
首先我們用ElmentPlus提供的CDN引入模式(注意:CDN不穩(wěn)定網(wǎng)站就無法顯示了)寫一個有兩個菜單的頁面,通過點(diǎn)擊菜單切換顯示的內(nèi)容。這里需要引入vue、element-plus的css和js(安裝 | Element Plus)。
說明1:可以通過瀏覽器調(diào)試界面查看當(dāng)前使用的vue和elementplus版本,在CDN鏈接中指定版本和實(shí)際css與js鏈接,這樣可以避免版本升級后引入問題,并且省去幾次302跳轉(zhuǎn)加快加載時間。
說明2:C/S版本將css和js都下載到本地打包,不使用CDN。
<script src="https://unpkg.com/vue@3.2.33/dist/vue.global.js"></script>
<!-- import CSS -->
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.2.0/dist/index.css">
<!-- import JavaScript -->
<script src="https://unpkg.com/element-plus@2.2.0/dist/index.full.js"></script>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>我的發(fā)票夾</title>
<script src="https://unpkg.com/vue@3.2.33/dist/vue.global.js"></script>
<!-- import CSS -->
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.2.0/dist/index.css">
<!-- import JavaScript -->
<script src="https://unpkg.com/element-plus@2.2.0/dist/index.full.js"></script>
<style>
body {
margin: 0;
}
.el-header {
--el-header-padding: 0 0;
}
</style>
</head>
<body>
<div id="app">
<div>
<el-container>
<el-header>
<el-menu
:default-active="activeMenuIndex"
class="el-menu-demo"
mode="horizontal"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
@select="handleMenuSelect"
>
<el-menu-item index="1"> 發(fā)票夾</el-menu-item>
<el-menu-item index="2">設(shè)置</el-menu-item>
</el-menu>
</el-header>
<el-main>
<div v-show="activeMenuIndex==='1'">
發(fā)票夾
</div>
<div v-show="activeMenuIndex==='2'">
設(shè)置
</div>
</div>
<script>
const App={
setup(){
const activeMenuIndex=Vue.ref('1');
const handleMenuSelect=(key, keyPath)=> {
activeMenuIndex.value=key;
};
return {
activeMenuIndex,
handleMenuSelect,
}
},
};
const app=Vue.createApp(App);
app.use(ElementPlus);
vm=app.mount("#app");
</script>
</body>
</html>
新版的ElementPlus提供了CDN模式的Icon,需要引入以下js,并且對圖標(biāo)組件進(jìn)行全局注冊。
<script src="https://unpkg.com/@element-plus/icons-vue@1.1.4/dist/index.iife.min.js"></script>
const app=Vue.createApp(App);
app.use(ElementPlus);
//注冊icon組件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
下面為發(fā)票夾和設(shè)置添加圖標(biāo):
<el-menu-item index="1"><el-icon><folder></folder></el-icon>發(fā)票夾</el-menu-item>
<el-menu-item index="2"><el-icon><setting></setting></el-icon>設(shè)置</el-menu-item>
圖標(biāo)就出來了
注意1:直接復(fù)制ElementPlus示例代碼到html中是不能正常顯示的,因?yàn)?lt;folder />這樣單標(biāo)簽的寫法是不可以的,因?yàn)檫@些標(biāo)簽都不是html原生的標(biāo)簽,必須寫成<folder></folder>這樣的雙標(biāo)簽。
注意2:使用兩個或以上單詞的組件,如<FolderAdd/>,需要使用-隔開單詞<Folder-Add></Folder-Add>。
當(dāng)然,不使用Icon組件,直接使用SVG也可以。例如上面的folder圖標(biāo),將源碼中的SVG直接拷貝出來使用就可以。
<el-icon><folder></folder></el-icon>
<!--直接替換svg-->
<el-icon><svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-ba633cb8=""><path fill="currentColor" d="M128 192v640h768V320H485.76L357.504 192H128zm-32-64h287.872l128.384 128H928a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32z"></path></svg></el-icon>
因?yàn)镋lementPlus默認(rèn)語言是英語,所以需要引入中文國際化組件才能顯示中文。引入方法如下:
<!--引入中文國際化-->
<script src="https://unpkg.com/element-plus@2.2.0/dist/locale/zh-cn.js"></script>
app.use(ElementPlus, {locale: ElementPlusLocaleZhCn,});
這樣的單html頁面,在js加載完之前,會顯示一些頁面標(biāo)簽和文字,然后再展示正常頁面。如下圖:
可以先將body設(shè)置為不顯示,然后onload后再顯示。
<body style="display:none">
......
<script>
......
window.onload=()=> document.body.style.display='block'
</script>
</script>
</body>
頁面就是施展CV大法了,選擇需要使用的組件,將ElementPlus頁面上的示例代碼拷貝粘貼,修改樣式和JS代碼,基礎(chǔ)頁面就寫完了。接下來就是定義接口、設(shè)計數(shù)據(jù)庫和編寫前后端邏輯代碼了~
eb 開發(fā)主要會用到 HTML 和 CSS,而可視化則較少涉及 HTML 和 CSS。可視化更多地要同瀏覽器的 Canvas、SVG、WebGL 等其他圖形 API 打交道。
Web 開發(fā)著重于處理普通的文本和多媒體信息,渲染普通的、易于閱讀的文本和多媒體內(nèi)容;可視化開發(fā)則著重于處理結(jié)構(gòu)化數(shù)據(jù),需要深入渲染引擎層,從而控制細(xì)節(jié),讓瀏覽器渲染出各種相對復(fù)雜的圖表和圖形元素。
可視化用一句話來說,本質(zhì)上就是將數(shù)據(jù)信息組織起來后,以圖形的方式呈現(xiàn)出來。
現(xiàn)代瀏覽器的 HTML、CSS 表現(xiàn)能力很強(qiáng)大,完全可以實(shí)現(xiàn)常規(guī)的圖表展現(xiàn),比如,我們常見的柱狀圖、餅圖和折線圖。
一些簡單的可視化圖表,用 CSS 來實(shí)現(xiàn)很有好處,既能簡化開發(fā),又不需要引入額外的庫,可以節(jié)省資源,提高網(wǎng)頁打開的速度。
用 CSS 實(shí)現(xiàn)柱狀圖其實(shí)很簡單,原理就是使用網(wǎng)格布局(Grid Layout)加上線性漸變(Linear-gradient)。代碼及效果如下:
.bargraph {
display: grid;
width: 150px;
height: 100px;
padding: 10px;
transform: scaleY(3);
grid-template-columns: repeat(5, 20%);
}
.bargraph div {
margin: 0 2px;
}
.bargraph div:nth-child(1) {
background: linear-gradient(to bottom, transparent 75%, #37c 0, #37c 85%, #3c7
}
.bargraph div:nth-child(2) {
background: linear-gradient(to bottom, transparent 74%, #37c 0, #37c 89%, #3c7
}
.bargraph div:nth-child(3) {
background: linear-gradient(to bottom, transparent 60%, #37c 0, #37c 83%, #3c7
}
.bargraph div:nth-child(4) {
background: linear-gradient(to bottom, transparent 55%, #37c 0, #37c 75%, #3c7
}
.bargraph div:nth-child(5) {
background: linear-gradient(to bottom, transparent 32%, #37c 0, #37c 63%, #3c7
}
而要實(shí)現(xiàn)餅圖,可以使用圓錐漸變,方法也很簡單,上代碼。
.piegraph {
display: inline-block;
width: 250px;
height: 250px;
border-radius: 50%;
background-image: conic-gradient(#37c 30deg, #3c7 30deg, #3c7 65deg, orange 6
}
除此之外,用 HTML 和 CSS 也可以實(shí)現(xiàn)折線圖。可以用高度很小的 Div 元素來模擬線段,用 transform 改變角度和位置,這樣就能拼成折線圖了。另外,如果使用 clip-path 這樣的高級屬性,我們還能實(shí)現(xiàn)更復(fù)雜的圖表,比如,用不同的顏色表示兩個不同折線的面積。
從 CSS 代碼里,很難看出數(shù)據(jù)與圖形的對應(yīng)關(guān)系,有很多換算也需要開發(fā)人員自己來做。這樣一來,一旦圖表或數(shù)據(jù)發(fā)生改動,就需要我們重新計算,維護(hù)起來會很麻煩。
其次,HTML 和 CSS 作為瀏覽器渲染引擎的一部分,為了完成頁面渲染的工作,除了繪制圖形外,還要做很多額外的工作。比如說,瀏覽器的渲染引擎在工作時,要先解析 HTML、SVG、CSS,構(gòu)建 DOM 樹、RenderObject 樹和 RenderLayer 樹,然后用 HTML(或 SVG)繪圖。當(dāng)圖形發(fā)生變化時,我們很可能要重新執(zhí)行全部的工作,這樣的性能開銷是非常大的。
傳統(tǒng)的 Web 開發(fā),因?yàn)樯婕?UI 構(gòu)建和內(nèi)容組織,所以這些額外的解析和構(gòu)建工作都是必須做的。而可視化與傳統(tǒng)網(wǎng)頁不同,它不太需要復(fù)雜的布局,更多的工作是在繪圖和數(shù)據(jù)計算。所以,對于可視化來說,這些額外的工作反而相當(dāng)于白白消耗了性能。
因此,相比于 HTML 和 CSS,Canvas 和 WebGL 更適合去做可視化這一領(lǐng)域的繪圖工作。它們的繪圖 API 能夠直接操作繪圖上下文,一般不涉及引擎的其他部分,在重繪圖像時,也不會發(fā)生重新解析文檔和構(gòu)建結(jié)構(gòu)的過程,開銷要小很多。
SVG 是一種基于 XML 語法的圖像格 式,可以用圖片(img 元素)的 src 屬性加載。而且,瀏覽器更強(qiáng)大的是,它還可以內(nèi)嵌 SVG 標(biāo)簽,并且像操作普通的 HTML 元素一樣,利用 DOM API 操作 SVG 元素。甚至, CSS 也可以作用于內(nèi)嵌的 SVG 元素。
比如,上面的柱狀圖,如果用 SVG 實(shí)現(xiàn)的話,可以用如下所示的代碼來實(shí)現(xiàn):
<!--
dataset={
total: [25, 26, 40, 45, 68],
current: [15, 11, 17, 25, 37],
}
-->
<svg xmlns="http://www.w3.org/2000/svg" width="120px" height="240px" viewbox="<g" transform="translate(0, 100) scale(1, -1)">
<g>
<rect x="1" y="0" width="10" height="25" fill="#37c" />
<rect x="13" y="0" width="10" height="26" fill="#37c" />
<rect x="25" y="0" width="10" height="40" fill="#37c" />
<rect x="37" y="0" width="10" height="45" fill="#37c" />
<rect x="49" y="0" width="10" height="68" fill="#37c" />
</g>
<g>
<rect x="1" y="0" width="10" height="15" fill="#3c7" />
<rect x="13" y="0" width="10" height="11" fill="#3c7" />
<rect x="25" y="0" width="10" height="17" fill="#3c7" />
<rect x="37" y="0" width="10" height="25" fill="#3c7" />
<rect x="49" y="0" width="10" height="37" fill="#3c7" />
</g>
</svg>
從上面的 SVG 代碼中,可以一目了然地看出,數(shù)據(jù) total 和 current 分別對應(yīng) SVG 中兩個 g 元素下的 rect 元素的高度。也就是說,元素的屬性和數(shù)值可以直接對應(yīng)起來。而 CSS 代碼并不能直觀體現(xiàn)出數(shù)據(jù)的數(shù)值,需要進(jìn)行 CSS 規(guī)則轉(zhuǎn)換。具體如下圖所示:
在上面這段 SVG 代碼中,g 表示分組,rect 表示繪制一個矩形元素。除了 rect 外,SVG 還提供了豐富的圖形元素,可以繪制矩形、圓弧、橢圓、多邊形和貝塞爾曲線等等。具體可查看 MDN SVG。
SVG 繪制圖表與 HTML 和 CSS 繪制圖表的方式差別不大,只不過是將 HTML 標(biāo)簽替換成 SVG 標(biāo)簽,運(yùn)用了一些 SVG 支持的特殊屬性。
HTML 的不足之處在于 HTML 元素的形狀一般是矩形,雖然用 CSS 輔助,也能夠繪制出各種其它形狀的圖形,甚至不規(guī)則圖形,但是總體而言還是非常麻煩的。而 SVG 則彌補(bǔ)了這方面的不足,讓不規(guī)則圖形的繪制變得更簡單了。因此,用 SVG 繪圖比用 HTML 和 CSS 要便利得多。
SVG 圖表也有缺點(diǎn)。在渲染引擎中,SVG 元素和 HTML 元素一樣,在輸出圖形前都需要經(jīng)過引擎的解析、布局計算和渲染樹生成。而且,一個 SVG 元素只表示一種基本圖形,如果展示的數(shù)據(jù)很復(fù)雜,生成圖形的 SVG 元素就會很多。這樣一來,大量的 SVG 元素不僅會占用很多內(nèi)存空間,還會增加引擎、布局計算和渲染樹生成的開銷,降低性能,減慢渲染速度。這也就注定了 SVG 只適合應(yīng)用于元素較少的簡單可視化場景。
除了 SVG,使用 Canvas 上下文來繪制可視化圖表也很方便,但是在繪制方式上, Canvas 和 HTML/CSS、SVG 又有些不同。
無論是使用 HTML/CSS 還是 SVG,它們都屬于聲明式繪圖系統(tǒng),也就是我們根據(jù)數(shù)據(jù)創(chuàng)建各種不同的圖形元素(或者 CSS 規(guī)則),然后利用瀏覽器渲染引擎解析并渲染出來。 但是 Canvas 不同,它是瀏覽器提供的一種可以直接用代碼在一塊平面的“畫布”上繪制圖形的 API,使用它來繪圖更像是傳統(tǒng)的“編寫代碼”,簡單來說就是調(diào)用繪圖指令,然后引擎直接在頁面上繪制圖形。這是一種指令式的繪圖系統(tǒng)。
首先,Canvas 元素在瀏覽器上創(chuàng)造一個空白的畫布,通過提供渲染上下文,賦予開發(fā)者繪制內(nèi)容的能力。只需要調(diào)用渲染上下文,設(shè)置各種屬性,然后調(diào)用繪圖指令完成輸出,就能在畫布上呈現(xiàn)各種各樣的圖形。
為了實(shí)現(xiàn)更加復(fù)雜的效果,Canvas 還提供了非常豐富的設(shè)置和繪圖 API,我們可以通過操作上下文,來改變填充和描邊顏色,對畫布進(jìn)行幾何變換,調(diào)用各種繪圖指令,然后將繪制的圖形輸出到畫布上。具體可以查看MDN Canvas。
總結(jié)來說,Canvas 能夠直接操作繪圖上下文,不需要經(jīng)過 HTML、CSS 解析、構(gòu)建渲染樹、布局等一系列操作。因此單純繪圖的話,Canvas 比 HTML/CSS 和 SVG 要快得多。
因?yàn)?HTML 和 SVG 一個元素對應(yīng)一個基本圖形,所以我們可以很方便地操作它們,比如在柱狀圖的某個柱子上注冊點(diǎn)擊事件。而同樣的功能在 Canvas 上就比較難實(shí)現(xiàn),因?yàn)閷τ?Canvas 來說,繪制整個柱狀圖的過程就是一系列指令的執(zhí)行過程,其中并沒有區(qū)分“A 柱子”、“B 柱子”,很難單獨(dú)對 Canvas 繪圖的局部進(jìn)行控制。不過這并不代表就不能控制 Canvas 的局部了。實(shí)際上,通過數(shù)學(xué)計算是可以通過定位的方式來獲取局部圖形的。
Canvas 和 SVG 的使用也不是非此即彼的,它們可以結(jié)合使用。因?yàn)?SVG 作為一種圖形格式,也可以作為 image 元素繪制到 Canvas 中。舉個例子,可以先使用 SVG 生成某些圖形,然后用 Canvas 來渲染。這樣,既可以享受 SVG 的便利性,又可以享受 Canvas 的高性能了。
WebGL 繪制比前三種方式要復(fù)雜一些,因?yàn)?WebGL 是基于 OpenGL ES 規(guī)范的瀏覽器實(shí)現(xiàn)的,API 相對更底層,使用起來不如前三種那么簡單直接。 一般情況下,Canvas 繪制圖形的性能已經(jīng)足夠高了,但是在三種情況下有必要直接操作更強(qiáng)大的 GPU 來實(shí)現(xiàn)繪圖。
第一種情況,如果要繪制的圖形數(shù)量非常多,比如有多達(dá)數(shù)萬個幾何圖形需要繪制,而且它們的位置和方向都在不停地變化,即使用 Canvas 繪制了,性能還是會達(dá)到瓶頸。這個時候,就需要使用 GPU 能力,直接用 WebGL 來繪制。
第二種情況,如果要對較大圖像的細(xì)節(jié)做像素處理,比如,實(shí)現(xiàn)物體的光影、流體效果和一些復(fù)雜的像素濾鏡。由于這些效果往往要精準(zhǔn)地改變一個圖像全局或局部區(qū)域的所有像素點(diǎn),要計算的像素點(diǎn)數(shù)量非常的多(一般是數(shù)十萬甚至上百萬數(shù)量級的)。這時候即使采用 Canvas 操作,也會達(dá)到性能瓶頸,所以也要用 WebGL 來繪制。
第三種情況是繪制 3D 物體。因?yàn)?WebGL 內(nèi)置了對 3D 物體的投影、深度檢測等特性,所以用它來渲染 3D 物體就不需要我們對坐標(biāo)做底層的處理了。在這種情況下,WebGL 無論是在使用上還是性能上都有很大優(yōu)勢。
HTML+CSS 的優(yōu)點(diǎn)是方便,不需要第三方依賴,甚至不需要 JavaScript 代碼。如果要繪制少量常見的圖表,可以直接采用 HTML 和 CSS。它的缺點(diǎn)是 CSS 屬性不能直觀體現(xiàn)數(shù)據(jù),繪制起來也相對麻煩,圖形復(fù)雜會導(dǎo)致 HTML 元素多,而消耗性能。
SVG 是對 HTML/CSS 的增強(qiáng),彌補(bǔ)了 HTML 繪制不規(guī)則圖形的能力。它通過屬性設(shè)置圖形,可以直觀地體現(xiàn)數(shù)據(jù),使用起來非常方便。但是 SVG 也有和 HTML/CSS 同樣的問題,圖形復(fù)雜時需要的 SVG 元素太多,也非常消耗性能。
Canvas 是瀏覽器提供的簡便快捷的指令式圖形系統(tǒng),它通過一些簡單的指令就能快速繪制出復(fù)雜的圖形。由于它直接操作繪圖上下文,因此沒有 HTML/CSS 和 SVG 繪圖因?yàn)樵囟鄬?dǎo)致消耗性能的問題,性能要比前兩者快得多。但是如果要繪制的圖形太多,或者處理大量的像素計算時,Canvas 依然會遇到性能瓶頸。
WebGL 是瀏覽器提供的功能強(qiáng)大的繪圖系統(tǒng),它使用比較復(fù)雜,但是功能強(qiáng)大,能夠充分利用 GPU 并行計算的能力,來快速、精準(zhǔn)地操作圖像的像素,在同一時間完成數(shù)十萬或數(shù)百萬次計算。另外,它還內(nèi)置了對 3D 物體的投影、深度檢測等處理,更適合繪制 3D 場景。
背景
現(xiàn)在訂閱數(shù)據(jù)分析平臺的客戶越來越多,行業(yè)也越來越寬泛,單純的標(biāo)準(zhǔn)化產(chǎn)品已經(jīng)無法滿足客戶多樣化的業(yè)務(wù)場景和需求。
數(shù)據(jù)分析平臺又有多種部署環(huán)境:私有化、分析云、SAAS,不同的客戶又使用不同的版本,即使我們能夠快速開發(fā)進(jìn)行迭代,為了滿足客戶需求我們可能需要將對應(yīng)的 feature pick 至不同的版本,否則客戶就只能升級到最新版才能使用對應(yīng)的功能。
所以我們需要提供一些插件化方案,插件的所實(shí)現(xiàn)的邏輯是可自定義、可實(shí)時更新的,不依賴于主項(xiàng)目的發(fā)版。數(shù)據(jù)分析平臺中的自定義圖表功能即是符合這樣的需求的一個插件化方案。
2
圖表插件化方案
下述是自定義圖表編輯頁面的截圖,如圖所示用戶需要編寫 HTML+CSS+JavaScript 代碼以生成對應(yīng)的圖表,圖表會通過 iframe 進(jìn)行渲染。如果想要復(fù)用這些代碼來創(chuàng)建圖表,則可以將代碼打包為一個 json 文件,以插件的形式安裝至數(shù)據(jù)分析平臺,用戶直接基于安裝的插件選擇視圖數(shù)據(jù)創(chuàng)建圖表,十分簡便快捷。
同時我們定義了一套通信機(jī)制,依托于這套通信機(jī)制,可以讓父頁面與iframe 進(jìn)行數(shù)據(jù)傳遞,如上圖中右側(cè)區(qū)域的表格數(shù)據(jù)即來自于父頁面?zhèn)魅氲囊晥D數(shù)據(jù)。
這種實(shí)現(xiàn)方式雖然自由度很高,但是也要求編輯者有一定的前端知識基礎(chǔ),大大提升了使用成本;又由于iframe 的隔離限制,我們很難為自定義圖表提供一些開放能力,比如數(shù)據(jù)格式化等;此外iframe 的加載會重建上下文,不僅慢且耗費(fèi)瀏覽器資源。考慮到這些限制,我們又推出了自定義圖表Lite。
3
圖表插件化方案升級
自定義圖表Lite 基于 ECharts 實(shí)現(xiàn),目的是為了讓用戶能更快更簡單地創(chuàng)建圖表,相較于前者僅需要編寫 JavaScript 代碼實(shí)現(xiàn) ECharts 繪圖所需要的 option 即可,對于一些簡單的圖表完全可以基于官方示例加以修改就能實(shí)現(xiàn),大大降低了圖表開發(fā)者的心智負(fù)擔(dān)。
下面的截圖展示了自定義圖表 Lite 的編輯界面,左側(cè) option 參考ECharts官方示例的基礎(chǔ)折線圖[1]實(shí)現(xiàn)。
繪制自定義圖表Lite 也不再使用 iframe,而是直接使用內(nèi)置的 BaseChart,脫離了 iframe 的限制,數(shù)據(jù)交互變得十分簡單,且可以使用很多內(nèi)置的能力,如前面提到的在 iframe 的場景下難以支持的數(shù)據(jù)格式。
當(dāng)然我們的場景遠(yuǎn)不止圖表能力擴(kuò)展這一種場景,上述圖表插件化的方案也只能為圖表這一項(xiàng)功能服務(wù)。假設(shè)我們想要實(shí)現(xiàn)更多自定義的業(yè)務(wù)場景,比如想要支持用戶自定義信息反饋,數(shù)據(jù)采集等場景,又該如何設(shè)計插件化方案呢?
4
插件化方案如何技術(shù)選型
我們需要考慮如下方面來進(jìn)行插件化方案的技術(shù)選型:
當(dāng)我們看到「隔離」時首先想到的是 iframe 的方案,但是iframe 也有很多劣勢,具體可以參考微前端框架qiankun 技術(shù)選型時未選擇 iframe 的這篇文章 Why Not Iframe[2]。
通過阿里巴巴的D2前端技術(shù)論壇和前端早早聊了解到很多公司已經(jīng)在生產(chǎn)環(huán)境使用 Web Components 技術(shù),不少網(wǎng)站也使用了 Web Components,如 youtube[3]、github[4],眾多落地場景也使得我們開始關(guān)注這項(xiàng)技術(shù)。
5
什么是 Web Components
Web Components 是一套可以讓我們創(chuàng)建可重用的自定義元素的技術(shù)。它于 2011 年被 Alex Russell 在 Fronteers Conference[5] 提出,2012 年 W3C 開始正式發(fā)起草案[6],2014年正式納入標(biāo)準(zhǔn)[7],后逐漸被瀏覽器所支持,其中谷歌 2015 年開始的 Polymer Project 項(xiàng)目,通過 polyfill 來臨時支持瀏覽器兼容,起了很大的推進(jìn)作用。如今使用的 Web Components 為它的第二個版本v1(上一個版本v0)。
Web Components 由 custom elements、shadow dom、html templates 三項(xiàng)核心技術(shù)組成。相關(guān)技術(shù)細(xì)節(jié)則不在此處贅述,感興趣則可以進(jìn)一步查看 MDN 上的介紹[8]。我們先來看看如何基于 Web Components 實(shí)現(xiàn)一個自定義元素。
class MyElement extends HTMLElement {
constructor() {
super()
// 創(chuàng)建一個 shadow Root
const shadowRoot=this.attachShadow({ mode: 'open' })
const container=document.createElement('div');
container.setAttribute('id', 'container');
container.innerText="hello, my custom element"
shadowRoot.appendChild(container)
}
}
customElements.define('my-element', MyElement)
上述 js 文件中實(shí)現(xiàn)了一個自定義元素 my-element,使用 customElements 的 define 方法即可以定義自定義元素對應(yīng)的實(shí)現(xiàn),我們可以在 html 文件中引入對應(yīng)的 js 文件,并使用該自定義元素,在瀏覽器中打開該 html 文件即可以看到內(nèi)容成功渲染。
<html>
<head>
<script src="./my-element.js"></script>
</head>
<body>
<my-element></my-element>
</body>
</html>
Shadow Dom 還有一個比較特殊的 css 偽類選擇器 :host,通過這個選擇器可以選中 Shadow Root,當(dāng)我們想要根據(jù)不同環(huán)境給自定義元素定義樣式時,可以使用 :host-context() 偽類選擇器。如下css 代碼即實(shí)現(xiàn)了「當(dāng)該自定義元素在 h1 標(biāo)簽中時,設(shè)置其背景色為紅色」的功能。
:host-context(h1) {
background-color: red;
}
Web Components 的功能遠(yuǎn)不止于此,其他更多使用可以參考官方示例[9]。在了解 Web Components 的使用方式后,該技術(shù)方案是否可以滿足現(xiàn)有的業(yè)務(wù)場景需求,如支持在頁面上自定義一個反饋入口,則還需要進(jìn)一步驗(yàn)證。
6
基于 Web Components 的插件化方案驗(yàn)證
由于數(shù)據(jù)分析平臺是基于 React 開發(fā)的,為了在相同的環(huán)境中進(jìn)行測試,我們使用 create-react-app 快速創(chuàng)建一個 React 項(xiàng)目。
我們看一下實(shí)現(xiàn)的效果:
對應(yīng)的 my-element.js 的實(shí)現(xiàn)如下:
class MyElement extends HTMLElement {
constructor () {
super();
this.init();
this.open=false
this.triggerOpen=this.triggerOpen.bind(this)
this.triggerClose=this.triggerClose.bind(this)
}
init () {
const shadowRoot=this.attachShadow({mode: 'open'});
const style=document.createElement('style');
style.textContent=`
#container { height: 100% }
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
width: 40px;
border-radius: 100%;
overflow: hidden;
background-color: #fff;
box-shadow: 0 2px 4px rgb(206, 224, 245);
cursor: pointer;
}
.icon-wrapper:hover {
box-shadow: 0 4px 6px rgba(57, 85, 163, 0.8);
}
.icon-wrapper svg {
width: 20px;
height: 20px;
}
.modal-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
visibility: hidden;
transform: scale(0);
transition: opacity 0.25s 0s, transform 0.25s;
}
.modal-wrapper.show {
visibility: visible;
transform: scale(1.0);
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 300px;
background-color: white;
border-radius: 2px;
padding: 12px;
max-height: 300px;
}
`
const container=document.createElement('div');
container.setAttribute('id', 'container');
const iconWrapper=document.createElement('div')
iconWrapper.setAttribute('class', 'icon-wrapper')
iconWrapper.innerHTML=`
<svg t="1667901570010" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="21577" width="200" height="200">
<path d="M511.908 955.75c-8.807 0-17.43-3.302-24.22-10.091L385.307 843.276c-13.394-13.394-13.394-34.861 0-48.255s34.861-13.395 48.256 0l78.346 78.346 78.347-78.346c6.422-6.422 15.045-10.092 24.22-10.092h238.893c18.898 0 34.127-15.229 34.127-34.128V204.76c0-18.715-15.229-34.128-34.127-34.128H170.816c-18.715 0-34.128 15.413-34.128 34.128V750.8c0 18.9 15.413 34.128 34.128 34.128h102.383c18.898 0 34.127 15.229 34.127 34.128s-15.229 34.127-34.127 34.127H170.816c-56.513 0-102.383-45.87-102.383-102.383V204.76c0-56.513 45.87-102.383 102.383-102.383h682.552c56.512 0 102.383 45.87 102.383 102.383V750.8c0 56.513-45.87 102.383-102.383 102.383H628.419l-92.291 92.475c-6.605 6.605-15.413 10.092-24.22 10.092z" p-id="21578"></path><path d="M324.206 511.908c-28.256 0-51.19-22.935-51.19-51.191s22.934-51.192 51.19-51.192 51.192 22.936 51.192 51.192-22.935 51.191-51.192 51.191z m204.766 0c-28.256 0-51.191-22.935-51.191-51.191s22.935-51.192 51.191-51.192 51.191 22.936 51.191 51.192-22.935 51.191-51.19 51.191z m204.949 0c-28.256 0-51.191-22.935-51.191-51.191s22.935-51.192 51.191-51.192c28.256 0 51.192 22.936 51.192 51.192s-23.12 51.191-51.192 51.191z" p-id="21579"></path>
</svg>
`
const modalWrapper=document.createElement('div')
modalWrapper.setAttribute('class', 'modal-wrapper')
const content=document.createElement('div')
content.setAttribute('class', 'modal-content')
modalWrapper.appendChild(content)
container.appendChild(iconWrapper)
container.appendChild(modalWrapper)
shadowRoot.appendChild(style);
shadowRoot.appendChild(container);
}
connectedCallback() {
// 添加事件監(jiān)聽
const iconWrapper=this.shadowRoot.querySelector('#container .icon-wrapper')
iconWrapper.addEventListener('click', this.triggerOpen)
const maskWrapper=this.shadowRoot.querySelector('#container .modal-wrapper')
maskWrapper.addEventListener('click', this.triggerClose)
}
disconnectedCallback () {
// 卸載事件監(jiān)聽
const wrapper=this.shadowRoot.querySelector('#container .icon-wrapper')
wrapper && wrapper.removeEventListener('click', this.triggerOpen)
const maskWrapper=this.shadowRoot.querySelector('#container .modal-wrapper')
maskWrapper && maskWrapper.removeEventListener('click', this.triggerClose)
}
triggerOpen () {
const modalWrapper=this.shadowRoot.querySelector('#container .modal-wrapper')
if(modalWrapper) {
const maskContent=modalWrapper.querySelector('.modal-content')
maskContent.innerHTML=`
<p>x: ${this.getAttribute('x')}</p>
<p>y: ${this.getAttribute('y')}</p>
`
modalWrapper.classList.add('show')
}
}
triggerClose () {
const modalWrapper=this.shadowRoot.querySelector('#container .modal-wrapper')
modalWrapper.classList.remove('show')
}
}
customElements.define('my-element', MyElement)
上述自定義元素的實(shí)現(xiàn)是基于原生的js語法,寫起來十分繁瑣,當(dāng)自定義元素的內(nèi)部結(jié)構(gòu)復(fù)雜度提升時,開發(fā)效率也會相應(yīng)地降低。
社區(qū)也有一些方案可以幫助我們快速構(gòu)建 Web Components,如Google 開源的 Lit[10],Lit 可以讓我們以編寫 React 類組件的方式來編寫 Web Components,大大提升開發(fā)體驗(yàn)。不過需要注意的是 Lit 是基于 ES2019 開發(fā)的,為了適應(yīng)低版本的瀏覽器,需要注意在打包時添加對應(yīng)的插件和polyfill。基于 Lit,也有很多 UI 組件庫開源,如 Wired Elements[11]、Lithops UI[12],感興趣的話也可以去參考這些庫的實(shí)現(xiàn)。
7
總結(jié)
Web Components 的技術(shù)方案已經(jīng)可以滿足我們當(dāng)前的業(yè)務(wù)場景:
插件化的場景層出不窮,我們也將繼續(xù)探索 Web Components 的潛力,為插件化實(shí)現(xiàn)更多可能。
8
參考文檔
參考資料
[1] 基礎(chǔ)折線圖: https://echarts.apache.org/examples/zh/editor.html?c=line-simple
[2] Why Not Iframe: https://www.yuque.com/kuitos/gky7yw/gesexv
[3] youtube: https://www.youtube.com/index
[4] github: https://github.com/
[5] Fronteers Conference: https://fronteers.nl/congres/2011/sessions/web-components-and-model-driven-views-alex-russell
[6] 草案: https://www.w3.org/TR/2012/WD-components-intro-20120522/
[7] 標(biāo)準(zhǔn): https://www.w3.org/TR/components-intro/
[8] MDN 上的介紹: https://developer.mozilla.org/en-US/docs/Web/Web_Components
[9] 官方示例: https://github.com/mdn/web-components-examples
[10] Lit: https://lit.dev/docs/
[11] Wired Elements: https://wiredjs.com/
[12] Lithops UI: https://github.com/cenfun/lithops-ui
[13] handling-data-with-web-components: https://itnext.io/handling-data-with-web-components-9e7e4a452e6e
[14] https://developer.mozilla.org/en-US/docs/Web/Web_Components: https://developer.mozilla.org/en-US/docs/Web/Web_Components
[15] https://qiankun.umijs.org/zh/guide: https://qiankun.umijs.org/zh/guide
[16] https://www.yuque.com/kuitos/gky7yw/gesexv: https://www.yuque.com/kuitos/gky7yw/gesexv
[17] https://lit.dev/docs/: https://lit.dev/docs/
作者:w.p,觀遠(yuǎn)前端開發(fā)工程師,本碩皆就讀于東北大學(xué)。實(shí)踐團(tuán)隊開發(fā)規(guī)范,提升開發(fā)質(zhì)量,挖掘前端知識細(xì)節(jié),致力于打造更易用的ABI產(chǎn)品。
來源-微信公眾號:觀遠(yuǎn)數(shù)據(jù)技術(shù)團(tuán)隊
出處:https://mp.weixin.qq.com/s/zIeuFnvzeT4pNrXuJ9IZEA
*請認(rèn)真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。