写在前面
现如今的可视化大屏产品设计已经非常成熟,其中大屏搭建页面基本采用了相同的设计方案。大屏搭建页面一般可以分为三个模块:组件及图层管理模块,大屏页面编辑模块,组件编辑和数据源配置模块。下图是某款可视化大屏产品的大屏搭建页面:
组件库是大屏产品的灵魂,组件库包含类型丰富的组件,能满足不同业务场景数据展示需求、展示酷炫丰富的动画效果。上图右侧的组件编辑模块,修改组件的展示状态并对组件数据惊进行配置。在设计大屏组件库的过程中,团队成员重新思考了组件库与大屏的关系,并逐步形成以下共识:
- 在大屏开发过程中,组件库开发将会持续一个长期过程,可以说大屏开发最终变成了组件库开发。
- 随着组件库的组件逐渐增多,整个大屏项目打包后,组件库打包的代码占据很大体积,如果一次性加载大量组件,势必导致打开页面耗时较长。
- 当大屏组件库需更新时(增、删、改组件),整个大屏项目都需要重新测试和上线。在大屏产品初期,组件库建设是重点,组建库将会频繁更新,意味着整个大屏产品也会频繁测试上线。因组件的更改而重新上线大屏,无疑推高了大屏的运维成本。
- 组件库在组织关系上完全可以做到与平台分离,并且成为单独的仓库开发。不仅可以满足组件新增、版本更新要求,同时也可以开放给更多开发人员参与。
确定以上想法和共识后,我们决定将大屏项目分为组件库和平台两个部分,分别创建仓库开发和版本管理。平台和组件分开后,第1点和第4点的想法可以立马落实;针对第2点,组件库打包后的代码不与平台放在一起,组件由服务端提供,大屏从服务端按需获取并渲染组件;第3点,组件的版本控制可通过配置文件灵活实现,或通过接口返回组件版本,然后结合第2点,对组件动态注册。
结合以上分析,得出3点需要解决的技术问题:
- 服务端返回组件,平台渲染组件
- 配置文件或接口实现组件版本控制
- 组件动态注册,按需从服务端拉取
通过异步方式从服务端获取组件
通过阅读Vue 3的官方文档里关于异步组件的说明,defineAsyncComponent这个API可实现从服务端获得组件。
同时结合app.component()全局注册组件,这样便实现了组件使用时执行defineAsyncComponent从服务端拉取组件。
将组件注册为使用时从服务端加载。以上3点技术问题中的第1点便可通过defineAsyncComponent解决,而第3点通过app.component()注册组件后,实现了组件按需从服务端异步加载。第2点可通过配置文件或接口,在defineAsyncComponent函数里拉取指定版本的组件,代码示例如下:
component对象里是组件的名称和组件服务器请求地址,component对象可以通过配置文件配置或通过接口获得。
使用Vue开发的组件都是.vue文件需要将.vue文件打包为js文件才能被浏览器解析。组件库的每一个组件都是一个单独的js文件,所以每一个组件都需要单独打包。不管是webpack还是vite都支持库模式的打包方式,组件可以使用库模式打包。
组件打包
vite.config.ts库模式的基本配置如下:
使用vite的库模式打包组件可以指定四种文件模块规范’es’、’cjs’、’umd’、’iife’。’es’示esm模块,遵循ES6模块规范;’cjs’模块遵循CommonJS规范的模块;’umd’模块兼容浏览器全局变量、AMD 规范、CommonJS 规范的规范;’iife’为立即执行函数。除了’cjs’不能被浏览器环境使用,剩余3种都可以在浏览器环境里使用。在’es’打包的代码里,使用import和export的模块加载语法,external里的外部依赖vue会被import引入,那么在代码执行的环境里,浏览器是无法理解import和export的语法,所以这种方式不行。’iife’模式是一个自执行函数,加载完就立即执行,组件将会挂载在浏览器全局对象window上。’umd’模式兼容性强,其中AMD 规范和兼容浏览器全局变量两种方式浏览器环境支持,我们可以灵活的选择使用AMD的方式,还是浏览器全局变量方式。
打包模式
在上面的讨论中,我们初步选择出’umd’、’iife’两种模式,现在比较下这两种模式。
使用 “iife” 模式打包
vite.config.ts的打包配置如下:
build.lib.name设置了暴露给全局的变量名为RemoteAsyncComponent,得到打包后的js文件主体结构如下:
从代码里看到,立即执行函数的第二个实参是全局Vue,第一个实参{}作为函数的结果返回。RemoteAsyncComponent暴露给全局,RemoteAsyncComponent.default就是组件。
vite.config.ts里将vue作为外部提供的依赖放在external里,在打包好的my-lb.iife.js文件里,外部依赖Vue作为立即执行函的参数。所以在执行这个立即执行函前,Vue需作为可被访问的全局变量。
使用”umd”模式打包
修改build.lib.formats为[‘umd’],得到打包后的文件主体结构如下:
得到的也是一个立即执行函数,函数的实参是this和函数function(exports2, vue)。立即执行函数的函数体是一串3元运算符,这串3元运算符判断当前的代码执行环境是符合CommonJS模块规范还是AMD 规范还是浏览器环境,并根据环境执行实参function(exports2, vue)这个函数。CommonJS模块规范不适合浏览器环境,主要看AMD 规范和浏览器兼容写法。
typeof define === "function" && define.amd ? define(["exports", "vue"], factory)
AMD规范不在这做介绍,看下前半句3元运算,判断全局变量define是不是一个函数,判断define.amd是否存在。若存在就执行define([“exports”, “vue”], factory)。(这种判断全局define是否为符合AMD规范的函数的方法比较简单)
AMD规范define函数符合以下定义:
第一个参数是模块的名称,第二个参数是模块的依赖数组,这些依赖供factory使用,第三个参数factory是模块的实现方法。
回到my-lib-umd.js中,如果3元元算结果取define([“exports”, “vue”], factory),factory实际就是function(exports2, vue),function(exports2, vue)执行后,组件挂载在exports2上并返回。3元运算符的后半句是浏览器的兼容写法:
(global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.HelloAsyncComponent = {}, global.Vue));
在浏览器环境里globaThis就是window,外部依赖Vue从浏览器window对象上获得,并且组件被挂载到window.HelloAsyncComponent上,这句factory(global.HelloAsyncComponent = {}, global.Vue))执行与iife模式的执行本质一样。
打包模式比较
对比iife模式和amd规范的模式发现,iife模式会将组件挂载到window上,存在全局变量污染,外部依赖需提前挂载到window上,而外部依赖引入并挂载到window上需在平台侧完成,但平台侧并不能感知需要提前引入哪些依赖。
amd规范的模式,放松了define函数的约束,只要满足两点要求(是函数类型且有”amd”属性)即可,如此用户便可自定义define函数。打包代码里define函数的第一个参数是外部依赖数组,第二个参数factory是组件实现函数,这个函数执行后会将组件返回。可以根据define函数的第一个参数提前加载好依赖,并将加载的依赖传递给factory,factory执行完成返回组件。
下表是两种打包模式的对比:
结论:通过表格对比,amd规范的模式比较具有优势,可以采用umd打包模式,通过自定义define函数实现外部依赖预加载。
RequireJS vs自定义实现define函数
确定使用umd模式打包后,如何处理打包文件并得到组件呢?目前有两种思路:
1. 使用RequireJS加载打包文件,在require回调函数获得组件并处理
2. 自定义实现define函数。在打好包的代码里,define函数的参数factory执行便可得到组件。
为了方便后续分析,仅保留umd打包后define函数的逻辑,如下:
RequireJS
在javascript有模块化概念前,RequireJS就是javascript文件的模块加载器。RequireJS加载的模块采用AMD规范。AMD规范里,define函数是模块定义函数,RequireJS使用require函数加载模块并执行回调。
回到打包的代码中,define函数是组件的模块定义,可以被RequireJS的require方法加载。require函数的第一个参数是一个依赖数组,第二个参数是回调函数,并且这个回调函数参数是模块的导出。RequireJS的简单示例如下:
示例中,require执行时会根据define的依赖列表异步加载jquery,加载完成后执行回调函数。打包好的代码可以这样使用RequireJS加载:
vue会作为外部依赖异步加载,requrire的回调函数的参数就是define函数的返回值。
在第一章节里,组件需要作为app.component()的参数被注册,现在组件能在require的回调函数中被访问,但回调函数能否访问app,目前来看还需想一些办法。先看下自定义实现define函数。
自定义实现define函数
相较使用RequireJS而言,自定义实现define就简单许多,其中关键的步骤就是根据define的第一个参数提供相应的依赖,并按照顺序作为第二个参数factory函数的参数,实现如下:
需要注意的是,要提前将define函数里的依赖提前挂载到window上。更进一步做到自动预加载依赖依赖,这部分的说明将放在下一篇中阐述。
小结
对比了两种加载组件的方法,RequireJS是已有的库,可以直接拿来使用,但是还需做一些调整,适配我们的需求,虽然可以异步加载外部依赖,但需通过配置config方法提前配置,还无法做到按需自动加载。自定义实现define函数自由度高,可适配性强,在按需加载外部依赖的地方可供我们编写代码实现,所以选择自定义实现define函数加载组件。
总结
本文对比了两种加载组件的方法,RequireJS是已有的库,可以直接拿来使用,但是还需做一些调整,适配我们的需求,虽然可以异步加载外部依赖,但需通过配置config方法提前配置,还无法做到按需自动加载。自定义实现define函数自由度高,可适配性强,在按需加载外部依赖的地方可供我们编写代码实现,所以选择自定义实现define函数加载组件。