做 CLI 工具的二次开发,最后一步往往是最容易被忽视的:怎么把包发出去?
公司项目基于 qwen-cli 做私有化改造,客户的环境是内网,连不了 npmjs.org。这时候就得自己搭一套私有 NPM 仓库,实现内部发包和分发。
这篇文章聊聊我们在这个过程中踩的坑和最终方案,主要涉及:
- Nexus 是什么,为什么选它
.npmrc的配置逻辑- CI/CD 中的自动化发包
- 一些容易踩的坑
为什么需要私有 NPM 仓库
先说背景。我们的 CLI 工具是给企业客户用的,有几个硬性要求:
- 内网部署 ------ 客户环境连不了公网,npm install 从 npmjs.org 拉包直接超时
- 版本可控 ------ 不能让客户随便装个 latest,出问题不好排查
- 安全合规 ------ 内部代码不能发到公网仓库
这三个需求一叠加,结论很明确:需要一个私有 NPM 仓库。
Nexus 是什么
Nexus 全称 Sonatype Nexus Repository Manager,是一个开源的仓库管理器。简单说,它就是一个"包的中转站和仓库"。
通俗理解
你可以把 Nexus 想象成一个企业内部的"快递中转站":
- npmjs.org 是公网的"京东仓库",所有人都能从那拉包
- Nexus 是你公司内部的"菜鸟驿站",可以:
- 存放公司自己的包(私有包)
- 缓存从公网拉过的包(加速 + 离线)
- 控制谁能取件、谁能寄件(权限管理)
Nexus 支持的仓库类型
Nexus 不只是给 Java/Maven 用的,它支持多种包格式:
| 包格式 | 说明 |
|---|---|
| npm | Node.js 包 |
| maven | Java 包 |
| docker | 容器镜像 |
| pypi | Python 包 |
| nuget | .NET 包 |
| raw | 任意文件 |
对于每种格式,Nexus 提供三种仓库类型:
scss
┌─────────────────────────────────────────────────────────────┐
│ Nexus Repository │
├─────────────────┬─────────────────┬─────────────────────────┤
│ Hosted │ Proxy │ Group │
│ (托管仓库) │ (代理仓库) │ (组合仓库) │
├─────────────────┼─────────────────┼─────────────────────────┤
│ 存放自己发布的包 │ 代理公网仓库 │ 把多个仓库合并成一个入口 │
│ │ 缓存已下载的包 │ │
│ 例:@mycompany/ │ 例:代理 npmjs │ 例:hosted + proxy │
│ 内部包 │ 加速访问 │ 统一入口 │
└─────────────────┴─────────────────┴─────────────────────────┘
我们的配置方案
对于 NPM 私有化发包,我们用的是 Hosted 托管仓库:
- 仓库地址:
https://nexus.example.com/repository/npm-private/ - 作用域:
@mycompany/* - 用途:存放公司内部的 CLI 包
为什么选 Nexus 而不是其他方案(比如 Verdaccio、cnpm):
| 方案 | 优点 | 缺点 |
|---|---|---|
| Nexus | 多格式支持、企业级权限、运维成熟 | 相对重量级 |
| Verdaccio | 轻量、专注 npm | 只支持 npm,企业功能少 |
| cnpm | 国内访问快 | 主要是代理,私有包支持弱 |
公司本来就有 Nexus 管 Maven 包,直接加个 npm 仓库就行,运维不用多学一套系统。
.npmrc:告诉 npm 去哪拉包
.npmrc 是 npm 的配置文件,决定了包从哪里下载、发布到哪里。
配置长这样:
ini
# 作用域路由:@mycompany 开头的包,走私有仓库
@mycompany:registry=https://nexus.example.com/repository/npm-private/
# 认证信息:Base64 编码的 用户名:密码
//nexus.example.com/repository/npm-private/:_auth=dXNlcm5hbWU6cGFzc3dvcmQ=
# 每次请求都带认证
//nexus.example.com/repository/npm-private/:always-auth=true
# 其他包走公共源
registry=https://registry.npmjs.org
逐行解释:
第一行:作用域路由
ini
@mycompany:registry=https://nexus.example.com/repository/npm-private/
npm 的 scoped package (作用域包)格式是 @scope/package-name。这行配置的意思是:
npm install @mycompany/cli→ 去 Nexus 私有仓库拉npm install react→ 走默认源(npmjs.org)
这样就实现了"私有包走内部、公共包走公网"的路由。
第二行:认证凭证
ini
//nexus.example.com/repository/npm-private/:_auth=dXNlcm5hbWU6cGFzc3dvcmQ=
私有仓库需要认证。_auth 是 Base64 编码的 用户名:密码。
生成方式:
bash
# 假设用户名是 deploy,密码是 mypassword
echo -n "deploy:mypassword" | base64
# 输出:ZGVwbG95Om15cGFzc3dvcmQ=
注意这行的格式://域名/路径/:配置项=值,开头是两个斜杠,不是 https://。
第三行:强制认证
ini
//nexus.example.com/repository/npm-private/:always-auth=true
每次请求都带上认证信息,包括只读操作。有些仓库允许匿名读取,可以不加这行。
第四行:默认仓库
ini
registry=https://registry.npmjs.org
没有匹配到作用域的包,走公共 npm 源。
配置文件的查找顺序
npm 会按以下顺序查找 .npmrc,后找到的会覆盖先找到的:
bash
1. /path/to/project/.npmrc ← 项目级(优先级最高)
2. ~/.npmrc ← 用户级
3. $PREFIX/etc/npmrc ← 全局级
4. /path/to/npm/npmrc ← npm 内置
最佳实践:
- 项目
.npmrc:只放作用域路由,提交到 Git,方便协作 - 用户
~/.npmrc:放认证信息,不提交到 Git
CI/CD 中的自动化发包
手动跑 npm publish 容易出错,我们用 GitHub Actions 实现自动化。
发布流程
关键配置:动态生成 .npmrc
CI 每次是全新环境,需要现场生成认证配置。GitHub Actions 示例:
yaml
- name: 'Setup npm authentication'
run: |
# 动态写入认证信息到 ~/.npmrc
echo "//nexus.example.com/repository/npm-private/:_password=$(echo -n '${{ secrets.NEXUS_PASSWORD }}' | base64)" >> ~/.npmrc
echo "//nexus.example.com/repository/npm-private/:username=${{ secrets.NEXUS_USERNAME }}" >> ~/.npmrc
echo "//nexus.example.com/repository/npm-private/:email=ci@example.com" >> ~/.npmrc
echo "//nexus.example.com/repository/npm-private/:always-auth=true" >> ~/.npmrc
echo "@mycompany:registry=https://nexus.example.com/repository/npm-private/" >> ~/.npmrc
- name: 'Publish to npm'
run: |
npm publish --tag "${NPM_TAG}" --registry https://nexus.example.com/repository/npm-private/
几个要点:
- 敏感信息用 GitHub Secrets ------ 用户名密码存在 Secrets,不硬编码到 yaml
- 动态写入 ~/.npmrc ------ 每次 CI 是干净环境,需要现场生成
- 显式指定 --registry ------ publish 命令再指定一次,双重保险
- 使用 dist-tag ------
--tag nightly/--tag latest区分发布渠道
dist-tag 的作用
npm 的 dist-tag 可以理解为"版本别名":
bash
npm publish --tag nightly # 发布到 nightly 标签
npm publish --tag latest # 发布到 latest 标签(默认)
# 用户安装时
npm install @mycompany/cli@nightly # 装 nightly 版
npm install @mycompany/cli # 装 latest 版(默认)
我们用 nightly 做每日构建,latest 做正式发布,preview 做预览版。
踩过的坑
坑一:_auth vs _password + username
Nexus 支持两种认证配置方式:
ini
# 方式 1:_auth(Base64 编码的 "用户名:密码")
//nexus.example.com/:_auth=ZGVwbG95Om15cGFzc3dvcmQ=
# 方式 2:分开配置
//nexus.example.com/:username=deploy
//nexus.example.com/:_password=bXlwYXNzd29yZA== # Base64 编码的密码(不是用户名:密码)
两种都能用,但 不能混用 。我们最开始同时配了 _auth 和 username,结果认证失败,查了半天。
建议统一用方式 2(分开配置),因为 CI 里动态生成更方便。
坑二:scope 大小写敏感
npm 的作用域是 大小写敏感 的:
json
// package.json
{ "name": "@MyCompany/cli" } // 大写 M
ini
# .npmrc - 这样配会找不到
@mycompany:registry=... # 小写 m,不匹配!
package.json 里写的是什么,.npmrc 里就得一模一样。
坑三:代理仓库的缓存问题
如果 Nexus 配了 Proxy 仓库代理 npmjs.org,要注意缓存:
css
用户请求 lodash@4.17.21
↓
Nexus Proxy 仓库
↓ (第一次)从 npmjs.org 拉取并缓存
↓ (之后)直接返回缓存
用户拿到包
问题是:如果 npmjs.org 上的包更新了(比如安全补丁),Nexus 可能还返回旧的缓存版本。
解决方案:
- 在 Nexus 配置里设置缓存过期时间(比如 24 小时)
- 或者手动在 Nexus 管理界面清理特定包的缓存
坑四:离线环境的公共依赖
纯内网环境下,不只私有包要考虑,公共依赖(react、lodash 等)也拉不了。
两个方案:
方案 1:提前打包所有依赖
bash
# 在有网环境
npm ci
tar -czf node_modules.tar.gz node_modules
# 部署到内网后
tar -xzf node_modules.tar.gz
简单粗暴,但 node_modules 体积大,不优雅。
方案 2:Nexus Proxy + 预热
- 在有网环境部署 Nexus,配置 Proxy 仓库代理 npmjs.org
- 跑一遍
npm install,让 Nexus 缓存所有依赖 - 把 Nexus 整个迁移到内网(或者同步缓存数据)
我们用的是方案 2,Nexus 的 blob store 可以直接复制迁移。
本地开发 vs CI 发布的配置分离
日常开发和 CI 发布对 .npmrc 的需求不太一样:
| 场景 | 需要认证 | 配置位置 |
|---|---|---|
| 本地开发(只拉包) | 看仓库配置 | 项目 .npmrc |
| CI 发布 | 必须 | 动态写入 ~/.npmrc |
| 本地发布(调试用) | 必须 | ~/.npmrc |
建议的做法:
ruby
项目根目录/.npmrc ← 只配作用域路由,提交到 Git
@mycompany:registry=https://nexus.example.com/repository/npm-private/
registry=https://registry.npmjs.org
~/.npmrc ← 配认证信息,不提交
//nexus.example.com/repository/npm-private/:_auth=xxx
//nexus.example.com/repository/npm-private/:always-auth=true
这样既方便团队协作(clone 下来就能拉包),又不泄露凭证。
总结
私有化部署 NPM 包,核心就三件事:
- 搭个私有仓库 ------ Nexus 是成熟方案,支持 npm/maven/docker 等多种格式,企业一般已有现成的
- 配好 .npmrc ------ 作用域路由实现公私分流,认证信息单独管理
- CI 自动化 ------ Secrets 管理凭证,动态生成配置,dist-tag 区分发布渠道
配置不复杂,但细节容易出错。希望这篇文章能帮你少踩几个坑。
如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:
Claude Code Skills (按需加载,意图自动识别,不浪费 token,介绍文章):
- code-review-skill - 代码审查技能,覆盖 React 19、Vue 3、TypeScript、Rust 等约 9000 行规则(详细介绍)
- 5-whys-skill - 5 Whys 根因分析,说"找根因"自动激活
- first-principles-skill - 第一性原理思考,适合架构设计和技术选型
vibe coding 原理学习:
- qwen-cli 学习网站 - 学习 qwen-cli 时整理的笔记,40+ 交互式动画演示 AI CLI 内部机制
全栈项目(适合学习现代技术栈):
- prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑
- chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB