如何編寫同時用于 Node 和瀏覽器的 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/