為什麼我從npm到yarn再到npm?

NO IMAGE

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-regexpconnect 下面的 path-to-regexp 的模塊是兩個不同的模塊。
那麼這個會帶來什麼影響呢?其實在使用上,並沒有什麼太大的影響,但是內存佔用過大。因為很多相同模塊位於不同模塊下面就會導致有多個實例的出現(為什麼會加載多個實例,請查看 Node 模塊加載)。你想想,都是同樣的功能,為什麼要實例這麼多次呢?不能就加載一次,複用實例麼?

上古時代的 npm 的缺點可以說還是很多的:

  • 目錄嵌套層級過深
  • 模塊實例無法共享
  • 安裝速度很慢,這其中有目錄嵌套的原因,也有安裝邏輯的問題。因為 npm 是請求完一個模塊之後再去請求另一個模塊,這就會導致同一個時刻,只有一個模塊在下載、解析、安裝。

軟鏈時代

後面,有人為了解決目錄嵌套層次過高的問題,引入的軟鏈接的方案。

簡單來說,就是將所有的包都扁平化安裝到一個位置,然後通過軟鏈接(windows 快捷方式)的方式組合到 node_modules 中。

- app/
- node_modules
- .modules/
- [email protected]/
- node_modules
- connect -> ../../[email protected]
- path-to-regexp -> ../../[email protected]
- ... -> ../../[email protected]
- [email protected]/
- [email protected]/
- ...others
- express -> ./.modules/[email protected]

這樣做的好處就是可以將整體的邏輯層級簡化到很少的幾層。而且對於 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 依賴 [email protected] 的版本,而 package-b 依賴 [email protected] 版本,那麼解決方案還是像之前的那種嵌套模式一樣。

- 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 版本。

  1. 引入了 package-lock.json,並且默認就會添加,和 yarn.lock 是一樣的作用,並且取代之前的 npm shrinkwrap。
  2. 默認情況下,安裝會自動添加 dependencies,不需要手動書寫 -S 參數
  3. 提升了安裝速度,和之前有了很大的進步,但是和 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。

有的時候感覺,第三方的終究是第三方,還是沒有原生的好用和方便,而且用起來安心。

相關文章

如何讓你的React『變慢』?探析ArrayDiff的一些邊角特性

可控組件?不可控組件?讓我們來討論一下下~

嚐鮮用ReactHook+Parcel構建真心話大冒險簡單頁面

【後知後覺系列】cssposition:sticky屬性以及某些場景的使用