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寫一個電子發票管理工具2:前端界面開發

          Python寫一個電子發票管理工具2:前端界面開發

          一系列文章介紹如何用Python寫一個發票管理小工具。

          發票管理小工具要支持B/S和C/S兩種部署模式,因為涉及到發票這種隱私數據,能夠安裝到自己電腦運行可能是大部分人更能接受的方式。

          先看一下最終的頁面效果。

          發票夾頁面


          設置頁面


          添加抬頭頁面


          技術選型

          這個工具我不用通常的Python可視化編程如tkinter或Qt來開發PC客戶端,給大家介紹一個不太一樣的套路,采用前后端分離的模式來實現。

          使用FastAPI做服務端,Vue做前端頁面。

          B/S模式將程序部署到服務器,用戶使用瀏覽器訪問即可;C/S模式用python自動打開瀏覽器頁面的方式來運行,打包成exe下載安裝。

          需求梳理

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


          CDN模式使用ElementPlus

          對Vue熟悉的朋友看下面的內容就相當簡單了,用Vue3和ElementPlus開發網頁。對于網頁前端或Vue不太熟悉的朋友可以先看一下Vue的文檔和ElementPlus的文檔,Vue學習起來還是很簡單的。

          因為功能很簡單這里我直接使用一個單頁面來開發這個頁面,這樣用Vue就相當于Jquery一樣。不需要nodejs,不需要腳手架,使用起來相當簡單。但是這種用法僅限于類似的簡單項目,稍微多幾個頁面還是需要模塊化開發,便于代碼復用、代碼閱讀和代碼管理。

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

          說明1:可以通過瀏覽器調試界面查看當前使用的vue和elementplus版本,在CDN鏈接中指定版本和實際css與js鏈接,這樣可以避免版本升級后引入問題,并且省去幾次302跳轉加快加載時間。

          說明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>我的發票夾</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"> 發票夾</el-menu-item>
                                      <el-menu-item index="2">設置</el-menu-item>
                                  </el-menu>
                              </el-header>
                              <el-main>
                                  <div v-show="activeMenuIndex==='1'">
                                      發票夾
                                  </div>
                                  <div v-show="activeMenuIndex==='2'">
                                    設置
                                  </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圖標

          新版的ElementPlus提供了CDN模式的Icon,需要引入以下js,并且對圖標組件進行全局注冊。

          <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);
          }

          下面為發票夾和設置添加圖標:

          <el-menu-item index="1"><el-icon><folder></folder></el-icon>發票夾</el-menu-item>
          <el-menu-item index="2"><el-icon><setting></setting></el-icon>設置</el-menu-item>


          圖標就出來了

          注意1:直接復制ElementPlus示例代碼到html中是不能正常顯示的,因為<folder />這樣單標簽的寫法是不可以的,因為這些標簽都不是html原生的標簽,必須寫成<folder></folder>這樣的雙標簽。

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

          當然,不使用Icon組件,直接使用SVG也可以。例如上面的folder圖標,將源碼中的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>

          國際化

          因為ElementPlus默認語言是英語,所以需要引入中文國際化組件才能顯示中文。引入方法如下:

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

          JS加載完再顯示頁面

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

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

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

          頁面代碼

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

          eb 開發主要會用到 HTML 和 CSS,而可視化則較少涉及 HTML 和 CSS。可視化更多地要同瀏覽器的 Canvas、SVG、WebGL 等其他圖形 API 打交道。

          Web 開發著重于處理普通的文本和多媒體信息,渲染普通的、易于閱讀的文本和多媒體內容;可視化開發則著重于處理結構化數據,需要深入渲染引擎層,從而控制細節,讓瀏覽器渲染出各種相對復雜的圖表和圖形元素。

          可視化用一句話來說,本質上就是將數據信息組織起來后,以圖形的方式呈現出來。

          Web端可視化的四種方式:

          方式一:Html + Css

          現代瀏覽器的 HTML、CSS 表現能力很強大,完全可以實現常規的圖表展現,比如,我們常見的柱狀圖、餅圖和折線圖。

          一些簡單的可視化圖表,用 CSS 來實現很有好處,既能簡化開發,又不需要引入額外的庫,可以節省資源,提高網頁打開的速度。

          用 CSS 實現柱狀圖其實很簡單,原理就是使用網格布局(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
          }
          



          而要實現餅圖,可以使用圓錐漸變,方法也很簡單,上代碼。

          .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 也可以實現折線圖。可以用高度很小的 Div 元素來模擬線段,用 transform 改變角度和位置,這樣就能拼成折線圖了。另外,如果使用 clip-path 這樣的高級屬性,我們還能實現更復雜的圖表,比如,用不同的顏色表示兩個不同折線的面積。


          從 CSS 代碼里,很難看出數據與圖形的對應關系,有很多換算也需要開發人員自己來做。這樣一來,一旦圖表或數據發生改動,就需要我們重新計算,維護起來會很麻煩。

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


          傳統的 Web 開發,因為涉及 UI 構建和內容組織,所以這些額外的解析和構建工作都是必須做的。而可視化與傳統網頁不同,它不太需要復雜的布局,更多的工作是在繪圖和數據計算。所以,對于可視化來說,這些額外的工作反而相當于白白消耗了性能。

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

          方式二:SVG

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

          比如,上面的柱狀圖,如果用 SVG 實現的話,可以用如下所示的代碼來實現:

          <!-- 
          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 代碼中,可以一目了然地看出,數據 total 和 current 分別對應 SVG 中兩個 g 元素下的 rect 元素的高度。也就是說,元素的屬性和數值可以直接對應起來。而 CSS 代碼并不能直觀體現出數據的數值,需要進行 CSS 規則轉換。具體如下圖所示:


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

          SVG 繪制圖表與 HTML 和 CSS 繪制圖表的方式差別不大,只不過是將 HTML 標簽替換成 SVG 標簽,運用了一些 SVG 支持的特殊屬性。

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

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

          方式三:Canvas

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

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

          首先,Canvas 元素在瀏覽器上創造一個空白的畫布,通過提供渲染上下文,賦予開發者繪制內容的能力。只需要調用渲染上下文,設置各種屬性,然后調用繪圖指令完成輸出,就能在畫布上呈現各種各樣的圖形。

          為了實現更加復雜的效果,Canvas 還提供了非常豐富的設置和繪圖 API,我們可以通過操作上下文,來改變填充和描邊顏色,對畫布進行幾何變換,調用各種繪圖指令,然后將繪制的圖形輸出到畫布上。具體可以查看MDN Canvas。

          總結來說,Canvas 能夠直接操作繪圖上下文,不需要經過 HTML、CSS 解析、構建渲染樹、布局等一系列操作。因此單純繪圖的話,Canvas 比 HTML/CSS 和 SVG 要快得多。

          因為 HTML 和 SVG 一個元素對應一個基本圖形,所以我們可以很方便地操作它們,比如在柱狀圖的某個柱子上注冊點擊事件。而同樣的功能在 Canvas 上就比較難實現,因為對于 Canvas 來說,繪制整個柱狀圖的過程就是一系列指令的執行過程,其中并沒有區分“A 柱子”、“B 柱子”,很難單獨對 Canvas 繪圖的局部進行控制。不過這并不代表就不能控制 Canvas 的局部了。實際上,通過數學計算是可以通過定位的方式來獲取局部圖形的。

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

          方式四:WebGL

          WebGL 繪制比前三種方式要復雜一些,因為 WebGL 是基于 OpenGL ES 規范的瀏覽器實現的,API 相對更底層,使用起來不如前三種那么簡單直接。 一般情況下,Canvas 繪制圖形的性能已經足夠高了,但是在三種情況下有必要直接操作更強大的 GPU 來實現繪圖。

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

          第二種情況,如果要對較大圖像的細節做像素處理,比如,實現物體的光影、流體效果和一些復雜的像素濾鏡。由于這些效果往往要精準地改變一個圖像全局或局部區域的所有像素點,要計算的像素點數量非常的多(一般是數十萬甚至上百萬數量級的)。這時候即使采用 Canvas 操作,也會達到性能瓶頸,所以也要用 WebGL 來繪制。

          第三種情況是繪制 3D 物體。因為 WebGL 內置了對 3D 物體的投影、深度檢測等特性,所以用它來渲染 3D 物體就不需要我們對坐標做底層的處理了。在這種情況下,WebGL 無論是在使用上還是性能上都有很大優勢。

          總結:

          HTML+CSS 的優點是方便,不需要第三方依賴,甚至不需要 JavaScript 代碼。如果要繪制少量常見的圖表,可以直接采用 HTML 和 CSS。它的缺點是 CSS 屬性不能直觀體現數據,繪制起來也相對麻煩,圖形復雜會導致 HTML 元素多,而消耗性能。

          SVG 是對 HTML/CSS 的增強,彌補了 HTML 繪制不規則圖形的能力。它通過屬性設置圖形,可以直觀地體現數據,使用起來非常方便。但是 SVG 也有和 HTML/CSS 同樣的問題,圖形復雜時需要的 SVG 元素太多,也非常消耗性能。

          Canvas 是瀏覽器提供的簡便快捷的指令式圖形系統,它通過一些簡單的指令就能快速繪制出復雜的圖形。由于它直接操作繪圖上下文,因此沒有 HTML/CSS 和 SVG 繪圖因為元素多導致消耗性能的問題,性能要比前兩者快得多。但是如果要繪制的圖形太多,或者處理大量的像素計算時,Canvas 依然會遇到性能瓶頸。

          WebGL 是瀏覽器提供的功能強大的繪圖系統,它使用比較復雜,但是功能強大,能夠充分利用 GPU 并行計算的能力,來快速、精準地操作圖像的像素,在同一時間完成數十萬或數百萬次計算。另外,它還內置了對 3D 物體的投影、深度檢測等處理,更適合繪制 3D 場景。

          背景

          現在訂閱數據分析平臺的客戶越來越多,行業也越來越寬泛,單純的標準化產品已經無法滿足客戶多樣化的業務場景和需求


          數據分析平臺又有多種部署環境:私有化、分析云、SAAS,不同的客戶又使用不同的版本,即使我們能夠快速開發進行迭代,為了滿足客戶需求我們可能需要將對應的 feature pick 至不同的版本,否則客戶就只能升級到最新版才能使用對應的功能。


          所以我們需要提供一些插件化方案,插件的所實現的邏輯是可自定義、可實時更新的,不依賴于主項目的發版。數據分析平臺中的自定義圖表功能即是符合這樣的需求的一個插件化方案。


          2

          圖表插件化方案

          下述是自定義圖表編輯頁面的截圖,如圖所示用戶需要編寫 HTML+CSS+JavaScript 代碼以生成對應的圖表,圖表會通過 iframe 進行渲染。如果想要復用這些代碼來創建圖表,則可以將代碼打包為一個 json 文件,以插件的形式安裝至數據分析平臺,用戶直接基于安裝的插件選擇視圖數據創建圖表,十分簡便快捷。



          同時我們定義了一套通信機制,依托于這套通信機制,可以讓父頁面與iframe 進行數據傳遞,如上圖中右側區域的表格數據即來自于父頁面傳入的視圖數據。


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


          3

          圖表插件化方案升級

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


          下面的截圖展示了自定義圖表 Lite 的編輯界面,左側 option 參考ECharts官方示例的基礎折線圖[1]實現。



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


          當然我們的場景遠不止圖表能力擴展這一種場景,上述圖表插件化的方案也只能為圖表這一項功能服務。假設我們想要實現更多自定義的業務場景,比如想要支持用戶自定義信息反饋,數據采集等場景,又該如何設計插件化方案呢?


          4

          插件化方案如何技術選型

          我們需要考慮如下方面來進行插件化方案的技術選型:

          • 環境隔離:通過插件引入的自定義代碼必須和主頁面進行隔離,防止造成樣式、變量等污染
          • 技術成熟度:該項技術需要已經十分成熟,對于各個瀏覽器的支持不能太差,社區活躍度較高
          • 適應性:對于跨平臺、跨框架有十分好的適應性,這樣可以一套代碼多端使用
          • 通信方式:太復雜的通信方式會增加實現的復雜度
          • DOM 結構共享:會對在視口居中顯示彈窗的場景有所助益
          • 支持動態加載和更新


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


          通過阿里巴巴的D2前端技術論壇和前端早早聊了解到很多公司已經在生產環境使用 Web Components 技術,不少網站也使用了 Web Components,如 youtube[3]、github[4],眾多落地場景也使得我們開始關注這項技術。


          5

          什么是 Web Components

          Web Components 是一套可以讓我們創建可重用的自定義元素的技術。它于 2011 年被 Alex Russell 在 Fronteers Conference[5] 提出,2012 年 W3C 開始正式發起草案[6],2014年正式納入標準[7],后逐漸被瀏覽器所支持,其中谷歌 2015 年開始的 Polymer Project 項目,通過 polyfill 來臨時支持瀏覽器兼容,起了很大的推進作用。如今使用的 Web Components 為它的第二個版本v1(上一個版本v0)。


          Web Components 由 custom elementsshadow domhtml templates 三項核心技術組成。相關技術細節則不在此處贅述,感興趣則可以進一步查看 MDN 上的介紹[8]。我們先來看看如何基于 Web Components 實現一個自定義元素。


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


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


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


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


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


          6

          基于 Web Components 的插件化方案驗證


          由于數據分析平臺是基于 React 開發的,為了在相同的環境中進行測試,我們使用 create-react-app 快速創建一個 React 項目。

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


          我們看一下實現的效果:



          對應的 my-element.js 的實現如下:


          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() {
                  // 添加事件監聽
                  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 () {
                  // 卸載事件監聽
                  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)
          
          


          上述自定義元素的實現是基于原生的js語法,寫起來十分繁瑣,當自定義元素的內部結構復雜度提升時,開發效率也會相應地降低。


          社區也有一些方案可以幫助我們快速構建 Web Components,如Google 開源的 Lit[10],Lit 可以讓我們以編寫 React 類組件的方式來編寫 Web Components,大大提升開發體驗。不過需要注意的是 Lit 是基于 ES2019 開發的,為了適應低版本的瀏覽器,需要注意在打包時添加對應的插件和polyfill。基于 Lit,也有很多 UI 組件庫開源,如 Wired Elements[11]、Lithops UI[12],感興趣的話也可以去參考這些庫的實現。


          7

          總結

          Web Components 的技術方案已經可以滿足我們當前的業務場景

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


          插件化的場景層出不窮,我們也將繼續探索 Web Components 的潛力,為插件化實現更多可能。

          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] 基礎折線圖: 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] 標準: 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,觀遠前端開發工程師,本碩皆就讀于東北大學。實踐團隊開發規范,提升開發質量,挖掘前端知識細節,致力于打造更易用的ABI產品。

          來源-微信公眾號:觀遠數據技術團隊

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


          主站蜘蛛池模板: 国产情侣一区二区三区| tom影院亚洲国产一区二区 | 一区二区在线电影| 一区二区在线观看视频| 日韩一区精品视频一区二区| 日韩免费一区二区三区在线播放 | 日韩一区二区在线免费观看| 亚洲AV成人一区二区三区在线看 | 波多野结衣av高清一区二区三区| 国产精品一级香蕉一区| 精品视频在线观看一区二区| 亚洲国产一区二区三区 | 激情内射亚州一区二区三区爱妻| 亚洲色大成网站www永久一区| 亚洲丰满熟女一区二区v| 国产精品视频一区二区猎奇| 亚洲午夜精品第一区二区8050| 久久无码人妻一区二区三区| 国产在线观看一区二区三区四区 | 亚洲AV无码一区二区三区牛牛| 人妻少妇一区二区三区| 成人精品一区二区三区校园激情 | 风间由美在线亚洲一区| 99精品国产一区二区三区2021| 久久久精品人妻一区二区三区四| 国产精品女同一区二区久久| 日本丰满少妇一区二区三区 | 国产免费一区二区三区VR| 午夜视频一区二区三区| 暖暖免费高清日本一区二区三区| 无码少妇丰满熟妇一区二区| 精品福利视频一区二区三区| 亚洲一区二区三区免费视频 | 无码人妻一区二区三区在线| 久久久精品一区二区三区| 日本亚洲国产一区二区三区| 亚洲AV成人一区二区三区AV| 99久久无码一区人妻a黑| 成人H动漫精品一区二区| 性色av无码免费一区二区三区| 国产精品一区二区三区久久 |