如何編寫同時用於 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 構建目標的簡單庫。
本文描述的專案(base64-encode-string) 已經發布到 npm 了,你可以去深入瞭解它。原始碼在 GitHub 上。
相關文章
- javascript事件迴圈(瀏覽器/node)JavaScript事件瀏覽器
- 瀏覽器eventLoop和node eventLoop瀏覽器OOP
- 瀏覽器和node的eventLoop的區別瀏覽器OOP
- 教你如何同時執行經典版和Chromium版Edge瀏覽器瀏覽器
- [譯] 如何在瀏覽器中編寫一款藍芽應用瀏覽器藍芽
- 瀏覽器的event loop和node的event loop瀏覽器OOP
- 瀏覽器和Node.js中的Event Loop瀏覽器Node.jsOOP
- astro中瀏覽器端使用lit編寫的componentsAST瀏覽器
- 利用 Powershell 編寫簡單的瀏覽器指令碼瀏覽器指令碼
- Node.js的勁敵來了:Deno是用於在Web瀏覽器之外執行JavaScript和TypeScript的執行環境Node.jsWeb瀏覽器JavaScriptTypeScript
- 瀏覽器event loop和node的event loop講解瀏覽器OOP
- 瀏覽器和Node不同的事件迴圈(Event Loop)瀏覽器事件OOP
- 開發一個適用於 nodejs 與瀏覽器的 npm 包 - 基於 rollupjsNodeJS瀏覽器NPM
- JavaScript判斷系統和瀏覽器JavaScript瀏覽器
- 瀏覽器中的事件流和node中處理時間迴圈的分析瀏覽器事件
- JavaScript 判斷瀏覽器的型別和版本JavaScript瀏覽器型別
- JavaScript瀏覽器事件物件JavaScript瀏覽器事件物件
- 谷歌瀏覽器禁用JavaScript谷歌瀏覽器JavaScript
- JavaScript中的瀏覽器檢測和DOM基礎JavaScript瀏覽器
- js事件迴圈機制 EventLoop 【瀏覽器和node】JS事件OOP瀏覽器
- edge瀏覽器能編輯pdf嗎?win10系統如何使用edge瀏覽器編輯pdf瀏覽器Win10
- 如何在瀏覽器中測試JavaScript程式碼?瀏覽器JavaScript
- 瀏覽器事件環和Node事件環不得不說的故事!瀏覽器事件
- 理解瀏覽器和node.js中的Event loop事件迴圈瀏覽器Node.jsOOP事件
- 寫一個簡單的支援Node.js&瀏覽器的自定義事件庫Node.js瀏覽器事件
- [譯] 如何編寫全棧 JavaScript 應用全棧JavaScript
- 如何更改iPhone和iPad的預設瀏覽器iPhoneiPad瀏覽器
- 關於瀏覽器相容瀏覽器
- 瀏覽器的session何時消失?瀏覽器Session
- JavaScript 複習之瀏覽器模型JavaScript瀏覽器模型
- 用 JavaScript 編寫 MPEG1 解碼器JavaScript
- 跨瀏覽器的JavaScript效能檢測工具瀏覽器JavaScript
- 翻譯 | 擺脫瀏覽器限制的JavaScript瀏覽器JavaScript
- 瀏覽器 Javascript 的 EventLoop 動態圖析瀏覽器JavaScriptOOP
- Mechanize庫,用於模擬瀏覽器行為瀏覽器
- Orchest是用於資料科學的基於瀏覽器的IDE資料科學瀏覽器IDE
- 如何重新整理瀏覽器的應用快取?瀏覽器快取
- puppeteer中如何複用啟動中的瀏覽器瀏覽器
- 如何在瀏覽器上新增一鍵式填寫瀏覽器