first post on http://blog.xgheaven.com/2018/05/03/npm-to-yarn-to-npm/
從接觸到 node 環境來說,其中一個不可或缺的一部分便是 npm 包管理,但是由於官方的 npm 有各種各樣的問題,於是催生了很多不同的版本,這其中的曲折也許只有過來人才知道。
放棄 npm?
上古時代
在上古版本(應該是 npm3 以前的版本,具體我也記不清了),npm 的安裝策略並不是扁平化的,也就是說比如你安裝一個 express
,那麼你會在 node_modules
下面只找到一個 express
的資料夾。而 express
依賴的專案都放在其資料夾下。
- app/
- package.json
- node_modules/
- express/
- index.js
- package.json
- node_modules/
- ...
複製程式碼
這個帶來的問題或許 windows 使用者深諳其痛,因為在這種安裝環境下,會導致目錄的層級特別高,而對於 windows 來說,最大的路徑長度限制在 248 個字元(更多請見此),再加上 node_modules
這個單詞又特別長,所以你懂得,哈哈哈。解決方案啥的自己去搜尋吧,反正估計現在也沒人會用上古版本了。
除了 windows 使用者出現的問題以外,還有一個更嚴重的問題,就是模組都是獨立的,比如說位於 express
下面的 path-to-regexp
和 connect
下面的 path-to-regexp
的模組是兩個不同的模組。
那麼這個會帶來什麼影響呢?其實在使用上,並沒有什麼太大的影響,但是記憶體佔用過大。因為很多相同模組位於不同模組下面就會導致有多個例項的出現(為什麼會載入多個例項,請檢視 Node 模組載入)。你想想,都是同樣的功能,為什麼要例項這麼多次呢?不能就載入一次,複用例項麼?
上古時代的 npm 的缺點可以說還是很多的:
- 目錄巢狀層級過深
- 模組例項無法共享
- 安裝速度很慢,這其中有目錄巢狀的原因,也有安裝邏輯的問題。因為 npm 是請求完一個模組之後再去請求另一個模組,這就會導致同一個時刻,只有一個模組在下載、解析、安裝。
軟鏈時代
後面,有人為了解決目錄巢狀層次過高的問題,引入的軟連結的方案。
簡單來說,就是將所有的包都扁平化安裝到一個位置,然後通過軟連結(windows 快捷方式)的方式組合到 node_modules
中。
- app/
- node_modules
- .modules/
- express@x.x.x/
- node_modules
- connect -> ../../connect@x.x.x
- path-to-regexp -> ../../path-to-regexp@x.x.x
- ... -> ../../package-name@x.x.x
- connect@x.x.x/
- path-to-regexp@x.x.x/
- ...others
- express -> ./.modules/express@x.x.x
複製程式碼
這樣做的好處就是可以將整體的邏輯層級簡化到很少的幾層。而且對於 node 的模組解析來說,可以很好的解決相同模組不同位置導致的載入多個例項,進而導致記憶體佔用的情況。
基於這種方案,有 npminstall 以及 pnpm 這個包實現了這種方案,其中 cnpm 使用的就是 npminstall,不過他們實現的方式和我上面講的是有差異的,具體請看。簡單來講,他們沒有 .modules
這一層。更多的內容,請看 npminstall 的 README。
總的來講這種解決方案有還有以下幾個好處:
- 相容性很好
- 在保證目錄足夠簡潔的情況下,解決了上面的兩個問題(目錄巢狀和多例項載入)。
- 安裝速度很快,因為採用了軟連線的方式加上多執行緒請求,多個模組同時下載、解析、安裝。
那麼缺點也是挺致命的:
- 一般情況下都是第三方庫實現這個功能,所以無法保證和 npm 完全一致的行為,所以遇到問題只能去找作者提交一下,然後等待修復。
- 無法和 npm 很方便的一起使用。最好是要麼只用 npm,要麼只用 cnpm/pnpm,兩者混用可能會產生很奇葩的效果。
npm3 時代
最大的改變就是將目錄層級從巢狀變到扁平化,可以說很好的解決了上面巢狀層級過深以及例項不共享的問題。但是,npm3 在扁平化方案下,選擇的並不是軟連線的方式,而是說直接將所有模組都安裝到 node_modules
下面。
- app/
- node_modules/
- express/
- connect/
- path-to-regexp/
- ...
複製程式碼
如果出現了不同版本的依賴,比如說 package-a
依賴 package-c@0.x.x
的版本,而 package-b
依賴 package-c@1.x.x
版本,那麼解決方案還是像之前的那種巢狀模式一樣。
- app/
- node_modules/
- package-a/
- package-c/
- // 0.x.x
- package-b/
- node_modules/
- package-c/
- // 1.x.x
複製程式碼
至於那個版本在外面,那個版本在裡面,似乎是根據安裝的先後順序有關的,具體的我就不驗證了。如果有人知道的話,歡迎告訴我。
在這個版本之後,解決了大部分問題,可以說 npm 跨入了一個新的世界。但是還要一個問題就是,他的安裝速度依舊很慢,相比 cnpm 來說。所以他還有很多進步的空間。
yarn 的誕生
隨著 Node 社群的越來越大,也有越來越多的人將 Node 應用到企業級專案。這也讓 npm 暴露出很多問題:
- 無法保證兩次安裝的版本是完全相同的。大家都知道 npm 通過語義化的版本號安裝應用,你可以限制你安裝模組的版本號,但是你無法限制你安裝模組依賴的模組的版本號。即使有 shrinkwrap 的存在,但是很少有人會用。
- 安裝速度慢。上文已經講過,在一些大的專案當中,可能依賴了上千個包,甚至還包括了 C++ Addon,嚴重的話,安裝可能要耗時 10 分鐘甚至到達半個小時。這很明顯是無法忍受的,尤其是配合上 CI/CD。
- 預設情況下,npm 是不支援離線模式的,但是在有些情況下,公司的網路可能不支援連線外網,這個時候利用快取構建應用就是很方便的一件事情。而且可以大大減少網路請求。
所以,此時 yarn 誕生了,為的就是解決上面幾個問題。
- 引入 yarn.lock 檔案來管理依賴版本問題,保證每次安裝都是一致的。
- 快取加並行下載保證了安裝速度
那個時候我還在使用 cnpm,我特地比較了一下,發現還是 cnpm 比較快,於是我還是繼續使用著 cnpm,因為對於我來說足夠了。但是後面發現 yarn 真的越來越火,再加上 cnpm 長久不更新。我也嘗試著去了用 yarn,在嘗試之後,我徹底放棄了 cnpm。而且直到現在,似乎還沒有加入 lock 的功能。
當然 yarn 還不只只有這麼幾個好處,在使用者使用方面:
- 提供了非常簡潔的命令,將相關的命令進行分組,比如說
yarn global
下面都是與全域性模組相關的命令。而且提示非常完全,一眼就能看明白是什麼意思。不會像 npm 一樣,npm --help
就是一坨字串,還不講解一下是什麼用處,看著頭疼。 - 預設情況安裝會儲存到 dependencies,不需要像 npm 一樣手動新增
-S
引數 - 非常方便的 yarn run 命令,不僅僅會自動檢視 package.json 中 scripts 下面的內容,還是查詢
node_modules/.bin
下的可執行檔案。這個是我用 yarn 最高的頻率。比如你安裝了yarn add mocha
,然後就可以通過yarn run mocha
直接執行mocha
。而不需要./node_modules/.bin/mocha
執行。是我最喜歡的一個功能 - 互動式的版本依賴更新。npm 你只能先通過
npm outdated
看看那些包需要更新,然後通過npm update [packages]
更新指定的包。而在 yarn 當中,可以通過互動式的方式,來選擇那些需要更新,那些不需要。 - 全域性模組的管理。npm 管理全域性模組的方式是通過直接在
/usr/lib/node_modules
下面安裝,然後通過軟連線連線到/usr/local/bin
目錄下。而 yarn 的做法是選擇一個目錄,這個目錄就是全域性模組安裝的地方,然後將所有的全域性模組當做一個專案,從而進行管理。這個好處就是,你可以直接備份這個目錄當中的 package.json 和 yarn.lock 檔案,從而可以很方便的在另一個地方還原你安裝了那些全域性模組。至於這個目錄的問題,通過yarn global dir
命令就可以找到,mac 下是在~/.config/yarn/global/
,linux 我沒有測試過。
可以說 yarn 用起來非常舒服,但是唯一的缺點就是不是 npm 官方出的,更新力度、相容性都會差一些。但這也阻擋不住 yarn 在 Node 社群的火熱程度。很快,大家紛紛從 npm 切換到 yarn 上面。
重拾 npm 5
在受到 yarn 的衝擊之後,npm 官方也決定改進這幾個缺點,於是釋出了和 Yarn 對抗(這個詞是我意淫的)的 npm5 版本。
- 引入了 package-lock.json,並且預設就會新增,和 yarn.lock 是一樣的作用,並且取代之前的 npm shrinkwrap。
- 預設情況下,安裝會自動新增 dependencies,不需要手動書寫
-S
引數 - 提升了安裝速度,和之前有了很大的進步,但是和 yarn 相比,還是略微慢一些
至此,yarn 和 npm 的差距已經非常非常小了,更多的差距體現在使用者體驗層面,我使用 yarn 的功能也只剩下全域性模組管理、模組互動式更新和 yarn run
這個命令了。
但是後面推出的 npx 讓我放棄了使用 yarn run
這個命令。不是說 npx 比 yarn 有多好,而是說 npm 整合了這個功能,也就沒必要再去使用第三方的工具了。而且 npx 還支援臨時安裝模組,也就是那種只用一次的命令,用完就刪掉了。
後面我又發現了 npm-check
這個工具,我用它來替代了 yarn 的互動式更新。
然而 npm6 的出現加入了快取,並且又進一步提升了速度,可以說直逼 yarn。
於是 yarn 對我來說只剩下一個全域性模組管理的功能了。我的整個開發流程以及從 yarn 切換回 npm 上面了。或許後面的日子我也會讓 npm 來接管全域性模組管理,從而放棄使用 yarn。但是我還是會裝 yarn,畢竟有一些老專案還是用 yarn 的。
總結
我經歷了從 npm -> cnpm -> yarn -> (npm + npm-check + npx) 的一個迴圈,也見證了 npm 社群的一步步發展。而且 yarn 的更新頻率也非常慢,可能一個月才更新一次,這也讓我逐漸放棄使用 yarn。
有的時候感覺,第三方的終究是第三方,還是沒有原生的好用和方便,而且用起來安心。