阅读视图

发现新文章,点击刷新页面。

你的 sideEffects 真的配对了吗?—— 深度拆解构建工具的 Tree-shaking 潜规则

🚀 省流助手(速通结论):

  1. sideEffects 是给宿主(用你包的项目)看的声明,不是给你自己构建减重用的。
  2. 只要包里包含 CSS/样式全局监听process.on)或修改全局变量绝不能简单设为 false
  3. /* @__PURE__ */ 的意思是  “这行没用到请删掉” ,而不是 “不能删”。
  4. Bundle 并不安全:即便你打包成了单文件,一旦声明了 false,宿主打包工具依然能从内部“抠掉”你的副作用代码。

一、 线上“失踪”案:谁偷走了我的初始化逻辑?

很多开发者都遇到过这种诡异场景:本地开发时一切正常的全局监听(如 process.on('exit'))或样式文件,发布成 npm 包被别人使用后,在生产环境竟然“失效”了。

检查代码,逻辑都在;检查产物,文件也引了。最后发现,根源竟然是你在 package.json 中随手写下的那行:

json

"sideEffects": false

请谨慎使用此类代码。

你以为是在帮宿主做性能优化,实际上你是在给自己的代码下“逐客令”。

二、 生效时刻:它是谁的“紧箍咒”?

误区:  认为在库里写了 sideEffects: false,自己执行 vite build 时包体积就会变小。

真相:它的真正战场是「宿主编译时刻」。

  1. 自身构建时:当你运行构建指令时,工具遵循作者意图。只要你在入口写了 import './effect.ts',这段代码就会物理存在于你的 dist 产物中。
  2. 宿主打包时:当其他项目安装了你的包,宿主工具(Vite/Webpack)会读取你的声明。如果你承诺了“无副作用”,一旦宿主没引用你该模块导出的变量,工具就会开启“外科手术”:即使你的单文件 Bundle 物理上包含了这段代码,工具也会在最终输出时将其精准剔除。

三、 穿透 Bundle 的“外科手术”

这是最隐蔽的陷阱。很多开发者认为:“我打包时已经把副作用合并进 index.js 了,宿主引用了 index.js 就安全了。”

错了。  现代打包工具具备 Module Concatenation(模块提升)  能力。它们能“看穿” Bundle 内部的结构。只要你声明了 false,它们有能力从一个大的文件块中只“抠”出用到的函数,而把剩下的(包括那段 import './effect.ts' 产生的内容)当作垃圾直接丢弃。

四、 微观博弈:/* @__PURE__ */ 到底在帮谁?

如果说 sideEffects 是文件级的“粗调”,那么 /* @__PURE__ */ 就是语句级的“微操”。

纠正一个常见误区:  它是标记“可以删”,而不是“不能删”。

假设你的工具库有一个文件导出了 100 个函数,宿主只用了其中 1 个。

  • 如果没有标记:剩下的 99 个导出中,如果包含 export const config = init() 这种函数执行,打包工具会因为不敢确定 init() 是否修改了全局变量而保守地保留这一行。
  • 如果加上标记:你是在给工具发“免责声明”。工具看到 /* @__PURE__ */,发现没人用 config,就会放心地把这一行代码从产物中抹除。

五、 避坑总结:白名单管理

为了不让代码被“误杀”,你不能在包含副作用的文件里写 false。最专业的做法是使用数组进行精准保护

哪些文件必须进 sideEffects 数组?

  1. 样式文件*.css*.scss
  2. 环境初始化:修改 global 或 window 的脚本。
  3. 进程监控:包含 process.on 或 interval 的逻辑。

推荐配置:

json

{
  "sideEffects": [
    "**/*.css",
    "./dist/_init/*.mjs"
  ]
}

请谨慎使用此类代码。

结语

Tree-shaking 是一场开发者与构建工具之间的博弈。工具的本质是“保守”的,而 sideEffects: false 是你交给工具的一把“激进”的剪刀。

在下一篇中,我们将深入探讨:如何通过工程架构设计,强制开发者在编写副作用代码时进行“决策”,从而构建一套永远不会被意外误删的“契约式”架构。

NPM 脚本避坑指南:如何优雅地区分 postinstall 的“开发”与“安装”环境?

前言

你是否遇到过这样的尴尬:给包写了一个 postinstall 钩子去做自动构建(如编译 C++ 模块或生成协议文件),结果自己本地开发跑 pnpm install 时,它也跟着在那编译半天,甚至因为环境问题报错卡死?

区分“宿主开发”与“依赖安装”环境是每个包作者的必修课。本文将从环境变量到路径特征进行全方位拆解,带你由浅入深寻找最优解。

🚀 如果你现在正急着解决这个问题:
请直接跳转到 方案三:特征路径扫描法。这是目前在独立包、Monorepo 以及跨平台场景下鲁棒性最强、最通用的解决方案。


方案一:基础路径比对法(INIT_CWD)

这是最直观的方案,利用 npm/pnpm 注入的环境变量 INIT_CWD

javascript

// scripts/postinstall.js
const path = require('path');

if (process.env.INIT_CWD && path.resolve(process.env.INIT_CWD) === path.resolve(process.cwd())) {
  console.log('宿主开发环境,跳过脚本');
  process.exit(0);
}

请谨慎使用此类代码。

  • 原理解析INIT_CWD 是你敲下安装命令时的路径,process.cwd() 是脚本执行时的路径。在最简单的独立包开发中,这两个路径通常是一致的。

  • 局限性

    • Monorepo 杀手:在 Monorepo 架构中,你在根目录运行 pnpm iINIT_CWD 指向项目根目录,但子包脚本执行时 cwd 指向 packages/xxx,两者永远不相等,导致拦截失效。
    • 软链接风险:在某些 OS 或特定包管理器下,路径的大小写或物理链接解析可能不一致,导致比对失败。

方案二:配置标志位法(npm_config_*)

通过包管理器注入的环境变量来判断当前的安装行为。

javascript

if (process.env.npm_config_global) {
  // 只有全局安装(-g)时执行
}

请谨慎使用此类代码。

  • 原理解析:利用包管理器在执行脚本时注入的 npm_config_ 系列变量。

  • 局限性

    • 粒度太粗:它能区分“全局”还是“本地”,但无法区分“本地开发源码”还是“作为他人项目的项目依赖”。
    • 兼容性碎片化:npm、yarn、pnpm 对这些变量的注入规则在 2026 年依然存在细微差异。

方案三:特征路径扫描法(全场最佳)

这是目前健壮性最高、也是社区最推崇的方案。它的逻辑非常纯粹:检查当前脚本执行的物理路径中是否包含 node_modules

javascript

const path = require('path');
const cwd = process.cwd();

// 核心逻辑:检查路径片段中是否包含标准的 node_modules 目录名
const isDependency = cwd.split(path.sep).includes('node_modules');

if (!isDependency) {
    console.log("🚀 检测到处于源码开发环境(独立包或 Monorepo),拦截 postinstall");
    process.exit(0);
}

// 只有被别人 add 之后,你的包才会出现在 node_modules 里
console.log("📦 正在作为依赖安装,执行初始化逻辑...");

请谨慎使用此类代码。

  • 为什么它既支持独立包又支持 Monorepo?

    • 独立包开发:你的路径是 /User/dev/my-pkg,不含 node_modules。拦截成功。
    • Monorepo 开发:你的子包路径是 /User/dev/repo/packages/my-pkg,依然不含 node_modules。拦截成功。
    • 别人安装时:无论对方怎么配置,你的包一定会被放置在对方项目的某个 node_modules 目录下。路径必含该关键字。逻辑触发。
  • 优势:完美规避了 INIT_CWD 在大仓中的路径偏移问题。


方案四:2026 现代包管理器配置法(pnpm 视角)

如果你和你的用户群体主要使用 pnpm v10+ ,除了代码层面的拦截,还可以利用 pnpm 的安全机制。

pnpm v10 默认会拦截未知的构建脚本。作为开发者,你可以在项目的 .npmrc 中通过 only-built-dependencies 显式控制。但为了给用户提供“开箱即用”的体验,在脚本内部通过 方案三 进行静默拦截依然是最佳实践。


总结:我该选哪种?

根据 2026 年的开发标准,建议在你的 postinstall.js 中使用以下终极兼容代码:

javascript

const path = require('path');

function shouldSkip() {
  const initCwd = process.env.INIT_CWD;
  const cwd = process.cwd();

  // 1. 尝试 INIT_CWD 比对(覆盖 90% 简单场景)
  if (initCwd && path.resolve(initCwd) === path.resolve(cwd)) return true;

  // 2. 特征路径扫描(覆盖 Monorepo 和 符号链接场景)
  // 只要路径里没出现 node_modules,就判定为是在自己家开发
  if (!cwd.split(path.sep).includes('node_modules')) return true;

  return false;
}

if (shouldSkip()) {
  process.exit(0);
}

// ... 执行真正的逻辑

请谨慎使用此类代码。

写在最后

一个优秀的 NPM 包不仅要有强大的功能,还要有“不打扰”的自修养。通过简单的几行环境判断,你可以让你的包在开发阶段轻量如初,而在生产安装时稳如泰山。

如果你觉得这篇文章解决了你的燃眉之急,欢迎点赞收藏!

❌