从 PHP 到 AI + Golang,程序员自救转型手记(二十六):icon 组件封装
这是一个系列 Blog作者将以一个 PHP 全栈工程师的身份利用 AI 工具claude code、codex、deepseek、豆包等从零开始学习 golang 语言并最终完成 ai-go-admingithub | gitee开源项目的制作全程记录分享。在上一期我们进行了 “后台布局迁移尝试”本期将完成icon 组件封装icon 组件封装上一期尝试让 AI 直接完成后台布局相关的迁移失败了手动迁移过程发现布局相关已经会使用很多的icon所以先来完成icon组件的封装。核心需求其实就是使用name属性就能渲染出不同的icon原来的写法script setup import { Smile } from lucide/vue; /script template Smile color#3e9392 / /template目标的写法template !-- lucide 图标 -- Icon namelucide-smile color#3e9392 / !-- element puls 图标 -- Icon nameel-smile color#3e9392 / /template即单行代码即可显示图标无需手动导入这种实现的好处之一就是后台的菜单的图标可以随时修改。图标选型让 AI 找了很多高星的图标库项目其实挺看好iconpark的可惜他们的 github 仓库已经归档了最后选了lucide因为他 star 多现代vue3 兼容性好图标总量不多不少。图标加载我建立了src\components\icon\index.vue图标组件然后尝试使用resolveComponent(图标名)直接进行加载失败了很合理接下来让 CCsrc\components\icon\index.vue 中的 Smile 是来自 lucide/vue 库的图标我希望后续使用该库的图标免去import{Smile}fromlucide/vue直接使用 resolveComponent(Smile)之类的方式使用即提供一个图标名即可渲染这么一个需求对于 cc 来讲当然不在话下它额外提醒我构建产物体积会增加很多改造要点 lucide/vue 库导出了一个 icons 对象它是一个 图标名 → 组件 的映射表如{Smile: FunctionalComponent, ArrowRight: FunctionalComponent,...}直接用它即可按名渲染无需全局注册或逐个 import。 导入 icons 会将 所有 Lucide 图标打入包中约3000个这是按名动态使用图标的必然代价。如果后续包体积成为问题可考虑使用 Vite 的 import.meta.glob 做按需分块加载。我又让它确定一下具体会增加多大看看在不在可接受范围以内● 实测结果 ┌─────────────────────────────┬─────────┐ │ 指标 │ 大小 │ ├─────────────────────────────┼─────────┤ │ 全部1737个图标源码 │ ~1.5 MB │ ├─────────────────────────────┼─────────┤ │ 构建后 minified │ ~615 KB │ ├─────────────────────────────┼─────────┤ │ 构建后 gzip实际网络传输 │ ~134 KB │ └─────────────────────────────┴─────────┘ 也就是说引入 icons 映射后用户实际下载的增量约为134KBgzip这在现代前端项目中是可以接受的。 如果后续觉得134KB 还是偏大可以考虑按需分块方案import.meta.glob defineAsyncComponent只加载用到的图标但实现复杂度会高不少。当前方案是简单和体积之间的合理折中。很遗憾我现在就觉得大因为并不是所有用户都会开启 gzip而且我不知道 AI 测试下的 gzip 压缩等级是多少构建后的大小是 615KB做到按需加载那么大小会呈指数级下降目前的构建后是 615 kb且全量加载改造后构建产物理论上不会小但是是按需加载可以理解为用的图标大小 5kb 就只加载 5kb现在就完成按需分块方案import.meta.glob defineAsyncComponent只加载用到的图标而且将import* as elIcons fromelement-plus/icons-vue的所有图标也注册为按需加载失算了花了 10 分钟28k tokens它还没搞定这个可能有点复杂构建成功。1760 个 chunkLucide 图标不再打入主包。 Element Plus 图标由于在单个文件中不能实现按需加载。这一轮element plus 图标无法按需加载那就还是按我自己之前的方式动态的注册全部组件即可官方文档之前看过几行代码后续手搓。1760 个 chunk 当然是不行的让 cc 配置分包规则减少 chunk 数量能否配置打包的分包规则减少 chunk 数量这太多了● 理解1737 个 chunk 部署时确实太多。最务实的方案Lucide 图标合并为单个 lazy chunk不在主包中用到才加载。# ......● 这样 Rollup 将所有 Lucide 图标合并为一个 lazy chunk配合 HTTP/2 一次性下载128KB比1737个小文件逐个请求高效得多。单个chunk 128kb看起来是在加载任意图标时又回到了全量加载其实我的想法是实现比如按图标前缀分组 chunk 的方式只是这种方案也很复杂且并不完美好像是卡瓶颈了只能再多和 AI 沟通沟通让它再想想是否还有其他办法先切换到plan mode我需要真正的按需加载且 chunk 不要那么多是否还有其他办法Leavening…(7m 5s · ↓27.2k tokens)首次使用任意 Lucide 图标时一次性下载128KB(gzip)之后所有图标即时可用。这比首屏就加载615KB 要好得多也比部署1760个文件实际可用得多。看得出来 AI 自我感觉良好它告诉我现在的方案非常好但是实际情况是首屏一般就有使用 Lucide 图标所以这 128KB 会在首屏完成加载这显得懒加载功能很多余…这时候可能会有小伙伴说换图标库有的图标库从第三方 cdn 按需加载图标但那不是我想要的。整理现状和所有方案这里确定是遇到瓶颈了让 AI 总结一下现在遇到的问题和可行的方案● Import 只是一个图标名不是工具函数。所以 lucide/vue 没有内置按需加载机制。 --- 核心矛盾在于 Vite8/ Rolldown 的代码分割机制 - 每个 import()→1个独立 chunk这是代码分割的基础无法改变 - import.meta.glob → 为每个匹配文件生成一个 import()→1737个 chunk - manualChunks 在 Rolldown 中无法可靠合并这些 chunk我们已验证过 - 单次 import()且内部静态引用多个模块 →1个 chunkbarrel 方案的核心原理 所以能达成真正按需 chunk 数量可控的路径只有 ┌──────────────────────┬──────────────────────────────────────────────────────────────┬──────────────────────────┬──────────────────────────────┐ │ 方案 │ 原理 │ chunk 数 │ 维护负担 │ ├──────────────────────┼──────────────────────────────────────────────────────────────┼──────────────────────────┼──────────────────────────────┤ │ A. Barrel 生成脚本 │ 脚本生成5个静态 re-export 文件各一个 import()│5│ 加一个生成脚本 postinstall │ ├──────────────────────┼──────────────────────────────────────────────────────────────┼──────────────────────────┼──────────────────────────────┤ │ B. Vite 插件虚拟模块 │ 同 A但不落盘插件 load 钩子动态生成 barrel 内容 │5│ 加一个轻量 Vite 插件 │ ├──────────────────────┼──────────────────────────────────────────────────────────────┼──────────────────────────┼──────────────────────────────┤ │ C. 接受单 chunk │ import(lucide/vue)│1│ 零 │ ├──────────────────────┼──────────────────────────────────────────────────────────────┼──────────────────────────┼──────────────────────────────┤ │ D. 手动import│ 放弃按名动态使用哪里用哪里import{X}fromlucide/vue│0tree-shaking 到主包 │ 零 │ └──────────────────────┴──────────────────────────────────────────────────────────────┴──────────────────────────┴──────────────────────────────┘ BVite 插件 最优雅——不需要落盘文件、不污染代码库、始终与 lucide/vue 版本同步只需一个 ~30 行的 Vite 插件。 你倾向哪个 ✻ Cogitatedfor38s我现在想到了一个新的方案在编译期间确定使用了那些图标然后写入到一个文件内记录程序内再完成动态注册是一种别样的按需加载。但是我后续要做图标选择器组件该组件需要所有 lucide icon 的 name 和将图标渲染出来供用户选择最终还是全量加载所以目前感觉方案 B 是最佳的现在让 AI 实现最终方案Vite 插件 虚拟模块 不需要生成脚本、不需要 postinstall、不需要落盘文件。全部逻辑在 vite.config.ts 中一个 ~40 行的自定义插件 lucideIconBatches()。 原理 菜单配置写icon:Smile↓IconnameSmile/↓ iconLoader.ts → getBatchName(Smile)→q-s↓ import(virtual:lucide-icons/q-s)← 动态 import产生独立 chunk ↓ Vite 插件 load 钩子 → 读取 node_modules返回315个图标 re-export ↓ Rolldown 将315个静态 re-export 打包为1个 chunk95 KB /24KB gzip 构建产物 ┌───────────────────────┬──────────────┬────────┬────────────────────┐ │ Chunk │ min /gzip│ 图标数 │ 触发条件 │ ├───────────────────────┼──────────────┼────────┼────────────────────┤ │ a-c-*.js │140/36KB │514│ 首字母 a–c │ ├───────────────────────┼──────────────┼────────┼────────────────────┤ │ d-l-*.js │124/33KB │424│ 首字母 d–l │ ├───────────────────────┼──────────────┼────────┼────────────────────┤ │ m-p-*.js │73/20KB │262│ 首字母 m–p │ ├───────────────────────┼──────────────┼────────┼────────────────────┤ │ q-s-*.js │95/24KB │315│ 首字母 q–s │ ├───────────────────────┼──────────────┼────────┼────────────────────┤ │ t-z-*.js │62/18KB │223│ 首字母 t–z │ ├───────────────────────┼──────────────┼────────┼────────────────────┤ │ createLucideIcon-*.js │1.3/0.8KB │ — │ 公共依赖仅一次 │ └───────────────────────┴──────────────┴────────┴────────────────────┘ - 首屏0 KB Lucide 代码 - 使用 Smile仅加载 q-s chunk24 KB gzip - 总 chunk 数27之前1760 - 构建时间1.08s 菜单配置用法{menus:[{title:仪表盘,icon:LayoutDashboard},{title:用户管理,icon:Users},{title:设置,icon:Settings}]}改配置即可换图标无需改任何代码。 ✻ Sautéedfor1m 47s测试对比数据准备目前尚未对最终方案的代码review先测试一下是否达成需求由于需要和未实现按需加载之前做对比我将本次工作区的全部改动使用git stash暂存起来。未加载任何图标前浏览器禁用缓存总是使用同一 vue 页面测试开发环境下47 个请求传输 4.7MB4.7MB 项资源。构建产物assets内共 17 个项目1.55 MB、14 个请求传输 1.6MB1.6MB 项资源上 100kb 的文件有以下几个http://localhost:8000/assets/index-Cww9–Nk.js1078kbhttp://localhost:8000/assets/vue.runtime.esm-bundler-58Gmz7_F.js111kbhttp://localhost:8000/assets/style-C1fZPW92.css370kb方案数据开发环境下475 个请求传输 5.4MB5.3MB 项资源。我勒个豆一个 icon 一个请求不测了先让 AI 改将同一 Chunk 的 icon 放到一个文件可以吗目前单 chunk 的请求数达到了 475 个● 完美。每个 chunk1个 HTTP 请求。 a-c 140KB0个 import()只有1个静态 import(共享 createLucideIcon)d-l 124KB0个 import()m-p 73KB0个 import()q-s 95KB0个 import()t-z 62KB0个 import()根因与修复export{default as X}from...re-export→ Rolldown 将其视为透传绑定不内联模块导致每个图标一个子请求。importX from...exportconst NameX显式绑定→ Rolldown 视为本地依赖必须内联到同一个 chunk 文件。 修改的就是 Vite 插件 load 函数中生成的代码格式从一个exportfrom 变为importexportconst。其余代码iconLoader.ts、Icon 组件无需改动。 ✻ Crunchedfor1m 24s完美个 der还是 475 个请求我开关 Vite 开发服务多次确定数据没错。此时考虑到可能是因为开发环境的原因但是也得改因为大批量的文件加载会让正常的调试和 log 变得很麻烦这里提醒一下 AI我在开发环境。● 最终方案总结 开发模式(pnpm dev) import(lucide/vue)→1个请求加载全部图标 避免虚拟模块475个子请求 生产模式(pnpm build) import(virtual:lucide-icons/a-c)→1chunk1 请求140KB / 38KBgzipimport(virtual:lucide-icons/d-l)→1chunk1 请求127KB / 35KBgzipimport(virtual:lucide-icons/m-p)→1chunk1 请求75KB / 21KBgzipimport(virtual:lucide-icons/q-s)→1chunk1 请求97KB / 26KBgzipimport(virtual:lucide-icons/t-z)→1chunk1 请求63KB / 19KBgzipcreateLucideIcon(共享)→1chunk1 请求1.3KB 首屏 HTML0 KB Lucide 代码 使用IconnameSmile/触发 q-s chunk(26KBgzip) createLucideIcon(0.8KB)import.meta.env.DEV 是编译时常量Vite 在构建时将无效分支完全删除零运行时开销。方案数据再次统计开发环境下51 个请求传输 5.5MB5.5MB 项资源。构建产物assets内共 24 个项目2.17 MB、17 个请求传输 1.9MB1.9MB 项资源上 100kb 的文件多了以下一个http://localhost:8000/assets/a-c-CdQL3xxB.js144kb请求数合理请求大小合理但是又发现了一个大问题图标特喵根本没显示空白的。继续让 AI 改幸好只是个图标大小写转换的小问题修改好了之后再次测试逐一使用 a d m q t 开头的图标确定以下文件是按需加载的文件大小分布也较为合理AI还是行的http://localhost:8000/assets/a-c-Byxc3NbE.js144kbhttp://localhost:8000/assets/d-l-D6J0NQGY.js127kbhttp://localhost:8000/assets/m-p-Cti2KRmr.js75.2kbhttp://localhost:8000/assets/q-s-CGvOdbjT.js97.3kbhttp://localhost:8000/assets/t-z-d79zgzJ8.js63.5kb好了宣布成功后面再慢慢review吧Icon 组件核心难点已经完美解决。