Question
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, nodejs needs to fill in
fetch/FormData
- Platform-specific type definitions: such as
Blob
in the browser andBuffer
in nodejs
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 also become a must thing. Fortunately, the packaging tool rollup provides corresponding configurations to support output files in different formats.
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-limited code
-Package different export files through different entry files, and specify environment-related code through browser
, such as dist/browser.js
/dist/node.js
: you need to pay attention to the packaging tool when using it (transfer the cost To users)
-Use code to determine the dynamic loading of the operating environment
Comparison | Different Exports | Code Judgment |
---|---|---|
Advantages | More thorough code isolation | Does not depend on packaging tool behavior |
The final code only contains the code of the current environment | ||
Disadvantages | 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 codes, 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, please refer to detect jest and use http adapter instead of XMLHTTPRequest
Pack different export files through different entry files
// 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 runtime environment
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 use of
import
as a dependency: it will explode in different environments (for example,node-fetch
will explode in the browser) - It is judged in the code that the dependency is dynamically introduced through
require
at runtime: it will cause it to be packaged and loaded even if it is not used - Dynamically introduce dependencies through
import()
when judged in the code at runtime: it will lead to code segmentation, and dependencies are selectively loaded as separate files - Package 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)
Contrast | 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, dynamically introduce dependencies through require
// 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) ||
{}
// The key is the dynamic require here
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 through import()
// src/adapters/BaseAdapter.ts
import { BaseAdapter } from './BaseAdapter'
export class BrowserAdapter implements BaseAdapter {
// Note that this has become an asynchronous function
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 ployfill 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
(My generation always feels like digging a hole for myself)
Users may encounter compatibility issues when using rollup packaging. In fact, they need to choose whether to inline the code or package them separately into a file. Refer to: https://rollupjs.org/guide/en/#inlinedynamicimports
Inline => outline
// inline
export default {
output: {
file: 'dist/extension.js',
format: 'cjs',
sourcemap: true,
},
}
// Outreach
export default {
output: {
dir: 'dist',
format: 'cjs',
sourcemap: true,
},
}
Platform-limited type definition
The following solutions are essentially multiple bundles
- Mixed type definition. E.g. axios
- Pack different export documents and type definitions, and require users to specify the required documents by themselves. For example, load different functions through
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
Comparison | Multiple type definition files | Mixed type definition | Multi-module |
---|---|---|---|
Advantages | Clearer environmental designation | Unified entrance | Clearer environmental designation |
Disadvantages | Need to choose by the user | Type definition redundancy | Need to choose by the user |
Redundant dependencies | 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
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 the plug-in system to separate the adaptation code of different environments into multiple submodules
Simply put, if you want to spread runtime dependencies into different submodules (such as the node-fetch
above), or your plug-in API is very powerful, then you can use some official adaptation code Separate into plug-in sub-modules.
Top comments (0)