如何编写浏览器用crypto、服务端回退到node:crypto的npm包
自动适配浏览器/服务端的Crypto方案(无需用户额外配置)
针对你的react-querybuilder包在不同环境下的crypto适配需求,这里提供两种无需用户手动配置的解决方案:
方案一:单文件自动适配(改进现有代码)
现有代码的环境判断不够精准,在Webpack5这类默认不polyfill的构建工具中可能失效。下面是优化后的代码,能自动识别环境并加载对应crypto实现:
let cryptoModule: Crypto | undefined; // 优先匹配浏览器环境的crypto if (typeof window !== 'undefined' && window.crypto) { cryptoModule = window.crypto; } else if (typeof globalThis !== 'undefined' && globalThis.crypto) { cryptoModule = globalThis.crypto; } // Node.js环境下加载内置webcrypto else if (typeof process !== 'undefined' && process.release?.name === 'node') { // 动态import避免浏览器打包时引入node模块 const { webcrypto } = await import('node:crypto'); cryptoModule = webcrypto as Crypto; } // 兜底处理Webpack5/CRA v5这类需要polyfill的场景 if (!cryptoModule) { try { // 自动加载crypto-browserify作为polyfill const crypto = await import('crypto-browserify'); cryptoModule = crypto.webcrypto as Crypto; } catch (e) { throw new Error('crypto模块未找到,请确保环境支持Web Crypto API或安装crypto-browserify'); } } const template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"; const position19vals = ["8", "9", "a", "b"]; const re = /[xy]/g; const container = new Uint32Array(32); export const generateID = () => { if (!cryptoModule) throw new Error('crypto模块初始化失败'); cryptoModule.getRandomValues(container); let i = -1; return template.replaceAll(re, (char) => { i++; return char === "y" ? position19vals[container[i] % 4] : (container[i] % 16).toString(16); }); };
关键优化点:
- 明确区分浏览器/Node.js环境,避免在浏览器中触发Node模块的require逻辑
- 采用动态import加载node:crypto和crypto-browserify,只有在对应环境才会引入,不增加无关环境的打包体积
- 兜底逻辑自动处理Webpack5/CRA v5这类需要polyfill的场景,用户无需手动配置polyfill规则
如果需要同步初始化(避免await),可以用条件require配合打包工具的tree-shaking:
// Node.js环境同步加载 else if (typeof process !== 'undefined' && process.release?.name === 'node') { const { webcrypto } = require('node:crypto'); cryptoModule = webcrypto as Crypto; }
这种方式需要确保打包工具(如Webpack)能正确tree-shake掉Node环境的代码,避免浏览器打包时引入多余模块。
方案二:分环境构建(客户端/服务端分开打包)
如果接受拆分代码的方案,分环境构建会更清晰,且打包体积更小。用Rollup、Vite等工具配置两个入口:
1. 客户端入口(client.ts)
const cryptoModule = window.crypto; const template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"; const position19vals = ["8", "9", "a", "b"]; const re = /[xy]/g; const container = new Uint32Array(32); export const generateID = () => { cryptoModule.getRandomValues(container); let i = -1; return template.replaceAll(re, (char) => { i++; return char === "y" ? position19vals[container[i] % 4] : (container[i] % 16).toString(16); }); };
2. 服务端入口(server.ts)
import { webcrypto } from 'node:crypto'; const cryptoModule = webcrypto; const template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"; const position19vals = ["8", "9", "a", "b"]; const re = /[xy]/g; const container = new Uint32Array(32); export const generateID = () => { cryptoModule.getRandomValues(container); let i = -1; return template.replaceAll(re, (char) => { i++; return char === "y" ? position19vals[container[i] % 4] : (container[i] % 16).toString(16); }); };
3. 配置package.json入口
在package.json中指定不同环境的加载入口,让工具自动匹配:
"main": "./dist/server.cjs", "module": "./dist/server.esm.js", "browser": "./dist/client.esm.js", "types": "./dist/types/index.d.ts"
这样浏览器环境会自动加载browser字段对应的文件,Node.js环境加载main/module字段的文件,用户无需任何额外配置。
方案选择
- 方案一适合不想拆分代码的场景,兼容性更广,但需要处理异步加载或依赖打包工具的tree-shaking
- 方案二更轻量、清晰,适合对打包体积和性能有要求的场景
内容的提问来源于stack exchange,提问作者Jake Boone
相关产品推荐
相关产品推荐

