别把工程化搞成“造慢轮子”:我用 Go 重构组件库图标流水线的实践
做了十年切图仔,见过的前端自动化脚本多如牛毛。不知道大家有没有发现一个现象:很多团队一聊到“前端工程化”,第一反应就是装依赖。一个百来行逻辑的代码生成脚本,能给你拉进来半个 node_modules,包含三层 AST 解析器外加两个编译器。
去年我们在折腾自研组件库的时候,图标库(Icon Component)这一块就踩了坑。设计师导出了 800 多个 SVG,我们需要把这些资产自动转换成 Vue 3 / React 组件,同时做统一的属性注入、颜色替换和 TypeScript 类型推导。
一开始大家很自然地用 Node.js 写了一套脚本,基于 @svgr/core 外加一堆正则。功能没毛病,但跑一次要 20 多秒,遇到 CI 打包或者本地发版,那个等待时间属实让人烦躁。单线程跑几百个文件的 IO 和字符串替换,Node 确实有点吃力。
后来我花了一个周末,顺手用 Golang 把这套流水线重写了。结果很现实:执行时间直接从 22 秒缩短到了 300 毫秒以内。
今天不聊宏大叙事,就聊聊怎么用简单、务实的工具思维,解决前端日常开发里的性能卡点。
痛点:为什么之前的 Node.js 脚本那么慢
写脚本的时候,大家图省事经常会掉进这几个坑:
- 依赖过重,冷启动慢:跑一个简单的 CLI,V8 引擎启动、加载上千个小文件、解析模块依赖,还没进入业务逻辑,两三秒已经过去了。
- 看似异步,实则单核卡顿:虽然 Node 有异步 IO,但大量的字符串处理、模板渲染还是吃 CPU 单核。想上
worker_threads?通信开销和样板代码又写得让人头大。 - CI 环境的依赖安装成本:每次在流水线跑这个工具,如果它是单独的 npm 包,CI 还要先
pnpm install一遍,白白浪费几分钟。
做性能优化,很多时候与其在 JS 层面做各种微操,不如直接换一个更擅长并发处理的工具。
方案:用 Go 写一个极简的转换 CLI
这个工具的核心需求其实就三步:
- 遍历指定的 SVG 资产目录。
- 清理无用属性(比如 Sketch 导出的 metadata、固定宽高、硬编码的 fill 色值),提取
viewBox和内部路径。 - 并发写入符合组件规范的
.tsx或.vue文件。
用 Go 的 sync.WaitGroup 和管道,写一个并发 Worker 池非常自然,十几行代码搞定。
核心代码实现
下面是一个简化版的处理核心,没有依赖任何沉重的第三方 AST 库,全部基于标准库和简单的文本处理:
package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"text/template"
)
// 组件模板,支持自定义颜色与尺寸
const vueTemplate = `<template>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="size"
:height="size"
viewBox="{{.ViewBox}}"
:fill="color"
v-bind="$attrs"
>
{{.InnerSVG}}
</svg>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
size?: number | string
color?: string
}>(), {
size: '1em',
color: 'currentColor',
})
</script>
`
type IconData struct {
Name string
ViewBox string
InnerSVG string
}
var (
viewBoxRegex = regexp.MustCompile(`viewBox="([^"]+)"`)
innerRegex = regexp.MustCompile(`<svg[^>]*>([\s\S]*?)<\/svg>`)
fillRegex = regexp.MustCompile(`fill="[^"]+"`)
)
func processSVG(filePath string, tmpl *template.Template, outDir string) error {
content, err := os.ReadFile(filePath)
if err != nil {
return err
}
// 1. 提取 viewBox,拿不到就保底给一个默认值
viewBox := "0 0 1024 1024"
if matches := viewBoxRegex.FindSubmatch(content); len(matches) > 1 {
viewBox = string(matches[1])
}
// 2. 提取 SVG 内部标签并清理硬编码颜色
innerSVG := ""
if matches := innerRegex.FindSubmatch(content); len(matches) > 1 {
cleanContent := fillRegex.ReplaceAll(matches[1], []byte(""))
innerSVG = strings.TrimSpace(string(cleanContent))
}
// 3. 计算组件文件名 (如 icon-user.vue)
baseName := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
componentName := "Icon" + strings.Title(baseName)
var buf bytes.Buffer
data := IconData{
Name: componentName,
ViewBox: viewBox,
InnerSVG: innerSVG,
}
if err := tmpl.Execute(&buf, data); err != nil {
return err
}
// 4. 写出目标文件
outPath := filepath.Join(outDir, componentName+".vue")
return os.WriteFile(outPath, buf.Bytes(), 0644)
}
func main() {
inputDir := "./svgs"
outputDir := "./components"
_ = os.MkdirAll(outputDir, 0755)
tmpl := template.Must(template.New("icon").Parse(vueTemplate))
files, _ := filepath.Glob(filepath.Join(inputDir, "*.svg"))
var wg sync.WaitGroup
// 限制并发数量,避免文件句柄被撑爆
sem := make(chan struct{}, 16)
for _, file := range files {
wg.Add(1)
sem <- struct{}{}
go func(f string) {
defer wg.Done()
defer func() { <-sem }()
if err := processSVG(f, tmpl, outputDir); err != nil {
fmt.Printf("处理文件失败: %s, err: %v\n", f, err)
}
}(file)
}
wg.Wait()
fmt.Printf("处理完成,共生成 %d 个图标组件\n", len(files))
}
接入前端生态:别让前端同学装 Go 环境
既然是给前端团队写工具,最忌讳的就是一句“你得先装个 Go 环境”。没人愿意为了跑个构建命令去配环境变量。
我的做法很朴素:
- 多架构交叉编译:Go 天生支持交叉编译,写个 GitHub Actions,每次打 Tag 自动输出
darwin-arm64、darwin-amd64、linux-amd64、windows-amd64四个静态二进制文件。 - 包装为本地 CLI:
通过 npm 发包,包里只包含编译好的二进制文件和一个简易的 JS 入口(用
child_process.execFileSync根据当前process.platform和process.arch调用对应二进制)。
前端同学在组件库里只需要在 package.json 写一行:
{
"scripts": {
"generate:icons": "icon-compiler --input ./raw-icons --output ./src/icons"
}
}
整个过程零感知,不用装 Go,不用管 CGO,运行体验跟普通的 npm CLI 一模一样,但速度快了两个数量级。
总结
做了这么多年工程化,我觉得一个合格的工具开发策略应该遵循这几个原则:
- 别为了架构而架构:如果只是做文本处理、文件拼接,别动不动就上 Babel/PostCSS AST,正则和模板引擎往往能搞定 90% 的需求,而且性能好得多。
- 工具链多语言化是必然趋势:从 esbuild 到 Turbopack,原生语言替代 Node 做密集计算已经是共识。日常业务逻辑写 TS 保证开发体验,但这种批处理工具、代码扫描器,用 Go 或者 Rust 来写,收益非常高。
- 性能优化的第一步是消除无效开销:把 20 秒的事情优化到 0.3 秒,省下来的不仅是时间,还有本地热更新时的注意力被打断的成本。
折腾工具的最终目的,永远是让自己早点下班。少折腾虚头巴脑的配置,多看看底层瓶颈到底在哪,工程化才能真正服务于业务。
许可协议:CC BY-NC 4.0
更新于 12 小时前
觉得文章有帮助?点个赞吧!
0 条评论


