模块化组织:按功能拆分,支持按需导入
在构建一个健壮、可维护的工具库时,模块化组织是至关重要的设计决策。它不仅影响代码的可读性,更决定了库的性能、可扩展性和开发者体验。
通过将 is、deepGet、omit、pipe 等函数按功能分类,拆分到独立的文件中(如 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设计原则
按功能而非类型划分:
- 不要按
string.ts,number.ts这样划分,因为功能分散。 - 而
object/聚合所有对象操作,逻辑清晰。
- 不要按
避免“大杂烩”文件:
- 拒绝
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 生效?
- 使用 ES Module 语法(
import/export),而非 CommonJS(require/module.exports)。 - 避免副作用:入口文件
index.ts应只做导出,不执行逻辑。 - 正确的
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结语:模块化是工程化的基石
一个设计良好的模块化结构,是工具库从“能用”到“好用”的关键跃迁。
它不仅仅是文件的物理分割,更是一种架构思维:
- 关注点分离:每个模块只做一件事。
- 可组合性:小函数通过
pipe、compose组合成大功能。 - 可扩展性:新增功能只需添加新文件。
- 性能友好:Tree Shaking 确保用户只加载所需代码。
当你将 deepGet 放入 object/,将 pipe 放入 function/,你不仅在组织文件,更在构建一个清晰、可推理、可持续演进的系统。
在前端工程化的今天,模块化不是选择,而是专业性的体现。