给客户做私有化部署,我是如何优雅搞定 NPM 依赖管理的?

做 CLI 工具的二次开发,最后一步往往是最容易被忽视的:怎么把包发出去?

公司项目基于 qwen-cli 做私有化改造,客户的环境是内网,连不了 npmjs.org。这时候就得自己搭一套私有 NPM 仓库,实现内部发包和分发。

这篇文章聊聊我们在这个过程中踩的坑和最终方案,主要涉及:

  • Nexus 是什么,为什么选它
  • .npmrc 的配置逻辑
  • CI/CD 中的自动化发包
  • 一些容易踩的坑

为什么需要私有 NPM 仓库

先说背景。我们的 CLI 工具是给企业客户用的,有几个硬性要求:

  1. 内网部署 ------ 客户环境连不了公网,npm install 从 npmjs.org 拉包直接超时
  2. 版本可控 ------ 不能让客户随便装个 latest,出问题不好排查
  3. 安全合规 ------ 内部代码不能发到公网仓库

这三个需求一叠加,结论很明确:需要一个私有 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 实现自动化。

发布流程

flowchart LR A[代码合并到 main]:::info --> B[CI 触发] B --> C[跑测试] C --> D{测试通过?} D -->|是| E[动态配置 .npmrc]:::warning D -->|否| F[失败通知]:::error E --> G[npm publish] G --> H[创建 GitHub Release]:::success classDef info fill:#cce5ff,stroke:#0d6efd,color:#004085 classDef success fill:#d4edda,stroke:#28a745,color:#155724 classDef error fill:#f8d7da,stroke:#dc3545,color:#721c24 classDef warning fill:#fff3cd,stroke:#ffc107,color:#856404

关键配置:动态生成 .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/

几个要点:

  1. 敏感信息用 GitHub Secrets ------ 用户名密码存在 Secrets,不硬编码到 yaml
  2. 动态写入 ~/.npmrc ------ 每次 CI 是干净环境,需要现场生成
  3. 显式指定 --registry ------ publish 命令再指定一次,双重保险
  4. 使用 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 编码的密码(不是用户名:密码)

两种都能用,但 不能混用 。我们最开始同时配了 _authusername,结果认证失败,查了半天。

建议统一用方式 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 + 预热

  1. 在有网环境部署 Nexus,配置 Proxy 仓库代理 npmjs.org
  2. 跑一遍 npm install,让 Nexus 缓存所有依赖
  3. 把 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 包,核心就三件事:

  1. 搭个私有仓库 ------ Nexus 是成熟方案,支持 npm/maven/docker 等多种格式,企业一般已有现成的
  2. 配好 .npmrc ------ 作用域路由实现公私分流,认证信息单独管理
  3. CI 自动化 ------ Secrets 管理凭证,动态生成配置,dist-tag 区分发布渠道

配置不复杂,但细节容易出错。希望这篇文章能帮你少踩几个坑。


如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:

Claude Code Skills (按需加载,意图自动识别,不浪费 token,介绍文章):

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
相关推荐
间彧2 小时前
混沌工程在SpringBoot项目中的实践与应用
后端
C_心欲无痕2 小时前
vue3 - markRaw标记为非响应式对象
前端·javascript·vue.js
隔壁阿布都2 小时前
使用LangChain4j +Springboot 实现大模型与向量化数据库协同回答
人工智能·spring boot·后端
qingyun9892 小时前
深度优先遍历:JavaScript递归查找树形数据结构中的节点标签
前端·javascript·数据结构
熬夜敲代码的小N2 小时前
Vue (Official)重磅更新!Vue Language Tools 3.2功能一览!
前端·javascript·vue.js
90后的晨仔2 小时前
用 Python 脚本一键重命名序列帧图片的名称
前端
辰同学ovo2 小时前
Vue 2 路由指南:从入门到实战优化
前端·vue.js
小彭努力中2 小时前
1.在 Vue 3 中使用 Cesium 快速展示三维地球
前端·javascript·vue.js·#地图开发·#cesium·#vue3
上进小菜猪2 小时前
基于 YOLOv8 的智能火灾识别系统设计与实现— 从数据集训练到 PyQt5 可视化部署的完整工程实践
后端