前几年大家聊前端工程化,动不动就是“自研下一代构建工具”,开口 Rollup 闭口 Rust。但干了十年开发,我最大的感受是:团队里真正缺的,往往不是颠覆性的打包引擎,而是几个能严丝合缝卡进业务流程的小工具。
很多痛点其实极其具体。比如设计团队扔过来几百个 SVG 图标要求转成 React 组件,或者老项目重构要批量替换废弃的 Design Token。这时候靠手改会死人,写个简陋的 Node.js 脚本又经常在 CI 上跑出内存溢出。
今天聊聊我自己做前端工具开发的一些体会,顺便拆一个我用 Golang 写的图标处理 CLI 作为例子。
工具的第一原则:解决痛点,别造幻觉
很多同学写工具容易陷进一个误区:先把架构画得很大,插件系统、配置中心、交互式 CLI 一应俱全,结果核心逻辑还没写完,业务场景已经变了。
务实的做法永远是:
- 明确输入与输出:输入是原始文件还是 AST?输出是代码还是报告?
- 拒绝过度配置:初期约定优于配置,先把单一路径走通。
- 性能要跟规模匹配:处理几十个文件,Node.js 随便写写;处理上万个文件,就要考虑并发和内存占用。
实战:给组件库搞个极速 SVG 转组件 CLI
拿我们之前组件库的图标构建来说。设计规范一改,800 多个 SVG 要洗一遍(干掉无用属性、替换颜色为 currentColor、生成 TypeScript 定义和 React 组件)。
早期组里用 SVGR 加一堆 Node.js 脚本,跑一次要十几秒,本地 watch 体验很差。后来我用 Go 顺手搓了一个小 CLI,利用 Goroutine 并发清洗和模板渲染,两百毫秒直接跑完。
核心逻辑其实很简单,分三步:遍历读取、正则清洗、模板渲染。
1. 核心处理逻辑 (Go 示例)
package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"text/template"
)
// 组件模板
const componentTpl = `import React from 'react';
export const {{.Name}} = (props: React.SVGProps<SVGSVGElement>) => (
{{.Content}}
);
`
type IconData struct {
Name string
Content string
}
func sanitizeSVG(raw []byte) string {
svg := string(raw)
// 干掉 xml 声明和不必要的无用属性
regXml := regexp.MustCompile(`<\?xml.*?\?>`)
svg = regXml.ReplaceAllString(svg, "")
// 强制把 fill 替换为 currentColor,方便 CSS 控制
regFill := regexp.MustCompile(`fill="[^"]*"`)
svg = regFill.ReplaceAllString(svg, `fill="currentColor"`)
// 简单粗暴替换为 JSX 属性 (真实场景建议用更严谨的词法解析)
svg = strings.ReplaceAll(svg, "class=", "className=")
return strings.TrimSpace(svg)
}
func processFile(srcPath, outDir string, tpl *template.Template) error {
baseName := filepath.Base(srcPath)
ext := filepath.Ext(baseName)
componentName := "Icon" + strings.Title(strings.TrimSuffix(baseName, ext))
raw, err := os.ReadFile(srcPath)
if err != nil {
return err
}
cleanContent := sanitizeSVG(raw)
var buf bytes.Buffer
if err := tpl.Execute(&buf, IconData{Name: componentName, Content: cleanContent}); err != nil {
return err
}
targetPath := filepath.Join(outDir, componentName+".tsx")
return os.WriteFile(targetPath, buf.Bytes(), 0644)
}
2. 并发处理与落地
Go 在这种 I/O 和轻量文本处理的场景下优势太明显了。一个简单的 sync.WaitGroup 就能吃满多核性能:
func main() {
srcDir := "./svgs"
outDir := "./src/components/icons"
os.MkdirAll(outDir, 0755)
tpl := template.Must(template.New("icon").Parse(componentTpl))
files, _ := filepath.Glob(filepath.Join(srcDir, "*.svg"))
var wg sync.WaitGroup
// 避免句柄耗尽,可以加个 channel 控制并发度,这里演示简化处理
for _, f := range files {
wg.Add(1)
go func(path string) {
defer wg.Done()
if err := processFile(path, outDir, tpl); err != nil {
fmt.Printf("处理失败 %s: %v\n", path, err)
}
}(f)
}
wg.Wait()
fmt.Printf("处理完毕,共生成 %d 个组件。\n", len(files))
}
不需要装几百兆的 node_modules,编译成一个二进制文件丢给前端同学,不管是本地 hook 还是 CI 流程,秒级跑完。
做工具容易忽视的几个细节
写业务代码和写工具代码完全是两种思路。很多前端转写 CLI 工具时,最容易掉在这几个坑里:
- 别吞错误,但也不要无脑吐 Stack Trace 用户只关心“哪个文件出错了”、“第几行”、“大概是什么原因”。丢一行几百字的底层调用栈出去,别人只会找你提 issue。
- 重视跨平台差异
路径分隔符(Windows 的反斜杠和 POSIX 的正斜杠)、文件换行符(CRLF vs LF)、大小写敏感。如果写 Node.js 工具,千万别手写字符串拼路径,乖乖用
path.resolve。 - CI 环境的静默模式
本地跑命令行可以有酷炫的 Ora 转圈动画和彩色粉刷,但在 Jenkins 或 GitHub Actions 里,这些控制字符会变成一堆乱码,还会刷爆日志。检测到
process.env.CI时,记得自动退回到极简纯文本日志输出。
总结
前端工程化的下半场,拼的不是谁用的新名词多,而是谁能把开发链路上的毛刺抹平。
遇到构建性能瓶颈或者繁琐的体力劳动,别害怕走出现有的生态。写个 Node.js 脚本很快,如果遇到吞吐量瓶颈,换 Golang 写个跨平台二进制小工具往往有奇效。能给团队每天省下十分钟摸鱼的工具,就是好工具。
许可协议:CC BY-NC 4.0
更新于 12 小时前
觉得文章有帮助?点个赞吧!
0 条评论


