Original blog: https://blog.rxliuli.com/p/b8a95af9134a488e9d94463bd18768c9/

problem

Compatibility problems are caused by the use of platform-specific functions, which can lead to the following situations

  • Different modular specifications: specify when rollup is packaged
  • Platform-specific code: For example, it contains adaptation codes for different platforms
  • Platform-specific dependencies: For example, 060cd80ecb0f40 needs to be filled in fetch/FormData
  • Platform-specific type definition: For example, Blob in the browser and 060cd80ecb0f68 in Buffer

Different modular specifications

This is a very common thing. There are already multiple specifications including cjs/amd/iife/umd/esm, so supporting them (or at least supporting mainstream cjs/esm) has become a must thing. Fortunately, the packaging tool rollup provides corresponding configurations to support output files in different formats.

GitHub sample project

Shaped like

// 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()],
})

Then specify in package.json

{
  "main": "dist/index.js",
  "module": "dist/index.esm.js",
  "types": "dist/index.d.ts"
}
Many libraries support cjs/esm, such as rollup , but there are also libraries that only support esm, such as unified.js series

Platform-specific code

  • Pack different export files through different entry files, and specify environment-related codes browser dist/browser.js / dist/node.js : When using, you need to pay attention to the packaging tool (transfer the cost to the user)
  • Use code to determine the dynamic loading of the operating environment
ComparedDifferent exportsCode judgment
advantageCode isolation is more thoroughDoes not depend on packaging tool behavior
The final code only contains the code of the current environment
DisadvantageDepends on the behavior of the user's packaging toolThe code for judging the environment may not be accurate
The final code contains all the code, but is selectively loaded
axios combines the above two methods to achieve browser and nodejs support, but at the same time it leads to the shortcomings of the two methods and a little confusing behavior. Refer to getDefaultAdapter . For example, in the jsdom environment, it will be considered as a browser environment, refer to 160cd80ecb117a detect jest and use http adapter instead of XMLHTTPRequest

Package different export files through different entry files

GitHub sample project
// 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"
  }
}

Use code to determine the dynamic loading of the operating environment

GitHub sample project

Basically, it is judged in the code and then 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()],
})
Note: vitejs cannot bundle this kind of package, because the nodejs native package does not exist in the browser environment. This is a known error. Refer to: Cannot use amplify-js in browser environment (breaking vite/snowpack/esbuild) .

Platform-specific dependencies

  • Direct import dependent use: it will blow up in different environments (for example, node-fetch will blow up in the browser)
  • In the code, it is judged that the require runtime: it will cause it to be packaged and loaded even if it is not used
  • In the code, it is judged that the import() runtime: it will lead to code segmentation, and the dependency is selectively loaded as a separate file
  • Pack different export files through different entry files, such as dist/browser.js / dist/node.js : you need to pay attention when using it (transfer the cost to the user)
  • Declare peerDependencies optional dependencies, let users fill in by themselves: pay attention when using (pass the cost to the user)
Comparedrequireimport
Will it be loadedYesno
Does the developer need to pay attentionnono
Will it be loaded multiple timesnoYes
Is it synchronizedYesno
rollup supportYesYes

In the code to determine the runtime through the dynamic introduction of dependencies require

GitHub project example
// 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()
  }
}

1624018106300

In the code to determine the runtime, dynamically introduce dependencies import()

GitHub project example
// 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()
  }
}

Pack the result

1624018026889

Some sub-problems encountered

  • How to judge whether there are global variables

    typeof fetch === 'undefined'
  • How to write polyfill for global variables in different environments

    const globalVar: any =
      (typeof globalThis !== 'undefined' && globalThis) ||
      (typeof self !== 'undefined' && self) ||
      (typeof global !== 'undefined' && global) ||
      {}
  • TypeError: Right-hand side of 'instanceof' is not callable : Mainly axios will judge FormData , and form-data has a default export, so you need to use (await import('form-data')).default (we always feel like we are digging for ourselves)
    1622828175546

Users may encounter compatibility issues when using rollup to package. In fact, they need to choose between inline to the code or separately packaged into a file. Refer to: https://rollupjs.org/guide/en/#inlinedynamicimports

Inline => Outbound

// 内联
export default {
  output: {
    file: 'dist/extension.js',
    format: 'cjs',
    sourcemap: true,
  },
}
// 外联
export default {
  output: {
    dir: 'dist',
    format: 'cjs',
    sourcemap: true,
  },
}

Platform-specific type definition

The following solutions are essentially multiple bundles

  • Mixed type definition. E.g. axios
  • Different export documents and type definitions are packaged, and users are required to specify the required documents by themselves. For example, module/node / module/browser (in fact, it is very close to the plug-in system, it is nothing more than whether to separate multiple modules)
  • Use the plug-in system to separate the adaptation codes of different environments into multiple sub-modules. E.g. remark.js community
ComparedMultiple type definition filesMixed type definitionMulti-module
advantageEnvironmental designation is clearerUnified entranceEnvironmental designation is clearer
DisadvantageNeed users to choose by themselvesType definition redundancyNeed users to choose by themselves
dependencies redundant Relatively troublesome to maintain (especially when the maintainer is not alone)

Pack different export documents and type definitions, and require users to specify the required documents by themselves

GitHub project example

It is mainly to make a layer of abstraction in the core code, and then extract the platform-specific code out and package it separately.

// 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()],
  },
])

User example

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)))
// }

Use a plug-in system to separate the adaptation code of different environments into multiple sub-modules

Simply put, if you want to spread the runtime dependencies into different submodules (such as the node-fetch above), or your plug-in API is very powerful, then you can separate some official adaptation code into plug-in submodules.

select

兼容 nodejs 与浏览器的库的技术方案选择.drawio.svg


rxliuli
709 声望26 粉丝

算了,还是不在这儿玩了,国内什么论坛大了都会变成泥潭。。。