
其實(shí),我們每天都在使用 npm 或 yarn 在 https://www.npmjs.com 平臺(tái)下載一些第三方的包,它們都屬于開源的,任何人都可以免費(fèi)下載并使用。
假設(shè)公司或者團(tuán)隊(duì)里面,想把一些可復(fù)用的模塊抽離并形成 NPM 包,因涉及公司業(yè)務(wù)或其他原因,不能發(fā)布到 NPM 平臺(tái)上。這時(shí)候我們可以在公司內(nèi)部建立一個(gè)類似 https://www.npmjs.com 的私有的 NPM 平臺(tái),怎么做呢?
下面,我們從零到一搭建個(gè)人的 NPM 私有服務(wù)器。
一、選擇
選擇 Verdaccio 作為我們私有 NPM 倉庫的平臺(tái),主要原因是免費(fèi)、零配置,開箱即用。
當(dāng)然,公司應(yīng)該選擇其他更加可靠、穩(wěn)定付費(fèi)平臺(tái)了,本文適合個(gè)人玩耍~
二、NPM 平臺(tái)搭建
安裝、啟動(dòng)都非常簡單~
# install
$ npm i -g verdaccio
# run
$ verdaccio
Verdaccio 跑起來之后,可以看到倉庫地址就是:http://localhost:4873/。
這是基于默認(rèn)配置,暫時(shí)不作修改,配置文件在 /Users/frankie/.config/verdaccio/config.yaml。
frankie@MacBook-Pro Verdaccio % ?? verdaccio
warn --- config file - /Users/frankie/.config/verdaccio/config.yaml
warn --- Plugin successfully loaded: verdaccio-htpasswd
warn --- Plugin successfully loaded: verdaccio-audit
warn --- http address - http://localhost:4873/ - verdaccio/5.1.1
http --- 127.0.0.1 requested 'GET /'
http --- 304, user: null(127.0.0.1), req: 'GET /', bytes: 0/0
http --- 127.0.0.1 requested 'GET /-/verdaccio/packages'
http --- 304, user: null(127.0.0.1), req: 'GET /-/verdaccio/packages', bytes: 0/0
目前沒有發(fā)包上來,就長這樣...

我們修改以下 npm 或 yarn 的鏡像源 http://localhost:4873/。
# 添加私有源(我使用了 nrm 來管理 npm 源)
$ nrm add frankie-loc http://localhost:4873/
# 切換源
$ nrm use frankie-loc
# or
# npm set registry http://localhost:4873/
# 注冊用戶,對應(yīng)你 NPM 賬號(hào)密碼(若沒有,用郵箱注冊一個(gè)即可)
$ npm adduser
# 查看當(dāng)前用戶是否是注冊用戶
$ npm who am i
關(guān)于 nrm 安裝使用,看這里。
三、發(fā)包
3.1 創(chuàng)建 NPM 項(xiàng)目
我們來創(chuàng)建一個(gè)最簡單的 NPM 包項(xiàng)目,目錄如下:
privative-npm
├── .gitignore // 相應(yīng)目錄,發(fā)布時(shí)會(huì)忽略上傳
├── .npmignore // 同理,會(huì)忽略上傳
├── index.js // 作為入口
├── package.json // 包描述文件
└── README.md // 項(xiàng)目說明
一個(gè) NPM 包,其中 name 和 version 是必需的,其他都可以省略,而且 name 不能與平臺(tái)上已有包重名。
// package.json
{
"name": "privative-npm", // 必需,不能有大寫字母、空格、下劃線
"version": "1.0.0", // 必需,請嚴(yán)格遵循語義化
"description": "Test only, not published to NPM.",
"author": "Frankie <1426203851@qq.com>",
"license": "MIT",
"type": "module",
"main": "./index.js"
}
由于演示而已,我們就只導(dǎo)出一個(gè)方法吧。注意,若在 node 下運(yùn)行此包,我們使用了 ESM,因此需要在 package.json 設(shè)置 "type": "module"。
// index.js
export default function log(str) {
console.log(str)
}
3.2 發(fā)布 NPM 包
在發(fā)包之前,你需要去 NPM 平臺(tái)官網(wǎng)注冊一個(gè)賬號(hào)。很簡單省略...
完了之后,登錄你的 NPM 賬號(hào):
# add
$ npm set registry http://localhost:4873/
# switch registry
$ npm config set registry http://localhost:4873/
# login npm account
$ npm adduser
# 登錄過用 login,第一次則用 adduser,它包括了登錄操作。
$ npm login
登錄成功,長這樣:
frankie@MacBook-Pro privative-npm % ?? npm login
Username: xxx
Password:
Email: (this IS public) 1426203851@qq.com
Logged in as xxx on http://localhost:4873/.
在跟目錄下,執(zhí)行命令 npm publish 即可?;蚩稍?package.json 腳本命令中定義。(后者更合適)
frankie@MacBook-Pro privative-npm % ?? npm publish
npm notice
npm notice ?? privative-npm@1.0.0
npm notice === Tarball Contents ===
npm notice 54B index.js
npm notice 212B package.json
npm notice 36B README.md
npm notice === Tarball Details ===
npm notice name: privative-npm
npm notice version: 1.0.0
npm notice package size: 416 B
npm notice unpacked size: 302 B
npm notice shasum: 887836aa4a154902faf31b13e60b8adcdd07b924
npm notice integrity: sha512-fyzitNqmif188[...]hFY+zmcIW6qTg==
npm notice total files: 3
npm notice
+ privative-npm@1.0.0
看到已經(jīng)上傳成功了,刷新頁面就能看到:

3.3 更新包
如果我們要更新包,其中版本號(hào) version 一定要修改,否則會(huì)更新失敗,如下:
frankie@MacBook-Pro privative-npm % ?? npm publish
...
npm ERR! code EPUBLISHCONFLICT
npm ERR! publish fail Cannot publish over existing version.
npm ERR! publish fail Update the 'version' field in package.json and try again.
npm ERR! publish fail
npm ERR! publish fail To automatically increment version numbers, see:
npm ERR! publish fail npm help version
npm ERR! A complete log of this run can be found in:
npm ERR! /Users/frankie/.npm/_logs/2021-07-14T06_16_31_553Z-debug.log
這里我就只改個(gè)版本號(hào)吧,更新包的命令仍然是 npm publish,上傳成功后,刷新頁面可以看到新版本了。

另外,假設(shè)你開發(fā)了一個(gè) NPM 包,然后要發(fā)布到 NPM 開源供其他開發(fā)者使用,一定要按照語義化版本規(guī)律進(jìn)行更新。如果像本文在本地或個(gè)人服務(wù)器搭建著玩,就愛咋咋地!
3.4 撤銷包
需要注意的是,在 NPM 平臺(tái)撤銷包,是有非常嚴(yán)格限制的,不是隨意就能撤銷已發(fā)布到平臺(tái)的包的,詳情可看:NPM Unpublish Policy。
# 撤銷包的某個(gè)版本
$ npm unpublish [<@scope>/]<pkg>@<version>
# 撤銷包
$ npm unpublish [<@scope>/]<pkg>
如果你的目的是鼓勵(lì)用戶升級,或者您不想再維護(hù)軟件包,請考慮改用 deprecate 命令。
$ npm deprecate <pkg>[@<version>] <message>
我們在安裝一些依賴包的時(shí)候,不是經(jīng)??吹玫筋愃频臇|西嗎,就是 deprecate 搞的鬼~
npm WARN deprecated core-js@1.2.7: core-js@<3.3 is no longer maintained and not recommended for usage due to the number of issues.
四、使用包
我們將 NPM 發(fā)布到私有或公開的 NPM 平臺(tái)后,都可以通過 npm、yarn 等包管理工具去安裝到我們的項(xiàng)目中。
由于我們上面示例,是將 privative-npm 包發(fā)布到我們私有的 NPM 服務(wù)器下,因此需要將 npm 鏡像源切換至 http://localhost:4873/ 即可:
# use npm
$ npm config set registry http://localhost:4873/
# use yarn
$ yarn config set registry http://localhost:4873/
隨便創(chuàng)建一個(gè)項(xiàng)目,并安裝 privative-npm 依賴包,在 node 環(huán)境運(yùn)行一下 index.js 可以看到打印出 OK!,那說明成功了!

五、其他
假設(shè)我們在使用 http://localhost:4873/ 鏡像源,去安裝 react、vue 等包,它是怎么處理的呢?
首先,我們了解一下正常使用 NPM 安裝、共享、發(fā)包的流程:

當(dāng)我們使用 npm 或 yarn 去安裝一個(gè)模塊(包)時(shí),先檢查 node_modules 目錄是否已經(jīng)緩存了該模塊,如果沒有便會(huì)向 NPM 平臺(tái)查詢。
NPM 提供了一個(gè)模塊信息查詢服務(wù),通過訪問:
registry.npmjs.org/packaename/version
就可以查到某個(gè)發(fā)布在 NPM 平臺(tái)上模塊的具體信息,以及下載地址。然后下載并解壓到本地完成安裝。
如果我們啟用了私有 NPM 服務(wù)器,流程又有什么變化呢?

當(dāng)我們啟動(dòng) Verdaccio 時(shí),可以看到配置文件是在用戶根目錄下的:/Users/frankie/.config/verdaccio/config.yaml。
#
# 配置文件(這里我刪除了一些默認(rèn)注解)
# 更多請看: https://github.com/verdaccio/verdaccio/blob/master/packages/config/src/conf/default.yaml
#
# 上傳的所有包存放目錄
storage: ./storage
# 插件目錄
plugins: ./plugins
# web 服務(wù),即我們可以通過 web 查看我們上傳的包。
web:
title: Verdaccio
# 一些關(guān)于 web 頁面的配置項(xiàng),我刪掉了
# 驗(yàn)證信息
auth:
htpasswd:
# 用戶信息存儲(chǔ)目錄
file: ./htpasswd
# 公有倉庫配置
uplinks:
npmjs:
# 默認(rèn)
# url: https://registry.npmjs.org/
# 我們可以改成淘寶鏡像源
url: https://registry.npm.taobao.org/
packages:
'@*/*':
# scoped packages
access: $all
publish: $authenticated
unpublish: $authenticated
# 代理。當(dāng)我們安裝一些私有服務(wù)器上沒有的包時(shí),它就會(huì)往這里找,即上面的 uplinks 配置
proxy: npmjs
'**':
# 三種角色:所有人、匿名用戶、認(rèn)證(登錄)用戶
# "$all", "$anonymous", "$authenticated"
# 可訪問包角色
access: $all
# 可發(fā)包、撤包角色
publish: $authenticated
unpublish: $authenticated
# if package is not available locally, proxy requests to 'npmjs' registry
proxy: npmjs
# 服務(wù)連接活躍時(shí)間
server:
keepAliveTimeout: 60
middlewares:
audit:
enabled: true