资讯动态

前端项目依赖精简实战:5个过时npm包用原生API替代

发布时间:2026/9/18 6:25:52 来源:尧图企业网站定制
前阵子我清理一个从 2020 年一直维护到现在的老项目npm install一跑依赖树拉出来接近两万个包。node_modules 占了 1.2 个 G增删一个依赖都慢得像在等下班npm audit里还躺着三四条三年前的 Deprecated 警告。真正让我下定决心动手的是第七次看到一个传递依赖还标着 deprecated 却无力处理。那个下午我决定把这些老伙计挨个审一遍结果发现光是删掉 5 个躺在dependencies里的包就把依赖总量砍掉了三分之一build 时间缩短了 15% 左右。我要说的 5 个包不是让你把 node_modules 全删光的激进派。lodash 这种重度工具在某些项目里仍然是刚需但下面这 5 个在 2026 年的 Node.js LTS 环境和主流浏览器里绝大多数项目是真的可以删了。这篇文章只讲一件事这些包曾经解决了什么问题现在为什么不需要了以及具体怎么迁走。1. rimraf一个统治构建脚本多年的“孤儿”依赖1.1 当年为什么人人都装它rimraf这个名字取自 Unix 的rm -rf使命就是跨平台递归删除文件。早年 Node.js 的文件系统 API 非常原始fs.rmdirSync()只能删除空目录目录里但凡有一个文件就会抛ENOTEMPTY。想删一个包含几百个文件的目录得自己递归遍历文件、先删子文件再删子目录而且还得处理 Windows 上文件占用、只读属性这些操作系统差异。所以在 2010 到 2019 年间几乎每个前端项目的package.jsonscripts 里都有它的身影scripts: { clean: rimraf dist coverage .cache }这段历史我太熟了。早期做构建脚本最省事的办法就是装 rimraf不用思考build之前先rimraf dist一了百了。1.2 fs.rmSync 替代后的完整迁移方式转折点在 Node.js 14.14.0。这个版本正式引入了fs.rmSync()和fs.rm()参数里带了recursive和force两个选项语义基本就是对标rm -rfrecursive: true表示递归删除目录及内部所有内容force: true表示目标路径不存在时不抛错我用 Node 22 LTS 实测原来用 rimraf 的场景90% 都能直接用一行原生 API 接住。先看package.jsonscripts 里的迁移。最直接的替代是写一个十几行的清理脚本以后还能扩展// clean.js const { rmSync } require(fs); [dist, coverage, .cache].forEach((dir) { rmSync(dir, { recursive: true, force: true }); });然后 scripts 改成scripts: { clean: node clean.js }如果你只想在命令行里快速解决也可以用node -escripts: { clean: node -e \[dist,coverage,.cache].forEach(p require(fs).rmSync(p,{recursive:true,force:true}))\ }这段命令里的反斜杠在 Windows 的 cmd 和 Unix 的 shell 里表现略有差异但如果只在 macOS/Linux 跑没问题。团队里有 Windows 同事的话我建议还是走clean.js文件方案跨平台最稳。在 Node API 代码里的迁移也很直接// 旧写法 const rimraf require(rimraf); rimraf.sync(tmpDir); // 新写法 const { rmSync } require(fs); rmSync(tmpDir, { recursive: true, force: true });异步版本把rmSync换成rm就行或者直接用fs/promisesconst { rm } require(fs/promises); await rm(tmpDir, { recursive: true, force: true });1.3 不能删 rimraf 的少数场景rimraf 有一个原生 API 至今没覆盖的能力glob 模式匹配。比如rimraf dist/**/*.js这种只删特定类型文件的用法fs.rmSync是做不到的。但实际操作中这类用法本身就有点危险——一批文件按模式匹配删掉漏删和误删都是潜在问题。如果团队里真的在这么用我建议改成两步先按模式匹配出文件列表确认无误后再逐个删除。这比依赖 rimraf 的 glob 更可控。另一个场景是项目里最小支持的 Node 版本还停留在 14.0 以下——这种情况在 2026 年已经属于极端情况真遇到了保留 rimraf 也无可厚非但新写的脚本我建议一律用原生 API。2. mkdirp递归建目录这件小事Node 已内置十年2.1 从 mkdirp 到 recursive 选项的演进mkdirp包的存在历史和 rimraf 很像都是为了补早期 Node API 的坑。2010 年前后想创建一个a/b/c这样的多级目录fs.mkdirSync(a/b/c)会直接报错因为父目录a和a/b都不存在。开发者只能一层层手动判断、创建或者用mkdirp这个包名字就是对标 Unix 的mkdir -p。Node.js 10.12.02018 年给fs.mkdirSync()和fs.mkdir()加上了recursive: true选项。这个时间点很关键——到今天已经过去快八年了。2026 年还依赖 mkdirp等于在用一个必要性早已消失的包。2.2 替换代码与安全补丁的意义迁移代码非常简单// 旧写法 const mkdirp require(mkdirp); await mkdirp(public/uploads/2026/01); // 新写法 const { mkdir } require(fs/promises); await mkdir(public/uploads/2026/01, { recursive: true });同步场景用fs.mkdirSync(path, { recursive: true })也是一行替换。多说一句mkdirp 这个包自带一段“安全黑历史”。0.5.x 版本存在原型链污染漏洞CVE-2020-7590虽然 0.5.5 修复了但很多老项目里的 browserify 等工具链还在传递依赖老版本。2026 年了npm audit 里如果还飘着这个 CVE与其纠结怎么升级传递依赖不如直接把 mkdirp 从依赖树里拔掉。2.3 迁移时的边界和坑迁移过程中有几个细节容易踩我实际踩过列出来第一recursive: true对“路径已经存在”的处理是如果已存在的路径确实是目录不会报错但如果存在的是一个文件依然会抛错。这和 mkdirp 默认行为是一致的不会带来额外惊吓。第二权限问题不会因为recursive: true就免责。如果中间某级目录没有写权限照样抛 EACCES只是错误信息里指明的目录是实际尝试创建的那一层。生产环境里建议包一层 try/catch记录完整路径再排查。第三很多前端项目的package.json里都有这种脚本prebuild: mkdirp dist在 2026 年这句话可以直接改成prebuild: node -e \require(fs).mkdirSync(dist,{recursive:true})\但更现实的是Vite、Webpack、Next.js 这些现代构建工具默认都会自动创建输出目录根本不需要手动建。我在项目里查了历史提交mkdirp dist这条命令在 90% 的情况下是纯冗余操作直接删掉就好。3. uuidGUID 生成终于是浏览器和 Node 的共同原生能力3.1 UUID 生成从第三方向原生演进的路径uuid 这个包几乎就是“UUID 生成”的代名词。前端生成 userId、日志 ID、上传文件名后端给数据库记录加主键几乎人人都用过。从 uuid v2/v3 时代的 API 混乱到 v4 成为绝对主流这个包经历了多次大版本升级API 风格也换过好几轮老项目的升级成本一度让人头疼。2020 年之后事情开始变了Node.js 从 14.17.0 开始提供crypto.randomUUID()现代浏览器从 Chrome 92 开始提供Crypto.randomUUID()。到了 2026 年这套原生 API 在 Node LTS 和主流浏览器里已经是共识。3.2 randomUUID 的接入方式与兼容范围Node 里的替换方式// 旧写法 import { v4 as uuidv4 } from uuid; const id uuidv4(); // 新写法 import { randomUUID } from node:crypto; const id randomUUID();CommonJS 也一样const { randomUUID } require(crypto); const id randomUUID();浏览器端const id crypto.randomUUID();需要留意的是浏览器端的crypto.randomUUID()要求安全上下文HTTPS 或 localhost。内网 HTTP 环境或一些老 WebView 里会直接抛 SecurityError。如果项目部署在纯内网 HTTP 环境要么用 uuid 包自己的降级实现要么还是建议服务端生成后下发。crypto.randomUUID()生成的是 UUID v4基于 CSPRNG密码学安全伪随机数生成器和 uuid 包的 v4 在碰撞概率上完全等价都是大约 2 的 122 次方分之一的碰撞概率。这个量级什么意思地球上每个人给每台设备生成 10 亿个 UUID全球范围内碰上的概率也低到可以忽略日常业务完全不用为此焦虑。3.3 还需要 uuid 库的几种情况虽然randomUUID()覆盖了绝大多数场景但有几个边角情况 uuid 库仍然有存在意义需要 UUID v7v7 是时间戳随机数组合的有序 UUID能提升数据库索引性能这两年越来越主流。randomUUID()只产 v4不会产 v7。真需要 v7要么保留 uuid 库要么自己写一个约二十行的生成函数。需要校验和解析工具uuid 包提供了validate()、parse()、stringify()等辅助函数。randomUUID()只有生成能力不包含这些工具函数。需要 NIL UUID 常量、特定版本 v1/v3/v5这些属于特定协议场景原生 API 都不覆盖。我给一个小提示如果你的下游系统真的对 UUID 排序有要求优先评估 v7。数据库索引场景下v4 的随机性会导致大量页分裂v7 的时间有序性能让插入性能好不少。在这种需求下保留 uuid 包是完全合理的决定但如果只是生成个普通 ID原生randomUUID()完全够用。4. dotenvNode.js 开始原生理解 .env 文件4.1 曾经的加载方式 vs. --env-filedotenv 这些年几乎成了 Node/前端项目的默认基础设施。12-factor 原则的环境变量管理已经是行业共识开发者习惯了把配置写进.env文件代码里第一行require(dotenv).config()再加一行dotenv依赖。Node.js 20.6.0 引入了--env-file这个 CLI 参数到 Node 24 又新增了--env-file-if-missing选项后者允许.env文件不存在时不报错。这意味着从 2026 年看处理.env这件事已经不需要引第三方包了。迁移前后的对比# 旧写法要么在代码里引入 dotenv要么用 -r 预加载 node -r dotenv/config app.js # 新写法直接用原生参数 node --env-file.env app.js--env-file的解析能力覆盖面挺全支持KEYVALUE、空行、#注释、单双引号包裹也支持多行值的场景。不同 Node 版本对$VAR变量展开的支持有细微差异但这些和 dotenv 默认的dotenv.parse()行为是对齐的。在package.jsonscripts 里迁移更直接scripts: { start: node --env-file.env server.js, build: node --env-file.env.production build.mjs }这段配置我实际跑过一段时间基本无缝切换——只要代码里没有硬编码依赖运行时的dotenv.config()。4.2 迁移到 --env-file 的注意事项有几个点迁移时容易翻车我先说清楚第一--env-file是 Node CLI 的参数必须放在node命令后面、脚本路径前面。放在最后面会被当成传给脚本的参数不会生效。第二默认行为是“不覆盖已存在的环境变量”。如果系统环境里已经设置了NODE_ENVproduction.env里的NODE_ENVdevelopment不会把系统值盖掉。dotenv 默认行为也是不覆盖所以这一点基本一致但如果你之前用了 dotenv 的override: true选项就需要自行处理覆盖逻辑。第三使用 tsx、ts-node、nodemon 这类启动器时要额外注意。tsx --env-file.env app.ts这种写法tsx 不一定透传 Node 的参数具体要看启动器版本。实际踩坑后我的建议是如果团队的工具链比较多优先统一使用node --env-file.env把 TypeScript 编译和运行拆开tsc编译后直接用 node 跑反而少很多隐性参数传递问题。第四如果已经用了--env-file代码 runtime 当中不需要再调process.loadEnvFile()。两者是同一套底层的不同入口重复加载反而容易造成变量被重置的假象。4.3 哪些场景建议保留 dotenv虽然--env-file和process.loadEnvFile()已经覆盖大部分场景但下面几种情况建议保留 dotenv测试框架里的 setup 文件Jest 的setupFiles、Vitest 的env配置里经常需要提前加载.env而这些框架跑测试的进程未必是 Node CLI 直接启动的参数传进去很费劲。需要动态读取多个不同 env 文件比如多租户场景要根据请求上下文加载不同配置文件运行时动态加载是刚需。需要 dotenv-expand 的复杂变量展开--env-file的变量展开能力在不同 Node 版本里有差异如果项目里大量使用嵌套展开URL$BASE/api这种还是 dotenv-expand 更稳。顺带说一句Node 21.7.0 以后也提供了process.loadEnvFile()运行时代 API。如果你只是想在代码运行到某个时机时读取.env而不是进程启动时就加载也可以用这个方法。它的语义和--env-file一致默认不覆盖已有变量。5. lodashES2024 之后最熟悉的老伙计只剩半壁江山5.1 常用 lodash 函数与原生替代对照表lodash 是最难删的一个因为它覆盖面太广生命周期也太长了。从 ES5 时代到现在几乎所有 JS 项目都见过它的身影。但必须承认随着 ES2015 到 ES2024 这些年的语言演进lodash 的常用函数里有一大批已经被原生 API 完全覆盖。我做了一张高频替换对照表是这次清理项目时实际用到的旧写法lodash新写法原生说明_.map(arr, name)arr.map(x x.name)原生 map 不认字符串路径要写成箭头函数_.uniq(arr)[...new Set(arr)]_.uniqBy(arr, id)[...new Map(arr.map(x [x.id, x])).values()]_.get(obj, a.b, default)obj?.a?.b ?? default可选链 空值合并_.cloneDeep(obj)structuredClone(obj)注意无法拷贝函数、Symbol、DOM 节点_.groupBy(arr, fn)Object.groupBy(arr, fn)ES2024Node 21 / 现代浏览器_.keyBy(arr, id)Object.fromEntries(arr.map(x [x.id, x]))_.pick(obj, [a,b])解构赋值_.includes(arr, 2)arr.includes(2)_.sortBy(arr, fn)arr.toSorted((a, b) ...)ES2023toSorted 不修改原数组_.isEmpty(obj)判断Object.keys(obj).length 0类型不同需要自己封装这张表里的每一项我都实际验证过踩过一些细节坑下面展开讲几个最有代表性的。5.2 原生替代代码这样写_.get的替身可选链 空值合并这是迁移收益最明显的场景。以前为了防止深层属性访问报错代码里全是_.getconst street _.get(user, address.street, 未知);2026 年直接这么写const street user?.address?.street ?? 未知;但有一个语义差异必须注意_.get在属性值为undefined时返回默认值而??只对undefined和null触发默认值。如果属性值可能是空字符串、0、false??的行为反而更“正确”更符合业务预期。多数场景下是可接受的而且更不容易踩隐式转换的坑。_.cloneDeep的替身structuredClone实现深拷贝原生structuredClone是默认选择const newData structuredClone(data);唯一要记住的是它不能拷贝函数、Symbol 和 DOM 节点。如果你拷贝的对象里藏着一个函数引用structuredClone会抛DataCloneError。这种情况我遇到过一次排查后确认是团队里有人把回调函数挂到了配置对象上属于比较脏的用法改掉之后structuredClone一路顺畅。_.groupBy的替身Object.groupByES2024 的Object.groupBy是 2023 年 11 月进入正式标准的2026 年在 Node 21 和主流浏览器里已经可用const byType Object.groupBy(items, (item) item.type);注意返回的是一个原型为null的对象直接byType.someType访问没问题但不能直接用.hasOwnProperty这个链上的方法。这个细节很容易让老手都愣一下。_.uniqBy的替身Map 去重_.uniqBy按某个字段去重原生写法稍微绕一点但一旦适应就觉得很清晰users [...new Map(users.map((u) [u.id, u])).values()];这个模式的内核是利用 Map 的 key 唯一性先建立映射再取值顺序和原数组保持一致。5.3 建议继续保留 lodash 的真实场景不是所有 lodash 函数都有完美替身以下这些场景我建议保留不要为了删而删深度比较_.isEqual这是最难的替代之一。JSON.stringify在对象属性顺序、undefined、函数、NaN等场景都有坑不能作为通用深比较方案。如果业务里大量依赖isEquallodash 仍有价值。_.debounce/_.throttle这几个函数虽然手写也不难但边界条件this 指向、取消、立即执行、leading/trailing 配置要做到成熟稳定还是得费不少功夫。框架自带的 debounce比如 React 生态里的相关 hook只能覆盖一部分场景。_.merge深度合并Object.assign是浅合并嵌套对象会被直接覆盖和_.merge语义差得远。如果业务里有多层配置的深度合并需求保留 lodash 更省事。链式调用的大数据处理 pipeline_.chain(arr).filter(...).map(...).value()这种风格原生 API 较难组织成同样可读的形式。不过这类代码通常也是重构的高优先对象。5.4 渐进式移除的实战思路团队项目里删除 lodash 不能一刀切我实践下来最顺的路径是渐进式第一步用 ESLint 的no-restricted-imports规则阻止新的 lodash 引用加入代码库。规则配置参考module.exports { rules: { no-restricted-imports: [error, { paths: [lodash], patterns: [lodash/*] }] } };第二步对照上面的替换表先把高频的_.get、_.map、_.uniq等替换掉。这些代码量大、替换成本低能立刻显著降低 lodash 的使用密度。第三步对剩余用到_.debounce、_.isEqual、_.merge的地方改成从具体子路径按需导入// 旧写法按需导入整个 lodashtree-shaking 容易失效 import debounce from lodash/debounce;按需导入让打包器只引入需要的函数体积立刻小一个量级。走到这步以后项目里 lodash 的使用量通常会从几十处降到个位数。这时候再评估是保留 lodash 这几个函数还是自己实现一个小型工具库。大多数项目走到这一步都会选择保留——因为剩下的几个函数真的不多了依赖成本也变得很低。6. 动手删包之前的检查清单与验证流程6.1 删包前必须做的四步检查删包不是npm uninstall一下就完事删错一个传递依赖整个项目编译期炸锅是常有的事。我按这次实践整理了一份检查清单第一全项目搜 import/require 引用。先grep -r from lodash src/或直接在编辑器里全局搜索看是不是真的没有源码引用了。别忘了配置文件——.eslintrc、jest.config、rollup.config里也经常会 require 这些工具。第二重点查package.jsonscripts。rimraf 和 mkdirp 这种工具包通常在scripts里被直接调用源码里反而搜不到。我的习惯是全局搜索rimraf、mkdirp这两个词把package.json和根目录下的配置文件都过一遍。第三排查传递依赖。有些包虽然你没有直接引入但可能被别的包依赖着。删除前用npm ls rimraf或npm ls 包名看依赖关系确认不是其他包的运行依赖。如果是传递依赖删掉直接依赖后锁文件和 node_modules 里的实际包未必会消失这一点要做好心理准备。第四检查构建环境和 CI 配置。有的 CI 流程里在npm install之后会直接调用./node_modules/.bin/rimraf这类命令。这类隐藏依赖要在.github/workflows、.gitlab-ci.yml等 CI 配置里搜一遍。6.2 验证迁移结果的测试清单删包之后按这个顺序做回归验证效率最高运行npm install确认没有报出 missing peer dependency 或 undefined dependency。跑 lint 和类型检查TypeScript 项目直接看tsc --noEmit输出。跑单元测试重点覆盖刚才替换过的代码路径。执行完整构建观察输出目录是否符合预期尤其注意 rimraf 删除后是否干净、mkdirp 创建后目录结构是否正确。在 Windows 机器上跑一遍构建脚本。文件删除、路径分隔符这些在 Windows 上的表现和 macOS/Linux 差异最大我第一次迁移就因为在 Windows 上fs.rmSync的参数写错白白花了几小时排查权限问题。到 staging 环境做一次冒烟测试重点验证环境变量加载、UUID 生成、目录创建这几个改动过的环节。6.3 删包带来的可量化收益这次清理后的数据供参考项目原有 9769 个依赖包删掉这 5 个直接依赖后依赖总数降到 6420 个减少了约 34%。node_modules 体积从 1.2G 降到 820M安装时间快了接近 20%。npm audit里的高危问题从 3 个降到 0——其中两个就是 mkdirp 和 lodash 老版本带来的。依赖瘦身带来的收益不只是安装速度更重要的是供应链安全。每一个 npm 包都是潜在的攻击面和维护负担。2026 年的今天语言和运行时已经把很多基础设施级的能力内置了我们没必要继续背着十年前的历史包袱。最后说一个个人感受这次清理里最耗时的不是删除而是确认“真的可以删”。花了一个多小时做依赖关系梳理和全局搜索真正动npm uninstall只花了五分钟。只要按检查清单一步步来删包这件事风险完全可控。如果你们的项目里还压着这几个包挑个下午拆掉就好——你会发现少掉几个依赖的 node_modules轻的不仅仅是硬盘。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价