从零搭建私有 npm 仓库:一次完整的实战学习笔记

这不是一篇简单的操作教程,而是一份真实的对话记录与思考复盘。通过记录我在部署过程中遇到的每一个疑问,以及如何一步步找到答案,帮助未来的自己真正理解每个环节

第一部分:为什么需要私有 npm 仓库?

我的疑问: 为什么要自己搭一个 npm 仓库?直接用 npm 官方源不就行了吗?

答案:

  • 公司内部有多个项目需要共享组件时,不可能把代码发到公共 npm

  • 私有组件需要版本管理,和公共包一样方便

  • 团队协作时,npm install 就能自动安装私有包

理解: 私有仓库本质是一个"缓存 + 权限管理"的服务。它既存储我们自己发布的包,也可以代理公共包,加速下载。

第二部分:部署 Verdaccio

2.1 为什么要用 Docker 部署?

我的疑问: 直接 npm install verdaccio 不行吗?为什么要用 Docker?

答案:

  • Docker 保证环境一致性,不管在什么系统上运行结果都一样

  • 数据持久化方便,容器删了数据还在

  • 管理和升级更简单,一条命令就能启动/停止

理解: Docker 就像给应用装了一个"盒子",里面自带了运行所需的一切环境。服务器上只需要有 Docker,就能跑任何应用。

2.2 挂载目录是什么意思?服务器目录和容器目录有什么区别?

我的疑问:

复制代码
docker run -v /opt/verdaccio/storage:/verdaccio/storage

这行命令里,/opt/verdaccio/storage/verdaccio/storage 分别代表什么?为什么要有两个目录?

答案:

  • /verdaccio/storage容器内部的目录,由 Verdaccio 程序自己决定,不能改

  • /opt/verdaccio/storage服务器上的目录,可以随便起名字

  • 挂载就是把服务器的目录"映射"到容器内部,这样容器写数据时,实际上写到了服务器上

理解:

复制代码
服务器真实目录 ←→ 容器虚拟目录
/opt/verdaccio/storage  ←→  /verdaccio/storage

容器以为自己在操作 /verdaccio/storage,实际数据在 /opt/verdaccio/storage。这样即使删除容器,数据还在服务器上。

2.3 plugins 路径是什么?要不要配置?

我的疑问:

复制代码
plugins: /verdaccio/plugins

这个配置是什么意思?我需不需要?

答案:

  • 这是告诉 Verdaccio 去哪里找插件(如 GitLab 登录、S3 存储等)

  • 如果不需要扩展功能,可以注释掉或删除

  • 我目前不需要,所以没配置

理解: 配置文件要"按需配置",不需要的功能就不要写,保持简洁。

2.4 为什么添加用户时报 500 错误?

我的疑问:

复制代码
npm error 500 Internal Server Error

明明部署成功了,为什么不能注册用户?

答案: 目录权限问题。Verdaccio 容器以用户 10001 运行,但服务器上的目录所有者是 root,容器没有写入权限。

解决方案:

复制代码
chown -R 10001:65533 /opt/verdaccio
docker restart verdaccio

理解: 容器内的用户(10001)和服务器上的用户(root)是隔离的。容器要写入服务器目录,必须确保该目录对容器用户有写入权限。

第三部分:创建 Monorepo 项目

3.1 为什么要用 Monorepo?

我的疑问: 为什么要把组件和工具函数放在同一个仓库?分开不行吗?

答案:

  • 组件库和工具函数版本需要同步更新时,放在一起更方便

  • 共享依赖,节省磁盘空间

  • 统一的构建和发布流程

理解: Monorepo 就像一个大文件夹,里面放了多个相关的小项目。它们可以独立版本管理,但又可以方便地互相引用。

3.2 pnpm-workspace.yaml 中的 packages 是什么意思?

我的疑问:

yaml

复制代码
packages:
  - "packages/*"
  - "playground"

packages/* 代表什么?为什么不能写成 packages

答案:

  • packages/* 匹配 packages 下的直接子目录(如 components、utils)

  • 如果写成 packages,只匹配 packages 目录本身,不匹配子目录

  • 通配符 * 很重要,决定了哪些目录被识别为独立包

理解:

  • packages/* → 匹配 packages/components、packages/utils

  • packages → 只匹配 packages(如果 packages 本身是一个包)

  • packages/** → 匹配所有子目录(包括嵌套的)

3.3 playground 也在 workspace 里,会不会被构建?

我的疑问: 执行 pnpm -r run build 时,playground 也会被构建吗?它只是一个调试环境,不应该被发布。

答案: 会!因为 playground 在 workspace 中,且有 build 脚本,所以会被执行。

解决方案:

复制代码
packages:
  - "packages/*"
  # - "playground"  # 注释掉,不加入 workspace

或者使用 --filter 过滤:

json

复制代码
{
  "scripts": {
    "build": "pnpm --filter \"./packages/*\" run build"
  }
}

理解: workspace 是"工作区"概念,所有在里面的包都会被 pnpm 管理。如果某个目录不需要被构建发布,就不要放进 workspace。


第四部分:开发组件

4.1 组件命名:Button 还是 MyButton?

我的疑问:

复制代码
app.component("MyButton", Button);

为什么要有两个名字?一个 Button,一个 MyButton?

答案:

  • ButtonJavaScript 导入名import { Button } from '...'

  • MyButtonVue 模板标签名<MyButton />

理解: 两个名字服务于不同场景:

  • 按需导入时用 Button

  • 全局注册后在模板中用 MyButton

  • 这样可以避免与原生 HTML 标签 <button> 冲突

4.2 默认导出和命名导出有什么区别?

我的疑问:

复制代码
export default Button   // 默认导出
export { Button }       // 命名导出

这两个可以共存吗?能不能去掉一个?

答案: 可以共存,服务于不同使用场景:

  • 默认导出import Button from './Button'(简洁)

  • 命名导出import { Button } from './index'(按需导入)

理解:

复制代码
默认导出 → 导入时可以任意取名
命名导出 → 导入时必须用原名字(或重命名)

同时保留两种导出,让使用者更灵活。

4.3 样式文件:为什么一定要导入?

我的疑问: 组件里明明写了 <style scoped>,为什么使用时还要 import '@locfly/vue-components/style.css'

答案: 因为构建工具会把所有样式提取到单独的 CSS 文件,而不是内嵌在 JS 里。

理解

复制代码
开发时:<style scoped> 写在 .vue 文件里
构建后:样式被抽离到 style.css
使用时:必须导入这个 CSS 文件,样式才能生效

无论是否使用 scoped,样式都会被提取。这是组件库的标准设计。


第五部分:打包和发布

5.1 package.json 的 main 字段为什么要指向 dist?

我的疑问:

复制代码
"main": "./dist/index.js"

为什么不能指向源码 ./src/index.ts

答案:

  • 用户安装后运行的是 JS 代码,不是 TypeScript

  • .ts 文件在 node_modules 里无法直接执行

  • 必须指向构建后的 JS 文件

理解:

复制代码
源码 (.vue, .ts) → 构建 → 产物 (.js, .d.ts, .css)
                                      ↓
                                 用户安装使用

用户不需要看到你的源码,只需要构建好的产物。

5.2 files 字段有什么作用?

我的疑问:

json

复制代码
"files": ["dist", "README.md"]

答案: 告诉 npm 发布时只上传这些文件,其他文件(源码、测试、配置文件)都不会上传。

理解: 这样包体积更小,用户下载更快,也不会暴露源码。

5.3 publishConfig 是做什么的?

我的疑问:

复制代码
"publishConfig": {
  "registry": "http://47.108.252.189:4873/"
}

答案: 指定发布到哪个仓库。即使全局 registry 是淘宝镜像,发布时也会用这个地址。

理解:

复制代码
npm config get registry  →  https://registry.npmmirror.com  (下载用)
publishConfig.registry   →  http://47.108.252.189:4873     (发布用)

下载和发布可以指向不同的源,互不影响。

5.4 发布时怎么知道是发到私有仓库?

我的疑问: 执行 npm publish 时,系统怎么知道要发到私有仓库而不是公共 npm?

答案: 优先级顺序:

  1. package.json 中的 publishConfig.registry(最高)

  2. 命令行 --registry 参数

  3. 项目级 .npmrc

  4. 用户级 .npmrc

  5. npm 全局配置

  6. 默认源

理解: 我在 package.json 里配置了 publishConfig.registry,所以无论在哪里执行 npm publish,都会发到私有仓库。

5.5 当用这个"publish:all": "pnpm run build && pnpm -r publish --access public",命令来发布的时候,打包是playground一起打包了,但是为什么没传到私有仓库,只是传了component以及utils

核心原因:package.json中的private: true 阻止了发布


第六部分:使用私有包

6.1 .npmrc 配置了私有源,会不会所有包都从私有仓库下?

我的疑问:

ini

复制代码
registry=http://47.108.252.189:4873/

这样配置后,安装 vue 也会去私有仓库找吗?

答案: 会!但你的 Verdaccio 配置了代理(uplinks),找不到的包会自动去淘宝镜像下载。

理解: 私有仓库像一个"中间人":

  1. 请求先到私有仓库

  2. 私有仓库检查本地有没有

  3. 有则返回,没有则去代理源下载并缓存

6.2 怎么控制部分包从私有仓库下载,部分从公共源?

我的疑问: 我不想所有包都经过私有仓库,能不能分开?

答案: 使用 scope 配置:

ini

复制代码
# 只有 @locfly 开头的包走私有仓库
@locfly:registry=http://47.108.252.189:4873/

# 其他包走淘宝镜像
registry=https://registry.npmmirror.com/

理解:

  • @locfly/vue-components → 从私有仓库下载

  • vue → 从淘宝镜像下载

  • 完美分离!

6.3 为什么安装私有包后,组件没有样式?

我的疑问: 组件能显示,但样式全没了。

答案: 没有导入 CSS 文件!

复制代码
// 只导入了组件
import { Button } from '@locfly/vue-components'

// 缺少样式导入 ❌

解决方案:

typescript

复制代码
import { Button } from '@locfly/vue-components'
import '@locfly/vue-components/style.css'  // ✅ 必须导入

理解: 组件库的样式是独立的,不会自动注入。这是为了支持按需加载和 tree-shaking。


第七部分:整个流程的心智模型

经过这次实践,我建立了以下心智模型:

7.1 部署阶

复制代码
服务器上运行 Verdaccio 容器
    ↓
容器读写 /verdaccio/storage
    ↓
实际映射到 /opt/verdaccio/storage(数据持久化)
    ↓
对外暴露 4873 端口

7.2 开发阶段

复制代码
编写组件(.vue)和工具函数(.ts)
    ↓
playground 通过 alias 直接引用源码(热更新)
    ↓
开发完成后执行 build
    ↓
生成 dist/index.js + dist/style.css + dist/*.d.ts

7.3 发布阶段

复制代码
执行 npm publish
    ↓
读取 publishConfig.registry(私有仓库)
    ↓
读取 files 字段(只打包 dist)
    ↓
执行 prepublishOnly(自动构建)
    ↓
上传到私有仓库

7.4 使用阶段

复制代码
配置 .npmrc(@locfly:registry)
    ↓
npm install @locfly/vue-components
    ↓
从私有仓库下载 dist 文件
    ↓
导入组件 + 导入样式
    ↓
在模板中使用 <MyButton />

第八部分:问题速查表

问题 原因 解决方案
添加用户 500 错误 目录权限不足 chown -R 10001:65533 /opt/verdaccio
发布 409 Conflict 用户已存在未登录 npm login
样式不生效 未导入 CSS import '@locfly/vue-components/style.css'
找不到模块 .npmrc 配置错误 检查 @locfly:registry 配置
发布到公共仓库 缺少 publishConfig 添加 publishConfig.registry
defineProps 警告 不需要导入 删除 import { defineProps }

结语

这次学习最大的收获不是"学会了部署",而是理解了每一个命令背后的原理

  • 知道了 Docker 挂载是为了数据持久化

  • 知道了 pnpm workspace 是为了管理多包

  • 知道了 exports 字段是为了控制包的导出方式

  • 知道了 publishConfig 是为了分离下载和发布的源

  • 知道了样式必须单独导入是因为构建时被提取了

希望未来的自己再看这篇文章时,能快速回忆起这些知识点。如果还有其他疑问,随时可以继续探索!

本回答由 AI 生成,内容仅供参考,请仔细甄别。

相关推荐
南境十里·墨染春水2 小时前
C++ 笔记 赋值兼容原则(公有继承)(面向对象)
开发语言·c++·笔记
罗罗攀2 小时前
PyTorch学习笔记|单层神经网络
人工智能·pytorch·笔记·神经网络·学习
ACGkaka_2 小时前
ES 学习(五):DSL常用操作整理
大数据·学习·elasticsearch
Hammer_Hans2 小时前
DFT笔记35
笔记
lizhihai_993 小时前
股市学习心得-新手生存法则
学习
ouliten11 小时前
cuda编程笔记(37)--逐行量化的kernel分析
笔记
MimCyan11 小时前
面向开发者的 LLM 入门课程(个人笔记记录-2026.03.30)
笔记·ai
Slow菜鸟11 小时前
AI学习篇(三) | AI效率工具指南(2026年)
人工智能·学习
Hammer_Hans11 小时前
DFT笔记34
笔记