博客原文: https://blog.rxliuli.com/p/b8...
兼容問題是因爲使用了平臺特定的功能致使,會致使下面幾種狀況node
fetch/FormData
Blob
和 nodejs 中的 Buffer
這是很常見的一件事,如今就已經有包括 cjs/amd/iife/umd/esm 多種規範了,因此支持它們(或者說,至少支持主流的 cjs/esm)也成爲必須作的一件事。幸運的是,打包工具 rollup 提供了相應的配置支持不一樣格式的輸出文件。ios
GitHub 示例項目
形如git
// rollup.config.js export default defineConfig({ input: 'src/index.ts', output: [ { format: 'cjs', file: 'dist/index.js', sourcemap: true }, { format: 'esm', file: 'dist/index.esm.js', sourcemap: true }, ], plugins: [typescript()], })
而後在 package.json 中指定便可github
{ "main": "dist/index.js", "module": "dist/index.esm.js", "types": "dist/index.d.ts" }
許多庫都支持 cjs/esm,例如 rollup,但也有僅支持 esm 的庫,例如 unified.js 系列
browser
指定環境相關的代碼,例如 dist/browser.js
/dist/node.js
:使用時須要注意打包工具(將成本轉嫁給使用者)對比 | 不一樣出口 | 代碼判斷 |
---|---|---|
優勢 | 代碼隔離的更完全 | 不依賴於打包工具行爲 |
最終代碼僅包含當前環境的代碼 | ||
缺點 | 依賴於使用者的打包工具的行爲 | 判斷環境的代碼可能並不許確 |
最終代碼包含全部代碼,只是選擇性加載 |
axios 結合以上兩種方式實現了瀏覽器、nodejs 支持,但同時致使有着兩種方式的缺點並且有點迷惑行爲,參考 getDefaultAdapter。例如在 jsdom 環境會認爲是瀏覽器環境,參考 detect jest and use http adapter instead of XMLHTTPRequest
GitHub 示例項目
// rollup.config.js export default defineConfig({ input: ['src/index.ts', 'src/browser.ts'], output: [ { dir: 'dist/cjs', format: 'cjs', sourcemap: true }, { dir: 'dist/esm', format: 'esm', sourcemap: true }, ], plugins: [typescript()], })
{ "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "types": "dist/index.d.ts", "browser": { "dist/cjs/index.js": "dist/cjs/browser.js", "dist/esm/index.js": "dist/esm/browser.js" } }
GitHub 示例項目
基本上就是在代碼中判斷而後 await import
而已typescript
import { BaseAdapter } from './adapters/BaseAdapter' import { Class } from 'type-fest' export class Adapter implements BaseAdapter { private adapter?: BaseAdapter private async init() { if (this.adapter) { return } let Adapter: Class<BaseAdapter> if (typeof fetch === 'undefined') { Adapter = (await import('./adapters/NodeAdapter')).NodeAdapter } else { Adapter = (await import('./adapters/BrowserAdapter')).BrowserAdapter } this.adapter = new Adapter() } async get<T>(url: string): Promise<T> { await this.init() return this.adapter!.get(url) } }
// rollup.config.js export default defineConfig({ input: 'src/index.ts', output: { dir: 'dist', format: 'cjs', sourcemap: true }, plugins: [typescript()], })
注: vitejs 沒法捆綁處理這種包,由於 nodejs 原生包在瀏覽器環境確實不存在,這是一個已知錯誤,參考: Cannot use amplify-js in browser environment (breaking vite/snowpack/esbuild)。
import
依賴使用:會致使在不一樣的環境炸掉(例如 node-fetch
在瀏覽器就會炸掉)require
動態 引入依賴:會致使即使用不到,也仍然會被打包加載import()
動態引入依賴:會致使代碼分割,依賴做爲單獨的文件選擇性加載dist/browser.js
/dist/node.js
:使用時須要注意(將成本轉嫁給使用者)peerDependencies
可選依賴,讓使用者自行填充:使用時須要注意(將成本轉嫁給使用者)對比 | require | import |
---|---|---|
是否必定會加載 | 是 | 否 |
是否須要開發者注意 | 否 | 否 |
是否會屢次加載 | 否 | 是 |
是否同步 | 是 | 否 |
rollup 支持 | 是 | 是 |
require
動態引入依賴GitHub 項目示例
// src/adapters/BaseAdapter.ts import { BaseAdapter } from './BaseAdapter' export class BrowserAdapter implements BaseAdapter { private static init() { if (typeof fetch === 'undefined') { const globalVar: any = (typeof globalThis !== 'undefined' && globalThis) || (typeof self !== 'undefined' && self) || (typeof global !== 'undefined' && global) || {} // 關鍵在於這裏的動態 require Reflect.set(globalVar, 'fetch', require('node-fetch').default) } } async get<T>(url: string): Promise<T> { BrowserAdapter.init() return (await fetch(url)).json() } }
import()
動態引入依賴GitHub 項目示例
// src/adapters/BaseAdapter.ts import { BaseAdapter } from './BaseAdapter' export class BrowserAdapter implements BaseAdapter { // 注意,這裏變成異步的函數了 private static async init() { if (typeof fetch === 'undefined') { const globalVar: any = (typeof globalThis !== 'undefined' && globalThis) || (typeof self !== 'undefined' && self) || (typeof global !== 'undefined' && global) || {} Reflect.set(globalVar, 'fetch', (await import('node-fetch')).default) } } async get<T>(url: string): Promise<T> { await BrowserAdapter.init() return (await fetch(url)).json() } }
打包結果json
怎麼判斷是否存在全局變量axios
typeof fetch === 'undefined'
怎麼爲不一樣環境的全局變量寫入 ployfill瀏覽器
const globalVar: any = (typeof globalThis !== 'undefined' && globalThis) || (typeof self !== 'undefined' && self) || (typeof global !== 'undefined' && global) || {}
TypeError: Right-hand side of 'instanceof' is not callable
: 主要是 axios 會判斷 FormData
,而 form-data
則存在默認導出,因此須要使用 (await import('form-data')).default
(吾輩總有種在給本身挖坑的感受)使用者在使用 rollup 打包時可能會遇到兼容性的問題,實際上就是須要選擇內聯到代碼仍是單獨打包成一個文件,參考:https://rollupjs.org/guide/en...app
內聯 => 外聯dom
// 內聯 export default { output: { file: 'dist/extension.js', format: 'cjs', sourcemap: true, }, }
// 外聯 export default { output: { dir: 'dist', format: 'cjs', sourcemap: true, }, }
如下解決方案本質上都是多個 bundle
module/node
/module/browser
加載不一樣的功能(其實和插件系統很是接近,無非是否分離多個模塊罷了)對比 | 多個類型定義文件 | 混合類型定義 | 多模塊 |
---|---|---|---|
優勢 | 環境指定更明確 | 統一入口 | 環境指定更明確 |
缺點 | 須要使用者自行選擇 | 類型定義冗餘 | 須要使用者自行選擇 |
dependencies 冗餘 | 維護起來相對麻煩(尤爲是維護者不是一我的的時候) |
GitHub 項目示例
主要是在覈心代碼作一層抽象,而後將平臺特定的代碼抽離出去單獨打包。
// src/index.ts import { BaseAdapter } from './adapters/BaseAdapter' export class Adapter<T> implements BaseAdapter<T> { upload: BaseAdapter<T>['upload'] constructor(private base: BaseAdapter<T>) { this.upload = this.base.upload } }
// rollup.config.js export default defineConfig([ { input: 'src/index.ts', output: [ { dir: 'dist/cjs', format: 'cjs', sourcemap: true }, { dir: 'dist/esm', format: 'esm', sourcemap: true }, ], plugins: [typescript()], }, { input: ['src/adapters/BrowserAdapter.ts', 'src/adapters/NodeAdapter.ts'], output: [ { dir: 'dist/cjs/adapters', format: 'cjs', sourcemap: true }, { dir: 'dist/esm/adapters', format: 'esm', sourcemap: true }, ], plugins: [typescript()], }, ])
使用者示例
import { Adapter } from 'platform-specific-type-definition-multiple-bundle' import { BrowserAdapter } from 'platform-specific-type-definition-multiple-bundle/dist/esm/adapters/BrowserAdapter' export async function browser() { const adapter = new Adapter(new BrowserAdapter()) console.log('browser: ', await adapter.upload(new Blob())) } // import { NodeAdapter } from 'platform-specific-type-definition-multiple-bundle/dist/esm/adapters/NodeAdapter' // export async function node() { // const adapter = new Adapter(new NodeAdapter()) // console.log('node: ', await adapter.upload(new Buffer(10))) // }
簡單來講,若是你但願將運行時依賴分散到不一樣的子模塊中(例如上面那個 node-fetch
),或者你的插件 API 很是強大,那麼即可以將一些官方適配代碼分離爲插件子模塊。