代码的边界:从项目到包的重构实践

3 min read
Zekari
npm重构抽象架构

复用代码的冲动来得很自然。当你构建了一个功能完整的博客系统,自然会想"如果能在其他项目中快速使用就好了"。但这个想法从未真正简单。

问题不在于技术实现,而在于:什么应该被复用,什么必须留给使用者? 这是一个边界问题。

第一层边界:包含的代价

复用的第一步不是 cp -r,而是理解边界。

我的博客系统包含 32 个组件、18 个依赖、若干配置文件,以及最重要的——内容。文章、首页、个人介绍,这些构成了这个博客的灵魂。

但包不应该有灵魂。包应该是框架,是可能性的集合。

应该包含的:

  • 32 个博客组件(BlogCard, BlogLayout, MDX 组件等)
  • 库函数(getAllPosts, filterPosts 等)
  • 路由模板(app/blog/page.tsx 作为模板,而非实现)
  • 配置模板(next.config, tailwind.config)

必须排除的:

  • 具体的文章内容(content/blog/ 只保留示例)
  • 首页实现(每个博客都应该不同)
  • 个人化的配置(域名、作者信息)
  • 项目特定的资源(图片、字体)

这个边界看似清晰,实则需要反复权衡。比如 i18n 系统——我选择包含框架(LanguageProvider, translations.ts),但不包含具体的翻译内容。这是一个灰色地带,取决于你对"可配置性"的理解。

最困难的部分是路径别名。原项目中所有组件都使用 @/ 导入,这在 npm 包中是一个问题。我没有重写所有导入(那会改变太多代码),而是保留了 @/ 配置,让 tsconfig 继续工作。这不完美,但够用。

这就是第一个洞见:边界不是纯粹的分离,而是在"完全耦合"和"完全解耦"之间找到平衡点。

第二层边界:使用的接口

确定了"什么"之后,需要定义"如何"。

一个 npm 包可以被导入:

import { BlogCard, getAllPosts } from 'fenz007-nextjs-blog'

这很灵活。但对于博客这种结构化的系统,纯导入不够——用户还需要:

  • 创建路由文件
  • 配置 Next.js 和 Tailwind
  • 设置内容目录结构
  • 安装 18 个依赖

如果每次都手动操作,复用的价值就大打折扣。

我选择提供三种使用方式:

1. 创建新项目

npx fenz007-nextjs-blog create my-blog

自动创建 Next.js 项目 + 初始化博客系统。

2. 初始化现有项目

npm install fenz007-nextjs-blog
npx fenz007-nextjs-blog init

复制路由模板、配置文件、安装依赖。

3. 直接导入组件

import { BlogCard } from 'fenz007-nextjs-blog'

高级用户的自定义方式。

这三种方式对应不同的使用场景和技术水平。关键是 init 命令——它是边界的具体化:哪些文件应该被创建,哪些配置应该被合并,哪些依赖应该被安装。

CLI 的实现包含三个工具模块:

  • file-ops.ts - 文件操作(复制模板、合并配置)
  • dependency.ts - 依赖管理(检测包管理器、安装依赖)
  • validator.ts - 项目验证(检查是否是 Next.js 项目)

这些模块本身也定义了边界:什么操作应该自动完成,什么应该留给用户决定。比如配置文件,我选择不自动覆盖,而是显示需要添加的内容,让用户手动合并。因为配置是个人化的,强制覆盖会破坏用户现有的设置。

第二个洞见:接口设计就是边界设计。CLI 不仅是便利工具,更是使用边界的明确表达。

第三层边界:发布的取舍

实践中的边界从来不完美。

TypeScript 类型定义

构建包时遇到了类型错误:

error TS2305: Module '"./components/blog/blog-card"'
has no exported member 'default'.

原因是组件混用了 export default 和命名导出。修复方案有三种:

  1. 统一所有组件的导出方式(耗时,改动大)
  2. 手动创建 .d.ts 文件(维护成本高)
  3. 暂时禁用类型生成(务实但不完美)

我选择了第三种:

export default defineConfig({
  dts: false,  // Temporarily disabled due to export issues
})

包依然可用,只是没有完整的 TypeScript 类型提示。这是一个取舍。

在理想世界中,我们应该:

  • 统一所有组件的导出方式
  • 提供完整的类型定义
  • 确保 100% 的类型安全

在现实世界中,我们需要问:

  • 这个问题现在必须解决吗?
  • 解决它的成本是多少?
  • 不解决它的影响是什么?

对于一个刚发布的 v1.0.0 包,功能可用比类型完美更重要。类型定义可以在后续版本中修复。这不是懒惰,而是优先级判断。

Scope 的问题

最初我使用 @zekari/nextjs-blog 作为包名(scoped package),发布时遇到错误:

npm ERR! 404 Scope not found

原因是 @zekari 这个 scope 不存在。解决方案:

  1. 注册 npm organization(需要付费)
  2. 改用 @fenz007/nextjs-blog(用户名 scope)
  3. 改用简单包名 fenz007-nextjs-blog

我最终选择了第三种。虽然 scoped package 看起来更专业,但对于个人项目,简单包名就够了。这又是一个边界判断:形式 vs 实用。

第三个洞见:边界不是固定的,而是在约束条件下的最优解。完美是边界的敌人。

核心理解:边界即设计

将 32 个组件、3 个 CLI 命令、18 个依赖打包成一个 npm 包,整个过程用了 3-4 小时。但真正的挑战不在代码量,而在边界的定义。

回顾整个过程,我在三个层次上定义了边界:

  1. 内容边界 - 什么应该被包含(组件 yes,文章 no)
  2. 使用边界 - 如何被使用(CLI + 导入的混合方式)
  3. 质量边界 - 什么是必需的(功能 yes,完美类型 not now)

每个边界都是一次权衡。而权衡的本质是理解:这个包想要解决什么问题,为谁解决问题。

💡 Click the maximize icon to view in fullscreen

当你下次想要"复用代码"时,不要急于 copy-paste。先问:

  • 边界在哪里?
  • 如何使用最自然?
  • 什么可以妥协?

边界清晰了,代码就活了。


实践细节

包结构:

  • 32 个组件(22 个博客 + 10 个 UI)
  • 3 个 CLI 命令(create, init, update)
  • 18 个核心依赖(MDX, Tailwind, Radix UI 等)
  • 包大小:523 KB(压缩后 118 KB)

工具链:

  • tsup - 构建工具(ESM + CJS)
  • commander - CLI 框架
  • fs-extra - 文件操作
  • chalk - 终端样式

发布:

npm publish  # 公开包,免费

使用:

npx fenz007-nextjs-blog create my-blog
  1. TypeScript 类型定义不完整

    • 原因:组件导出方式不统一
    • 影响:IDE 类型提示不全
    • 计划:v1.1.0 修复
  2. 路径别名依赖

    • 原因:保留了 @/ 导入
    • 影响:某些构建环境可能需要额外配置
    • 计划:长期逐步迁移到相对路径
  3. CLI 错误处理

    • 原因:时间限制,仅实现基础错误处理
    • 影响:某些边缘情况可能崩溃
    • 计划:v1.2.0 增强

承认这些不完美很重要。v1.0.0 的目标是"可用",而非"完美"。后者需要更多迭代。

相关思考

这次实践让我重新理解了"复用"这个概念。复用不是技术问题,而是设计问题——如何在通用性和特定性之间找到平衡。

边界的清晰度决定了复用的质量。模糊的边界导致过度耦合或过度抽象,清晰的边界让代码既灵活又简单。

下次当你构建可复用的东西,试着用"边界"这个视角思考。不是"我如何分离代码",而是"什么应该属于哪一侧"。

边界清晰,代码就自然分离了。

Related Posts

Articles you might also find interesting

适配器模式:对现实的妥协

4 min read

当 PayPro 要求 IP 白名单而 Stripe 不需要,当一个按秒计费另一个按请求计费,架构设计不是消除约束——而是管理约束。适配器模式不是优雅设计,而是对现实混乱的务实投降。

架构支付集成
Read More

调用链路追踪法:从断点到根因

2 min read

功能失效的背后,是一条完整的调用链路。追踪这条链路,定位断点,才能从根本上解决问题。

架构调试
Read More

离屏渲染:照片捕获为什么需要独立的 canvas

2 min read

实时流与静态合成的本质冲突,决定了系统必须分离。理解这种分离,就理解了架构设计中最重要的原则。

架构前端开发
Read More

集中式配置:让 Reddit 组件脱离重复泥潭

2 min read

当同一份数据散落在多个文件中,维护成本呈指数级增长。集中式配置不是技术选择,而是对抗熵增的必然手段。

架构React 组件
Read More

约束驱动设计:为何选择内存追踪

2 min read

在 Cloudflare Workers 环境中实现追踪系统,持久化和内存存储之间的权衡不是技术偏好,而是约束驱动的必然选择。

架构Cloudflare Workers
Read More

依赖注入

2 min read

依赖注入不是关于框架或工具,而是关于控制权的转移。理解这个转移,就理解了软件设计的核心原则。

软件工程系统思维
Read More

双重导出管道的架构选择

2 min read

在用户生成内容场景中,速度与质量的权衡决定了导出架构。理解两种不同管道的设计逻辑,能够更准确地把握产品体验的边界。

架构图像导出
Read More

Purikura的页面系统

3 min read

通过五层分层继承复用架构,实现零代码修改的页面生成系统。从类型定义到页面渲染,每一层专注单一职责,实现真正的数据驱动开发。

架构React
Read More

从意图到架构

3 min read

技术方案不是设计出来的,而是从问题中涌现的。理解这个过程,就理解了软件设计的本质。

软件工程架构
Read More

重复数据的迁移实践:从 N 个文件到 1 个真相源

3 min read

当同一份 Reddit posts 配置散落在多个文件中,维护成本以文件数量指数增长。迁移到集中式配置不是技术选择,而是对复杂度的清算。

架构配置管理
Read More

在运行的系统上生长新功能

3 min read

扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。

系统设计架构
Read More

多厂商 AI 调度:统一混乱的供应商生态

3 min read

当你依赖第三方 AI 服务时,单点故障是最大的风险。多厂商调度不只是技术架构,更是对不确定性的应对策略。

Purikura 项目架构
Read More

分布式 Workers 的解耦设计

3 min read

通过微服务架构和队列系统,实现高可用的 AI 任务处理。从单体到分布式,每个设计决策都是对复杂度的权衡。

Purikura 项目架构
Read More

队列、可靠性与系统边界

1 min read

探讨消息队列系统如何通过时间换空间,用异步换解耦,以及可靠性背后的权衡

架构可靠性
Read More

Studio 系统架构:从状态机到端到端流程

3 min read

深入 Studio 系统的状态管理中心、组件协调机制和 AI 生成的完整数据流,理解前后端集成的设计逻辑

Purikura 项目架构
Read More

编码前的思考

1 min read

软件设计不是从代码开始的。在动手之前,有一套思维框架值得遵循:理解边界、定义数据、设计函数、建立抽象。

软件工程思维框架
Read More

统一积分系统的设计实践

2 min read

从多套积分到单一积分池的架构演进,以及背后的原子性、一致性设计

架构数据库设计
Read More