如果不瞭解 npm 和 ES6 模組,那就看過來

段昕理發表於2016-08-18

JavaScript 的格局日新月異,網站和應用的依賴關係也隨之而變。

這篇文章適合那些大量使用 script 標籤來載入 JS 的程式設計師,隨著網頁數目增多和專案規模的擴大,他們感覺到依賴管理變得越來越笨重。

如果想要深入瞭解每一個細節,同時想對 CommonJS 和 AMD 規範做比較的話,請參考 Axel Rauschmayer 的 Exploring ES6 book , 尤其是第 17 章。

什麼是 JavaScript 模組?

JavaScript 模組允許我們把專案中的程式碼分散成一個個單獨的檔案,或者使用通過 npm 安裝的開源模組。用模組化的方式寫程式碼有助於(專案的)組織、維護、測試,以及最重要的依賴管理。

當我們編寫 JavaScript 時,理想情況是保障每個模組都專注一件事並把這件事做好。這種分工可以讓我們在需要某個模組時再去做相應的載入。模組化是是 npm 背後的核心原則。當需要某個特定的功能時,我們能安裝相應的模組並載入到應用當中。

隨著時間推移,我們發現那種大而全的框架變少了,看到更多的是 專注一件事並把這件事做好 的小型模組。

舉個例子,大部分人都用過 jQuery。這個庫包含了從 CSS 操作到 ajax 呼叫幾乎全部的方法。現如今,很多人開始遷移到 React 這類庫上,我們常常需要載入額外的模組來完成一些任務如 ajax 或路由。

這篇文章將會帶你大致瞭解 npm 和 ES6 模組的使用。對於其它一些的包管理(如 Bower) 和模組載入工具(如 CommonJSAMD),已經有大量的文章和話題在討論了。

不管你是做 Node 開發還是前端開發,我相信 ES6 模組和 npm 是未來的方向。如果你去看當下流行的開源專案,像 Reactlodash,你會發現他們都採用了 ES6 模組 + npm

當前的開發流程

很多 JavaScript 的開發流程是這樣的:

  1. 找一個符合要求的外掛或庫然後從 GitHub 上下載。
  2. 通過 script 標籤載入到網站。
  3. 用全域性變數呼叫或以 jQuery 外掛的方式呼叫。

這類開發流程這麼多年來表現一直不錯,除了這幾個問題:

  1. 外掛必須手動升級 - 很難及時得知(該外掛)何時修復了嚴重的bug或是有哪些新功能可用。
  2. 混亂的原始碼版本控制 - 所有依賴都要加入原始碼,當庫更新時會導致非常不愉快的結果(版本問題)。
  3. 基本沒有依賴管理 - 很多指令碼的功能是重複的,如果分成小模組則可以很輕鬆的實現共享。
  4. 程式碼汙染和潛在的全域性名稱空間衝突。

編寫模組化的 JavaScript 這種理念並不新鮮,不過隨著 ES6 的到來和業界將 npm 作為 JavaScript 的首選包管理工具,我們看到大量的開發摒棄了從前的工作流程,遷移到使用 ES6 和 npm 的標準化流程上來。

等等,npm?那不是 Node 專用的嗎?

很久以前,npm 是作為 Node.js 的包管理工具起步的,如今它已經進化為JavaScript和前端開發的包管理工具。現在,我們可以把庫的安裝過程簡化為 2 個步驟:

  1. 從 npm 安裝依賴,例如:
  2. 在當前檔案中匯入剛才的依賴,例如:

這套開發流程還需要很多的工作要做,同時關於模組的匯入匯出也有很多需要學習,現在讓我們來深入瞭解一下吧。

模組背後的理念

我們使用匯入和匯出語句來在檔案之間共享程式碼(變數、函式、資料、任何程式碼),而不是把所有程式碼載入到全域性名稱空間下。每一個模組匯入需要的依賴,也可以為其它檔案匯出需要的程式碼。

讓程式碼在瀏覽器執行還需要一個打包的步驟,我們會在文章的後面加以討論,現在,讓我們專注於 JavaScript 模組背後的核心理念。

建立自己的模組

假設我們正在構建一個線上購物的應用,需要一個檔案存放所有的輔助函式。我們可以建立一個模組命名為 helpers.js ,檔案包含一些輔助函式 - formatPrice(price)、 addTax(price) 和 discountPrice(price, percentage),還有一些關於該線上商城的變數。

我們的 helpers.js 檔案看起來如下:

現在,每個檔案都持有自己的本地函式和變數,只要沒有明確地匯出,它們絕不會滲透到其它檔案的作用域中。在上面的例子裡,我們可能不想讓其它模組訪問 taxRate 變數,但在模組內部確實需要這個變數。

我們怎麼讓其它模組訪問到這些函式和變數呢?答案是匯出它們。在 ES6 中有兩種匯出方式,命名匯出和預設匯出。由於需要讓多個函式和 couponCodes 變數被訪問到,我們使用命名匯出。稍後我們會加以詳述。

從模組匯出程式碼,最簡單直白的方式就是在行首加上 export 關鍵字,像這樣:

我們也可以在最後再匯出:

或者裡一次性匯出:

還有很多其它很便捷的匯出方式,如果碰到有些情形無法滿足你的工作需求,請檢視 MDN 的文件。

預設匯出

前面提到,從模組中有兩種匯出方式 - 命名或預設。上面的例子用了命名匯出。如果要讓其它模組匯入這些出口,我們必須要知道我們希望匯入的變數/函式的名稱 - 接下來會有一個例子說明。使用命名匯出的好處是你可以從一個模組中匯出多項內容。

另一種匯出方式是預設匯出。當需要匯出多個變數/函式時,可以用命名匯出,當你的模組只需要匯出一個變數/函式的時候,使用預設匯出就行了。儘管你可以在一個模組中同時使用預設匯出和命名匯出,我還是建議你每個模組只採用一種方式。

預設匯出的例子是一個單獨的 StorePicker React 元件,或者是某個陣列。例如,下面這個陣列需要讓其它元件訪問到,我們可以用預設匯出。

和上面一樣,你可以在想要匯出的函式到前面加上 export default 關鍵字。

匯入自己建立的模組

既然我們已經把程式碼分成了小模組並且按需求匯出了,現在可以向前一步,把這些模組匯入到應用的其它部分了。

如果要匯入的模組是程式碼庫的一部分,我們使用 import 語句,然後指定檔案相對於當前模組的路徑 - 跟你平時在 HTML 中匯入資源路徑或 CSS 背景圖是一樣的。你會發現我們去掉了 .js 字尾,因為字尾不是必需的。

需要注意並不是匯入一次模組,整個應用就能像全域性變數一樣去訪問了。每當模組依賴其它模組時 - 比如我們上面的程式碼需要一個 lodash 方法 - 我們必須將它匯入進來。如果我們有5個模組都需要同樣的 lodash 函式,我們需要匯入 5 次。這有助於保持作用域清晰,同時讓模組更加輕便且可重複使用。

匯入命名匯出

我們最先匯出的時我們的輔助模組。這裡使用的是 命名匯出,現在,有很多種方式將它們匯入:

匯入預設匯出

如果你回想一下,我們還從 people.js 中匯出了一個 first names 的陣列,這是那個模組唯一需要匯出的部分。

預設匯出可以用任何名稱匯入 - 不需要知道匯出的變數、函式、或類的名稱。

從 npm 匯入模組

我們使用的大量模組來自於 npm。不管你需要一個像 jQuery 這種完整庫,還是 lodash 這種只有工具函式的庫,又或是 superagent 這種提供 Ajax 請求的庫,我們都可以用 npm 來安裝。

一旦這些包存在在 node_modules/ 目錄下了,就可把它們匯入到我們的程式碼。預設情況下,Babel 會把 ES6 的匯入語句編譯成 CommonJS。所以,只要使用一個可以解析模組語法的打包工具(webpack 或者 browserify),你就可以玩轉 node_modules/ 目錄了。我們打匯入語句只需要包含模組的名稱就可以了。其它打包工具可能需要外掛或配置才能從 node_modules/ 目錄匯入。

上面的程式碼能正常工作是因為 jQuery 是按 CommonJS 模式匯出的, Babel 將 ES6 的 import 語句轉譯使其適配 jQuery 的 CommonJS 匯出(語句)。

我們再試試 superagent。和 jQuery 一樣,superagent 對整個庫使用了CommonJS規範的預設匯出,所以我們可以任意命名 - 一般命名成 request。

“精挑細選”——部分匯入

我最喜歡 ES6 模組的一個特性就是,有大量的庫允許你只選擇你需要的程式碼。lodash 是個非常棒的工具庫,裡面有很多有用的 JavaScript 方法。

我們可以將整個庫用 _(下劃線) 變數匯入,這是因為 lodash 將整個庫作為一個主模組來匯出(同樣Babel 會將我們的 import 進行轉譯,效果等同於 lodash 使用了預設匯出)

不過,更常見的是你只想匯入一兩個方法,而不是整個庫。因為 lodash 已經將每個方法都作為一個模組匯出了,所以我們可以選擇只匯入我們想要的部分! 同樣Babel 的轉譯保證了這種做法的可行性。

確保模組是最新的

一些反對“微模組”編碼的人們認為,這麼做會導致 npm 中的庫存在大量的相互依賴。

JavaScript 生態發展非常迅速,要讓你的依賴維持最新狀態非常困難。我們都知道不管是程式碼還是依賴都存在 bug、安全漏洞或者某些程式碼不再易用。 我們需要清楚的瞭解專案的各種不安全、被棄用的、過時的或無用的程式碼。

要解決這個問題,bitHound 公司提供了一項很棒的服務,這項服務能夠持續監控你的程式碼,當你的依賴出現了任何錯誤都會及時向你報告,同時還會提供一個專案的綜合統計評分。通過這項服務可以看出你的專案到底有多健壯,它對所有開源專案都是免費的。

bitHound 整合了 GitHub 和 BitBucket 並且推出了自動提交分析功能,這樣當你的程式碼庫發生變更時會通知 bitHound。一旦依賴庫過期,你的 Slack 或 HipChat 就會收到通知,或收到一份包含詳細資訊的郵件。

如果不瞭解 npm 和 ES6 模組,那就看過來

bitHound 還包含了 Pull Requests 的分支狀態 - 設定 通過/失敗 標準,之後 bitHound 會將這些狀態資訊提交到 GitHub 或者 Bitbucket。

有個叫 npm-check-updates 的工具可以非常好地搭配 bitHound 工作。使用 npm install npm-check-updates -g 在你開發機器上全域性安裝,然後執行 ncu 命令,可以快速檢查你的程式碼包是否需要升級。如果需要,你可以執行 ncu –upgradeAll 命令自動升級 package.json 裡面的所有包。之後一定要執行 npm install 命令來從 NPM 上獲取最新程式碼。

打包操作

因為瀏覽器現在還不支援 ES6 模組,我們需要一些工具讓它們能正常工作。 JavaScript 打包工具可以把這些模組轉譯成一個單獨的 JavaScript 檔案,或者把應用程式的各個部分打包成多個檔案。

最終我們不再需要打包工具,HTTP/2 載入時會自動請求所有的 import 語句。

這有一些流行的打包工具,大部分都使用 Babel 將 ES6 模組轉譯成 CommonJS。

  • Browserify 最初用來在瀏覽器中使用 nodejs 風格的程式碼。同樣支援 ES6 模組
  • webpack 在 React 社群非常流行。不僅支援 ES6 ,還支援很多其它格式的模組程式碼。
  • Rollup 為 ES6 而生,但是source maps(一個記錄程式碼轉換前和轉換後的位置資訊檔案)似乎有些問題 - 我會在一兩個月後再看看。
  • JSPM 基於 npm SystemJS.
  • Ember CLI 為Ember使用者提供的與 webpack 類似的簡單命令列工具。使用了 Broccoli 引擎。

該怎麼選呢?選最適合你的。我是 Browserify 和 webpack 的忠實粉絲,因為前者上手簡單,後者容易與 React 整合。編寫 ES6 模組的迷人之處在於你寫的不是 Browserify 或 webpack 模組 - 你可以隨時切換打包工具。有很多選用哪個工具的觀點,快速搜尋一下你會發現每個工具都有大量的論述。

如果你已經在執行任務時,使用了gulp, grunt, 或npm對現有的JavaScript和CSS檔案進行操作,那麼將模組引入工作流程非常簡單。

實現打包非常簡單 - 你可以將它作為一個 gulp 的子任務來執行,或者通過 webpack 配置、npm 指令碼 或是直接使用命令列。

我建立了一個程式碼庫,你可以通過幾個簡單模組的程式碼詳細瞭解如何使用 webpack 和 Browserify。

匯入非模組程式碼

如果你正在致力於將程式碼庫模組化,但又很難一步做到,你可以簡單的通過匯入“檔名”來載入並執行檔案中的程式碼。這不算 ES6,但它是打包工具的一個功能。

這個概念與連續執行多個 js 檔案並無不同,除非你匯入的程式碼作用域限制在匯入的模組中。

需要全域性變數的程式碼

有些庫,比如說 jQuery 外掛,不太適合 JavaScript 的模組化系統。整個 jQuery 外掛系統都假設有一個叫window.jQuery的全域性變數 ,每個外掛可以附加在該變數之上。但是我們剛剛學過 ES6 模組沒有全域性變數。所有程式碼的作用域都侷限於當前模組,除非你把一些程式碼明確地設定在window物件上。

要想解決這個問題,首先要問問自己是否真得需要這個外掛,還是可以自己寫一個。很多 JavaScript 外掛系統已經重寫並移除 jQuery 依賴,成為獨立的 JavaScript 模組。

如果不是,你需要檢查你的構建流程,看看能不能幫助解決問題。Browserify 有 Browserify Shim(用於解決不相容Browserify或不相容CommonJS的問題),webpack 也有相關文件。

陷阱

當匯出一個函式時,不要在最後包含分號。很多打包工具仍然允許使用額外的分號,但是在函式宣告後面去掉分號是最佳實踐,這樣當你切換其它打包工具時可以避免碰到一些奇葩的問題。

延伸閱讀

希望這是一篇關於使用 npm 和 ES6 模組化不錯的介紹。還有更多需要學習的內容,我認為,最好的學習方式就是在你的下一個專案中使用它。下面有一些些非常棒的學習資源,可以在你的前進道路上提供幫助。

Thanks + Updates

非常感謝 Stephen MargheimKent C. DoddsMike ChenBen BermanJuan Pablo Osorio Ospina 幫助校對並且提供了高質量的反饋意見。

如果你有任何意見、程式碼示例、技術升級或者說明,想加進來,請提交 pull request

相關文章