
去年下半年接手的一个行业数据查询类项目前后端约定所有列表接口的响应体都走密文传输前端拿到数据之后先在本地解开、再交给状态管理渲染。项目本身不复杂麻烦的地方在于它是个两年多的老仓库webpack 从 4 升到 5 之后一直没彻底清账直到这次要往里面塞一个基于国密 SM4 的解密模块构建就直接炸了——终端里甩出一行Module not found: Error: Cant resolve crypto本地 dev 起不来CI 也跟着红。这篇文章就是把这套“数据解密 国密 SM4 webpack 打包模块补齐”的完整处理过程摊开讲一遍从我最初对需求的拆解到库的选型到 webpack 配置怎么补、补哪些、哪些其实不该补再到联调阶段被 base64 和 padding 折腾的两天最后是安全边界和打包体积的收尾。适合两类人看一类是手里有类似密文接口、要在浏览器里做国密解密的前端另一类是仓库里躺着一堆 Node 核心模块引用、每次构建都提心吊胆、想一次性把 webpack 模块补齐这件事弄明白的同学。我会尽量把“为什么这么配”讲清楚而不是只丢一份能跑的配置让你抄。1. 先把需求拆开这个项目到底要解决什么动手改配置之前我在白板上把整条链路画了一遍因为经验告诉我Cant resolve crypto这种报错最怕的就是头痛医头——你补了 crypto它下一个报 stream补了 stream 又报 buffer来回折腾一下午最后还是没搞明白自己的代码到底依赖了什么。所以第一步不是改 webpack而是把“谁在用什么”这件事搞清楚。1.1 数据链路还原密文从哪儿来到哪儿去这个项目的接口形态是这样的后端返回的 JSON 里业务字段不是明文对象而是一个字符串字段名叫data里面是 base64 编码的密文同一个响应里还有一个timestamp和nonce。前端拿到之后用约定的密钥和偏移量做 SM4 解密得到一个 JSON 字符串JSON.parse之后才是真正的业务数据。也就是说前端在这里承担了一个“解密代理”的角色。这条链路里有两个关键点值得先说清楚。第一解密发生在浏览器内存里意味着密钥最终一定会出现在用户机器的内存甚至源码里这一点后面第 6 章会专门讲先按下不表。第二解密是同步的、纯 CPU 计算没有网络请求、没有异步等待所以它必须足够快——列表页一屏 20 条数据每条都要解一次如果单次解密超过 1 毫秒滚动加载就会有明显的卡顿感。这两个约束直接决定了后面的选型不能用需要加载 wasm 的异步方案也不适合引入体积巨大的加密套件。我当时做了一件事把线上真实的一条响应体抓下来存成 fixture 文件放在项目里当测试用例。这个习惯强烈建议你养成解密这种“要么全对要么全错”的逻辑有一个真实样本在手排查效率会高一个数量级。后面所有关于 base64、padding、中文字符的坑都是靠这个 fixture 复现和验证的。1.2 为什么业务方选了 SM4 而不是常见的 AES这个问题的答案其实不在我这边但作为实现方必须理解否则你会在选库的时候走偏。SM4 是分组长度和密钥长度都是 128 位的对称分组密码和 AES-128 在结构定位上是对标的都属于“用同一把钥匙加锁和解锁”的类型。项目方选择它主要原因是合规层面有明确要求数据在传输和落盘环节需要使用国密算法族SM3 做摘要、SM4 做对称加密、SM2 做非对称。这是硬性约束不是技术偏好所以“能不能换成 AES”这个念头从一开始就不用有。理解了这一点选型的方向就很明确了我需要的是一个纯 JavaScript 实现、体积可控、API 简单、支持 ECB 和 CBC 两种模式、能处理 PKCS#7 填充的 SM4 库。之所以强调“纯 JS”是因为浏览器原生的 Web Crypto API 至今不支持 SM4crypto.subtle.importKey传SM4-CBC会直接抛错没有商量余地。这一点很多人第一次做国密前端时会踩以为能像 AES 一样调原生 API结果文档翻半天发现压根没这个算法。注意Node.js 环境是另一回事。Node 从 12 版本开始底层 OpenSSL 就带上了国密支持crypto.createCipheriv(sm4-cbc, key, iv)在服务端是可直接用的。如果你的项目有 SSR 或同构需求服务端和浏览器端可以走两套实现这个后面 6.2 会讲。1.3 “模块补齐”补的到底是什么坑回到报错本身。webpack 5 做了一个在当年引起很大争议的决策不再自动为 Node.js 核心模块注入 polyfill。webpack 4 时代你在代码里写require(crypto)打包器会默默地给你塞一个浏览器版实现进去一切看起来很正常。webpack 5 把这个行为取消了理由是——大多数前端项目其实根本不需要这些模块自动注入只会白白增加包体积而且会让开发者对自己代码的真实依赖产生误判。于是问题来了我这个老仓库里除了新加的 SM4 库还躺着几个年代久远的依赖它们内部引用了crypto、stream、buffer、path这些 Node 内置模块。在 webpack 4 下这些引用被自动兜住了升级到 5 之后就成了定时炸弹平时没人动它就不报错一旦你在业务代码里也引入了类似依赖整条依赖链就被拉进构建图报错集中爆发。所以“模块补齐”这个动作的本质是给 webpack 一份明确的映射表当代码里出现crypto这个模块标识符时去哪个 npm 包里找浏览器实现。它不是“修复 bug”而是“把 webpack 4 时代的隐式行为显式地写回来”。想通这一点后面配置里的resolve.fallback你就不会觉得是在抄玄学配置了。2. 方案选型库怎么挑polyfill 怎么配选型这一步我花了大半天主要是在几个 SM4 实现之间做对比顺便把 webpack 侧的补齐方案定下来。这里我把当时的对比过程和结论完整写出来你可以直接拿这个结论去用但更建议你理解对比的维度因为换个项目约束条件就变了。2.1 三款常见 SM4 实现的横向对比我实际试过三种路线一个是社区里用得最多的sm-crypto一个是基于它封装的gm-crypt还有一个是把 Java 端 BouncyCastle 的行为照搬、自己手写封装的方案。对比维度我列了五个体积、API 友好度、模式支持、依赖情况、维护活跃度。方案压缩后体积API 风格模式支持依赖适用场景sm-crypto约 30-40KB函数式sm4.encrypt/decryptECB、CBC无 Node 核心模块强依赖大多数前端项目首选gm-crypt约 40-50KB面向对象new SM4({...})ECB、CBC间接引入 sm-crypto需要统一配置对象的项目自研封装视实现而定完全可控按需可能需要 crypto/buffer有特殊 padding 或编码要求最后我选了sm-crypto理由不复杂它不强制依赖 Node 核心模块API 足够简单一个sm4.decrypt(cipher, key, { mode: cbc, iv })就完事而且我查了它的源码内部用的是纯数组运算没有偷偷require(crypto)。这一点非常关键——很多“模块补齐”的坑其实是你引入的第三方库自己带进来的。如果你选的库本身就依赖 Node 核心模块那你在 webpack 里补的那一堆 fallback本质上是在给别人的代码擦屁股包体积白白增加。gm-crypt我也试了它的好处是把 key、iv、mode、cipherType 全部收进一个配置对象代码读起来更整齐适合一个项目里有多套加密参数的场景。但它内部还是依赖sm-crypto等于多了一层封装和一点体积开销。如果你的需求就是“一个密钥、一种模式”没必要上这一层。提示选库之前一定去看一眼package.json的dependencies和源码里的require语句。我在一个项目里见过某加密库为了生成随机数引入了整个crypto-browserify直接让主包大了 200KB这种亏吃一次就够了。2.2 webpack 5 不再自动 polyfill 的前因后果这一节稍微展开讲讲原理因为理解了它你配fallback的时候就知道每一行在干嘛。webpack 的模块解析有一个概念叫“模块标识符到实现的映射”。当你的代码里写import crypto from cryptowebpack 会先判断这是不是相对路径不是的话就去node_modules里找名为crypto的包。在 Node 环境里crypto是内置模块不需要安装运行时直接可用但浏览器里没有这个东西打包器必须给它一个替代品这个替代品就叫 polyfill。webpack 4 的做法是内置一张表表中列出所有 Node 核心模块和它们对应的浏览器实现包遇到就自动替换。webpack 5 把这张表删了改成让你自己通过resolve.fallback声明。同时官方提供了一个node-polyfill-webpack-plugin插件可以一键把整张表加回来——但我个人不建议无脑用原因很简单它会把你项目里所有 Node 核心模块的引用全部补齐包括那些你压根走不到的代码路径包体积会明显膨胀。更精细的做法是逐个声明只补真正触发报错的那几个。另外还有一个容易被忽略的配置是ProvidePlugin。它解决的不是“模块找不到”而是“变量找不到”。比如某些库直接用全局的Buffer或process代码里没有 import 语句webpack 自然也不会去做模块解析这时候就需要ProvidePlugin主动把这些全局变量注入进去。这两个配置配合使用基本能覆盖 95% 的补齐场景。2.3 依赖清单与版本锁定我最终装的东西不多列一下npm i sm-crypto npm i -D crypto-browserify stream-browserify buffer processsm-crypto是业务依赖后面四个是构建期的补齐依赖。注意crypto-browserify本身还依赖randombytes、create-hash等一串包装完之后node_modules会多出几十个目录这是正常的不用慌。版本上我做了一件事把sm-crypto锁在次版本号写在package.json里用精确版本而不是^。理由是加密库的行为变更风险比业务库高万一某次小版本升级改了默认 padding 或者 hex/base64 的处理方式线上会出现大面积解密失败而且这种问题很难在测试环境复现因为测试环境装的可能是旧版本。锁版本这个动作看起来保守但在加密相关的依赖上我认为是必要的。3. 实操落地从报错到跑通这一章是完整的动手过程我按当时的实际顺序写先定位、再补配置、然后封装模块、最后用标准向量自证。每一步我都写了“为什么”和“踩到什么坑”你可以按这个顺序复现。3.1 第一步定位到底是哪个模块缺失报错信息通常长这样Module not found: Error: Cant resolve crypto in /project/node_modules/xxx/lib注意in后面那个路径它会告诉你到底是哪个包在引用。如果路径指向你自己的src目录说明是你写的代码直接引用了如果指向node_modules下某个包说明是间接依赖。这两种情况的处理方式不一样前者你其实可以考虑改掉自己的代码后者只能补齐。我当时的做法是用--stats输出一份详细构建报告看看依赖图里到底有哪些模块被标记为 external 或者 unresolvednpx webpack --stats detailed --json stats.json然后在 stats 里搜crypto、stream、buffer这几个关键词看它们分别被哪些模块引用。这一步做完我心里就有数了crypto是被一个老版本的哈希工具库引用的buffer是被 SM4 库间接带的stream则是那个哈希库的依赖链带出来的。三个模块三个来源其中只有buffer是我真正需要的。心得不要看到报错就一次性把crypto、stream、buffer、path、os、fs全配上。先补报错的那一个重新构建看下一个报什么再补。这样你至少知道每个补丁对应什么问题将来出问题也知道从哪儿查。一次性全配的结果是包体积莫名其妙变大而且你已经不知道是哪一行配置导致的。3.2 第二步webpack 配置补齐的完整写法定位清楚之后改配置。我用的是 webpack 5配置写在webpack.config.js里核心是两块resolve.fallback和plugins里的ProvidePlugin。const webpack require(webpack) const path require(path) module.exports { resolve: { fallback: { crypto: require.resolve(crypto-browserify), stream: require.resolve(stream-browserify), buffer: require.resolve(buffer/), // 明确告诉 webpack这些模块我们确实不用直接置空 fs: false, path: false, os: false, }, }, plugins: [ new webpack.ProvidePlugin({ Buffer: [buffer, Buffer], process: process/browser, }), ], }这里有几个细节值得单独说。buffer那行写的是require.resolve(buffer/)注意结尾那个斜杠——buffer这个包在本体里也有一个同名文件不加斜杠 webpack 可能会解析到错误的入口这个坑我踩过症状是Buffer is not defined或者类型判断异常。fs、path、os这几个置为false意思是“如果真被引用到就直接返回一个空对象”而不是报错。这招在只做浏览器端打包时很管用因为这些模块大概率只在某些库的服务端分支里被条件引用浏览器端根本走不到。ProvidePlugin里的Buffer注入也很关键。有些库不写import { Buffer } from buffer直接用全局变量webpack 不注入就会在运行时报Buffer is not defined。这个报错特别迷惑人因为构建阶段一切正常只有运行时才炸而且是白屏级别的炸。如果你项目里用了 Vite 或者 Rollup等价的做法是用resolve.alias加插件思路一致这里不展开。如果你确实懒得一个个配可以用node-polyfill-webpack-pluginconst NodePolyfillPlugin require(node-polyfill-webpack-plugin) module.exports { plugins: [new NodePolyfillPlugin()], }但我还是那句话能用精细配置就用精细配置插件方式适合“我就是想快速验证一下业务逻辑”的场景长期维护的项目还是老老实实一个个补。3.3 第三步封装一个可复用的解密模块配置通了之后我写了一个独立的解密模块src/utils/decryptor.js把所有编码转换、模式判断、错误处理都收在里面业务代码只调一个函数。const { sm4 } require(sm-crypto) /** * base64 转 hexsm-crypto 的默认输入是 hex */ function base64ToHex(base64) { const buf Buffer.from(base64, base64) return buf.toString(hex) } /** * SM4 解密主函数 * param {string} cipherText base64 编码的密文 * param {string} keyHex 32 位 hex 字符串16 字节 * param {string} ivHex 32 位 hex 字符串16 字节 * returns {object} 解析后的业务对象 */ function decryptPayload(cipherText, keyHex, ivHex) { if (!cipherText) return null // 参数校验前置避免把错误抛给库函数 if (!/^[0-9a-fA-F]{32}$/.test(keyHex)) { throw new Error([decrypt] key 必须是 32 位 hex 字符串) } if (!/^[0-9a-fA-F]{32}$/.test(ivHex)) { throw new Error([decrypt] iv 必须是 32 位 hex 字符串) } const hexCipher base64ToHex(cipherText) let plain try { plain sm4.decrypt(hexCipher, keyHex, { mode: cbc, iv: ivHex, output: string, }) } catch (e) { throw new Error([decrypt] 解密失败: ${e.message}) } if (!plain) { throw new Error([decrypt] 解密结果为空请检查密钥、IV 或密文格式) } try { return JSON.parse(plain) } catch (e) { throw new Error([decrypt] 明文不是合法 JSON: ${plain.slice(0, 50)}) } } module.exports { decryptPayload, base64ToHex }这段代码里有三个设计决策我解释一下为什么这么写。第一为什么要做参数校验前置。sm-crypto在 key 长度不对的时候行为不一定是抛错有时候是返回空字符串有时候是返回一堆乱码。你拿着乱码去JSON.parse报的是Unexpected token错误信息指向的地方跟真实原因差了十万八千里。把校验放前面错误信息直接告诉你“key 长度不对”排查时间从半小时缩短到半分钟。第二为什么要捕获解密异常并重新包装。原始异常信息可能是Invalid array length这种毫无指向性的描述重新包装后至少能让你在日志里一眼看出是解密环节出的问题。第三为什么明文解析失败时要把明文前 50 个字符打出来。这是我在实际排查中救过自己很多次的一招解密成功但明文乱码通常意味着 key 或 iv 错了或者模式不对但代码层面一切都是“成功”的。把那 50 个字符打在错误信息里你一眼就能看出是乱码还是差了几个字符判断起来快得多。3.4 第四步用标准测试向量先自证写完之后我没有直接接业务接口而是先拿标准测试向量自测。SM4 有一个公开的标准示例密钥和明文都是0123456789abcdeffedcba9876543210ECB 模式、单分组、无填充的情况下密文是681edf34d206965e86b3e94f536e4246。const { sm4 } require(sm-crypto) const key 0123456789abcdeffedcba9876543210 const plain 0123456789abcdeffedcba9876543210 const cipher sm4.encrypt(plain, key, { padding: none }) console.log(cipher) // 期望输出681edf34d206965e86b3e94f536e4246这个自测的价值在于它把“库有没有问题”和“我的参数有没有问题”这两件事彻底分开了。如果这一步过不了那说明库选错了或者调用方式错了后面的联调压根不用开始如果这一步过了那后面联调出问题原因一定在业务参数上排查范围瞬间缩小一半。注意padding: none这个选项标准向量是单分组无填充的如果你用默认的 PKCS#7 填充加密结果会多出一个分组跟标准值对不上别以为自己写错了。4. 联调阶段编码、padding、中文乱码配置通了、模块封装好了、自测也过了我以为半小时能收工结果在联调上耗了整整两天。这一章把踩过的坑按类型整理出来都是那种“看起来很简单但就是不对”的问题。4.1 base64 与 hex 的错位第一个坑是编码格式。sm-crypto的sm4.decrypt默认把输入当成 hex 字符串处理但后端传过来的是 base64。我第一次调用的时候没做转换直接把 base64 字符串丢进去结果解出来是一堆乱码还不报错。为什么会这样因为 base64 的字符集里包含、/、这些字符在 hex 解析时会被当成非法字符跳过或者按某种规则截断剩下的部分仍然是一个“长度合法”的字符串库函数不会认为参数有问题就照常往下算了算出来的当然是无意义的结果。这种“静默错误”比直接抛异常难查十倍。解决办法就是我在 3.3 里写的base64ToHex先Buffer.from(base64, base64)转成字节数组再toString(hex)。反过来如果你的场景是加密输出也需要从 hex 转 base64const hexCipher sm4.encrypt(plain, key, { mode: cbc, iv }) const base64Cipher Buffer.from(hexCipher, hex).toString(base64)注意跟后端确认编码格式的时候不要只问“是 base64 吗”要问“密文的原始字节是怎么编码成字符串的”。有些后端会先做 base64 再做一次 URL 编码有些会做十六进制大写这些细节不问清楚你就得靠猜。4.2 padding 模式与密钥长度第二个坑是填充。SM4 是分组密码分组长度固定 16 字节如果你的明文不是 16 的整数倍就必须填充。前端这边sm-crypto默认用 PKCS#7 填充解密时会自动去掉填充。但问题在于后端用什么填充、用什么模式得对上。我当时遇到的情况是后端用的是 PKCS#5前端默认 PKCS#7。听起来不一样实际上在 16 字节分组下两者是完全等价的PKCS#5 名义上只定义 8 字节分组但实现上就是 PKCS#7 的子集所以这个不影响。真正出问题的是模式——后端用的 CBC我一开始没传mode默认走了 ECB解出来的前 16 字节是对的后面全是乱的。这个现象很有迷惑性因为你能看到部分正确内容会以为是编码问题而不是模式问题。密钥长度也是常见坑。SM4 的密钥固定 128 位也就是 16 字节、32 位 hex 字符。有些后端会把密钥做一次 MD5 或者 SHA 派生再使用前端如果直接拿原始密钥去解必然失败。这种情况你需要跟后端要派生规则或者干脆让他给你一个已经派生好的 hex 密钥。我遇到过最离谱的一次是后端把密钥字符串做了 UTF-8 编码后取前 16 字节而前端直接把这个字符串当 hex 解析——两边的“16 字节”根本不是同一批字节。4.3 中文与特殊字符的处理第三个坑是中文。解密出来的字符串如果JSON.parse报Unexpected token先别怀疑 JSON 格式八成是字符集问题。sm-crypto的output: string是按 UTF-8 解码的如果后端加密前把明文按 GBK 编码成字节前端按 UTF-8 解出来就是乱码。判断方法很简单把output改成array拿到字节数组自己用Buffer.from(arr).toString(utf8)转一次如果还是乱码基本可以确定后端用的不是 UTF-8。这时候要么让后端改成 UTF-8要么前端引入一个 GBK 解码库比如iconv-lite注意它在浏览器端也需要补齐 buffer。还有一个更隐蔽的情况解密出来的 JSON 本身是合法的但某些字段的中文变成了????。这通常是后端在加密前做了一次String.getBytes()用了平台默认字符集而不是显式指定 UTF-8。这种问题只能让后端改前端怎么解都救不回来。5. 常见问题速查与排查思路这一章把我在这个项目以及之前几个类似项目里遇到过的问题整理成速查表配合排查顺序你遇到报错可以先扫一遍大概率能直接命中。5.1 构建期报错速查表报错信息根本原因处理方式Cant resolve cryptowebpack 5 不再自动注入 polyfillresolve.fallback.crypto require.resolve(crypto-browserify)Cant resolve stream同上多由哈希类库间接引入stream-browserify或若走不到该分支则置falseCant resolve buffer库内部使用 Buffer 但未显式导入buffer/加ProvidePlugin注入全局Buffer is not defined运行时构建通过但运行时缺全局变量ProvidePlugin({ Buffer: [buffer,Buffer] })process is not defined运行时代码里用了process.envProvidePlugin({ process: process/browser })Module parse failed: Unexpected token引入的库用了 ESM 但没配 babel在module.rules里对该库做转译或换 CJS 版本排查顺序我建议这样走先看报错路径在node_modules还是src再看这个模块是被谁引用的用 stats.json 或者--stats detailed最后决定是补 polyfill 还是改代码。能改代码就不要补 polyfill因为 polyfill 是纯增量成本而改代码通常能让依赖树更干净。5.2 运行期解密失败速查表现象可能原因验证方法解密结果为空字符串key 或 iv 长度不对打日志确认是 32 位 hex解密结果部分正确部分乱码模式不对ECB/CBC 混用确认mode和iv是否都传了解密结果全是乱码密文编码格式错base64 当 hex先做 base64 转 hex 再解JSON.parse 报错但内容看着正常存在不可见字符或 BOM打印JSON.stringify(plain)看转义中文变问号后端编码非 UTF-8让后端确认getBytes的字符集偶发解密失败密钥轮换导致的前后端不同步检查密钥版本号字段我特别想强调最后一行。如果后端做了密钥轮换响应体里通常会带一个keyVersion之类的字段前端需要根据这个字段选择对应的密钥。我见过一个项目密钥轮换后前端没处理这个字段导致轮换那一刻开始大约 5% 的请求解密失败而且因为失败率不高监控也没报警愣是拖了一周才被发现。6. 安全边界与工程化收尾功能跑通只是及格线这个模块上线前还有几件事必须处理密钥怎么放、包体积怎么控、异常怎么兜。这一章讲我实际做的处理也包括一些“知道但做不到完美”的现实妥协。6.1 前端密钥必然暴露怎么降低风险先把话说透只要密钥被写进前端代码它就不是秘密。无论是硬编码在 JS 里、放在.env里被DefinePlugin注入、还是运行时从接口拉取用户打开开发者工具都能拿到。混淆只能提高一点点门槛防不住真正想看的人。但这不代表什么都不用做。我实际采取的措施有三条。第一密钥分环境测试环境和生产环境的密钥完全独立即使测试环境泄露也不影响线上。第二密钥带有效期由后端在登录响应里下发一个短期有效的密钥和 iv前端存内存不存 localStorage页面刷新或超时后重新获取。这样即使有人抓到了密钥窗口期也有限。第三服务端做二次校验解密出来的业务请求带上签名服务端验证签名有效性防止用户篡改明文后重放。这三条里第二条和第三条是真正有安全价值的第一条是基础卫生。至于用 SM2 做密钥协商这种更完整的方案我评估过实现复杂度上升不少而且需要后端配合改造这个项目的时间窗口不允许就先用短期密钥 签名校验这套折中方案顶上。提示如果你看到某个方案号称“前端加密绝对安全”可以直接跳过。前端加密的目标是提高攻击成本、防止明文裸奔不是做到不可破解把这个定位摆正方案设计就不会走偏。6.2 样式之外的收尾体积、SSR 与测试包体积这块补齐 polyfill 之后我的主包大概涨了 60KBgzip 后约 20KB主要来自crypto-browserify那条依赖链。优化手段有两个一是把解密模块拆成独立的 chunk用动态import()按需加载首屏不加载它二是在splitChunks里把crypto-browserify相关模块单独分组避免它被重复打进多个 chunk。optimization: { splitChunks: { cacheGroups: { cryptoPolyfill: { test: /[\\/]node_modules[\\/](crypto-browserify|randombytes|create-hash)[\\/]/, name: crypto-polyfill, chunks: all, priority: 10, }, }, }, }SSR 场景要特别注意。服务端跑的是 NodeBuffer、crypto这些是原生模块不需要 polyfill但如果你用同一份 webpack 配置打两端polyfill 会被打进服务端产物运行时可能跟原生模块冲突。正确的做法是用两份配置服务端配置里把target设成noderesolve.fallback全部去掉让 Node 自己去解析原生模块。测试方面我把 3.4 里的标准向量写成了单测用例另外加了一条“真实响应体解密”的集成测试用前面存的 fixture 文件。这两条测试在后续每次升级依赖时都跑一遍帮我拦下过一次sm-crypto次版本升级导致的 padding 行为变更。加密相关的代码测试覆盖率比业务代码更值得投入因为它的错误是全局性的一个参数错整站数据都出不来。6.3 我的几点实操心得最后分享几条这个项目里沉淀下来的经验都是那种文档里不会写、只有踩过才知道的东西。第一条先跑通最小闭环再谈工程化。我一开始想一次性把 webpack 配置、密钥管理、错误上报全部做完结果在配置上卡了两天一行业务代码没写。后来调整策略先用最简单的方式硬编码密钥 单个文件把一条数据解密出来确认链路通了再回头做配置优化和安全加固。这个顺序很重要因为它让你始终有一个“已知可用”的基线。第二条错误信息要写给未来的自己看。解密失败时的错误信息如果只是decrypt failed三个月后你看到日志会一脸懵。把 key 长度、iv 长度、密文长度、模式、解密结果前几十个字符全部打进去虽然日志会长一点但排查时能省下大量时间。当然密钥本身绝对不能打进日志这个红线要守住。第三条别怕回退。中途我一度想把 webpack 升级到最新版本顺便把所有 polyfill 换成插件方案结果构建时间从 40 秒涨到 3 分钟热更新也不工作了。我纠结了半天最后还是回退到原来的精细配置。老项目的构建配置能不动就不动你的目标是加一个解密模块不是重构整个构建体系。边界感这件事在别人的仓库里尤其重要。这套东西上线到现在运行了几个月解密失败率稳定在万分之几基本都是网络传输截断导致的重试即可。真正让我觉得值的是后来团队里另一个项目也要做类似的密文接口我把decryptor.js和那份 webpack 配置片段直接拷过去半小时就跑通了——这说明当初把它拆成独立模块、把每个参数校验写清楚是对的。