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

霜寒2010 7年前發布 | 12K 次閱讀 JavaScript開發 JavaScript

我多次看到大家在這個問題上產生困惑,甚至經驗豐富的 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 構建目標的簡單庫。

 

 

來自:http://web.jobbole.com/90678/

 

 本文由用戶 霜寒2010 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!