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 inBuffer
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
Compared | Different exports | Code judgment |
---|---|---|
advantage | Code isolation is more thorough | Does not depend on packaging tool behavior |
The final code only contains the code of the current environment | ||
Disadvantage | Depends on the behavior of the user's packaging tool | The 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)
Compared | require | import |
---|---|---|
Will it be loaded | Yes | no |
Does the developer need to pay attention | no | no |
Will it be loaded multiple times | no | Yes |
Is it synchronized | Yes | no |
rollup support | Yes | Yes |
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()
}
}
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
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 judgeFormData
, andform-data
has a default export, so you need to use(await import('form-data')).default
(we always feel like we are digging for ourselves)
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
Compared | Multiple type definition files | Mixed type definition | Multi-module |
---|---|---|---|
advantage | Environmental designation is clearer | Unified entrance | Environmental designation is clearer |
Disadvantage | Need users to choose by themselves | Type definition redundancy | Need 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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。