- 开发工具
【免费下载链接】just
A library of dependency-free JavaScript utilities that do just one thing.
本文以 packages/array-flatten/CHANGELOG.md 为主线,逐条解析just-flatten-it从 5.1.0 到 5.2.0 四个版本的技术变更,并结合源码、类型定义、打包配置与测试用例,完整还原这个零依赖数组扁平化工具的实现原理与发布演进。读完本文,你将能理解其双模块(CJS/ESM)导出机制、递归类型设计、基于栈的展平算法,以及版本变更背后对应的真实代码改动。
一、工具概览:一个只做一件事的数组展平器
just-flatten-it是 just 库(A library of dependency-free JavaScript utilities that do just one thing)中的一个零依赖 npm 模块,功能只有一个:返回展平后的数组。其 package.json 中dependencies为空、keywords明确标注了no-dependencies,安装方式与其他 npm 包一致:
npm install just-flatten-ityarn add just-flatten-it基本用法(README.md):
import flatten from 'just-flatten-it'; flatten([[1, [2, 3]], [[4, 5], 6, 7, [8, 9]]]); // [1, 2, 3, 4, 5, 6, 7, 8, 9] flatten([[1, [2, 3]], [[4, 5], 6, 7, [8, 9]]], 1); // [1, [2, 3], [[4, 5], 6, 7, [8, 9]]]省略depth参数时数组被完全展平;传入depth时只展平到指定层数。这两个语义正是后文版本演进与实现解析的落脚点。
二、版本脉络:CHANGELOG 记录的四个关键节点
CHANGELOG 完整记录了 4 个版本(当前最新版本 5.2.0 与 package.json 中version: "5.2.0"一致):
| 版本 | 变更类型 | 变更内容 |
|---|---|---|
| 5.2.0 | Minor Changes | 将 Node 模块文件由.js重命名为.cjs |
| 5.1.2 | Patch Changes | 类型改进(Type improvements) |
| 5.1.1 | Patch Changes | 修复:调整导出顺序,将默认导出置于最后(issue #488) |
| 5.1.0 | Minor Changes | package.json 更新,修复 issue #467、#483 |
虽然每一条 changelog 都只有一句话,但结合仓库中的实际产物,可以一一还原每条变更对应的真实代码改动。下面按时间倒序逐条深挖。
三、5.2.0:Node 模块从.js重命名为.cjs,确立双模块导出
这是最近一次发布,也是最能体现包结构设计的一次变更:Node 侧的 CommonJS 入口文件由index.js重命名为index.cjs。在 packages/array-flatten 目录下,可以看到当前包同时维护三个入口文件:
- index.cjs:CommonJS 实现,文件末尾
module.exports = flatten; - index.mjs:ESM 实现,文件末尾
export {arrayFlatten as default}; - index.d.ts:TypeScript 类型声明
与之配套的是 package.json 中完整的exports条件导出映射:
{ "name": "just-flatten-it", "version": "5.2.0", "type": "module", "exports": { ".": { "types": "./index.d.ts", "require": "./index.cjs", "import": "./index.mjs" }, "./package.json": "./package.json" }, "main": "index.cjs", "types": "index.d.ts" }这里的关键点在于:
"type": "module"让整个包以 ESM 语义解析,因此 CommonJS 入口必须使用.cjs扩展名以显式声明自身模块格式——这正是 5.2.0 重命名的直接原因;exports字段为require与import分别指向index.cjs与index.mjs,Node 与打包工具可以按模块系统自动选择正确入口;main与types保留兼容旧解析逻辑的兜底字段。
有趣的是,index.mjs并不是手写的,而是由 Rollup 从 CJS 源文件构建生成的。每个包的 rollup.config.js 统一调用根目录的 config/createRollupConfig.js:
module.exports = function createRollupConfig(path) { return { input: resolve(path, './index.cjs'), output: { file: resolve(path, './index.mjs'), format: 'esm', }, plugins: [cjs()], }; };也就是说:手写维护一份 CJS 源码(index.cjs),通过@rollup/plugin-commonjs转译出 ESM 版本(index.mjs),构建命令为npm run build(根 package.json 中为lerna run build --stream,各包内为rollup -c)。两个文件的实现体完全一致,仅入口导出语句不同,可以互相印证。
四、5.1.1:修复导出顺序,默认导出置于最后(#488)
CHANGELOG 记录这条修复为“reorder exports to set default last”,即将默认导出语句调整到文件末尾。对照当前构建产物,确实可以看到该约定已被落实:
- index.mjs 中,文件开头先声明
var arrayFlatten = flatten;,实现函数定义在前,最后一行为export {arrayFlatten as default}; - index.cjs 中,
module.exports = flatten;同样位于文件最前,而实现主体在后。
这种“导出放最后”的顺序在混用多种模块系统、或部分工具链按顺序解析导出声明时,能减少默认导出被提前截断/覆盖的兼容性问题。虽然 changelog 只给出 issue 编号 #488,但当前产物中“默认导出出现在文件末尾”的形态,正是这次修复落地后可以观察到的结果。
五、5.1.2:类型改进——用递归类型描述任意层嵌套
5.1.2 的类型改进,对应到当前 index.d.ts,核心是引入了一个递归数组类型来精确描述可展平输入:
type RecursiveList<T> = readonly (T | readonly T[] | RecursiveList<T>)[]; declare function flatten<T>(arr: RecursiveList<T>, depth?: number): T[]; export default flatten;这个类型定义的表达能力体现在三方面:
- 递归引用自身(
RecursiveList<T>内再次出现RecursiveList<T>),因此任意深度的嵌套数组(如[[[[[4]]]]])都能被类型系统接受; - 支持 readonly 数组:
readonly修饰使flatten(numbers)(其中numbers: readonly number[])合法,而不强制要求可变数组; - 返回值类型为
T[]:无论嵌套多深,只要完全展平,输出就是元素类型T的一维数组。
配套的类型测试 index.tests.ts 用@ts-expect-error明确圈定了非法调用,比如:
// OK const numbers: readonly number[] = [1, 2, 3] flatten(numbers); flatten([1, [2, numbers], 4]); flatten([1, [2, [3]], [[[[[4]]]]]], 2); // Not OK(以下调用在编译期必须报错) // @ts-expect-error flatten(5); // @ts-expect-error flatten({ a: 5 }); // @ts-expect-error flatten([1], true); // @ts-expect-error flatten(new Set());这些用例覆盖了“非数组输入”“depth 为布尔值/字符串”“Map/Set 容器”等错误场景,保证类型定义在编译期就拦住非法调用(根 package.json 的test-types脚本通过tsc执行这一检查)。
六、核心实现:输入校验 + 基于栈的迭代展平
看完版本演进,再看实现本体。index.cjs 的入口函数先做严格的输入校验:
function flatten(arr, depth) { if (!Array.isArray(arr)) { throw new Error('expected an array'); } if (depth !== undefined && typeof depth !== 'number') { throw new Error('depth expects a number'); } return flattenHelper(arr, typeof depth === 'number' ? depth : Infinity); }两个关键语义:
- 非数组输入直接抛错(
expected an array),且不允许Map、Set等类数组容器; depth必须显式传undefined或数字;传null、{}、[]、函数等都会抛错(depth expects a number);省略时按Infinity处理,即完全展平。
真正执行展平的是 flattenHelper,一个值得玩味的栈式迭代实现(而非递归):
function flattenHelper(arr, depth) { var stack = arr.slice(); var result = []; while (stack.length) { var item = stack.pop(); if (Array.isArray(item) && depth > 0) { stack.push.apply(stack, item); depth--; } else { result.push(item); } } return result.reverse(); }算法要点:
- 用
arr.slice()复制一份输入,避免污染原数组(测试中“flattened arrays are unchanged”验证了这一点); - 每次
pop栈顶元素:若它还是数组且depth > 0,就把它展开推回栈中并把当前层的 depth 减一;否则直接收进result; - 由于从栈尾弹出、按“后进先出”顺序收集,最终结果与输入顺序相反,所以末尾用
result.reverse()还原原始顺序。
整个展平过程不依赖任何原生 API,也不修改输入数组。这一点在 test/array-flatten/index.cjs 中体现得尤其彻底:测试先保存原生Array.prototype.flat,将其临时置为null,跑完用例后再用Object.defineProperty恢复——从源码结构看,这是刻意验证实现不依赖原生 flat、即使原生方法缺失也能独立工作。
七、测试与质量保障:用例如何锁定版本承诺
test/array-flatten/index.cjs 用 tape 框架覆盖了五个维度的行为,恰好与上文实现一一对应:
| 测试用例 | 验证点 |
|---|---|
| flattened arrays are unchanged | 已展平数组原样返回,且不修改原数组 |
| unflattened arrays are flattened | 混合数组与对象数组均完全展平 |
| throws on non-arrays | {n: 4}、2、undefined均抛错 |
| flatten to specific depth | depth为 0/1/2/3/4 时逐层展平,结果逐一断言 |
| throws when depth is not a number | 'depth'、null、{}、[]、函数均抛错 |
其中深度语义的测试最为直观:
var arr = [1, [2, [3, [4, [5]]]]]; flatten(arr, 0); // [1, [2, [3, [4, [5]]]]] flatten(arr, 1); // [1, 2, [3, [4, [5]]]] flatten(arr, 2); // [1, 2, 3, [4, [5]]] flatten(arr, 3); // [1, 2, 3, 4, [5]] flatten(arr, 4); // [1, 2, 3, 4, 5]可见depth表示“允许展开的嵌套层数”:0表示完全不展平,层数越大展得越深,直到完全扁平。根 package.json 的test脚本将 lint、tape 行为测试与tsc类型测试串成一条流水线,确保行为与类型两方面的回归都在发布前被拦截。
八、小结:一条 changelog 背后的完整工程链路
回顾这 4 条 changelog,可以串联出just-flatten-it完整的工程链路:
- 5.1.0更新 package.json 导出相关配置(#467、#483),为后续模块格式调整铺路;
- 5.1.1规范默认导出的书写位置(#488),保障多模块系统下的兼容性;
- 5.1.2升级类型系统,用递归类型与 readonly 支持覆盖任意嵌套输入;
- 5.2.0最终将 Node 模块重命名为
.cjs,与"type": "module"的包级 ESM 语义解耦,形成 CJS/ESM 双入口并存的稳定形态。
配套的 config/createRollupConfig.js 负责从 CJS 源自动生成 ESM 产物,index.tests.ts 与 test/array-flatten/index.cjs 分别守住类型与行为边界。对于一个“只做一件事”的工具包而言,这份 changelog 记录的不只是版本号,而是一套从实现、类型到打包与测试的完整质量闭环——这也正是阅读 changelog 并结合源码回溯版本变更的价值所在。
- 开发工具
【免费下载链接】just
A library of dependency-free JavaScript utilities that do just one thing.
相关推荐
just-flatten-it:零依赖数组扁平化模块的用法与源码解析
just flatten it:零依赖数组扁平化模块的用法与源码解析 本篇以 just 工具库中的 just flatten it https://link.g
开发工具Tremor CategoryBar 组件全解析:从 changelog 版本演进到源码级实现
Tremor CategoryBar 组件全解析:从 changelog 版本演进到源码级实现 CategoryBar 是 Tremor 组件库中用于构建仪表盘
UI组件图表库前端Podman 依赖的 ssh_config Go 解析器演进全解:从 CHANGELOG 看版本特性与源码实现
Podman 依赖的 ssh_config Go 解析器演进全解:从 CHANGELOG 看版本特性与源码实现 ssh_config 是 Podman 仓库中内
容器运行时云原生CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考