从 0 到 1:用 TypeScript 封装一个 JavaScript SDK 并发布到 npm
最近我为 Bimiyun 的搜索 API 封装了一个 JavaScript SDK(bimiyun-javascript),从项目搭建到最终发布到 npm,整个过程踩了不少坑,也积累了一些经验。这篇文章完整记录了我封装 SDK 的全过程和注意事项,如果你也想把自己的 API 包装成一个 npm 包,希望这篇分享能帮到你。
一、为什么需要封装 SDK?
我们的 Bimiyun Search API 原本只提供了一个 REST 接口,开发者需要自己拼 URL、设置请求头、处理超时、解析 JSON 响应……虽然不难,但每次都要写一遍重复代码。封装 SDK 的核心目的就是:
- 降低接入成本 — 从"读文档 + 拼请求"变成"install + 调用方法"
- 统一错误处理 — 超时、网络异常、API 错误在 SDK 内部统一拦截
- 提供类型提示 — TypeScript 用户可以直接获得智能补全和类型检查
- 版本管理 — API 变更时通过 SDK 版本迭代平滑迁移
二、项目初始化
2.1 创建项目目录
mkdir bimiyun-javascript
cd bimiyun-javascript
npm init -y
2.2 安装 TypeScript 和依赖
SDK 选择用 TypeScript 编写,这样编译到 JavaScript 的同时还能生成 .d.ts 类型声明文件,使用者在 VS Code 等编辑器中可以直接获得智能提示。
npm install typescript @types/node @types/node-fetch --save-dev
npm install node-fetch
注意:这里选择
node-fetchv2 而不是内置的fetch,是为了兼容 Node.js 12+。如果只支持 Node.js 18+,可以省略这个依赖。
2.3 初始化 tsconfig.json
npx tsc --init
然后修改关键配置:
{
"compilerOptions": {
"target": "ES2020",
"module": "Node16",
"lib": ["ES2020"],
"declaration": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "Node16"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}
几个容易踩坑的配置说明:
| 配置项 | 说明 | 踩坑点 |
|---|---|---|
declaration: true |
生成 .d.ts 类型声明文件 |
不打开的话使用者没有类型提示 |
module + moduleResolution |
统一用 Node16 |
两者必须匹配,否则会有模块解析警告 |
outDir / rootDir |
编译输出 / 源码根目录 | rootDir 设错会导致编译时报"file is not under rootDir" |
strict: true |
开启严格类型检查 | 建议保持开启,能提前发现很多空值问题 |
三、核心代码编写
3.1 版本信息独立文件
把版本号和元信息抽到一个单独文件里,方便后续发布时统一管理:
// src/version.ts
export const __version__ = "0.1.0";
export const __title__ = "bimiyun-javascript";
export const __description__ = "Bimiyun Search API JavaScript SDK";
export const __author__ = "Bimiyun Team";
export const __license__ = "MIT";
export const __url__ = "https://github.com/bimiyun-ai/bimiyun-javascript";
3.2 客户端实现
核心逻辑放在 src/client.ts 中,包含:
ClientConfig— 配置接口(apiKey、baseUrl、timeout)SearchOptions— 搜索选项接口(lang、safe、mode、maxResults)BimiyunClient— 客户端类bimiyun()— 工厂函数
// src/client.ts(核心片段)
import fetch from "node-fetch";
import { __version__ } from "./version";
export interface ClientConfig {
apiKey: string;
baseUrl?: string;
timeout?: number;
}
export interface SearchOptions {
lang?: string;
safe?: boolean;
mode?: string;
maxResults?: number;
}
export class BimiyunClient {
private apiKey: string;
private baseUrl: string;
private timeout: number;
private headers: Record<string, string>;
constructor(
apiKey: string,
baseUrl: string = "https://search.bimiyun.com",
timeout: number = 30
) {
if (!apiKey || apiKey.trim() === "") {
throw new Error("API key must be provided");
}
this.apiKey = apiKey;
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
this.timeout = timeout * 1000;
this.headers = {
"X-Api-Key": apiKey,
"Content-Type": "application/json",
"User-Agent": `bimiyun-javascript/${__version__}`,
};
}
async search(query: string, options: SearchOptions = {}): Promise<BimiyunResponse> {
// ... 请求逻辑
}
}
export function bimiyun(config: ClientConfig): BimiyunClient {
return new BimiyunClient(config.apiKey, config.baseUrl, config.timeout);
}
设计决策:同时提供了类(
BimiyunClient)和工厂函数(bimiyun())两种创建方式。工厂函数的优势是可以用配置对象传参,比直接 new 更灵活,API 也更接近其他流行 SDK(如 OpenAI SDK)的使用习惯。
3.3 入口文件导出
// src/index.ts
import { BimiyunClient, bimiyun } from "./client";
import { __version__ } from "./version";
export { BimiyunClient, bimiyun, __version__ };
export * from "./client";
export default bimiyun;
注意事项:
- 用
export default同时支持了 CommonJS 的require('bimiyun-javascript').default和 ESM 的import bimiyun from 'bimiyun-javascript'export * from "./client"把所有类型定义也一起导出,使用者不需要额外 import
四、package.json 配置
这是发布到 npm 前最关键的一步,配置错了要么发布失败,要么用户安装后无法正常使用。
{
"name": "bimiyun-javascript",
"version": "0.1.1",
"description": "Bimiyun Search API JavaScript SDK",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "node tests/test-search.js",
"prepublishOnly": "npm run build"
},
"keywords": ["bimiyun", "search", "api", "sdk"],
"author": "Bimiyun Team",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/bimiyun-ai/bimiyun-javascript"
},
"bugs": {
"url": "https://github.com/bimiyun-ai/bimiyun-javascript/issues"
},
"homepage": "https://bimiyun.com",
"dependencies": {
"node-fetch": "^2.6.7"
},
"devDependencies": {
"@types/node": "^18.0.0",
"@types/node-fetch": "^2.6.11",
"typescript": "^4.7.4"
},
"engines": {
"node": ">=12.0.0"
}
}
几个容易踩坑的配置项:
| 配置 | 说明 | 踩坑点 |
|---|---|---|
main |
CommonJS 入口文件路径 | 必须指向编译后的 dist/index.js,不是 src/index.ts |
types |
TypeScript 类型声明文件路径 | 不配这个字段,使用者在 TS 项目中会报"找不到类型声明" |
files |
发布到 npm 时包含的文件 | 建议只包含 dist/ + README + LICENSE,不要把源码和 node_modules 发出去 |
prepublishOnly |
发布前自动执行的脚本 | 用这个钩子自动编译,避免忘记 build 就把旧代码发出去了 |
dependencies vs devDependencies |
node-fetch 是运行时依赖,必须放在 dependencies 里 |
错放到 devDependencies 会导致用户 npm install 后运行时报"Cannot find module ‘node-fetch’" |
engines |
声明支持的 Node.js 版本 | 写上这个字段可以让不兼容的用户提前知道 |
五、.gitignore 和 .npmignore
5.1 .gitignore
node_modules/
dist/
.env
*.log
注意:
dist/不需要提交到 Git,因为它是由 TypeScript 编译生成的。但发布到 npm 时需要包含。
5.2 .npmignore
如果你没有 .npmignore 文件,npm 会使用 .gitignore 的规则来过滤文件——但这不一定是你想要的行为,因为 Git 不需要但 npm 需要发布的文件(比如 dist/)会被排除。
有两种方案:
方案一(推荐):用 package.json 的 files 字段做白名单,如上所示。files 的优先级高于 .gitignore / .npmignore。
方案二:创建 .npmignore:
src/
tests/
tsconfig.json
.gitignore
建议:优先用
files白名单方式,比.npmignore黑名单更不容易出错。
六、测试
SDK 写完后,发布前一定要测试。我写了一个简单的测试脚本 tests/test-search.js:
const { BimiyunClient } = require('../dist');
async function runTest() {
const client = new BimiyunClient("ak-your-api-key");
const response = await client.search("hello world", { lang: "en" });
console.log("Search successful");
console.log(`Found ${response.organic?.length || 0} results`);
}
runTest();
运行测试:
npm run build # 先编译
npm run test # 执行测试
七、编写 README
一个专业的 README 是 SDK 的"门面"。我参考了其他流行 SDK 的结构,包含了以下内容:
- 一句话介绍
- 安装方式
- Quick Start 示例
- 完整的 API Reference
- 错误处理说明
- 测试方法
- 开源协议和联系方式
注意事项:
- 示例代码要能直接复制粘贴运行,不要把占位 API Key 写成真实值
- API Reference 的每个参数都要有类型和默认值说明
- 放上官方文档和 GitHub Issue 链接
八、发布到 npm
8.1 注册 npm 账号
如果没有的话先去 npmjs.com 注册。注册后在终端登录:
npm login
按照提示输入用户名、密码和邮箱。如果开了两步验证,还需要输入 OTP 验证码。
8.2 检查包名是否可用
npm view bimiyun-javascript
如果返回 npm ERR! code E404,说明这个包名还没被占用,可以使用。
8.3 编译 + 发布
npm run build # 编译 TypeScript 到 dist/
npm publish # 发布到 npm
注意事项:
prepublishOnly钩子会在npm publish前自动执行build,但第一次建议手动 build 确认没有编译错误- 如果
package.json中name是一个 scoped package(如@bimiyun/javascript),发布私有包需要npm publish --access restricted,公开包需要npm publish --access public
8.4 验证发布
发布成功后去 npmjs.com 搜索你的包名,应该能看到:
- README 正确渲染
- 可以下载 tarball
- 版本号和
package.json一致
然后在另一个项目中安装测试:
mkdir test-consumer && cd test-consumer
npm init -y
npm install bimiyun-javascript
const { bimiyun } = require('bimiyun-javascript');
console.log('SDK installed successfully:', typeof bimiyun);
// 输出: SDK installed successfully: function
九、版本更新
后续如果 SDK 有更新,修改 package.json 中的 version 后重新发布:
npm version patch # 0.1.0 -> 0.1.1(小修复)
npm version minor # 0.1.0 -> 0.2.0(新功能)
npm version major # 0.1.0 -> 1.0.0(破坏性变更)
npm publish
十、常见问题排查
问题 1:用户安装后报 Cannot find module 'node-fetch'
原因:node-fetch 被错误地放在了 devDependencies 而不是 dependencies。
解决:
npm install node-fetch --save # 确保放在 dependencies 里
npm version patch
npm publish
问题 2:用户报 Cannot find module 'bimiyun-javascript' or its corresponding type declarations
原因:package.json 中缺少 "types" 字段,或者 declaration: true 没有开启。
解决:检查 tsconfig.json 和 package.json 的 types 配置。
问题 3:npm publish 报 403 Forbidden
原因:包名已被他人占用,或者你没有登录正确的 npm 账号。
解决:
npm whoami # 检查当前登录的账号
npm view <name> # 检查包是否已存在
问题 4:用户 import 时得到 undefined
原因:ESM 和 CommonJS 的模块导出方式不匹配。
解决:确保 src/index.ts 中既有 export default 也有 export { },并同时支持两种导入方式。
十一、总结
回顾整个过程,从初始化到发布 npm 大约经历了这些步骤:
npm init+ 安装 TypeScript- 配置
tsconfig.json - 编写核心代码(接口定义、客户端类、工厂函数)
- 配置
package.json(入口、类型、files、依赖关系) - 写测试和 README
npm login+npm publish
整个过程最需要注意的是 package.json 的配置 和 依赖分类,这两个地方出错率最高。一旦配置正确,后续的版本更新就只需要改版本号然后 npm publish 了。
完整源码可以在 GitHub 查看:bimiyun-ai/bimiyun-javascript
更多推荐
所有评论(0)