在生產中部署 ES2015+ 程式碼

豐千郎發表於2022-12-29

大多數 Web 開發人員都喜歡編寫具有所有最新語言特性的 JavaScript——async/await、類、箭頭函式等。然而,儘管事實上所有現代瀏覽器都可以執行 ES2015+ 程式碼並原生支援我剛才提到的特性 , 大多數開發人員仍然將他們的程式碼轉換為 ES5 並將其與 polyfills 捆綁在一起,以適應仍在使用舊版瀏覽器的一小部分使用者。

這有點糟糕。 在理想情況下,我們不會寫不必要的程式碼。

使用新的 JavaScript 和 DOM API,我們可以有條件地載入 polyfill,因為我們可以在執行時檢測它們的支援。 但是對於新的 JavaScript 語法,這要複雜得多,因為任何未知的語法都會導致解析錯誤,然後所有程式碼都不會執行。

雖然我們目前沒有一個好的解決方案來檢測新語法的特性,但我們現在有辦法檢測基本的 ES2015 語法支援。

解決方案是 <script type="module">

大多數開發人員認為 <script type="module"> 是載入 ES 模組的方式(當然這是真的),但是 <script type="module"> 還有一個更直接和實用的用例——載入常規 JavaScript 具有 ES2015+ 特性並且知道瀏覽器可以處理的檔案!

換句話說,每個支援 <script type="module"> 的瀏覽器也支援你所知道和喜愛的大部分 ES2015+ 特性。 例如:

  • 每個支援 <script type="module"> 的瀏覽器也支援 async/await
  • 每個支援 <script type="module"> 的瀏覽器也支援類。
  • 每個支援 <script type="module"> 的瀏覽器也支援箭頭功能。
  • 每個支援 <script type="module"> 的瀏覽器也支援 fetch、Promises、Map、Set 等等!

剩下要做的唯一一件事就是為不支援 <script type="module"> 的瀏覽器提供回退。 幸運的是,如果我們當前正在生成程式碼的 ES5 版本,那麼我們已經完成了這項工作。 我們現在只需要生成一個 ES2015+ 版本!

本文的其餘部分解釋瞭如何實現此技術,並討論了釋出 ES2015+ 程式碼的能力將如何改變我們編寫模組的方式。


實現

如果你現在已經在使用像 webpackrollup 這樣的模組打包器來生成你的 JavaScript,你應該繼續這樣做。

接下來,除了我們當前的捆綁包之外,我們將生成第二個捆綁包,就像第一個捆綁包一樣; 唯一的區別是你不會一直轉譯到 ES5,也不需要包含遺留的 polyfill。

如果我們已經在使用 babel-preset-env(應該使用),則第二步非常簡單。 你所要做的就是將你的瀏覽器列表更改為僅支援 <script type="module"> 的瀏覽器,Babel 將自動不應用它不需要的轉換。

換句話說,它將輸出 ES2015+ 程式碼而不是 ES5。

例如,如果我們正在使用 webpack 並且我們的主指令碼入口點是 ./path/to/main.mjs,那麼我們當前的 ES5 版本的配置可能看起來像這樣(注意,我將這個包稱為 main.mjs)。 es5.js 因為它是 ES5):

module.exports = {
  entry: './path/to/main.mjs',
  output: {
    filename: 'main.es5.js',
    path: path.resolve(__dirname, 'public'),
  },
  module: {
    rules: [{
      test: /\.m?js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            ['env', {
              modules: false,
              useBuiltIns: true,
              targets: {
                browsers: [
                  '> 1%',
                  'last 2 versions',
                  'Firefox ESR',
                ],
              },
            }],
          ],
        },
      },
    }],
  },
};

要製作一個現代的 ES2015+ 版本,我們所要做的就是進行第二個配置並將您的目標環境設定為僅包括支援 <script type="module"> 的瀏覽器。 它可能看起來像這樣(注意,我在這裡使用 .mjs 副檔名,因為它是一個模組):

module.exports = {
  entry: './path/to/main.mjs',
  output: {
    filename: 'main.mjs',
    path: path.resolve(__dirname, 'public'),
  },
  module: {
    rules: [{
      test: /\.m?js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            ['env', {
              modules: false,
              useBuiltIns: true,
              targets: {
                browsers: [
                  'Chrome >= 60',
                  'Safari >= 10.1',
                  'iOS >= 10.3',
                  'Firefox >= 54',
                  'Edge >= 15',
                ],
              },
            }],
          ],
        },
      },
    }],
  },
};

執行時,這兩個配置將輸出兩個可用於生產的 JavaScript 檔案:

  • main.mjs(語法為 ES2015+)
  • main.es5.js(語法為 ES5)

下一步是更新我們的 HTML 以在支援模組的瀏覽器中有條件地載入 ES2015+ 包。 我們可以使用 <script type="module"><script nomodule> 的組合來做到這一點:

<!-- Browsers with ES module support load this file. -->
<script type="module" src="main.mjs"></script>

<!-- Older browsers load this file (and module-supporting -->
<!-- browsers know *not* to load this file). -->
<script nomodule src="main.es5.js"></script>

注意 :我們已經更新了本文中的示例,以對我作為模組載入的任何檔案使用 .mjs 副檔名。 由於這種做法相對較新,如果我不指出在使用它時可能遇到的一些問題,那我們就是失職了:

  • 我們的 Web 伺服器需要配置為使用 Content-Type 標頭 text/javascript 提供 .mjs 檔案。 如果我們的瀏覽器無法載入 .mjs 檔案,這可能就是原因。
  • 如果我們使用 Webpack 和 babel-loader 來捆綁 JavaScript,我們可能已經複製/貼上了一些僅轉譯 .js 檔案的配置程式碼。 將配置中的正規表示式從 /\.js$/ 更改為 /\.m?js$/ 應該可以解決我們的問題。
  • 較舊的 webpack 版本不會為 .mjs 檔案建立 sourcemap,但自 webpack 4.19.1 以來,此問題已得到修復。

重要注意事項

在大多數情況下,這種技術“有效”,但在實施這種策略之前,有一些關於如何載入模組的細節很重要,需要注意:

  1. 模組像 <script defer> 一樣載入,這意味著它們在文件被解析之前不會被執行。 如果我們的某些程式碼需要在此之前執行,最好將該程式碼拆分出來並單獨載入。
  2. 模組總是在嚴格模式下執行程式碼,因此如果出於任何原因您的任何程式碼需要在嚴格模式之外執行,則必須單獨載入它。
  3. 模組對待頂級 var 和函式宣告的方式與指令碼不同。 例如,在指令碼中 var foo = 'bar'function foo() {…} 可以從 window.foo 訪問,但在模組中情況並非如此。 確保我們不依賴程式碼中的這種行為。

警告 ! Safari 10 不支援 nomodule 屬性,但我們可以透過在使用任何 <script nomodule> 標記之前在 HTML 中內聯一段 JavaScript 片段來解決這個問題。 (注意:這已在 Safari 11 中修復)。


是時候開始將我們的模組釋出為 ES2015 了

目前該技術的主要問題是大多數模組作者不釋出其原始碼的 ES2015+ 版本,他們釋出轉譯後的 ES5 版本。

現在可以部署 ES2015+ 程式碼了,是時候改變它了。

我完全理解這對不久的將來提出了許多挑戰。 今天大多數構建工具都會發布文件,推薦假定所有模組都是 ES5 的配置。 這意味著如果模組作者開始將 ES2015+ 原始碼釋出到 npm,他們可能會破壞一些使用者的構建並且通常會引起混淆。

問題是大多數使用 Babel 的開發人員將其配置為不在 node_modules 中轉換任何內容,但是如果模組是使用 ES2015+ 原始碼釋出的,這就是一個問題。 幸運的是,修復很容易。 我們只需從構建配置中刪除 node_modules 排除項:

rules: [
  {
    test: /\.m?js$/,
    exclude: /node_modules/, // Remove this line
    use: {
      loader: 'babel-loader',
      options: {
        presets: ['env']
      }
    }
  }
]

不利的一面是,如果像 Babel 這樣的工具除了本地依賴項之外還必須開始轉譯 node_modules 中的依賴項,那麼構建速度會變慢。 幸運的是,這個問題可以在一定程度上透過持久的本地快取在工具級別得到解決。

不管我們在 ES2015+ 成為新的模組釋出標準的道路上可能會遇到什麼坎坷,我認為這是一場值得一戰的鬥爭。 如果我們作為模組作者,只將我們程式碼的 ES5 版本釋出到 npm,我們就會將臃腫和緩慢的程式碼強加給我們的使用者。

透過釋出 ES2015,我們給了開發者一個選擇,最終讓每個人都受益。

相關文章