项目使用的打包工具为esbuild,目标为构建出一个图标库。然后图标库需要适配一个提供自动导入功能的vite开源插件arco-design/plugin-vite-vue
,插件能让项目免于书写全局引入组件库或手动引入指定组件的代码,直接在Vue模板中书写对应的组件名或图标组件名即可自动导入使用。
[自动导入插件] 插件的总体思路比较简单:解析vue文件时,若监测有组件节点的名称以特定xxx开头,总体名称匹配上了库内的组件名以及其他节点信息匹配上了规则,则在webpack/vite解析vue文件和转化为JS文件时(通常在transform hook),在文件头部自动加上一句对应的导入语句即可。
关于自动导入插件
插件主要服务于一个UI组件库,该组件库的组件存放文件夹中还存放了一个icon文件夹(该文件夹内的图标转化为了.vue组件文件,后面将UI组件和图标组件统称为”组件“),插件默认以该组件库目录结构来进行代码解析。UI组件基本已满足业务效果,主要目标为适配一个自己项目的外部图标库。插件支持使用外部图标库,但解析时对图标库的路径结构,文件名称等配置有要求,因此需要改造该插件。
源码解析
-
当配置了
iconbox
时,插件会尝试动态读取该名称的库的信息,使用了require进行导入读取,插件的输出产物为CommonJS模块。因此需要图标库导出一个CommonJS模块的产物才能进行读取。 -
插件最初服务于
arco-design
UI组件库,该库内内置了一个图标库,因此插件默认也是以该库为目标模板结构开发的。插件的改造预期希望保留原来的效果不变(仍支持导入UI组件内的图标文件,虽然业务内基本不会使用),另外支持导入外部图标库依赖。因此需要区分内外图标库的格式,这里我直接用iconPrefix
的名称来做逻辑区分了。 -
插件为方便开发使用,效果设定为了开发环境(dev)时,首次监测到组件的使用时,自动导入的是1个内联所有组件bundle后的产物文件。打包时(build)则是按需引入的非内联的组件产物。
-
关于模块的动态导入功能,是插件通过读取声明文件记录了所有组件的文件路径,要求文件为es模块且为非bundle模式的产物,因此还需要产出一个ES Module的包。(业务的UI组件库已经使用vite打包的lib库模式都有对应的产物。图标库目前仅有一个ESM包,需要改造)
关于图标库
图标组件的生成
图标为调用Figma的API,读取业务内UI设计师的图标库内容,生成svg格式文件到本地。通过Vue template的包裹和文件生成.vue文件,最终通过esbuild的api打包成.js文件产物。
关于esbuild
实践发现,esbuild适用于单文件入口,内联bundle打包的场景,如网页html等,而对于lib库模块esbuild并不适合,而vite显然更适合做这件事情。但是由于图标库最开始维护时就已经使用了esbuild,因此不打算重新修改为vite打包。
因此最终咱们的需求其实是解析每个目录下的组件文件(包含组件代码xxx.vue文件、组件入口文件index.ts、组件综合收集文件xxx-ui.ts和组件综合入口文件index.ts),并依次保持原目录结构,生成esm结构的js文件。但发现使用esbuild时,按照官网文档做了一些配置后,发现了一些问题:
打包产物配置为esm,将bundle改为false(必须)。如果入口文件是组件components文件夹外部的单文件index.ts,会发现无论怎么修改outdir、outbase等配置,都无法达到目的效果。即单文件入口状态下,如果不做内联,无法自动递归读取和解析文件。
ts文件的导入另一个ts文件时,一般会要求去掉后缀。而esbuild对ts文件处理完后,会原原本本保留路径字符串的内容没有做更改。导致组件的入口index.js文件会出现诸如
require('xxx/comp.vue')
或require('xxx/index')
这种语句,但实际的产物都已经变成js文件了,导致了报错。
最终折腾半天,解决方案如下:
问题1:在打包单个组件的index.ts入口文件时,开启bundle模式,最终只生成一个.js文件,入口和组件合为一个。
问题2:选择预先读取和收集各类文件的路径形成数组文件,分别配置打包config,分批次打包。
关于图标库发布
-
一个npm库,因为最开始默认都是CommonJS模块,因此package.json的main和type更加支持以CommonJS为产物的包。
-
type影响产物被引入时的模块化类型,咱们的插件需要用require导入图标库,那么就要求图标库为CommonJS模块,因此type字段必须为
commonjs
。但图标库整体又为es module格式书写的,甚至在模块的外部最顶层使用了await关键字,这在commonjs是不被允许的。(这里又折腾了半天)
-
另外,es module支持导入commonjs模块,反之则不行。(谁叫人家esm后面出的呢......)
最终的解决方案暂时是开发时和打包修改type为module
,发布时修改为commonjs
。后续考虑写个发布前钩子脚本自动修改package.json文件的type。
其他一些遇到的问题
1、当vite图标库以单一esm包发布,插件为cjs无法导入esm产物,则想将插件改写为esm。实操并不大行。
-
考虑插件依赖问题,插件内部依赖4、5个babel,打包时若内联引入进去,大大增加打包体积,不大行。将几个babel提升成生产时依赖,下载插件时能同步下载依赖包,运行时通过导入语法加载(esm下为import)。在启动运行时调试发现,esm的import语法并不能导入需要的包,会提示相关导入的变量不存在(导入失败了),而在运行时的.pnpm里是可以发现pnpm有下载babel到同级目录的。而require是支持直接导入的。
-
当esm的插件依赖了一些cjs包,且内联进来时,打包工具会用转换语法把cjs的导入语法转换成esm模式的语法。但实际运行时还是会报错——esm不支持动态导入语法。
2、最终还是回到cjs插件+esm图标库
-
发现插件导入图标库仅仅是为了获取所有图标名的map,于是最终方案为——直接在图标库提供一个json文件给出图标信息,插件仅请求这一个文件。而导入图标的语法实际为固定路径的拼接和esm导入语法字符串的生成(语法解析和代码转换),并没有真正去导入该图标。
3、esm模块导入文件时的问题,
-
esm模块的导入语法
import xxx from 'aaa/bbb'
。如果aaa和bbb都是目录,即使bbb下有一个index.js文件,实际在运行时是不支持的(插件运行环境下)。会提示esm不支持导入一个目录 -
打包工具(esbuild,rollup)在打包ts项目时,并不支持能转换ts的路径解析结果。如
import {a} from 'xxx/index'
,开发时,ts在一般路径解析规则(如node)中,可以不书写文件类型后缀,可以最后只精确到目录(会自动去搜索其下的index.js)。但打包后,打包工具并不会修改这个路径,最终产物里面只有个不带后缀的相对路径,esm并不支持。(可能需要先进行ts的解析,在进行打包文件)
写在最后
感觉,esm虽然为大势所趋,但只要node环境一直固定为commonjs存在且泛用,总会需要考虑这些个模块化兼容的问题。
”纸上写来终觉浅,深知此事要躬行“。