博客原文:https://blog.rxliuli.com/p/b8…
问题
兼容问题是因为应用了平台特定的性能导致,会导致上面几种状况
- 不同的模块化标准:rollup 打包时指定
- 平台限定的代码:例如蕴含不同平台的适配代码
- 平台限定的依赖:例如在 nodejs 须要填充
fetch/FormData
- 平台限定的类型定义:例如浏览器中的
Blob
和 nodejs 中的Buffer
不同的模块化标准
这是很常见的一件事,当初就曾经有包含 cjs/amd/iife/umd/esm 多种标准了,所以反对它们(或者说,至多反对支流的 cjs/esm)也成为必须做的一件事。侥幸的是,打包工具 rollup 提供了相应的配置反对不同格局的输入文件。
GitHub 示例我的项目
形如
// 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 中指定即可
{
"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
而已
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()}
}
打包后果
遇到的一些子问题
-
怎么判断是否存在全局变量
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…
内联 => 外联
// 内联
export default {
output: {
file: 'dist/extension.js',
format: 'cjs',
sourcemap: true,
},
}
// 外联
export default {
output: {
dir: 'dist',
format: 'cjs',
sourcemap: true,
},
}
平台限定的类型定义
以下解决方案实质上都是多个 bundle
- 混合类型定义。例如 axios
- 打包不同的进口文件和类型定义,要求使用者自行指定须要的文件。例如通过
module/node
/module/browser
加载不同的性能(其实和插件零碎十分靠近,无非是否拆散多个模块罢了) - 应用插件零碎将不同环境的适配代码拆散为多个子模块。例如 remark.js 社区
比照 | 多个类型定义文件 | 混合类型定义 | 多模块 |
---|---|---|---|
长处 | 环境指定更明确 | 对立入口 | 环境指定更明确 |
毛病 | 须要使用者自行抉择 | 类型定义冗余 | 须要使用者自行抉择 |
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 十分弱小,那么便能够将一些 官网 适配代码拆散为插件子模块。
抉择