[ES6深度解析]15:模組 Module

Max力出奇跡發表於2021-08-27

JavaScript專案已經發展到令人瞠目結舌的規模,社群已經開發了用於大規模工作的工具。你需要的最基本的東西之一是一個模組系統,這是一種將你的工作分散到多個檔案和目錄的方法——但仍然要確保你的所有程式碼片段可以根據需要相互訪問——而且還要能夠有效地載入所有程式碼。所以很自然,JavaScript有一個模組系統。實際上,有不少模組系統。還有一些包管理器,用於安裝所有這些軟體和處理高階依賴關係的工具。你可能會認為,擁有新的模組語法的ES6有點晚了。

今天我們將看到ES6是否會在這些現有系統中新增任何東西,以及未來的標準和工具是否能夠在它的基礎上構建。但首先,讓我們深入瞭解一下ES6模組是什麼樣子的。

Module 基礎知識

ES6模組是一個包含JS程式碼的檔案。沒有特殊的module關鍵字;模組讀起來就像指令碼。有兩個區別。

  • ES6模組是自動的嚴格模式程式碼,即使你沒有寫use strict
  • 可以在模組中使用importexport

讓我們先談談export。預設情況下,在模組中宣告的所有內容都是該模組的區域性內容。如果你希望在模組中宣告的某些特性是公共的,以便其他模組可以使用它,則必須export該特性。有幾種方法可以做到這一點。最簡單的方法是新增export關鍵字。

// kittydar.js - Find the locations of all the cats in an image.

export function detectCats(canvas, options) {
  var kittydar = new Kittydar(options);
  return kittydar.detectCats(canvas);
}

export class Kittydar {
  ... several methods doing image processing ...
}

// This helper function isn't exported.
function resizeCanvas() {
  ...
}

可以export任何頂級functionclassvarletconst

這就是編寫模組所需要知道的全部內容!你不需要把所有的東西都放到IIFE回撥中。去宣告你需要的東西吧。由於程式碼是一個模組,而不是一個指令碼,所以所有的宣告都將作用域限定在該模組,而不是在所有指令碼和模組中全域性可見

除了export之外,模組中的程式碼基本上都是普通程式碼。它可以使用全域性變數,如ObjectArray。如果您的模組在web瀏覽器中執行,它可以使用documentXMLHttpRequest

在一個單獨的檔案中,我們可以匯入並使用detectCats()函式:

// demo.js - Kittydar demo program

import {detectCats} from "kittydar.js";

function go() {
    var canvas = document.getElementById("catpix");
    var cats = detectCats(canvas);
    drawRectangles(canvas, cats);
}

要從一個模組中匯入多個名稱,可以這樣寫:

import {detectCats, Kittydar} from "kittydar.js";

當你執行包含import宣告的模組時,它首先載入所匯入的模組,然後在依賴關係圖的深度優先遍歷中執行每個模組主體,通過跳過已經執行的任何內容來避免迴圈遍歷。這些是模組的基礎。這真的很簡單。;-)

export 列表

你可以用花括號{}括起來,列出你想要匯出的所有(方法,變數,類等)名稱,而不是給每個匯出的特性加上標籤:

export {detectCats, Kittydar};

// no `export` keyword required here
function detectCats(canvas, options) { ... }
class Kittydar { ... }

export列表不一定是檔案中的第一項內容;它可以出現在模組檔案的頂級作用域中的任何地方。您可以有多個export列表,或者將export列表與其他export宣告混合在一起,只要沒有名稱被多次重複匯出。

重新命名匯入和匯出

偶爾,匯入的名稱會與你需要使用的其他名稱發生衝突。所以ES6允許你在匯入時重新命名:

// suburbia.js

// Both these modules export something named `flip`.
// To import them both, we must rename at least one.
import {flip as flipOmelet} from "eggs.js";
import {flip as flipHouse} from "real-estate.js";
...

類似地,您可以在匯出時重新命名它們。如果你想在兩個不同的名稱下匯出相同的值,這是很方便的,這偶爾會發生:

// unlicensed_nuclear_accelerator.js - media streaming without drm
// (not a real library, but maybe it should be)

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

預設匯出

新標準旨在與現有的CommonJSAMD模組互操作。假設你有一個Node專案,你已經完成了npm install lodash。你的ES6程式碼可以從Lodash中匯入單獨的函式:

import {each, map} from "lodash";

each([3, 2, 1], x => console.log(x));

但也許你已經習慣看到_.each,而不是each,你仍然想要這樣寫。或者你把_作為函式使用,因為這在Lodash中很常用。為此,你可以使用稍微不同的語法:匯入沒有花括號的模組

import _ from "lodash";

這種簡寫等價於import {default as _} from "lodash"。所有的CommonJS和AMD模組在ES6中都有一個default export,這和你在require()函式中呼叫該模組時得到的是一樣的,也就是exports物件

ES6模組被設計成允許你匯出多個東西,但是對於現有的CommonJS模組,你只能得到預設的匯出。例如,在撰寫本文時,據我所知,著名的colors包沒有任何特殊的ES6支援。它是CommonJS模組的集合,就像npm上的大多數包一樣。但是你可以直接匯入到你的ES6程式碼中。

// ES6 equivalent of `var colors = require("colors/safe");`
import colors from "colors/safe";

如果你想要自己的ES6模組有一個預設的匯出,這很容易做到。預設匯出沒有什麼魔力;它就像任何其他匯出一樣,除了它被命名為default。你可以使用我們已經討論過的重新命名語法:

let myObject = {
  field1: value1,
  field2: value2
};
export {myObject as default};

或者更好的做法是,使用以下簡寫:

export default {
  field1: value1,
  field2: value2
};

關鍵字export default後面可以跟任何值:函式、類、物件字面量。

模組物件

import * as cows from "cows";

當你import *時,所匯入的是一個模組名稱空間物件(module namespace object)。它的屬性是模組的exports。因此,如果cows模組匯出了一個名為moo()的函式,那麼在以這種方式匯入cows之後,你可以寫:cows.moo()

聚合模組

有時候,一個包的主模組比匯入包的所有其他模組並以統一的方式匯出它們大不了多少。為了簡化這類程式碼,有一種一體化的import-and-export簡寫:

// world-foods.js - good stuff from all over

// import "sri-lanka" and re-export some of its exports
export {Tea, Cinnamon} from "sri-lanka";

// import "equatorial-guinea" and re-export some of its exports
export {Coffee, Cocoa} from "equatorial-guinea";

// import "singapore" and export ALL of its exports
export * from "singapore";

每個export-from語句都類似於import-from語句後跟export語句。與真正的匯入不同,它不會將重新匯出的繫結新增到您的作用域。因此,如果你打算在world-food.js中編寫一些利用Tea的程式碼,請不要使用這種簡寫。你會發現它並不存在。

如果singapore輸出的任何名稱與其他輸出名稱發生衝突,那將是一個錯誤,因此要小心使用export *

語法已經講完了!現在說說有趣的部分。

import實際上是做什麼的?

你會相信…什麼都不沒做嗎?

哦,你沒那麼容易上當。你能相信標準並沒有說import到底做了哪些事情嗎?這是件好事嗎?

ES6將模組載入的細節完全留給了實現。模組執行方式是詳細指定的。粗略地說,當你告訴JS引擎執行一個模組時,它必須表現得像以下四個步驟正在發生:

  1. 解析(Parsing):
    實現讀取模組的原始碼並檢查語法錯誤。

  2. 載入(Loading):
    實現載入所有匯入的模組(遞迴地)。這部分還沒有標準化。

  3. 連結(Linking):
    對於每個新載入的模組,實現建立一個模組作用域,並用該模組中宣告的所有繫結填充它,包括從其他模組匯入的內容。
    如果你試圖import {cake} from "paleo",但是paleo模組實際上沒有匯出任何名為cake的東西,你會得到一個錯誤。這太糟糕了,因為你離真正執行一些JS程式碼已經很近了。

  4. 執行時(Runtime):
    最後,實現執行每個新載入模組的程式碼體中的語句。此時,匯入處理已經完成,所以當執行到有import宣告的程式碼行時……什麼也沒有發生!

看到了嗎?我告訴過你答案是"import什麼都沒做",關於程式語言,我沒有撒謊。

現在我們來看看這個系統中有趣的部分。有一個很酷的技巧。因為系統不指定載入是如何工作的,因為你可以提前通過檢視原始碼import宣告算出所有的依賴關係,一種載入的實現方式是在編譯時完成所有的工作,你所有的模組打包成一個檔案,並把它放在網路上傳輸!像webpack這樣的工具可以做到這一點。

這是一件大事,因為通過網路載入指令碼需要花費時間,而且每次獲取一個指令碼時,您可能會發現它包含需要載入幾十個以上的匯入宣告。一個簡單的載入器將需要大量的網路往返通訊。但是有了webpack,你現在不僅可以使用帶有模組的ES6,還可以在不影響執行時效能的情況下獲得所有的軟體工程好處。

ES6中模組載入的詳細規範最初是計劃並構建的。它沒有出現在最終標準中的一個原因是,對於如何實現這個捆綁特性沒有達成共識。我希望有人能解決這個問題,因為我們將看到,模組載入確實應該標準化。捆綁銷售太好了,不能放棄。

靜態 vs 動態,或者:規則以及如何打破規則

作為一種動態語言,JavaScript讓自己擁有了一個令人驚訝的靜態模組系統。

  • 在一個模組中,所有型別的匯入和匯出都只允許在頂層。沒有條件匯入或匯出,並且不能在函式內使用匯入。
  • 所有匯出的識別符號必須在原始碼中按名稱顯式匯出。你無法通過程式設計方式遍歷陣列並以資料驅動的方式匯出一組名稱。
  • 模組物件被凍結(無法修改)。沒有辦法將一個新特性hack到一個模組物件中,polyfill風格。
  • 在任何模組程式碼執行之前,模組的所有依賴項都必須被載入、解析和連結。沒有語法可以實現按需惰性載入的匯入
  • 匯入發生錯誤沒有錯誤恢復。一個應用程式可能包含數百個模組,如果有任何模組無法載入或連結,就無法執行。不能把import包裹在try/catch塊中。(這裡的好處是,因為系統是靜態的,所以webpack可以在編譯時檢測到這些錯誤。)
  • 沒有鉤子允許模組在依賴項載入之前執行一些程式碼。這意味著模組無法控制它們的依賴項是如何載入的。

只要你的需求是靜態的,系統就相當不錯。但你難免有時候需要做一點定製,對吧?

這就是為什麼無論你使用什麼模組載入系統,都會有一個程式設計API來配合ES6的靜態import/export語法。例如,webpack包含一個API,你可以用它來“分割程式碼”,按需惰性載入一些模組包。同樣的API可以幫助您打破上面列出的大多數規則。

ES6模組語法是非常靜態的,這很好——它以強大的編譯時工具的形式得到了回報。但是這種靜態語法被設計為與豐富的動態、程式化載入器API一起工作。

相關文章