怎樣寫一個能同時用于 Node 和瀏覽器的 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