博客原文: 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 很是强大,那么即可以将一些官方适配代码分离为插件子模块。