一系列文章介绍如何用Python写一个发票管理小工具。
发票管理小工具要支持B/S和C/S两种部署模式,因为涉及到发票这种隐私数据,能够安装到自己电脑运行可能是大部分人更能接受的方式。
先看一下最终的页面效果。
发票夹页面
设置页面
添加抬头页面
这个工具我不用通常的Python可视化编程如tkinter或Qt来开发PC客户端,给大家介绍一个不太一样的套路,采用前后端分离的模式来实现。
使用FastAPI做服务端,Vue做前端页面。
B/S模式将程序部署到服务器,用户使用浏览器访问即可;C/S模式用python自动打开浏览器页面的方式来运行,打包成exe下载安装。
首先简单地用思维导图将页面需求整理一下。主要分为两个功能模块:发票管理(取名发票夹)和设置。发票夹功能为发票的增删改查以及导入导出。设置目前包括抬头管理和自定义费用类型管理。
对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>
新版的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,});
这样的单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 开发着重于处理普通的文本和多媒体信息,渲染普通的、易于阅读的文本和多媒体内容;可视化开发则着重于处理结构化数据,需要深入渲染引擎层,从而控制细节,让浏览器渲染出各种相对复杂的图表和图形元素。
可视化用一句话来说,本质上就是将数据信息组织起来后,以图形的方式呈现出来。
现代浏览器的 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 是一种基于 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 只适合应用于元素较少的简单可视化场景。
除了 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 是基于 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
插件化方案如何技术选型
我们需要考虑如下方面来进行插件化方案的技术选型:
当我们看到「隔离」时首先想到的是 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 elements、shadow dom、html 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 项目。
我们看一下实现的效果:
对应的 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 的技术方案已经可以满足我们当前的业务场景:
插件化的场景层出不穷,我们也将继续探索 Web Components 的潜力,为插件化实现更多可能。
8
参考文档
参考资料
[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
*请认真填写需求信息,我们会在24小时内与您取得联系。