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

XGHeaven發表於2018-05-03

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

  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。

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

相關文章