整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          Python寫一個(gè)電子發(fā)票管理工具2:前端界面開發(fā)

          Python寫一個(gè)電子發(fā)票管理工具2:前端界面開發(fā)

          一系列文章介紹如何用Python寫一個(gè)發(fā)票管理小工具。

          發(fā)票管理小工具要支持B/S和C/S兩種部署模式,因?yàn)樯婕暗桨l(fā)票這種隱私數(shù)據(jù),能夠安裝到自己電腦運(yùn)行可能是大部分人更能接受的方式。

          先看一下最終的頁(yè)面效果。

          發(fā)票夾頁(yè)面


          設(shè)置頁(yè)面


          添加抬頭頁(yè)面


          技術(shù)選型

          這個(gè)工具我不用通常的Python可視化編程如tkinter或Qt來(lái)開發(fā)PC客戶端,給大家介紹一個(gè)不太一樣的套路,采用前后端分離的模式來(lái)實(shí)現(xiàn)。

          使用FastAPI做服務(wù)端,Vue做前端頁(yè)面。

          B/S模式將程序部署到服務(wù)器,用戶使用瀏覽器訪問即可;C/S模式用python自動(dòng)打開瀏覽器頁(yè)面的方式來(lái)運(yùn)行,打包成exe下載安裝。

          需求梳理

          首先簡(jiǎn)單地用思維導(dǎo)圖將頁(yè)面需求整理一下。主要分為兩個(gè)功能模塊:發(fā)票管理(取名發(fā)票夾)和設(shè)置。發(fā)票夾功能為發(fā)票的增刪改查以及導(dǎo)入導(dǎo)出。設(shè)置目前包括抬頭管理和自定義費(fèi)用類型管理。


          CDN模式使用ElementPlus

          對(duì)Vue熟悉的朋友看下面的內(nèi)容就相當(dāng)簡(jiǎn)單了,用Vue3和ElementPlus開發(fā)網(wǎng)頁(yè)。對(duì)于網(wǎng)頁(yè)前端或Vue不太熟悉的朋友可以先看一下Vue的文檔和ElementPlus的文檔,Vue學(xué)習(xí)起來(lái)還是很簡(jiǎn)單的。

          因?yàn)楣δ芎芎?jiǎn)單這里我直接使用一個(gè)單頁(yè)面來(lái)開發(fā)這個(gè)頁(yè)面,這樣用Vue就相當(dāng)于Jquery一樣。不需要nodejs,不需要腳手架,使用起來(lái)相當(dāng)簡(jiǎn)單。但是這種用法僅限于類似的簡(jiǎn)單項(xiàng)目,稍微多幾個(gè)頁(yè)面還是需要模塊化開發(fā),便于代碼復(fù)用、代碼閱讀和代碼管理。

          首先我們用ElmentPlus提供的CDN引入模式(注意:CDN不穩(wěn)定網(wǎng)站就無(wú)法顯示了)寫一個(gè)有兩個(gè)菜單的頁(yè)面,通過(guò)點(diǎn)擊菜單切換顯示的內(nèi)容。這里需要引入vue、element-plus的css和js(安裝 | Element Plus)。

          說(shuō)明1:可以通過(guò)瀏覽器調(diào)試界面查看當(dāng)前使用的vue和elementplus版本,在CDN鏈接中指定版本和實(shí)際css與js鏈接,這樣可以避免版本升級(jí)后引入問題,并且省去幾次302跳轉(zhuǎn)加快加載時(shí)間。

          說(shuō)明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>


          使用Icon圖標(biāo)

          新版的ElementPlus提供了CDN模式的Icon,需要引入以下js,并且對(duì)圖標(biāo)組件進(jìn)行全局注冊(cè)。

          <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);
          //注冊(cè)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)就出來(lái)了

          注意1:直接復(fù)制ElementPlus示例代碼到html中是不能正常顯示的,因?yàn)?lt;folder />這樣單標(biāo)簽的寫法是不可以的,因?yàn)檫@些標(biāo)簽都不是html原生的標(biāo)簽,必須寫成<folder></folder>這樣的雙標(biāo)簽。

          注意2:使用兩個(gè)或以上單詞的組件,如<FolderAdd/>,需要使用-隔開單詞<Folder-Add></Folder-Add>。

          當(dāng)然,不使用Icon組件,直接使用SVG也可以。例如上面的folder圖標(biāo),將源碼中的SVG直接拷貝出來(lái)使用就可以。

          <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>

          國(guó)際化

          因?yàn)镋lementPlus默認(rèn)語(yǔ)言是英語(yǔ),所以需要引入中文國(guó)際化組件才能顯示中文。引入方法如下:

          <!--引入中文國(guó)際化-->
          <script src="https://unpkg.com/element-plus@2.2.0/dist/locale/zh-cn.js"></script>
          
          app.use(ElementPlus, {locale: ElementPlusLocaleZhCn,});

          JS加載完再顯示頁(yè)面

          這樣的單html頁(yè)面,在js加載完之前,會(huì)顯示一些頁(yè)面標(biāo)簽和文字,然后再展示正常頁(yè)面。如下圖:

          可以先將body設(shè)置為不顯示,然后onload后再顯示。

          <body style="display:none">
            ......
              <script>
                    ......
                    window.onload=()=> document.body.style.display='block'
                  </script>
              </script>
          </body>

          頁(yè)面代碼

          頁(yè)面就是施展CV大法了,選擇需要使用的組件,將ElementPlus頁(yè)面上的示例代碼拷貝粘貼,修改樣式和JS代碼,基礎(chǔ)頁(yè)面就寫完了。接下來(lái)就是定義接口、設(shè)計(jì)數(shù)據(jù)庫(kù)和編寫前后端邏輯代碼了~

          eb 開發(fā)主要會(huì)用到 HTML 和 CSS,而可視化則較少涉及 HTML 和 CSS??梢暬嗟匾瑸g覽器的 Canvas、SVG、WebGL 等其他圖形 API 打交道。

          Web 開發(fā)著重于處理普通的文本和多媒體信息,渲染普通的、易于閱讀的文本和多媒體內(nèi)容;可視化開發(fā)則著重于處理結(jié)構(gòu)化數(shù)據(jù),需要深入渲染引擎層,從而控制細(xì)節(jié),讓瀏覽器渲染出各種相對(duì)復(fù)雜的圖表和圖形元素。

          可視化用一句話來(lái)說(shuō),本質(zhì)上就是將數(shù)據(jù)信息組織起來(lái)后,以圖形的方式呈現(xiàn)出來(lái)。

          Web端可視化的四種方式:

          方式一:Html + Css

          現(xiàn)代瀏覽器的 HTML、CSS 表現(xiàn)能力很強(qiáng)大,完全可以實(shí)現(xiàn)常規(guī)的圖表展現(xiàn),比如,我們常見的柱狀圖、餅圖和折線圖。

          一些簡(jiǎn)單的可視化圖表,用 CSS 來(lái)實(shí)現(xiàn)很有好處,既能簡(jiǎn)化開發(fā),又不需要引入額外的庫(kù),可以節(jié)省資源,提高網(wǎng)頁(yè)打開的速度。

          用 CSS 實(shí)現(xiàn)柱狀圖其實(shí)很簡(jiǎn)單,原理就是使用網(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)餅圖,可以使用圓錐漸變,方法也很簡(jiǎ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 元素來(lái)模擬線段,用 transform 改變角度和位置,這樣就能拼成折線圖了。另外,如果使用 clip-path 這樣的高級(jí)屬性,我們還能實(shí)現(xiàn)更復(fù)雜的圖表,比如,用不同的顏色表示兩個(gè)不同折線的面積。


          從 CSS 代碼里,很難看出數(shù)據(jù)與圖形的對(duì)應(yīng)關(guān)系,有很多換算也需要開發(fā)人員自己來(lái)做。這樣一來(lái),一旦圖表或數(shù)據(jù)發(fā)生改動(dòng),就需要我們重新計(jì)算,維護(hù)起來(lái)會(huì)很麻煩。

          其次,HTML 和 CSS 作為瀏覽器渲染引擎的一部分,為了完成頁(yè)面渲染的工作,除了繪制圖形外,還要做很多額外的工作。比如說(shuō),瀏覽器的渲染引擎在工作時(shí),要先解析 HTML、SVG、CSS,構(gòu)建 DOM 樹、RenderObject 樹和 RenderLayer 樹,然后用 HTML(或 SVG)繪圖。當(dāng)圖形發(fā)生變化時(shí),我們很可能要重新執(zhí)行全部的工作,這樣的性能開銷是非常大的。


          傳統(tǒng)的 Web 開發(fā),因?yàn)樯婕?UI 構(gòu)建和內(nèi)容組織,所以這些額外的解析和構(gòu)建工作都是必須做的。而可視化與傳統(tǒng)網(wǎng)頁(yè)不同,它不太需要復(fù)雜的布局,更多的工作是在繪圖和數(shù)據(jù)計(jì)算。所以,對(duì)于可視化來(lái)說(shuō),這些額外的工作反而相當(dāng)于白白消耗了性能。

          因此,相比于 HTML 和 CSS,Canvas 和 WebGL 更適合去做可視化這一領(lǐng)域的繪圖工作。它們的繪圖 API 能夠直接操作繪圖上下文,一般不涉及引擎的其他部分,在重繪圖像時(shí),也不會(huì)發(fā)生重新解析文檔和構(gòu)建結(jié)構(gòu)的過(guò)程,開銷要小很多。

          方式二:SVG

          SVG 是一種基于 XML 語(yǔ)法的圖像格 式,可以用圖片(img 元素)的 src 屬性加載。而且,瀏覽器更強(qiáng)大的是,它還可以內(nèi)嵌 SVG 標(biāo)簽,并且像操作普通的 HTML 元素一樣,利用 DOM API 操作 SVG 元素。甚至, CSS 也可以作用于內(nèi)嵌的 SVG 元素。

          比如,上面的柱狀圖,如果用 SVG 實(shí)現(xiàn)的話,可以用如下所示的代碼來(lái)實(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 分別對(duì)應(yīng) SVG 中兩個(gè) g 元素下的 rect 元素的高度。也就是說(shuō),元素的屬性和數(shù)值可以直接對(duì)應(yīng)起來(lái)。而 CSS 代碼并不能直觀體現(xiàn)出數(shù)據(jù)的數(shù)值,需要進(jìn)行 CSS 規(guī)則轉(zhuǎn)換。具體如下圖所示:


          在上面這段 SVG 代碼中,g 表示分組,rect 表示繪制一個(gè)矩形元素。除了 rect 外,SVG 還提供了豐富的圖形元素,可以繪制矩形、圓弧、橢圓、多邊形和貝塞爾曲線等等。具體可查看 MDN SVG。

          SVG 繪制圖表與 HTML 和 CSS 繪制圖表的方式差別不大,只不過(guò)是將 HTML 標(biāo)簽替換成 SVG 標(biāo)簽,運(yùn)用了一些 SVG 支持的特殊屬性。

          HTML 的不足之處在于 HTML 元素的形狀一般是矩形,雖然用 CSS 輔助,也能夠繪制出各種其它形狀的圖形,甚至不規(guī)則圖形,但是總體而言還是非常麻煩的。而 SVG 則彌補(bǔ)了這方面的不足,讓不規(guī)則圖形的繪制變得更簡(jiǎn)單了。因此,用 SVG 繪圖比用 HTML 和 CSS 要便利得多。

          SVG 圖表也有缺點(diǎn)。在渲染引擎中,SVG 元素和 HTML 元素一樣,在輸出圖形前都需要經(jīng)過(guò)引擎的解析、布局計(jì)算和渲染樹生成。而且,一個(gè) SVG 元素只表示一種基本圖形,如果展示的數(shù)據(jù)很復(fù)雜,生成圖形的 SVG 元素就會(huì)很多。這樣一來(lái),大量的 SVG 元素不僅會(huì)占用很多內(nèi)存空間,還會(huì)增加引擎、布局計(jì)算和渲染樹生成的開銷,降低性能,減慢渲染速度。這也就注定了 SVG 只適合應(yīng)用于元素較少的簡(jiǎn)單可視化場(chǎng)景。

          方式三:Canvas

          除了 SVG,使用 Canvas 上下文來(lái)繪制可視化圖表也很方便,但是在繪制方式上, Canvas 和 HTML/CSS、SVG 又有些不同。

          無(wú)論是使用 HTML/CSS 還是 SVG,它們都屬于聲明式繪圖系統(tǒng),也就是我們根據(jù)數(shù)據(jù)創(chuàng)建各種不同的圖形元素(或者 CSS 規(guī)則),然后利用瀏覽器渲染引擎解析并渲染出來(lái)。 但是 Canvas 不同,它是瀏覽器提供的一種可以直接用代碼在一塊平面的“畫布”上繪制圖形的 API,使用它來(lái)繪圖更像是傳統(tǒng)的“編寫代碼”,簡(jiǎn)單來(lái)說(shuō)就是調(diào)用繪圖指令,然后引擎直接在頁(yè)面上繪制圖形。這是一種指令式的繪圖系統(tǒng)

          首先,Canvas 元素在瀏覽器上創(chuàng)造一個(gè)空白的畫布,通過(guò)提供渲染上下文,賦予開發(fā)者繪制內(nèi)容的能力。只需要調(diào)用渲染上下文,設(shè)置各種屬性,然后調(diào)用繪圖指令完成輸出,就能在畫布上呈現(xiàn)各種各樣的圖形。

          為了實(shí)現(xiàn)更加復(fù)雜的效果,Canvas 還提供了非常豐富的設(shè)置和繪圖 API,我們可以通過(guò)操作上下文,來(lái)改變填充和描邊顏色,對(duì)畫布進(jìn)行幾何變換,調(diào)用各種繪圖指令,然后將繪制的圖形輸出到畫布上。具體可以查看MDN Canvas。

          總結(jié)來(lái)說(shuō),Canvas 能夠直接操作繪圖上下文,不需要經(jīng)過(guò) HTML、CSS 解析、構(gòu)建渲染樹、布局等一系列操作。因此單純繪圖的話,Canvas 比 HTML/CSS 和 SVG 要快得多。

          因?yàn)?HTML 和 SVG 一個(gè)元素對(duì)應(yīng)一個(gè)基本圖形,所以我們可以很方便地操作它們,比如在柱狀圖的某個(gè)柱子上注冊(cè)點(diǎn)擊事件。而同樣的功能在 Canvas 上就比較難實(shí)現(xiàn),因?yàn)閷?duì)于 Canvas 來(lái)說(shuō),繪制整個(gè)柱狀圖的過(guò)程就是一系列指令的執(zhí)行過(guò)程,其中并沒有區(qū)分“A 柱子”、“B 柱子”,很難單獨(dú)對(duì) Canvas 繪圖的局部進(jìn)行控制。不過(guò)這并不代表就不能控制 Canvas 的局部了。實(shí)際上,通過(guò)數(shù)學(xué)計(jì)算是可以通過(guò)定位的方式來(lái)獲取局部圖形的。

          Canvas 和 SVG 的使用也不是非此即彼的,它們可以結(jié)合使用。因?yàn)?SVG 作為一種圖形格式,也可以作為 image 元素繪制到 Canvas 中。舉個(gè)例子,可以先使用 SVG 生成某些圖形,然后用 Canvas 來(lái)渲染。這樣,既可以享受 SVG 的便利性,又可以享受 Canvas 的高性能了。

          方式四:WebGL

          WebGL 繪制比前三種方式要復(fù)雜一些,因?yàn)?WebGL 是基于 OpenGL ES 規(guī)范的瀏覽器實(shí)現(xiàn)的,API 相對(duì)更底層,使用起來(lái)不如前三種那么簡(jiǎn)單直接。 一般情況下,Canvas 繪制圖形的性能已經(jīng)足夠高了,但是在三種情況下有必要直接操作更強(qiáng)大的 GPU 來(lái)實(shí)現(xiàn)繪圖。

          第一種情況,如果要繪制的圖形數(shù)量非常多,比如有多達(dá)數(shù)萬(wàn)個(gè)幾何圖形需要繪制,而且它們的位置和方向都在不停地變化,即使用 Canvas 繪制了,性能還是會(huì)達(dá)到瓶頸。這個(gè)時(shí)候,就需要使用 GPU 能力,直接用 WebGL 來(lái)繪制。

          第二種情況,如果要對(duì)較大圖像的細(xì)節(jié)做像素處理,比如,實(shí)現(xiàn)物體的光影、流體效果和一些復(fù)雜的像素濾鏡。由于這些效果往往要精準(zhǔn)地改變一個(gè)圖像全局或局部區(qū)域的所有像素點(diǎn),要計(jì)算的像素點(diǎn)數(shù)量非常的多(一般是數(shù)十萬(wàn)甚至上百萬(wàn)數(shù)量級(jí)的)。這時(shí)候即使采用 Canvas 操作,也會(huì)達(dá)到性能瓶頸,所以也要用 WebGL 來(lái)繪制。

          第三種情況是繪制 3D 物體。因?yàn)?WebGL 內(nèi)置了對(duì) 3D 物體的投影、深度檢測(cè)等特性,所以用它來(lái)渲染 3D 物體就不需要我們對(duì)坐標(biāo)做底層的處理了。在這種情況下,WebGL 無(wú)論是在使用上還是性能上都有很大優(yōu)勢(shì)。

          總結(jié):

          HTML+CSS 的優(yōu)點(diǎn)是方便,不需要第三方依賴,甚至不需要 JavaScript 代碼。如果要繪制少量常見的圖表,可以直接采用 HTML 和 CSS。它的缺點(diǎn)是 CSS 屬性不能直觀體現(xiàn)數(shù)據(jù),繪制起來(lái)也相對(duì)麻煩,圖形復(fù)雜會(huì)導(dǎo)致 HTML 元素多,而消耗性能。

          SVG 是對(duì) HTML/CSS 的增強(qiáng),彌補(bǔ)了 HTML 繪制不規(guī)則圖形的能力。它通過(guò)屬性設(shè)置圖形,可以直觀地體現(xiàn)數(shù)據(jù),使用起來(lái)非常方便。但是 SVG 也有和 HTML/CSS 同樣的問題,圖形復(fù)雜時(shí)需要的 SVG 元素太多,也非常消耗性能。

          Canvas 是瀏覽器提供的簡(jiǎn)便快捷的指令式圖形系統(tǒng),它通過(guò)一些簡(jiǎn)單的指令就能快速繪制出復(fù)雜的圖形。由于它直接操作繪圖上下文,因此沒有 HTML/CSS 和 SVG 繪圖因?yàn)樵囟鄬?dǎo)致消耗性能的問題,性能要比前兩者快得多。但是如果要繪制的圖形太多,或者處理大量的像素計(jì)算時(shí),Canvas 依然會(huì)遇到性能瓶頸。

          WebGL 是瀏覽器提供的功能強(qiáng)大的繪圖系統(tǒng),它使用比較復(fù)雜,但是功能強(qiáng)大,能夠充分利用 GPU 并行計(jì)算的能力,來(lái)快速、精準(zhǔn)地操作圖像的像素,在同一時(shí)間完成數(shù)十萬(wàn)或數(shù)百萬(wàn)次計(jì)算。另外,它還內(nèi)置了對(duì) 3D 物體的投影、深度檢測(cè)等處理,更適合繪制 3D 場(chǎng)景。

          背景

          現(xiàn)在訂閱數(shù)據(jù)分析平臺(tái)的客戶越來(lái)越多,行業(yè)也越來(lái)越寬泛,單純的標(biāo)準(zhǔn)化產(chǎn)品已經(jīng)無(wú)法滿足客戶多樣化的業(yè)務(wù)場(chǎng)景和需求。


          數(shù)據(jù)分析平臺(tái)又有多種部署環(huán)境:私有化、分析云、SAAS,不同的客戶又使用不同的版本,即使我們能夠快速開發(fā)進(jìn)行迭代,為了滿足客戶需求我們可能需要將對(duì)應(yīng)的 feature pick 至不同的版本,否則客戶就只能升級(jí)到最新版才能使用對(duì)應(yīng)的功能。


          所以我們需要提供一些插件化方案,插件的所實(shí)現(xiàn)的邏輯是可自定義、可實(shí)時(shí)更新的,不依賴于主項(xiàng)目的發(fā)版。數(shù)據(jù)分析平臺(tái)中的自定義圖表功能即是符合這樣的需求的一個(gè)插件化方案。


          2

          圖表插件化方案

          下述是自定義圖表編輯頁(yè)面的截圖,如圖所示用戶需要編寫 HTML+CSS+JavaScript 代碼以生成對(duì)應(yīng)的圖表,圖表會(huì)通過(guò) iframe 進(jìn)行渲染。如果想要復(fù)用這些代碼來(lái)創(chuàng)建圖表,則可以將代碼打包為一個(gè) json 文件,以插件的形式安裝至數(shù)據(jù)分析平臺(tái),用戶直接基于安裝的插件選擇視圖數(shù)據(jù)創(chuàng)建圖表,十分簡(jiǎn)便快捷。



          同時(shí)我們定義了一套通信機(jī)制,依托于這套通信機(jī)制,可以讓父頁(yè)面與iframe 進(jìn)行數(shù)據(jù)傳遞,如上圖中右側(cè)區(qū)域的表格數(shù)據(jù)即來(lái)自于父頁(yè)面?zhèn)魅氲囊晥D數(shù)據(jù)。


          這種實(shí)現(xiàn)方式雖然自由度很高,但是也要求編輯者有一定的前端知識(shí)基礎(chǔ),大大提升了使用成本;又由于iframe 的隔離限制,我們很難為自定義圖表提供一些開放能力,比如數(shù)據(jù)格式化等;此外iframe 的加載會(huì)重建上下文,不僅慢且耗費(fèi)瀏覽器資源。考慮到這些限制,我們又推出了自定義圖表Lite。


          3

          圖表插件化方案升級(jí)

          自定義圖表Lite 基于 ECharts 實(shí)現(xiàn),目的是為了讓用戶能更快更簡(jiǎn)單地創(chuàng)建圖表,相較于前者僅需要編寫 JavaScript 代碼實(shí)現(xiàn) ECharts 繪圖所需要的 option 即可,對(duì)于一些簡(jiǎn)單的圖表完全可以基于官方示例加以修改就能實(shí)現(xiàn),大大降低了圖表開發(fā)者的心智負(fù)擔(dān)。


          下面的截圖展示了自定義圖表 Lite 的編輯界面,左側(cè) option 參考ECharts官方示例的基礎(chǔ)折線圖[1]實(shí)現(xiàn)。



          繪制自定義圖表Lite 也不再使用 iframe,而是直接使用內(nèi)置的 BaseChart,脫離了 iframe 的限制,數(shù)據(jù)交互變得十分簡(jiǎn)單,且可以使用很多內(nèi)置的能力,如前面提到的在 iframe 的場(chǎng)景下難以支持的數(shù)據(jù)格式。


          當(dāng)然我們的場(chǎng)景遠(yuǎn)不止圖表能力擴(kuò)展這一種場(chǎng)景,上述圖表插件化的方案也只能為圖表這一項(xiàng)功能服務(wù)。假設(shè)我們想要實(shí)現(xiàn)更多自定義的業(yè)務(wù)場(chǎng)景,比如想要支持用戶自定義信息反饋,數(shù)據(jù)采集等場(chǎng)景,又該如何設(shè)計(jì)插件化方案呢?


          4

          插件化方案如何技術(shù)選型

          我們需要考慮如下方面來(lái)進(jìn)行插件化方案的技術(shù)選型:

          • 環(huán)境隔離:通過(guò)插件引入的自定義代碼必須和主頁(yè)面進(jìn)行隔離,防止造成樣式、變量等污染
          • 技術(shù)成熟度:該項(xiàng)技術(shù)需要已經(jīng)十分成熟,對(duì)于各個(gè)瀏覽器的支持不能太差,社區(qū)活躍度較高
          • 適應(yīng)性:對(duì)于跨平臺(tái)、跨框架有十分好的適應(yīng)性,這樣可以一套代碼多端使用
          • 通信方式:太復(fù)雜的通信方式會(huì)增加實(shí)現(xiàn)的復(fù)雜度
          • DOM 結(jié)構(gòu)共享:會(huì)對(duì)在視口居中顯示彈窗的場(chǎng)景有所助益
          • 支持動(dòng)態(tài)加載和更新


          當(dāng)我們看到「隔離」時(shí)首先想到的是 iframe 的方案,但是iframe 也有很多劣勢(shì),具體可以參考微前端框架qiankun 技術(shù)選型時(shí)未選擇 iframe 的這篇文章 Why Not Iframe[2]


          通過(guò)阿里巴巴的D2前端技術(shù)論壇和前端早早聊了解到很多公司已經(jīng)在生產(chǎn)環(huán)境使用 Web Components 技術(shù),不少網(wǎng)站也使用了 Web Components,如 youtube[3]、github[4],眾多落地場(chǎng)景也使得我們開始關(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)目,通過(guò) polyfill 來(lái)臨時(shí)支持瀏覽器兼容,起了很大的推進(jìn)作用。如今使用的 Web Components 為它的第二個(gè)版本v1(上一個(gè)版本v0)。


          Web Components 由 custom elementsshadow domhtml templates 三項(xiàng)核心技術(shù)組成。相關(guān)技術(shù)細(xì)節(jié)則不在此處贅述,感興趣則可以進(jìn)一步查看 MDN 上的介紹[8]。我們先來(lái)看看如何基于 Web Components 實(shí)現(xiàn)一個(gè)自定義元素。


          class MyElement extends HTMLElement {
              constructor() {
                  super()
                  // 創(chuàng)建一個(gè) 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)了一個(gè)自定義元素 my-element,使用 customElements 的 define 方法即可以定義自定義元素對(duì)應(yīng)的實(shí)現(xiàn),我們可以在 html 文件中引入對(duì)應(yīng)的 js 文件,并使用該自定義元素,在瀏覽器中打開該 html 文件即可以看到內(nèi)容成功渲染。


          <html>
             <head>
                 <script src="./my-element.js"></script>
             </head>
            <body>
                <my-element></my-element>
            </body>
          </html>


          Shadow Dom 還有一個(gè)比較特殊的 css 偽類選擇器 :host,通過(guò)這個(gè)選擇器可以選中 Shadow Root,當(dāng)我們想要根據(jù)不同環(huán)境給自定義元素定義樣式時(shí),可以使用 :host-context() 偽類選擇器。如下css 代碼即實(shí)現(xiàn)了「當(dāng)該自定義元素在 h1 標(biāo)簽中時(shí),設(shè)置其背景色為紅色」的功能。


          :host-context(h1) {
              background-color: red;
          }


          Web Components 的功能遠(yuǎn)不止于此,其他更多使用可以參考官方示例[9]。在了解 Web Components 的使用方式后,該技術(shù)方案是否可以滿足現(xiàn)有的業(yè)務(wù)場(chǎng)景需求,如支持在頁(yè)面上自定義一個(gè)反饋入口,則還需要進(jìn)一步驗(yàn)證。


          6

          基于 Web Components 的插件化方案驗(yàn)證


          由于數(shù)據(jù)分析平臺(tái)是基于 React 開發(fā)的,為了在相同的環(huán)境中進(jìn)行測(cè)試,我們使用 create-react-app 快速創(chuàng)建一個(gè) React 項(xiàng)目。

          • 在 public 目錄中添加 my-element.js 文件,在該文件中我們實(shí)現(xiàn)了 my-element 這個(gè)自定義元素,該元素主要是繪制了一個(gè) icon, 點(diǎn)擊 icon 可以打開一個(gè)彈窗,在彈窗中會(huì)展示傳入的參數(shù) x 和 y;
          • 在 index.html 中通過(guò) script 標(biāo)簽引入該文件,同時(shí)在 App.js 中的特定容器中渲染 my-element 標(biāo)簽,并通過(guò) atrribute 的方式傳參。


          我們看一下實(shí)現(xiàn)的效果:



          對(duì)應(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語(yǔ)法,寫起來(lái)十分繁瑣,當(dāng)自定義元素的內(nèi)部結(jié)構(gòu)復(fù)雜度提升時(shí),開發(fā)效率也會(huì)相應(yīng)地降低。


          社區(qū)也有一些方案可以幫助我們快速構(gòu)建 Web Components,如Google 開源的 Lit[10],Lit 可以讓我們以編寫 React 類組件的方式來(lái)編寫 Web Components,大大提升開發(fā)體驗(yàn)。不過(guò)需要注意的是 Lit 是基于 ES2019 開發(fā)的,為了適應(yīng)低版本的瀏覽器,需要注意在打包時(shí)添加對(duì)應(yīng)的插件和polyfill?;?Lit,也有很多 UI 組件庫(kù)開源,如 Wired Elements[11]、Lithops UI[12],感興趣的話也可以去參考這些庫(kù)的實(shí)現(xiàn)。


          7

          總結(jié)

          Web Components 的技術(shù)方案已經(jīng)可以滿足我們當(dāng)前的業(yè)務(wù)場(chǎng)景

          • 通過(guò) Shadow Dom 可以實(shí)現(xiàn)樣式隔離,同時(shí)又能做到 DOM 結(jié)構(gòu)共享;
          • 數(shù)據(jù)傳遞方式也很簡(jiǎn)單,正文部分的示例中只介紹了 attribute 傳參這種方式,這種方式只支持傳遞字符串類型,當(dāng)需要傳遞復(fù)雜數(shù)據(jù)類型時(shí),我們可以通過(guò) property 的方式來(lái)傳參,具體原理可以參考 handling-data-with-web-components[13] 這篇文章;
          • 通過(guò)一個(gè)引入的 js 文件來(lái)實(shí)現(xiàn)自定義元素,可動(dòng)態(tài)化,對(duì)該 js 文件可以設(shè)置協(xié)商緩存,這樣每次訪問頁(yè)面時(shí)即能獲取最新的內(nèi)容;


          插件化的場(chǎng)景層出不窮,我們也將繼續(xù)探索 Web Components 的潛力,為插件化實(shí)現(xiàn)更多可能。

          8

          參考文檔

          • https://developer.mozilla.org/en-US/docs/Web/Web_Components[14]
          • https://qiankun.umijs.org/zh/guide[15]
          • https://www.yuque.com/kuitos/gky7yw/gesexv[16]
          • https://lit.dev/docs/[17]


          參考資料

          [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)隊(duì)開發(fā)規(guī)范,提升開發(fā)質(zhì)量,挖掘前端知識(shí)細(xì)節(jié),致力于打造更易用的ABI產(chǎn)品。

          來(lái)源-微信公眾號(hào):觀遠(yuǎn)數(shù)據(jù)技術(shù)團(tuán)隊(duì)

          出處:https://mp.weixin.qq.com/s/zIeuFnvzeT4pNrXuJ9IZEA


          主站蜘蛛池模板: 国产视频一区在线播放| 久久精品动漫一区二区三区| 91精品福利一区二区三区野战| 亚洲一区视频在线播放 | 无码人妻精品一区二区三区不卡| 亚洲国产韩国一区二区| 日本一道高清一区二区三区| 立川理惠在线播放一区| 国产人妖在线观看一区二区| 99久久精品国产高清一区二区| 国产成人AV一区二区三区无码| 少妇无码一区二区三区免费| 日韩av片无码一区二区不卡电影| 色多多免费视频观看区一区| 不卡一区二区在线| 无码国产精品一区二区免费 | 精品一区二区三区视频| 久久国产精品一区二区| 亚洲欧美成人一区二区三区 | 无码国产精品一区二区免费16| 亚洲一区AV无码少妇电影| 精品乱码一区内射人妻无码| 亚洲AV日韩AV天堂一区二区三区| 亚洲大尺度无码无码专线一区 | 国产高清视频一区三区| 国产乱码精品一区二区三区香蕉| 国产福利电影一区二区三区久久久久成人精品综合 | 在线视频一区二区三区三区不卡| 无码国产伦一区二区三区视频 | 久久久av波多野一区二区| 日韩在线一区视频| 亚洲综合一区二区精品导航| 国产传媒一区二区三区呀| 亚洲一区二区三区久久久久| 精品久久久久一区二区三区| 国产精品一级香蕉一区| 中文字幕VA一区二区三区 | 亚洲线精品一区二区三区影音先锋| 亚洲AV香蕉一区区二区三区| 日本不卡免费新一区二区三区| 蜜桃视频一区二区|