怎樣寫一個能同時用于 Node 和瀏覽器的 JavaScript 包?

juak6655 7年前發布 | 9K 次閱讀 JavaScript開發 JavaScript

我在這個問題上見過很多困惑,即使是很有經驗的 JavaScript 開發者也可能難以把握其中的巧妙之處。因此我認為值得為它書寫一小段教程。

假設你有一個 JavaScript 的模塊想要發布到 npm 上,它是同時適用于 Node 和瀏覽器的。但是請注意!這個特殊的模塊在 Node 版本和瀏覽器版本上的實現有著細微的區別。

這種情況出現得實在頻繁,因為在 Node 和瀏覽器間有著很多微小的環境差別。在這種情況下,可以用比較巧妙的方法來正確地實現,尤其是當你在嘗試著使用最小的 browser 包(bundle)來優化的時候。

讓我們構建一個 JS 包

因此讓我們來寫一個小的 JavaScript 包,叫做 base64-encode-string 。它所做的只是接收一個字符串作為輸入,輸出其 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 ,來將它們打包進這些模塊。

對于這個簡單的九行模塊,我算了一下, 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 版的代碼,在 Browserfy 和 Webpack 里跑的人會得到瀏覽器版的代碼。

對于 Rollup 來說,這就有點復雜了,但也不需要太多額外的工作。Rollup 用戶需要使用 rollup-plugin-node-resolve 并在選項里將 browser 設置為 true 。

對 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.config.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');
  }
}

然后將 prepublish 步驟添加到 package.json 內,來生成文件:

{
  /* ... */
  "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 相關的代碼,對于瀏覽器只有瀏覽器相關的代碼。

作為附帶的福利,你可以配置 Rollup 來生成 ES 模塊構建,IIFE 構建,或者 UMD 構建。如果你想要示例的話,可以查看我的項目 marky ,這是一個擁有多個 Rollup 構建目標的簡單庫。

在這篇文章里描述的實際項目( base64-encode-string )也同樣被 發布到 npm 上 ,你可以審視它,看看它是怎么做到的。

 

來自:http://www.jianshu.com/p/b05869ac5fa8

 

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