Skip to content

模块化组织:按功能拆分,支持按需导入

在构建一个健壮、可维护的工具库时,模块化组织是至关重要的设计决策。它不仅影响代码的可读性,更决定了库的性能、可扩展性和开发者体验。

通过将 isdeepGetomitpipe 等函数按功能分类,拆分到独立的文件中(如 function.ts, object.ts, array.ts),我们可以实现:

  • 高内聚低耦合:功能相关的函数放在一起。
  • 按需导入:用户只加载需要的代码,减少打包体积。
  • 易于维护:定位和修改功能更简单。

目录结构设计

一个典型的工具库模块化结构如下:

src/
├── index.ts          # 入口文件,聚合所有导出
├── function/         # 函数式编程相关工具
│   ├── pipe.ts
│   ├── compose.ts
│   ├── curry.ts
│   └── sleep.ts
├── object/           # 对象操作工具
│   ├── deepGet.ts
│   ├── omit.ts
│   ├── pick.ts
│   └── isObject.ts
├── array/            # 数组操作工具
│   ├── chunk.ts
│   ├── flatten.ts
│   └── unique.ts
├── type/             # 类型判断工具
│   ├── is.ts         # isString, isNumber 等
│   └── isEmpty.ts
└── async/            # 异步工具
    ├── asyncPipe.ts
    ├── parallel.ts
    └── series.ts

设计原则

  1. 按功能而非类型划分

    • 不要按 string.ts, number.ts 这样划分,因为功能分散。
    • object/ 聚合所有对象操作,逻辑清晰。
  2. 避免“大杂烩”文件

    • 拒绝 utils.ts 这种不断膨胀的文件。
    • 每个模块职责单一。

实现:按文件组织函数

示例:object/deepGet.ts

ts
// src/object/deepGet.ts
export const deepGet = <T, D = undefined>(
  obj: T,
  path: string | string[],
  defaultValue?: D
): unknown => {
  if (obj == null) return defaultValue

  const paths = Array.isArray(path) ? path : path.split('.')
  let current: any = obj

  for (const key of paths) {
    if (current == null) return defaultValue
    current = current[key]
  }

  return current !== undefined ? current : defaultValue
}

示例:function/pipe.ts

ts
// src/function/pipe.ts
export const pipe = <T>(...fns: Array<(arg: T) => T>) => {
  return (value: T) => {
    return fns.reduce((acc, fn) => fn(acc), value)
  }
}

示例:type/is.ts

ts
// src/type/is.ts
const toString = Object.prototype.toString

const isType = <T>(type: string) => (value: unknown): value is T =>
  toString.call(value) === `[object ${type}]`

export const isString = isType<string>('String')
export const isNumber = isType<number>('Number')
export const isArray = isType<unknown[]>('Array')
// ... 其他 is 函数

入口文件:index.ts 聚合导出

ts
// src/index.ts
// 按模块聚合导出
export * from './function/pipe'
export * from './function/compose'
export * from './function/sleep'

export * from './object/deepGet'
export * from './object/omit'
export * from './object/pick'

export * from './type/is'
export * from './type/isEmpty'

export * from './async/asyncPipe'
export * from './async/parallel'

优点

  • 用户可通过 import { deepGet, omit } from 'my-utils' 一次性导入。
  • 也支持深层导入:import { deepGet } from 'my-utils/object'

支持按需导入:Tree Shaking 的关键

现代打包工具(如 Webpack、Vite、Rollup)支持 Tree Shaking——自动移除未使用的代码。

如何确保 Tree Shaking 生效?

  1. 使用 ES Module 语法import/export),而非 CommonJS(require/module.exports)。
  2. 避免副作用:入口文件 index.ts 应只做导出,不执行逻辑。
  3. 正确的 package.json 配置
json
{
  "name": "my-utils",
  "main": "dist/index.js",
  "module": "dist/esm/index.js",
  "sideEffects": false,
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    },
    "./object/deepGet": "./dist/esm/object/deepGet.js"
  }
}
  • "sideEffects": false 告诉打包工具:这个包没有副作用,可以安全地摇除未使用代码。
  • "exports" 提供深层路径导入支持。

用户按需导入示例

ts
// 只导入 deepGet,打包时不会包含 pipe、omit 等其他函数
import { deepGet } from 'my-utils/object/deepGet'

// 或通过聚合入口
import { deepGet } from 'my-utils'

实战:构建一个可发布的工具库

1. 项目结构

my-utils/
├── package.json
├── tsconfig.json
├── src/
│   └── ... 模块化文件
├── dist/             # 打包输出
│   ├── esm/          # ES Module 版本
│   └── cjs/          # CommonJS 版本

2. 打包配置(Rollup 示例)

js
// rollup.config.js
export default [
  // ESM 版本
  {
    input: 'src/index.ts',
    output: { format: 'es', dir: 'dist/esm' },
    external: [/node_modules/]
  },
  // CJS 版本
  {
    input: 'src/index.ts',
    output: { format: 'cjs', file: 'dist/cjs/index.js' },
    external: [/node_modules/]
  }
]

3. 发布到 npm

bash
npm publish

结语:模块化是工程化的基石

一个设计良好的模块化结构,是工具库从“能用”到“好用”的关键跃迁。

它不仅仅是文件的物理分割,更是一种架构思维

  • 关注点分离:每个模块只做一件事。
  • 可组合性:小函数通过 pipecompose 组合成大功能。
  • 可扩展性:新增功能只需添加新文件。
  • 性能友好:Tree Shaking 确保用户只加载所需代码。

当你将 deepGet 放入 object/,将 pipe 放入 function/,你不仅在组织文件,更在构建一个清晰、可推理、可持续演进的系统

在前端工程化的今天,模块化不是选择,而是专业性的体现