IMG_9748.txt.txt
Edited: Thursday 1 May 2025

Saturday, June 5, 2021 at 00:04

编 写 兼 容 nodejs/ 浏 览 器 的 库
rxliuli
rxliuli blog

问 题
兼 容 问 题 是 由 于 使 用 了 平 台 特 定 的 功 能 导 致 , 会 导 致 下 面 几 种 情 况

。 不 同 的 模 块 化 规 范 : rollup 打 包 时 指 定

。 平 台 限 定 的 代 码 : 例 如 包 含 不 同 平 台 的 适 配 代 码

。 平 台 限 定 的 依 赖 : 例 如 在 nodejs 需 要 填 充 fetch/Formpata
。 平 台 限 定 的 类 型 定 义 : 例 如 浏 览 器 中 的 Blob 和 nodejs 中 的

不 同 的 模 块 化 规 范

这 是 很 常 见 的 一 件 事 , 现 在 就 已 经 有 包 括 cjs/amdyiife/umd/esm 多 种 规
范 了 , 所 以 支 持 它 们 ( 或 者 说 , 至 少 支 持 主 流 的 cjslesm) 也 成 为 必 须
做 的 一 件 事 。 幸 运 的 是 , 打 包 工 其 rollup 提 供 了 相 应 的 配 置 支 持 不 同 格
式 的 输 出 文 件 。

GitHub 示 例 项 目
形 如
/ rollup.config.jsexport default defineConfig({ input:
“src/index.ts“, “outPut: [ { format: “cjs“, fiLe :
“aist/index.js“, sourcemap: true ] 1 { format: “esm “ , fiLe :

“Qist/index.esm.js“, sourcemap: true } , ] , PLugins :
[typescript() 1 心 ) ;
形 如

/ rollup.config.jsexport default defineConfig({ input:
“src/index.ts“, “outPut: [ { format: “cjs“, fiLe :
“aist/index.js“, sourcemap: true ] 1 { format: “esm “ , fiLe :
“Qist/index.esm.js“, sourcemap: true } , ] , PLugins :
[typescript()111) ;

然 后 在 package.json 中 指 定 即 可

{ “main“:; “dqist/index.js8“, “module“: “dist/index.esm.j58“,
“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

a

GitHub 示 例 项 目
/ rollup.config.jsexport default defineConfig({ _input:
[“src/index.ts“, “src/browser.ts“] , output: [ { Qir:
“Qist/cjs“,format: “cjs“, sourcemap: true ] 1 { Qir:
“Qist/esm“ , format: “esm “ , sourcemap: true } , ] , PLugins :

[typescript() 1 ) ;

{ atm「 8 “Qist/cjs/index.js“, “module“: “dist/esm/index.js“,
“types“: “dist/ index. dQ.ts“;「 “browser“ :

“dSEycjsyinaex- js“: “dist/cjs/browser .js“,
“aist/esm/index.js“: “dist/esm/browser.js“ ]
使 用 代 码 判 断 运 行 环 境 动 态 加 载

GitHub 示 例 项 目

基 本 上 就 是 在 代 码 中 判 断 然 后 avait import 而 已

import { BaseAdapter } from “./adapters/Basehdapter“;import {
CLass } from “type-fest“;export cLass Adapter implements
BaseAdapter { private adapter?: BaseAdapter; _private async

Linit() if (this.adapter) 《 return ; ] Let
Adapter: Class ; if (typeof fetch ===
“undefined“) 《 RAdapter = (await
import(“./adapters/NodeAdapter“) ) .NodeAdapter ; } else t
RAdapter = (await

import(“./adapters/BrowserAdapter“) ) .BrowserAdapter ; ]
this.adapter = new Adapter(); 】 “async get(urL: String ) :
Promise { await this .init ( ) ; returm
this.adapter!.get(urL); }}

/ rollup.config.jsexport default defineConfig({ _input:
“src/index.ts“, “_outPut: { Qir: “dist“, format: “cjs“,
sourcemap: true }, plugins: [typescript() ] 心 ) ;

基 本 上 就 是 在 代 码 中 判 断 然 后 avait import 而 已

import { BaseAdapter } from “./adapters/Basehdapter“;import {
CLass } from “type-fest“;export cLass Adapter implements
BaseAdapter { private adapter?: BaseAdapter; _private async

Linit() if (this.adapter) 《 return ; ] Let
Adapter: Class ; if (typeof fetch ===
“undefined“) 《 RAdapter = (await
import(“./adapters/NodeAdapter“) ) .NodeAdapter ; } else t
RAdapter = (await

import(“./adapters/BrowserAdapter“) ) .BrowserAdapter ; ]
this.adapter = new Adapter(); 】 “async get(urL: String ) :
Promise { await this .init ( ) ; returm

this.adapter!.get(urL); }}

/ rollup.config.jsexport default defineConfig({ _input:
“src/index.ts“, “_outPut: { Qir: “dist“, format: “cjs“,
sourcemap: true }, plugins: [typescript() ] 心 ) ;

注 : vitejs 无 法 捆 绑 处 理 这 种 包 , 因 为 nodejs 原 生 包 在 浏 览 器 环 境 确
实 不 存 在 , 这 是 一 个 已 知 错 误 , 参 考 : Cannot use amplify-js in
browser environment (breaking vite/snowpack/esbuild) 。

平 台 限 定 的 依 赖

。 直 接 import 依 赖 使 用 : 会 导 致 在 不 同 的 环 境 炸 掉 ( 例 如 noae-
fetch 在 浏 览 器 就 会 炸 掉 )

。 在 代 码 中 判 断 运 行 时 通 过 require 动 态 引 入 依 赖 : 会 导 致 即 便 用
不 到 , 也 仍 然 会 被 打 包 加 载

。 在 代 码 中 判 断 运 行 时 通 过 import ( ) 动 态 引 入 依 赖 : 会 导 致 代 码
分 割 , 依 赖 作 为 单 独 的 文 件 选 择 性 加 载

。 通 过 不 同 的 入 口 文 件 打 包 不 同 的 出 口 文 件 , 例 如
dist/browser 。 js/dist/node > js : 使 用 时 需 要 注 意 ( 将 成 本 转 嫁
给 使 用 者 )

团 声 明 必 SerDepepdencte5 可 选 诊 让 使 用 者 自 行 填 充 : 使 用 时
。 直 接 import 依 赖 使 用 : 会 导 致 在 不 同 的 环 境 炸 掉 ( 例 如 noae-
fetch 在 浏 览 器 就 会 炸 掉 )

。 在 代 码 中 判 断 运 行 时 通 过 require 动 态 引 入 依 赖 : 会 导 致 即 便 用
不 到 , 也 仍 然 会 被 打 包 加 载

。 在 代 码 中 判 断 运 行 时 通 过 import( ) 动 态 引 入 依 赖 : 会 导 致 代 码
分 割 , 依 赖 作 为 单 独 的 文 件 选 择 性 加 载

。 通 过 不 同 的 入 口 文 件 打 包 不 同 的 出 口 文 件 , 例 如
dist/browser .js/dist/node .js : 使 用 时 需 要 注 意 ( 将 成 本 转 嫁
给 使 用 者 )

s 声 阮 peerDependencies 可 选 依 赖 , 让 使 用 者 自 行 填 充 : 使 用 时
需 要 注 意 ( 将 成 本 转 嫁 给 使 用 者 )

对 比 redquire import

是 否 一 定 会 加 载 “ 是 否

是 否 需 要 开 发 者 注 意 否 否

是 否 会 多 次 加 载 “ 否 “ 是

是 否 同 步 是 。 否

rollup 支 持 是 “ 是

在 代 码 中 判 断 运 行 时 通 过 require 动 态 引 入 依 赖
GitHub 项 目 示 例

// src/adapters/BaseRdapter .tsimport { BaseAdapter } from
“./Basehdapter“;export clL1ass BrowserhRdapter implements

BaseAdapter { private static init() t if (typeof fetch ===
“undefined“) 《 const gLobalVar: any = (typeof
gLobalrhis !==“undefined“&& gLlobalrhis) | | (typeof
self !==“undefined“&g& self) | | (typeof gLobal !==
“undefined“&& gLlobal) | | 17 7 / 关 键 在 于 这 里 的 动 态
require Reflect.set(gLobalVar, “fetch“,require(“node-
fetch“) .default) ; } 》 “async get(url: String ) :
Promise { BrowserAdapter .init ( ) ; return (await

fetch(url1) ) .json( ); 7
GitHub 项 目 示 例

// src/adapters/BaseAdapter .tsimport { BaseAdapter } from
“./Basehdapter“;export cL1ass BrowserhRdapter implements
BaseAdapter { private static init() t if (typeof fetch
“undefined“) 《 const gLlobalVar: any = (typeof
globalrhis !==“undefined“ && globalrhis) | | (typeof
Se1f “undefined“&& selfE) | | (typeof gLobal !
“undefined“&& gLlobal) | | {77 / 关 键 在 于 这 里 的 动 态
require Reflect.set(gLobalVar, “fetch“,require(“node-
fetch“) .default) ; } 》 “async get(url: String ) :
Promise { BrowserAdapter .init ( ) ; return (await
fetch(url1)).json(); 7

E E e gt D 。 C
cortlene lin ninettn 日 1 s 0 一 at 一 > 观 Q 3
圈 hlet 、
v corhtenrlirdee 团 tody i dieaial or f e line 5 eat
o
lib = bject.freeze( )
requtrei8 = pethugaentedamespace(lib)
6 Brovserhdspter {
init0 {
6 ( fotoh 一 ) {
E globalmhis 丨 88 lobalhis) 川
( self 十 E
( glabal 十 8 global) 1

b82 4 Reflect.set(olobalyar requtrei8.default)
}
1
getorD)

C wu t0
白 rollup v2.82,1
bundles src/indexts -dist
d dist in 48tas

[2821-86-18 28:87:25] waiting for changes..。

n 国 heua cuee

里 nt io
GitHub 项 目 示 例

// src/adapters/BaseAdapter .tsimport { BaseAdapter } from
“./Basehdapter“;export clL1ass Browserhdapter implements
BaseAdapter { // 注 意 , 这 里 变 成 异 步 的 函 数 了 private static async
Linit() if (typeof fetch =“undefined“) 《 const
gLobalVar: any = (typeof gLobalThis “ undefined“ &&
gLobalrhis) | | (typeof self ! “undefined“&& selfE) | |
(typeof global !==,“undefined“g&g global) | | {7 7
Reflect.set(gLobalVar, “fetch“, (await import(“node-

fetch“) ) .default) ; } 》 “async get(urlL: String ) :
Promise { await BrowserRdapter .init.( ) ; return (await
fetch(url1)).json(); 7

打 包 结 果

E ec beu 0

om on

io

om
cuaremue object.defineproperty(exports E valuex 日

E
Psye nit0 t
( fotch 一 )
globaltar = ( globalihis 十 88 globalhis)
( self 十 88 self 1
( glabal 十 88 globaD) 川

Reflect.set(globallar ( prosiseresolve0.then( O { require(
); )-.default)
1
}
na urn geturl) 【
E Broveerhdapter.init(
wwwissar ttt0
2 o wua 00 : 一
Lup 2.52.1
iles src/indexts -dist
d dist in 72ms

21-86-18 28:p6:88] aiting for changes..。

。 怎 么 判 断 是 否 存 在 全 局 变 量

typeof fetch ===“undefined“;

。 怎 么 为 不 同 环 境 的 全 局 变 量 写 入 ployfil
const gLlobalVar: any = (typeof gLlobalThis !==“undefined「“
&& _globalThis) || “(typeof self !==“undefined“ && se1l5f)
11 (typeof global !==“undefined“ && glopbal) || {77}

TYypeError: Right-hand side of “instanceof「 is not
callable: 主 要 是 axios 会 判 断 Formpata, 而 form-data 则 存 在
默 认 导 出 , 所 以 需 要 使 用 (avait import(「form-

adata「 ) ) -aefault ( 吾 辈 总 有 种 在 给 自 己 挖 坑 的 感 觉 )

TypeError: Right-hand side of “instanceof is not callable

at |0bject.isFormData| (

at transformRequest 〔
at transform (

at 0bject.forEach (
at transformData (

at dispatchRequest (

at processTicksAndRejections (internal/process/task_queues.js:95:5)
at Adjax.request (

at Joplinhpi.pingPort (

at 0bject. (

使 用 者 在 使 用 rollup 打 包 时 可 能 会 遇 到 兼 容 性 的 问 题 , 实 际 上 就 是 需
选 择 内 联 到 代 码 还 是 单 独 打 包 成 一 个 文 件 , 参

考 : https://rollupjs.org/guide/en/…

内 联 => 外 联

//_ 内 联 export_default_{__output: { fiLe:
“aist/s extension . j , format: “cjs“ ] sourcemap: true ,
] 7
// 外 联 export default { “output: { GQir: “Qist“, format :
“cjs “ sourcemap: true, 了 1 ;

人 , 儿 开 | 宇 人
平 台 限 定 的 类 型 定 义
以 下 解 决 方 案 本 质 上 都 是 多 个 bundle

。 湾 合 类 型 定 义 。 例 如 axios
。 打 包 一 同 的 出 口 文 件 和 类 型 定 义 , 要 求 使 用 者 自 行 指 定 需 要 的 文
件 。 例 如 通 过 module/node/module/browser 加 载 不 同 的 功 能
( 其 实 和 插 件 系 统 非 常 接 近 , 无 非 是 否 分 离 多 个 模 块 罢 了 )

。 使 用 插 件 系 统 将 不 同 环 境 的 适 配 代 码 分 窗 为 多 个 子 模 块 。 例 如
remark.js 社 区

多 个 类 烈 定 义 文 混 忧 粘 型 多 模 块
环 境 指 定 更 明 确 统 一 入 口 “ 环 境 指 定 更 明 确
霭 霭 使 用 者 自 行 类 柯 定 义 “ 需 要 使 用 者 自 行 选 择

选 择
dependencies 维 护 起 来 相 对 麻 烦 ( 尤 其 是 维 护 者 不 是 一 个
元 余 人 的 时 候 )

汉 第 汝 弄 氏 岗

打 包 不 同 的 出 口 文 件 和 类 型 定 义 , 要 求 使 用 者 自 行 指 定
需 要 的 文 件

GitHub 项 目 示 例

者 要 是 在 核 心 仁 码 做 一 层 抽 象 , 然 后 将 平 台 特 定 的 代 码 抽 离 出 去 单 独 打
需 要 的 文 件
GitHub 项 目 示 例
者 要 是 在 核 心 仁 码 做 一 层 抽 象 , 然 后 将 平 台 特 定 的 代 码 抽 离 出 去 单 独 打

/ src/index.tsimport { BaseRdapter } from
“./adapters/Basehdapter“;export cl1ass Adapter implements
BaseAdapter { _upload: BaseAdapter[“upload「“ ] ;
constructor(private base: BaseAdapter) 《 this .uplLoad =
this.base.upload; }}

/ rollup.config.jsexport default defineconfig( [ input:
“src/index.ts“ , output: [ { dir: “dist/cjs“, format :
“cjs“, sourcemap: true ] 1 { dir: “dist/esm “ , format :
“esm “ , sourcemap: true ] , ] plLugins: [typescript() ]1, j} ,
{ input: [“src/adapters/BrowserRdapter .tS“,
“src/adapters/Nodehdapter .ts“] , Output: [ { Qir:
“aist/cjs/adapters“, format: “cjs“, sourcemap: true ] 1 {
dir: “dist/esm/adapters“ , format: “esm “ , sourcemap: true ] ,

1, plugins: [typescript(11, }11);

使 用 者 示 例

import { Adapter } from“platform-specific-type-definition-
multiple-bundle“;import { BrowserRdapter } from “platform-
specific-type-definition-mulLtiple-
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/NodeRdapter“// export async
function node() {/V const adapter = new Adapter (new
NodeAdapter( ) ) // console.Log(「node: “ await
adapter.upload(new Buffer(10)) )/7/ }

玄 J 乞 j 之 丨
那 个 node- fetch)

, 或 者 你 的 插 件 API 非 常 强 大 , 那 么 便 可 以 将 一 些
官 方 适 配 代 码 分 离 为 插 件 子 模 块 。
选 择

简 单 来 说 , 如 果 你 希 望 将 运 行 时 依 赖 分 散 到 不 同 的 子 模 块 中 ( 例 如 上 面

打 包 不 同 格 式 的
bundle 通 过 module
区 分

Backlinks