博客原文:https://blog.rxliuli.com/p/b8...

问题

兼容问题是因为应用了平台特定的性能导致,会导致上面几种状况

  • 不同的模块化标准:rollup 打包时指定
  • 平台限定的代码:例如蕴含不同平台的适配代码
  • 平台限定的依赖:例如在 nodejs 须要填充 fetch/FormData
  • 平台限定的类型定义:例如浏览器中的 Blob 和 nodejs 中的 Buffer

不同的模块化标准

这是很常见的一件事,当初就曾经有包含 cjs/amd/iife/umd/esm 多种标准了,所以反对它们(或者说,至多反对支流的 cjs/esm)也成为必须做的一件事。侥幸的是,打包工具 rollup 提供了相应的配置反对不同格局的输入文件。

GitHub 示例我的项目

形如

// rollup.config.jsexport 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.jsexport 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.jsexport 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 可选依赖,让使用者自行填充:应用时须要留神(将老本转嫁给使用者)
比照requireimport
是否肯定会加载
是否须要开发者留神
是否会屡次加载
是否同步
rollup 反对

在代码中判断运行时通过 require 动静引入依赖

GitHub 我的项目示例
// src/adapters/BaseAdapter.tsimport { 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.tsimport { 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.tsimport { 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.jsexport 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 十分弱小,那么便能够将一些官网适配代码拆散为插件子模块。

抉择