怎樣寫一個能同時用于Node和瀏覽器的JavaScript包?
我在這個問題上見過很多困惑,即使是很有經(jīng)驗(yàn)的 JavaScript 開發(fā)者也可能難以把握其中的巧妙之處。因此我認(rèn)為值得為它書寫一小段教程。
我在這個問題上見過很多困惑,即使是很有經(jīng)驗(yàn)的 JavaScript 開發(fā)者也可能難以把握其中的巧妙之處。因此我認(rèn)為值得為它書寫一小段教程。
假設(shè)你有一個 JavaScript 的模塊想要發(fā)布到 npm 上,它是同時適用于 Node 和瀏覽器的。但是請注意!這個特殊的模塊在 Node 版本和瀏覽器版本上的實(shí)現(xiàn)有著細(xì)微的區(qū)別。
這種情況出現(xiàn)得實(shí)在頻繁,因?yàn)樵?Node 和瀏覽器間有著很多微小的環(huán)境差別。在這種情況下,可以用比較巧妙的方法來正確地實(shí)現(xiàn),尤其是當(dāng)你在嘗試著使用最小的 browser 包(bundle)來優(yōu)化的時候。
讓我們構(gòu)建一個 JS 包
因此讓我們來寫一個小的 JavaScript 包,叫做 base64-encode-string
。它所做的只是接收一個字符串作為輸入,輸出其 base64 編碼的版本。
對于瀏覽器來說,這很簡單:我們只需要使用自帶的 btoa
函數(shù):
- module.exports = function (string) {
- return btoa(string);
- };
然而在 Node 里并沒有 btoa
函數(shù)。因此,作為替代,我們需要自己創(chuàng)建一個 Buffer
,然后在上面調(diào)用 buffer.toString():
- module.exports = function (string) {
- return Buffer.from(string, 'binary').toString('base64');
- };
對于一個字符串,這兩者都應(yīng)提供其正確的 base64 編碼版本,比如:
- var b64encode = require('base64-encode-string');
- b64encode('foo'); // Zm9v
- b64encode('foobar'); // Zm9vYmFy
現(xiàn)在我們只需要一些方法來檢測我們究竟是在瀏覽器上運(yùn)行還是在 Node 上,好讓我們能保證使用正確的版本。Browserify 和 Webpack 都定義了一個叫 process.browser
的字段,它會返回 true
(譯者注:即瀏覽器環(huán)境下),然而在 Node 上這個字段返回 false
。所以我們只需要簡單地:
- if (process.browser) {
- module.exports = function (string) {
- return btoa(string);
- };
- } else {
- module.exports = function (string) {
- return Buffer.from(string, 'binary').toString('base64');
- };
- }
現(xiàn)在我們只需要把我們的文件命名為 index.js
,鍵入 npm publish
,我們就完成了,對不對?好的吧,這個方法有效,但不幸的是,這種實(shí)現(xiàn)有一個巨大的性能問題。
因?yàn)槲覀兊?index.js
文件包含了對 Node 自帶的 process
和 Buffer
模塊的引用,Browserify 和 Webpack 都會自動引入 其 polyfill,來將它們打包進(jìn)這些模塊。
對于這個簡單的九行模塊,我算了一下, Browserify 和 Webpack 會創(chuàng)建 一個壓縮后有 24.7KB 的包 (7.6KB min+gz)。對于這種東西,用掉的空間實(shí)在是太多,因?yàn)樵跒g覽器里,只需要 btoa
就能表示這個。
“browser” 字段,我該如何愛你
如果你在 Browserify 或者 Webpack 文檔里找解決這個問題的提示,你可能最后會發(fā)現(xiàn) node-browser-resolve。這是一個對于 package.json
內(nèi) "browser"
字段的規(guī)范,可以被用于定義在瀏覽器版本構(gòu)建時需要被換掉的東西。
使用這種技術(shù),我們可以將接下來這段加入我們的 package.json
:
- {
- /* ... */
- "browser": {
- "./index.js": "./browser.js"
- }
- }
然后將函數(shù)分割成兩個不同的文件: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);
- };
有了這次改進(jìn)以后,Browserify 和 Webpack 會給出 更加合理的包:Browserify 的包壓縮后是 511 字節(jié)(315 min+gz),Webpack 的包壓縮后是 550 字節(jié)(297 min+gz)。
當(dāng)我們將我們的包發(fā)布到 npm 時,在 Node 里運(yùn)行 require('base64-encode-string')
的人將得到 Node 版的代碼,在 Browserfy 和 Webpack 里跑的人會得到瀏覽器版的代碼。
對于 Rollup 來說,這就有點(diǎn)復(fù)雜了,但也不需要太多額外的工作。Rollup 用戶需要使用 rollup-plugin-node-resolve 并在選項里將 browser
設(shè)置為 true
。
對 jspm 來說,很不幸地,沒有對 “browser” 字段的支持,但是 jspm 用戶可以通過 require('base64-encode-string/browser')
或者 jspm install npm:base64-encode-string -o "{main:'browser.js'}"
來迂回地解決問題。另一種方法是,包的作者可以在他們的 package.json
里 指定一個 “jspm” 字段。
進(jìn)階技巧
這種直接使用的 "browser"
方法可以工作得很好,但是對于大型項目來說,我發(fā)現(xiàn)它在 package.json
和代碼庫間引入了一種尷尬的耦合。比如說,我們的 package.json
會很快長成這樣:
- {
- /* ... */
- "browser": {
- "./index.js": "./browser.js",
- "./widget.js": "./widget-browser.js",
- "./doodad.js": "./doodad-browser.js",
- /* etc. */
- }
- }
在這種情況下,任何時候你想要一個適配于瀏覽器的模塊,都需要分別創(chuàng)建兩個文件,并且要記住在 "browser"
字段上添加額外行來將它們連接起來。還要注意不能拼錯任何東西!
并且,你會發(fā)現(xiàn)你在費(fèi)盡心機(jī)地將微小的代碼提取到分離的模塊里,僅僅是因?yàn)槟阆胍苊?if (process.browser) {}
檢查。當(dāng)這些 *-browser.js
文件積累起來的時候,它們會開始讓代碼庫變得很難跳轉(zhuǎn)。
如果這種情況變得實(shí)在太笨重了,有一些別的解決方案。我自己的偏好是使用 Rollup 作為構(gòu)建工具,來自動地將單個代碼庫分割到不同的 index.js
和 browser.js
文件里。這對于將你提供給用戶的代碼的解模塊化有額外的價值,節(jié)省了空間和時間。
要這樣做的話,先安裝 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
作為一種方便地在瀏覽器構(gòu)建和 Node 構(gòu)建間切換的方式。)
接下來,我們可以創(chuàng)建一個帶有單個函數(shù)的 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
內(nèi),來生成文件:
- {
- /* ... */
- "scripts": {
- "prepublish": "rollup -c > index.js && BROWSER=true rollup -c > browser.js"
- }
- }
生成的文件都相當(dāng)直白易讀:
- // 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
轉(zhuǎn)換成 true
或者 false
,然后去掉那些無用代碼。所以在生成的瀏覽器包里不會有對于 process
或者 Buffer
的引用。
使用這個技巧,在你的代碼庫里可以有任意個的 process.browser
切換,并且發(fā)布的結(jié)果是兩個小的集中的 index.js
和 browser.js
文件,其中對于 Node 只有 Node 相關(guān)的代碼,對于瀏覽器只有瀏覽器相關(guān)的代碼。
作為附帶的福利,你可以配置 Rollup 來生成 ES 模塊構(gòu)建,IIFE 構(gòu)建,或者 UMD 構(gòu)建。如果你想要示例的話,可以查看我的項目 marky,這是一個擁有多個 Rollup 構(gòu)建目標(biāo)的簡單庫。
在這篇文章里描述的實(shí)際項目(base64-encode-string
)也同樣被 發(fā)布到 npm 上 ,你可以審視它,看看它是怎么做到的。源碼 在 GitHub 上。