搞了快十年工程化,从当年的 Grunt、Gulp 拼接压缩,一路踩过 Webpack 的配置深坑,再到现在 Vite 满地跑。最近这两年,大家都在聊 Rust 和 Golang 重构前端工具链。
说实话,很多人以为「工具链开发」就是去写个新的打包器或者编译器。但在实际业务团队里,没那么多机会给你从头写一个 Rolldown 或者 Turbopack。我们平时真正要解决的,往往是那些藏在 CI/CD 流程里、本地 pre-commit 钩子里,卡得大家想去喝咖啡的小痛点。
今天不聊宏大叙事,就聊聊我自己平时给团队折腾工具链的务实路子。
一、别把 Node.js 当成唯一的锤子
以前写前端辅助脚本,第一反应都是拉个 scripts/check.js,然后装一大堆 npm 包:glob、fs-extra、chalk、ora。跑起来是能跑,但业务仓库代码量一上来,问题全漏出来了。
拿我们自己的自研组件库和几个核心业务仓来说,几千个文件跑一次全量废弃 API 扫描,或者做一次多语言 Key 的提取,用 Node.js 写的脚本经常要跑三四十秒。更恶心的是跨平台环境问题,本地跑得好好的,CI 上因为 Node 版本、缓存或者奇怪的依赖安装报错,隔三差五卡流水线。
后来我把一部分重活抽出来,换成 Golang 写独立的 CLI 工具。单二进制文件直接丢进仓库或者内网源,不管是 macOS、Linux CI 容器还是 Windows,直接调就行,不用配 node_modules,也不用担心装包网络超时。
二、实战:写一个并发的「废弃组件引用扫描器」
举个具体场景。做组件库重构时,我们废弃了几个旧的表单组件,需要排查全仓哪些页面还在强引用旧规范,顺便在 pre-push 环节做拦截。
用 Node.js 单线程扫几千个文件的正则匹配,慢得让人想关电脑。换成 Golang,靠 Goroutine 很容易就能把本地 CPU 吃满。
核心逻辑其实非常简单,根本不需要上复杂的 AST 解析库,先做粗粒度的并发行扫描,速度就能快上几十倍:
package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"regexp"
"sync"
"time"
)
// 定义需要扫描的废弃组件匹配规则
var deprecatedPattern = regexp.MustCompile(`import\s+.*\{.*OldForm.*\}.*from\s+['"]@my-ui/core['"]`)
func scanFile(path string, results chan<- string, wg *sync.WaitGroup) {
defer wg.Done()
file, err := os.Open(path)
if err != nil {
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
lineNum := 1
for scanner.Scan() {
line := scanner.Text()
if deprecatedPattern.MatchString(line) {
results <- fmt.Sprintf("%s:%d -> %s", path, lineNum, line)
}
lineNum++
}
}
func main() {
start := time.Now()
rootDir := "./src"
var wg sync.WaitGroup
results := make(chan string, 100)
// 后台消费匹配结果
go func() {
for res := range results {
fmt.Println("[Deprecated Hit]", res)
}
}()
// 遍历目录并发扫描
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 只看前端源文件
if !info.IsDir() && (filepath.Ext(path) == ".tsx" || filepath.Ext(path) == ".vue") {
wg.Add(1)
go scanFile(path, results, &wg)
}
return nil
})
if err != nil {
fmt.Println("遍历目录失败:", err)
return
}
wg.Wait()
close(results)
fmt.Printf("扫描完成,耗时: %v\n", time.Since(start))
}
这段逻辑写完编译成二进制,大概就几兆体积。在我们的一个包含 4200 多个 .tsx 的巨石项目里跑,原来的 Node 脚本跑了 38 秒,Go 的工具只要 600 毫秒左右就退出了。
把这个工具塞进团队的本地 Git Hook 里,再也没人吐槽提交代码卡顿了。
三、写好工具链的几个原则
折腾了这么久工具链,我有几个踩坑总结出来的原则:
1. 能正则不 Babel,能缓存不重算
有些同学做工具链有「强迫症」,做个简单的文本检测非要拉 @babel/parser 去遍历完整的 AST。一个几百 K 的文件,构造一次 AST 树耗费的内存和 CPU,比你做几个预编译正则匹配高好几个数量级。在不需要精确重写代码的场景下,能用流式读取加正则过滤搞定的,就别拿牛刀。
2. 善用单二进制(Single Binary)
只要工具要在不同人机器上跑,依赖越少越好。如果用 Node.js,就用 @vercel/ncc 或 pkg 打包,减少对 node_modules 的依赖;如果是高频性能敏感场景,优先用 Go 或 Rust 编译成独立可执行程序,CI 下载直接 chmod +x 就能跑,能省下大量维护 npm 缓存崩掉的时间。
3. 报错信息一定要带「上下文」
自己写的工具最忌讳抛出来一个死板的 Error: exit 1。至少要告诉开发者:
- 哪一行出错了。
- 为什么错(比如:引了哪个废弃字段)。
- 应该改成什么(贴一个文档链接或迁移示例)。
工具链的目的是提高团队效率,如果出了问题别人还要花二十分钟来找你问原因,那这工具写得就算不上合格。
前端工程化早就过了拼谁能配出一套花哨构建流程的时代了。现在的工具链开发,核心还是看能不能用更低的运行成本、更高的性能,把业务开发从无意义的等待里捞出来。把注意力放回到性能、稳定性和开发体感上,比追最新的名词要务实得多。
License: CC BY-NC 4.0
Updated 13 hours ago
Was this article helpful? Give it a like.
0 comments


