如何編寫同時用於 Node 和瀏覽器的 JavaScript 包

Viyi發表於2017-03-02

我多次看到大家在這個問題上產生困惑,甚至經驗豐富的 JavaScript 開發者都可能錯過它的一些微妙之處。所以我認為應該寫這麼一個簡短的教程。

假設有一個 JavaScript 模組想釋出在 npm 中,它既能在 Node 中執行,又能在瀏覽器中執行。這會產生一個問題!這個特定的模組對於 Node 和瀏覽器的執行,會有一點不同的實現。

這種情況相當常見,因為這 Node 和瀏覽器之間存在許多微小的環境差異。如何正確實現相當棘手,尤其是想在針對瀏覽的實現中極儘可能地減少依賴庫的時候。

構建一個 JS 包

來寫一個很小的,稱為 base64-encode-string 的 JavaScript 包。它的作用是將輸入的字串以 base64 編碼之後輸出。

對瀏覽器來說,使用內建的 btoa 函式很容易就能實現:

module.exports = function (string) {
  return btoa(string);
};

但 Node 中沒有 btoa 函式,所以我們要建立一個  Buffer,然後呼叫 buffer.toString()

module.exports = function (string) {
  return Buffer.from(string, 'binary').toString('base64');
};

兩種方法都能輸出正確的 base64 編碼,比如:

var b64encode = require('base64-encode-string');
b64encode('foo');    // Zm9v
b64encode('foobar'); // Zm9vYmFy

現在我們需要一些方法來檢驗它是執行在瀏覽器中還是執行在 Node 中,然後我們才能呼叫正確的版本。Browserify 和 Webpack 都定義了 process.browser,在瀏覽器中它返回 true,而在 Node 中它返回 false。所以我們很容易做到:

if (process.browser) {
  module.exports = function (string) {
    return btoa(string);
  };
} else {
  module.exports = function (string) {
    return Buffer.from(string, 'binary').toString('base64');
  };
}

我們把檔案命名為 index.js,鍵入 npm publish,然後一切都搞定了。但這種方法存在一個巨大的效能問題。

index.js 中包含了對 Node 內建的 process 和 Buffer 的引用,Browserify 和 Webpack 都會在打包的時候自動包含相應的 polyfill(引1引2)。

雖然這個模組只有 9 行,但 Browserify 和 Webpack 最小化並打包出來有 24.7KB(7.6KB min+gz)。在瀏覽器中只需要 btoa 就能解決的問題居然需要引用這麼大的東西!

超愛“browser” 選項

如果在 Browserify 和 Webpack 的檔案中尋找解決辦法,最終會找到 node-browser-resolve。這涉及到package.json 中的 “browser” 選項。它定義在為瀏覽器構建模組時的行為。

使用這個技術,需要在 package.json 中新增:

{
  /* ... */
  "browser": {
    "./index.js": "./browser.js"
  }
}

然後將兩個函式分拆到 index.js 和 browser.js 兩個檔案中:

// index.js
module.exports = function (string) {
  return Buffer.from(string, 'binary').toString('base64');
};
// browser.js
module.exports = function (string) {
  return btoa(string);
};

這之後,Browserify 和 Webpack 會產生更合適的結果:Browserify 最小隻有 511 欄位(315 min+gz),Webpack 則是 550 位元組(297 min+gz)。

這個包釋出到 npm 之後,在 Node 中 執行 require(‘base64-encode-string’) 都是引用 Node 版本,而使用 Browerify 或 Webpack 則會引用瀏覽器版本。成功!

對於 Rollup 來說會更復雜一點。Rollup 使用者需要使用 rollup-plugin-node-resolve 並在選項中設定 browser 為 ture。

對於 jspm 來說就很不幸了,它不支援 “browser” 選項。不過 jspm 使用者可以通過如下方法繞過去:require(‘base64-encode-string/browser’) 或者 jspm install npm:base64-encode-string -o “{main:’browser.js’}”。另外,包作者可以在 package.json 中指定“jspm”選項。

高階技巧

直接使用“browser”的方法很好,但對於大型專案來說,package.json 和程式碼的耦合就很尷尬了。例如,package.json 很快會變成下面這個樣子:

{
  /* ... */
  "browser": {
    "./index.js": "./browser.js",
    "./widget.js": "./widget-browser.js",
    "./doodad.js": "./doodad-browser.js",
    /* etc. */
  }
}

你每需要一個瀏覽器模組,就必須建立兩個單獨的檔案,然後在 “browser” 選項中新增一行來關聯它們。還得小心不要寫錯什麼!

而且你會發現自己需要將部分程式碼提取為單獨的模組,因為你不想使用 if (process.browser) {} 來進行檢查。當這些 *-browser.js 檔案逐漸積累起來,就會使程式碼導航越來越困難。

解決這個問題有幾個不同的解決方案。我個人喜歡使用 Rollup 來作為構建工具,它會自動將一個程式碼庫中的程式碼拆分成 index.js 和 browser.js 檔案,節約空間和時間。

想這樣做需要安裝 rollup 和 rollup-plugin-replace,然後定義 rollup.cofnig.js 檔案:

import replace from 'rollup-plugin-replace';
export default {
  entry: 'src/index.js',
  format: 'cjs',
  plugins: [
    replace({ 'process.browser': !!process.env.BROWSER })
  ]
};

(我們會使用 process.env.BROWSER 來切換針對瀏覽器的構建和針對 Node 的構建。)

接下來,建立 src/index.js 檔案,它包含一個單獨的函式,其中用到了 process.browser 條件:

export default function base64Encode(string) {
  if (process.browser) {
    return btoa(string);
  } else {
    return Buffer.from(string, 'binary').toString('base64');
  }
}

然後在 package.json 中新增 prepublish 步驟,用於生成檔案:

{
  /* ... */
  "scripts": {
    "prepublish": "rollup -c > index.js && BROWSER=true rollup -c > browser.js"
  }
}

生成的檔案相當簡單而且易讀:

// index.js
'use strict';

function base64Encode(string) {
  {
    return Buffer.from(string, 'binary').toString('base64');
  }
}

module.exports = base64Encode;
// browser.js
'use strict';

function base64Encode(string) {
  {
    return btoa(string);
  }
}

module.exports = base64Encode;

你會發現 Rollup 根據需要自動將 process.browser 轉變為 true 或 false,然後去掉無用的程式碼。因此在針對瀏覽器的生成結果中不會引用 process 或 Buffer。

這種技術讓你可以在程式碼中任意使用 process.browser 條件,釋出出來的結果總是兩個小檔案,一個 index.js,一個 browser.js。在 Node 環境只有 Node 相關的程式碼,而在瀏覽器環境則只有瀏覽器相關的程式碼。

你還可以配置 Roolup 生成 ES 模組構建、IIFE 構建,或 UMD 構建。比如我的 marky 專案就是一個擁有多個 Rollup 構建目標的簡單庫。

本文描述的專案(base64-encode-string) 已經發布到 npm 了,你可以去深入瞭解它。原始碼在 GitHub 上。

相關文章