Node.js · 原创实验

同一个包、同一个版本,为什么读到了两份状态?

自建一个 0.0.0 包,分别从 require 和 import 进入。计数器确实分成了两份;让两个入口指向同一内核后,再对照对象身份与计数。

记录时间: (北京时间) · Node v22.22.3 · darwin 27.0.1 / arm64

文章语言: English

一处调用已经把计数器加到 1,另一处读取时还是 0。实验目录里只有一个包,版本号也只有一个:0.0.0。差异出在这两处调用最终加载的文件。

这里自建了两个小包。第一个为 require 和 import 各写一份状态;第二个保留两个入口,但把状态交给同一份 CommonJS 内核。每个例子启动一个新 Node 进程,两轮结果一致。

两轮实际结果

同一脚本两轮独立运行的结果
情况第 1 轮第 2 轮
重复 require 同一个入口同一对象:true;require 侧 1;另一侧 1同一对象:true;require 侧 1;另一侧 1
require 与 import 各走一个入口同一对象:false;require 侧 1;另一侧 0同一对象:false;require 侧 1;另一侧 0
两个入口共用 core.cjs同一对象:true;require 侧 2;另一侧 2同一对象:true;require 侧 2;另一侧 2

一个安装目录里可以有两份实现

lab-dual 的导出表让 require 进入 index.cjs,让 import 进入 index.mjs。这两个文件各自创建了一个 store。对其中一份调用 increment,只会修改那份对象。

混合加载的实际结果是 sameStore=false,require 侧 count=1,import 侧 count=0。这里没有第二个安装版本。只看包名和版本,无法替代对最终入口及其状态来源的检查。

{
  "name": "lab-dual",
  "version": "0.0.0",
  "exports": {
    "import": "./index.mjs",
    "require": "./index.cjs"
  }
}

先排除重复 require 本身

第一组在同一个进程里 require 两次 lab-dual,再从第一处增加计数。两个调用拿到的 store 是同一个对象,读数都是 1。

把这组与混合加载放在一起,才能把问题缩小到不同入口。否则很容易把任何重复初始化都归因于“Node 缓存失效”,忽略包自己创建了两份状态。

const a = require('lab-dual');
const b = await import('lab-dual');
a.increment();
console.log(a.store === b.store, a.store.count, b.store.count);
// 本次混合加载输出:false 1 0

给两个入口接上同一个内核

lab-shared 的 require 条件直接指向 core.cjs。ESM 入口 wrapper.mjs 先导入这个内核,再导出它的 store 和 increment。两边使用同一个对象,各增加一次后,两处读数都为 2。

这个包装器只验证共享对象内部 count 的变化。若内核以后把整个 store 属性重新赋值,包装器里保存的引用会怎样,需要另写例子。不要把这次对象共享的结果推广成所有导出形式都等价。

// wrapper.mjs
import core from './core.cjs';
export const store = core.store;
export const increment = core.increment;

排查时把入口一起带上

收集最小复现时,除了精确版本,还应写出调用端使用 require 还是 import,以及 exports 为这两个条件选中了哪些文件。再沿着文件找状态到底在哪里创建。

NPMBot 的兼容性检查能帮助追踪声明入口;对象是否真的共享,要运行像这次这样的用例。六次独立子进程只覆盖这些自建文件,没有经过打包器,也没有加载任何真实 npm 包。

下载脚本,在自己的机器上复跑

使用 Node.js 22 或更高版本,不需要安装依赖。脚本只使用 Node 内置模块;每次执行都会跑两轮并检查断言。记录路径必须尚不存在,避免覆盖上次结果。

node --version
node run.mjs
node run.mjs --record my-results.json

实验在同一 Node 进程内比较对象身份;两轮各使用新目录,三组各使用新进程。它不验证跨进程共享、打包产物去重或真实库的单例设计。完整自建源码和摘要随记录提供。

这是同一操作者在本机完成的两轮记录,尚无独立读者复跑反馈。换环境后请保留自己的输出;断言失败会终止脚本。

核对脚本 SHA-256

0e3096e8f2163b919c68e1974f5149d9842edeadaa8cbb0f3b9d5ba0fb7326a1

摘要对应页面提供的脚本文件内容。完整运行数据在 verification.json。