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

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

1.從前端模塊化提及

當咱們如今用 JavaScript 大型單頁應用以及服務端程序時,誰又能想到 JavaScript 這門語言在誕生之初目的只是爲了替服務端完成一些輸入驗證操做。功能的複雜意味着代碼量的提高,而模塊化正是爲了解決所以帶來的維護困難,結構混亂,代碼重複冗餘等問題。很不幸的是在 ES6 以前,JavaScript 並不自然支持模塊化編程。
不過雖然 JavaScript 不支持模塊化編程,可是咱們能夠經過對象,命名空間,當即執行函數等方法實現"模塊"的效果,具體的一些方法能夠參考阮一峯的:Javascript 模塊化編程(一):模塊的寫法html

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 開發者玉伯的回答):java

  1. 對於依賴的模塊,AMD 是提早執行,CMD 是延遲執行。不過 RequireJS 從 2.0 開始,也改爲能夠延遲執行(根據寫法不一樣,處理方式不一樣)。CMD 推崇 as lazy as possible。
  2. CMD 推崇依賴就近,AMD 推崇依賴前置

寫法以下:node

// 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 規範,寫法以下webpack

//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' 來使用此功能,代碼以下:git

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

其中有一些要注意的點是,不管是被引入方仍是被引入方都要設置 type='module',並且對路徑也有一些要求,具體能夠參考這篇文章瀏覽器中的 ES6 module 實現github

而在 node 中要使用 es module 要配合命令行參數--experimental-modules 和 mjs 文件後綴名。這個具體能夠參考 nodejs 官方的相關文檔
因爲是官方規範,因此普及的很是快,除了直接在 node 中使用不太方便,如今前端開發基本都參照此寫法風格,事實說明一個道理,官方發力,碾壓一切。web

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 update

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要你們點贊,都散了吧)。

所有文章列表

相關文章
相關標籤/搜索