3、依赖声明文件:package.json、setup.py、Cargo.toml 等配置文件的作用与结构

依赖声明文件,说白了就是 SDK 的「身份证」加「购物清单」。

我刚开始做 SDK 发布时,总觉得这些文件就是个形式。直到有一次,我发布的 Python 包因为 setup.py 里漏写了一个依赖,导致用户安装后直接报错……嗯,从那以后我再也不敢小看这些配置文件了。

3.1 它们到底在干什么?

每个语言生态都有自己的包管理工具。它们都依赖一个核心文件来声明三件事:

  • 我是谁 —— 包名、版本号、作者、描述
  • 我需要什么 —— 运行时依赖、开发依赖
  • 我怎么用 —— 入口文件、脚本、构建配置

你想想看,如果没有这些文件,用户下载了你的 SDK,连依赖都装不全,那还怎么用?

核心原则:依赖声明文件是 SDK 与包管理器之间的契约。你写清楚,它帮你搞定一切。

3.2 三大主流生态的配置文件

我平时主要接触 JavaScript、Python 和 Rust 三个生态。它们的配置文件各有特点,但核心逻辑是相通的。

3.2.1 package.json(JavaScript / TypeScript)

这是 Node.js 生态的标配。我个人习惯用 npm init 生成初始模板,然后手动调整。

{
  "name": "@myorg/sdk-core",
  "version": "2.1.0",
  "description": "核心 SDK 包",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "test": "jest",
    "prepublishOnly": "npm run build"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "typescript": "^5.3.0",
    "jest": "^29.7.0"
  },
  "peerDependencies": {
    "react": ">=17.0.0"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}

这里有几个关键字段,我重点说一下:

字段 作用 我的经验
name 包名,必须唯一 建议加 scope,比如 @公司名/包名
version 语义化版本号 严格遵循 semver,别乱改
dependencies 运行时依赖 能少就少,减少用户安装体积
peerDependencies 宿主环境依赖 比如 React 插件,让用户自己装 React
engines 限制 Node 版本 我曾经没写这个,用户用 Node 14 跑我的 SDK,直接崩了

小技巧:用 ^ 符号表示兼容版本,比如 ^1.6.0 表示 >=1.6.0 且 <2.0.0。这样既能获取小版本更新,又不会引入破坏性变更。

3.2.2 setup.py(Python)

Python 生态的配置文件。说实话,Python 的打包工具变化挺大的,从 setup.py 到 setup.cfg 再到 pyproject.toml。但我个人觉得,setup.py 依然是最直观的。

from setuptools import setup, find_packages

setup(
    name="my-sdk",
    version="1.2.0",
    description="一个实用的 SDK",
    packages=find_packages(exclude=["tests*"]),
    install_requires=[
        "requests>=2.28.0",
        "pydantic>=2.0.0",
    ],
    extras_require={
        "dev": ["pytest>=7.0.0", "black"],
        "ml": ["numpy>=1.24.0", "scikit-learn>=1.3.0"],
    },
    python_requires=">=3.9",
    entry_points={
        "console_scripts": [
            "my-sdk-cli=my_sdk.cli:main",
        ],
    },
)

这里有个坑,我曾经踩过:

注意:install_requires 里不要写死版本号,比如 requests==2.28.0。这样会和其他包冲突。用 >= 或者 ~= 更安全。

extras_require 是个好东西。你可以把可选依赖分组,用户按需安装:

  • pip install my-sdk —— 只装核心依赖
  • pip install my-sdk[ml] —— 装核心 + 机器学习相关依赖

3.2.3 Cargo.toml(Rust)

Rust 的配置文件,我个人觉得是三个里面设计最优雅的。它用 TOML 格式,清晰又简洁。

[package]
name = "my-sdk"
version = "0.4.1"
edition = "2021"
description = "高性能 Rust SDK"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.35", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }

[dev-dependencies]
criterion = "0.5"

[features]
default = ["std"]
std = []
async = ["tokio", "reqwest"]

[lib]
name = "my_sdk"
path = "src/lib.rs"

Rust 的依赖管理有几个特点:

  • 特性开关(features):可以按需编译,减少二进制体积。我在项目中就用这个做「轻量版」和「完整版」
  • 语义化版本:Cargo 默认用 ^ 语义,和 npm 类似
  • 编译缓存:Cargo 的依赖缓存机制很智能,第二次编译快很多

我的习惯:在 Cargo.toml 里尽量少开 features。默认只开最核心的,用户需要什么自己加。这样能避免「依赖地狱」。

3.3 依赖声明文件的通用结构

虽然语法不同,但核心结构其实差不多。我画了一张图,帮你理清思路:

依赖声明文件核心结构 依赖声明文件 元信息 包名、版本、描述、作者 依赖声明 运行时、开发、可选依赖 构建配置 入口、脚本、编译选项 约束条件 引擎版本、平台限制 不同语言语法不同,但核心逻辑一致

3.4 版本号声明的最佳实践

依赖声明里,版本号怎么写,其实很有讲究。我总结了几条经验:

  1. 用范围,别用精确版本 —— ^1.2.0 比 1.2.0 好,给用户留点弹性
  2. 锁定关键依赖 —— 如果某个依赖的 API 经常变,可以锁死大版本,比如 ~2.3.0
  3. 区分依赖类型 —— 运行时依赖、开发依赖、可选依赖,分开放,别混在一起
  4. 定期更新 —— 我习惯每季度跑一次 npm outdated 或 pip list --outdated,看看有没有安全更新

我曾经踩过的坑:有一次我把 devDependencies 里的测试框架写到了 dependencies 里。结果用户安装我的 SDK 时,莫名其妙装了一堆测试工具……嗯,从那以后我每次发布前都会检查一遍依赖分类。

3.5 多语言项目的依赖管理

如果你像我一样,维护一个跨语言 SDK(比如 Python 核心 + JavaScript 前端),那依赖声明文件就更重要了。

我的做法是:

  • 每个语言独立维护自己的配置文件
  • 用 CI 脚本统一检查版本号是否一致
  • 发布时打 tag,确保所有语言的版本号同步

说白了,依赖声明文件就是 SDK 的门面。写得好,用户用得爽;写得差,用户骂得狠。我见过太多因为 package.json 里少写一个字段导致整个发布流程卡住的案例了。

最后一个小建议:每次发布前,用 npm pack --dry-run 或 python setup.py sdist 模拟一下打包过程。看看哪些文件会被包含进去,哪些依赖会被安装。提前发现问题,总比用户反馈强。

好了,关于依赖声明文件,我就说这么多。记住一句话:配置文件写清楚,SDK 发布少踩坑。

可可精品资料库