從模組化到NPM私有倉庫搭建

wangzy2019發表於2019-03-17

不知從什麼時候開始,網上非常流行面試類的技術文章,講述某次失敗或者成功的面試過程以及面試中被問到的題目,這些文章中的題目大部分都是鬆散零碎,毫無關聯的。可能這些文章會幫助你瞭解到你不曾掌握的點,但僅僅就是了解,真的掌握僅僅只靠面試題是不夠,就好比平時學習不努力,靠考前做幾套名校試卷,或者模擬套題是不夠的。
雖然自己也未能免俗,收藏了一堆面試文章,但是還是更願意看一些更有技術針對性的文章,甚至能自己寫一篇。這是第一次寫文,緣由是工作中的一個需求,多個專案需要共用一些元件,那麼比較方便的做法就是將這些元件封成一個包,釋出到 NPM 上。但是由於這些元件是公司自用,和公司業務緊密關聯,不便於釋出成公共包,雖然 NPM 現在也提供了私有包服務,但是由於某些不可抗拒的網路因素,即使付費可能也享受不到好的服務,所以考慮內部搭建一個 NPM 私有倉庫。
這篇文章就是這次搭建私有倉庫涉及到的相關知識,總結之後發現是一個比較完整知識鏈,所以分享給大家,水平不高,能力有限希望大家多擔待,本文參考了一些內容,也儘量保證都是自己親自驗證過的,如果有錯誤或疏漏歡迎大家指正。

1.從前端模組化說起

當我們現在用 JavaScript 大型單頁應用以及服務端程式時,誰又能想到 JavaScript 這門語言在誕生之初目的只是為了替服務端完成一些輸入驗證操作。功能的複雜意味著程式碼量的提升,而模組化正是為了解決因此帶來的維護困難,結構混亂,程式碼重複冗餘等問題。很不幸的是在 ES6 之前,JavaScript 並不天然支援模組化程式設計。
不過雖然 JavaScript 不支援模組化程式設計,但是我們可以通過物件,名稱空間,立即執行函式等方法實現"模組"的效果,具體的一些方法可以參考阮一峰的:Javascript 模組化程式設計(一):模組的寫法

CommonJs 規範

這種情況直到 nodejs 出現,並參照 CommonJS 實現了模組功能才得到改善。
在 nodejs 中我們可以很方便的匯出和引入模組:

// module.js
const name = 'wang';
const age = 18;
function showName() {
    console.log(wang);
}
function showAge() {
    console.log(age);
}

module.exports = {
    showName,
    showAge
};

// page.js
const module = require('./module.js');
module.showName(); // wang
module.showAge(); //18
複製程式碼

AMD 和 CMD 規範

但是 CommonJS 規範不適用於瀏覽器環境,不說瀏覽器沒有 require 方法,服務端檔案 require 一個包只是讀取本地的一個檔案,而客戶端則是網路載入,網路載入速度和硬碟讀寫速度差距可不是一星半點,這種寫法總不能讓客戶端 require 時假死在那裡,前端處理這種問題的方法首先想到應該就是通過非同步載入回撥來處理。
為了讓瀏覽器支援模組化開發,於是出現了基於AMD規範的RequireJS和基於CMD規範的SeaJS
它們的區別主要是(參考知乎上 SeaJs 開發者玉伯的回答):

  1. 對於依賴的模組,AMD 是提前執行,CMD 是延遲執行。不過 RequireJS 從 2.0 開始,也改成可以延遲執行(根據寫法不同,處理方式不同)。CMD 推崇 as lazy as possible。
  2. CMD 推崇依賴就近,AMD 推崇依賴前置

寫法如下:

// CMD
define(function(require, exports, module) {
    var a = require('./a');
    a.doSomething();
    var b = require('./b'); // 依賴可以就近書寫
    b.doSomething();
});

// AMD 預設推薦的是
define(['./a', './b'], function(a, b) {
    // 依賴必須一開始就寫好
    a.doSomething();
    b.doSomething();
});
複製程式碼

ES6 的 modules 規範

JavaScript 多年以後終於在 ES6 時提出了自己的 modules 規範,寫法如下

//module.js
function showName() {
    console.log('wang');
}
function showAge() {
    console.log(18);
}

export { showName, showAge };

//page.js
import { showName, showAge } from 'module.js';

showName(); //wang
showAge(); //18
複製程式碼

在 chrome61 之後可以通過給 script 標籤加 type = 'module' 來使用此功能,程式碼如下:

<script type="module" src="./module.js"></script>
<script type="module">
    import { showName } from './module.js';
    showName(); //wang
</script>
複製程式碼

其中有一些要注意的點是,不論是被引入方還是被引入方都要設定 type='module',而且對路徑也有一些要求,具體可以參考這篇文章瀏覽器中的 ES6 module 實現

而在 node 中要使用 es module 要配合命令列引數--experimental-modules 和 mjs 檔案字尾名。這個具體可以參考 nodejs 官方的相關文件
由於是官方規範,所以普及的非常快,除了直接在 node 中使用不太方便,現在前端開發基本都參照此寫法風格,事實說明一個道理,官方發力,碾壓一切。

webpack

有人會問 webpack 和它們是什麼關係,這裡總結一下。 CommonJS,AMD,CMD,ES Modules 都是規範,RequireJs,SeaJS 是分別基於 AMD 和 CMD 的前端模組化具體實現,是一種線上模組編譯方案,引入這兩個庫後,就可以按照規範進行模組化開發。而 webpack 是一個打包工具,它是一種預編譯模組方案,不管是上面哪種規範它都能夠識別,並編譯打包成瀏覽器認識的 js 檔案,以實現模組化開。可能還有人聽過 UMD,UMD 是 AMD 和 CommonJS 的糅合,解決跨平臺的問題,具體就是它會判斷是否支援 Node.js 的模組(exports 是否存在),存在則使用 Node.js 模組模式。 再判斷是否支援 AMD(define 是否存在),存在則使用 AMD 方式載入模組,這種規範 webpack 也是能夠識別的。

2.NPM

以前我們想要引入一個第三方包,一般是要將包檔案下載下來放入到我們的專案中,然後在 html 中通過 script 標籤引入,或者這個包有 CDN 服務,那麼可以直接在 script 中引入這個包的 CDN 網路地址。這個過程是繁瑣且低效的,那麼有沒有什麼工具能夠讓我們方便的引入第三方包,那就是 npm。
npm 可以理解為一個包的倉庫,市場,人們可以將自己的程式碼在 npm 上釋出,讓別人可以下載分享,npm 本來是作為 nodejs 的包管理工具隨同 nodejs 一起安裝的,現在基本已經成為了整個前端標配的包管理工具,通過 npm 我們可以很方便的引入第三方包。因為很常用,就不多說了。

npm cnpm yarn

npm 是我們最常用的包管理工具,但是在早期版本中存在一些缺陷:

  1. 安裝策略不是扁平化的,node_modules 中各自的依賴放到各自的資料夾下,導致目錄巢狀層級過深,且會出現重複安裝依賴。
  2. 模組例項無法共享(跟第一條有關)
  3. 安裝速度慢(跟第一條也有關)
  4. 依賴版本不明確(早期 npm 中是沒有 package-lock 檔案的)

目錄結構大概是這個樣子:

├── node_modules
│ └── moduleA
│  └── node_modules
│    └──moduleC
│ └── moduleB
│  └── node_modules
│    └──moduleC
└── package.json
複製程式碼

cnpm
是淘寶 NPM 映象,官方的說法是

這是一個完整 npmjs.org 映象,你可以用此代替官方版本(只讀),同步頻率目前為 10 分鐘 一次以保證儘量與官方服務同步。

它的出現解決了前三條問題,將所有的依賴置於 node_modules 下層,並新增軟連結(快捷方式)。這也就是為什麼通過 cnpm 安裝你會在 node_modules 下發現很多資料夾快捷方式。而且由於 cnpm 的伺服器是在國內,所以安裝速度非常快,但是依然沒有解決第四條問題。 目錄結構大概是這個樣子:

├── node_modules
│ ├── _moduleA@1.0.0
│ │ └── node_modules
│ │   └──moduleC
│ ├── _moduleB@1.0.0
│ │ └── node_modules
│ │   └──moduleC
│ │── _moduleC@1.0.0
│ │── moduleA  //軟連結(快捷方式)moduleA@1.0.0
│ │── moduleB  //軟連結(快捷方式)moduleB@1.0.0
│ └── moduleC  //軟連線(快捷方式)moduleC@1.0.0
└── package.json
複製程式碼

yarn 是一個非常牛逼的專案,曾經有一段時間它將 npm 按在地上摩擦,作為一個可替代 npm 的包管理器,它解決了 npm 的大部分痛點,又加入了一些自己的功能。

  1. 扁平化安裝策略,將所有依賴安裝在 node_modules 下層
  2. 並行下載,支援離線(這是速度快的重要原因)
  3. yarn run 可以查詢 node_modules/.bin 下的可執行命令
  4. 通過 yarn.lock 檔案明確依賴
  5. 命令簡單,輸出簡潔

這些優點讓許多人紛紛投向 yarn 的懷抱(包括我),但是還是那句話官方發力,碾壓一切。
npm3 之後:

  • 採用扁平化安裝策略

npm5 之後:

  • 加入 package-lock 檔案
  • 優化命令(npm i 安裝一個包時不再需要--save 或者-S)
  • 加入臨時安裝命令 npx
  • 加入離線和快取
  • 安裝速度也大幅提升

總之現在沒有什麼太多的理由讓我們還能捨棄官方的包管理器而選擇第三方。
(此節參考了文章為什麼我從 npm 到 yarn 再到 npm?

package.json 及版本號

現代前端專案的根目錄下面一般都會有一個 package.json 檔案,它是在初始化專案的時候,通過 npm init 命令自動生成的,包含了這個專案所需的依賴和配置資訊。

//package.json
{
    "name": "demo",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
        "npmv-test": "^2.1.0"
    }
}
複製程式碼

如上面檔案展示,專案名稱,作者,描述等不細說,主要來說一下 dependencies,dependencies 是專案依賴(還有 devDependencies 等,不展開細說),可以看到這個專案依賴了一個名為 npmv-test 的包,後面的^2.1.0,是描述版本範圍,下面是關於這個版本號的相關的總結。

  • 版本號一般格式為 x.y.z,解釋為主版本.次要版本.補丁版本,一般更新原則為:
    1. 如果只是修復 bug,需要更新 z 位。
    2. 如果是新增了功能,但是向下相容,需要更新 y 位。
    3. 如果有大變動,向下不相容,需要更新 x 位。
  • 如果省略 y 和 z 則相當於補 0,如 2 相當於 2.0.0,2.1 相當於 2.1.0。
  • 版本號前沒有描述符號表示按照指定版本安裝,意味著寫死了版本號,例如 2.0.0,可安裝版本為 2.0.0。
  • 版本號為一個*表示可安裝任意版本,一般為最新版本。
  • 版本號前面有~:
    • 當有次要版本號時,固定次要版號進行升級。例如~2.1.3,可裝版本為 2.1.z(z>=3)。
    • 如果沒有次要版本號,則固定主版本號進行升級。例如~1,可安裝版本為 1.y.z(y>0,z>0),和^1 行為一致。
  • 版本號前面有^:
    • 固定第一個非 0 版本號升級,例如^2.1.3,則可裝版本為 2.y.z(y.z>=1.3)。^0.1.3,則可裝版本為 0.1.z(z>=3)。^0.0.3,則可裝版本為 0.0.3。
  • 在用 npm i 安裝一個包時,package.json 中版本的描述符號預設設定為^(即使你指定版本號安裝,此處依然會設定為^,而並非有些人認為的前面會不加描述符寫死版本,除非是接下來說道這種情況),但如果包的主版本和次要版本都為 0,如 0.0.3,這表示這個包處在不穩定開發階段,會省略掉^,避免更新。

package-lock.json

因為 package.json 中描述依賴包的版本都是範圍,這就造成了一些不確定性,無法確保每次安裝依賴的版本都一致,也無法在出問題時確定依賴包的版本,而 package-lock.json 就是的出現就是為了解決這個問題。這個檔案詳細的描述了依賴關係和依賴版本,可以說是 node_modules 資料夾結構和資訊的一個快照,每次 node_modules 的變動都會導致 package-lock.json 的更新。
這是上面 package.json 對應的 package-lock.json 檔案,我們可以看下區別:

{
    "name": "demo",
    "version": "1.0.0",
    "lockfileVersion": 1,
    "requires": true,
    "dependencies": {
        "ms": {
            "version": "2.1.1",
            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
            "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
        },
        "npmv-test": {
            "version": "2.1.0",
            "resolved": "https://registry.npmjs.org/npmv-test/-/npmv-test-2.1.0.tgz",
            "integrity": "sha512-tNUwr+sdUek+lyJFmGT2H6Jox50NwA5EmNKAZTL3N5fYU1W7Aucfw+rNVsDinnQnhOF1hNvdU5RCUOvgcRWzng==",
            "requires": {
                "ms": "^2.1.1"
            }
        }
    }
}
複製程式碼

這裡有個問題就是,由於 package-lock.json 頻繁變動,有些人會將 package-lock.json 檔案排除在原始碼倉庫的追蹤之外。根據官方文件說法,是建議將該檔案提交到原始碼倉庫的,一個是為了專案每次安裝的依賴版本一致,二是由於我們一般將 node_modules 排除在倉庫之外,所以我們需要在出了問題時能夠還原當時的 node_modules 情況。

npm i 和 npm update

眾所周知 npm i 加包名是安裝一個包並新增到 package.json 的依賴中,如果 npm i 不加包名,則是安裝 package.json 依賴中的所有包,而 npm update 對應的則是更新。但是有的時候可能會有一些疑惑,在執行 npm i 命令時好像也更新了包,有的時候 package.json 中的版本明明偏低但是執行 npm update 卻沒有更新,這些問題希望通過下面兩張圖可以幫助大家找打答案,這個兩張圖是隻是為了幫助加理解,真實的執行過程順序並不一定一致,有興趣的朋友可以去看一下原始碼的實現。
npm i

從模組化到NPM私有倉庫搭建

npm update

從模組化到NPM私有倉庫搭建

3.私有倉庫的搭建

多個專案中重複使用的相同程式碼封裝打包成為一個通用元件庫,既避免了重複造輪子,也利於後期維護和管理,那麼這個東西要怎麼實現有這麼一些方法。 首先既然是庫,肯定是要將這些元件單獨拎出來放到一個原始碼倉庫裡維護,如果你將這些元件直接打包釋出到 npm 上,那麼這就是一個 npm 公共包,誰都可以下載使用。但是如果不想這樣,那麼有下面這些方法(原始碼倉庫為 git):

  1. 通過 git 子模組實現,直接將這個原始碼倉庫作為子模組引入到你的專案,缺點很明顯,子模組也是一個 git 專案,要手動更新,可修改上傳,相當於在一個 git 專案裡面又維護了一個 git 專案。
  2. 通過 npm + git 實現,npm 是支援直接安裝 git 資源的,優點是簡單方便,缺點是要用 tag 來控制版本,而且如果是私有 git 倉庫,要確保有訪問許可權,方法就是配置公鑰或者直接使用帶使用者名稱和密碼的倉庫地址。
  3. 通過搭建私有倉庫實現。

這次我們選擇的方案就是通過搭建私有倉庫來實現,NPM 私有倉庫的工作原理,大概就是將 NPM 命令註冊地址指向我們的私有倉庫,當我們通過 npm 命令進行安裝時,如果在私有倉庫中存在則從私有倉庫中獲取,如果私有倉庫中沒有則去配置的 CNPM 或者 NPM 官方倉庫中獲取。
目前市面上比較常見的私有倉庫搭建方法為:

  • 通過 Sinopia 或 verdaccio 搭建(Sinopia 已經停止維護,verdaccio 是 Fork 自 Sinopia,基本上大同小異),其優點是搭建簡單,不需要其他服務。
  • 通過 cnpm 搭建,需要資料庫服務,後期也支援了 redis 快取(當 redis 設定了密碼,訪問好像有些問題),目前用的人最多,cnpm 推薦的是用 docker 作為容器。
  • 通過 cpm 搭建,應該是參考了 cnpm 的一些東西,和 cnpm 一樣需要資料庫服務和支援 Redis,頁面比較清新,配置更簡單一些,通過 PM2 程式守護。

它們具體的搭建方法都有相應的文件,上面的連結就指向文件地址,這裡就不細說了,這三種方法我都跑過都是可行的,最後選擇了 cpm,而且由於目前 cpm 用到人還不多,可以和開發者快速交流及時反饋問題,這裡就打個廣告,推薦一下專案地址

最後

這篇文章的目的不是教大家怎麼搭建一個私有倉庫(這個是文件乾的事),而是通過搭建一個私有倉庫引出相關的內容並串聯起來幫助整體理解,能展開的儘量展開,該點到為止的就點到為止。第一次寫文章,發現比想象中的要累,但卻很有成就感,歡迎大家多多批評指正。也感謝各個社群分享知識的作者,希望能向他們學習,分享更多的東西和大家討論學習。
(沒有公眾號二維碼,也沒有github要大家點贊,都散了吧)。

相關文章