JavaScript 动态执行代码:new Function、eval 与 Node.js vm 沙箱
三种方式对比方式 能访问外部作用域 安全性 推荐场景eval() 是 低 几乎不推荐new Function() 否 中 浏览器/Node 通用vm.runInContext() 否(隔离) 高 Node.js 执行用户代码new Function(推荐) // 无参数 const fn = new Function("return 1 + 2"); console.log(fn()); // 3// 带参数 const add = new Function("a", "b", "return a + b"); console.log(add(1, 2)); // 3// 多行代码 const code = ` const x = 10; return x * 2; `; console.log(new Function(code)()); // 20new Function 创建的函数只能访问全局作用域,不能访问创建它时的局部变量——这是它比 eval 更安全的原因。 eval(不推荐) const result = eval("1 + 2"); // 3eval 可以访问当前闭包内的变量,是潜在的代码注入入口,大多数代码规范禁止使用。 注入上下文:with(data) 在表单引擎、规则配置等场景中,需要让动态代码访问对象属性: function runExpression(expr, data) { return new Function("data", `with(data){ return ${expr} }`)(data); }runExpression("price * count", { price: 10, count: 2 }); // 20 runExpression("amount > 100", { amount: 200 }); // truewith(data) 把 data 的属性提升为词法作用域,表达式中可以直接使用属性名。 执行用户脚本(带函数体) 从数据库加载 JS 函数字符串并执行: const scriptStr = ` function check(order) { return order.amount > 100; } `;// 提取函数体,传入参数执行 const fn = new Function("order", ` ${scriptStr} return check(order); `);console.log(fn({ amount: 200 })); // true异步版本:AsyncFunction // 获取异步函数构造器 const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;const code = ` const res = await Promise.resolve(123); return res; `;const fn = new AsyncFunction(code); fn().then(console.log); // 123Node.js vm 沙箱(服务端执行用户代码) vm 模块提供真正的上下文隔离,适合服务端运行不受信任的代码: const vm = require("vm");// 创建隔离上下文 const context = { a: 1, b: 2, result: 0 }; vm.createContext(context);vm.runInContext("result = a + b", context); console.log(context.result); // 3设置超时,防止死循环: try { vm.runInContext( "while(true){}", context, { timeout: 1000 } // 1秒超时 ); } catch (e) { console.log("超时:", e.message); }完整执行流程: function safeExec(code, data, timeoutMs = 500) { const ctx = { ...data, __result: undefined }; vm.createContext(ctx); vm.runInContext( `__result = (function(){ ${code} })()`, ctx, { timeout: timeoutMs } ); return ctx.__result; }safeExec("return a * b", { a: 3, b: 4 }); // 12vm 模块的局限 vm 不是完全安全的沙箱,能被构造特定代码逃逸(prototype chain escape)。对安全要求更高的场景,使用 vm2 库(已停止维护)或 isolated-vm(推荐): import ivm from "isolated-vm";const isolate = new ivm.Isolate({ memoryLimit: 64 }); const ctx = await isolate.createContext(); const result = await ctx.eval("1 + 2");
JS 动态执行字符串代码:new Function、eval 与 Node.js vm 沙箱
new Function(推荐) // 无参数 const code = "return 1 + 2"; const fn = new Function(code); console.log(fn()); // 3// 带参数 const fn2 = new Function("a", "b", "return a + b"); console.log(fn2(1, 2)); // 3// 多行代码 const fn3 = new Function(` const x = 10; return x * 2; `); console.log(fn3()); // 20和 eval 的主要区别:有独立函数作用域,不会污染外部变量 可以明确传入参数,接口更清晰 不能访问外部局部变量(只能访问全局)eval(不推荐) const result = eval("1 + 2"); console.log(result); // 3问题:和外部作用域共享变量,容易出现意外副作用 引擎无法优化,性能差 各种代码规范和 CSP 都禁止使用异步代码执行 const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;const code = ` const res = await Promise.resolve(123); return res; `;const fn = new AsyncFunction(code); fn().then(console.log); // 123Node.js vm 模块(沙箱执行) 适合在 Node 服务端执行用户提交的代码,比 eval 更安全: const vm = require("vm");const context = { a: 1, b: 2 }; vm.createContext(context);const result = vm.runInContext("a + b", context); console.log(result); // 3设置超时防止死循环: try { vm.runInContext("while(true){}", context, { timeout: 1000 }); } catch (e) { console.error("超时:", e.message); // Script execution timed out }执行来自数据库的函数字符串: const str = ` function check(order) { return order.amount > 100; } `;const check = new Function(`${str}; return check;`)(); console.log(check({ amount: 200 })); // true实战:规则引擎表达式 带上下文变量执行表达式: function runExpression(expr, data) { return new Function( "data", `with(data){ return ${expr} }` )(data); }runExpression("price * count", { price: 10, count: 2 }); // 20微信小程序:eval 和 Function 均被禁用 小程序运行环境为了安全禁止动态代码生成: eval("1+1"); // eval is not a function new Function("return 1"); // Function constructor is disabled替代方案一:JSON 规则引擎 { "op": "and", "rules": [ { "field": "amount", "operator": ">", "value": 100 }, { "field": "vip", "operator": "=", "value": true } ] }function runRule(rule, data) { return rule.rules.every(item => { switch (item.operator) { case ">": return data[item.field] > item.value; case "=": return data[item.field] === item.value; } }); }替代方案二:globalThis 注册函数表 // 初始化时注册所有可调用函数 globalThis.actions = { checkVip(data) { return data.vip; }, checkAmount(data) { return data.amount > 100; } };// 运行时按名称调用 function dispatch(funcName, data) { return globalThis.actions[funcName]?.(data); }dispatch("checkAmount", { amount: 200 }); // trueglobalThis 在小程序中可用(等价于浏览器的 window),但 window、document 在小程序里不存在。这种函数表模式是小程序低代码/规则引擎的常见做法。
Mermaid Flowchart 语法常见报错:冒号、圆柱节点、边标签空格的修复
Mermaid flowchart 渲染失败通常没有明确报错信息,图表直接显示为空白或语法错误提示。以下是三类最常见的问题。 问题一:节点文本含冒号 端口号和 URL 路径中的冒号会让部分 Mermaid 版本解析失败: HN[Hardhat Node :8545] ← 可能报错修复:用括号替代冒号,或加引号包裹整个文本: HN["Hardhat Node (8545)"] RP["rpc-proxy (8546)"] Backend["Go Backend (8080)"]问题二:圆柱节点 [( )] 与中文混用 OS[(OrderStore 内存)] ← 部分渲染器报错修复:改用英文,或改为普通方块节点: OS[("OrderStore")] OS[OrderStore]问题三:边标签含空格 UI -->|9. 轮询 processedOrders| RP ← 旧版本可能报错修复:用双引号包裹边标签: UI -->|"9. 轮询 processedOrders"| RPsubgraph 加引号 subgraph 名称含中文、括号、空格时,用双引号包裹避免问题: subgraph Frontend["React Frontend"] subgraph Backend["Go Backend (8080)"]修复后完整示例 flowchart TB subgraph Frontend["React Frontend"] UI["用户界面"] MM["MetaMask钱包"] end subgraph Backend["Go Backend (8080)"] API["Gin HTTP API"] OS["OrderStore"] end subgraph Chain["以太坊链"] HN["Hardhat Node (8545)"] PG["PaymentGateway 合约"] end UI -->|"1. POST /api/orders"| API API -->|"2. 创建订单"| OS HN -->|"3. 执行合约"| PG PG -.->|"Deposit事件"| HN通用规则节点文本有特殊字符(:、/、()→ 加双引号 边标签有空格或特殊字符 → 加双引号 中文混合复杂格式 → 尽量简化或加引号Mermaid 版本差异较大,加引号是最兼容的防御性写法。
Mermaid 复杂流程图渲染失败?五个改起来最快的写法
Mermaid 是文档利器,画时序图、流程图不用切工具。但节点文本里稍微带点特殊字符——冒号、括号、中文——渲染就报错。总结几个"看着简单但真被咬"的改法。 症状:节点文本有 : 或 () 写: HN[Hardhat Node :8545] Backend[Go Backend :8080] RP[rpc-proxy :8546]有些 Mermaid 版本会把 : 当分隔符炸掉。改成用引号包住整段文本,或者换成 (8545) 这种: HN["Hardhat Node (8545)"] Backend["Go Backend (8080)"] RP["rpc-proxy (8546)"]通用规则:节点文本里出现非字母数字字符(中文除外),一律用双引号包起来。 症状:中文 + 特殊形状节点 圆柱体节点 [(...)]: OS[(OrderStore 内存)]某些渲染器对中文 + 圆柱体混用敏感。加引号: OS[("OrderStore 内存")]不行就退回矩形: OS[OrderStore]症状:subgraph 名字带空格 写: subgraph Backend[Go Backend :8080]改成: subgraph Backend["Go Backend (8080)"]subgraph 的显示名称和标题一样,遇到特殊字符必须引号。 症状:边上的中文文字 有些老版本对边文字里的空格敏感: UI -->|9. 轮询 processedOrders| RP用引号: UI -->|"9. 轮询 processedOrders"| RP或去空格: UI -->|9.轮询processedOrders| RP引号版本更清晰。 症状:复合 arrow 之间冲突 -.->|"文字"| 和 -->|"文字"| 混用时,某些版本解析 -. 的 dashed 箭头出错。检查每个箭头的正确写法:想要 语法实线箭头 -->虚线箭头 -.->粗线箭头 ==>无箭头连线 ---带文字 `-->不要在 -. 中间插空格,- . -> 是错的。 一个可复用的修复清单 复制过去一份混乱的流程图,按这四步过一遍:所有节点文本都用 ["..."] 双引号包 subgraph 用 subgraph X["Y"] 引号包 边上文字全部 |"..."| 引号包 把 : 换成 () 或去掉改完再渲染基本就通了。 快速验证工具 不用等博客/Notion 重新渲染,直接:mermaid.live 官方在线编辑器,实时看错误 VSCode 装 "Mermaid Preview" 插件本地预览一句话总结 "Mermaid 报错 = 有特殊字符没加引号"。四类地方都用 "..." 包起来(节点、subgraph、边文字、特殊符号周围),复杂图也不容易崩。
Hardhat 本地链卡链?先看是不是关了自动挖矿
Hardhat 本地链跑合约,await tx.wait() 卡住不返回,感觉链死了。看着像 bug,其实基本都是配置问题。 快速定位 一条命令看当前区块号: cast block-number --rpc-url http://127.0.0.1:8545没有 cast 就用 curl: curl -X POST http://127.0.0.1:8545 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'隔几秒再跑一次,区块号不变——链停了。 原因 1:配了 auto: false hardhat.config.js 里如果有: networks: { hardhat: { mining: { auto: false, // 关掉了自动挖矿 // interval: 5000 // 或者只按间隔挖 } } }交易发到内存池后不会被打包,得手动 mine: await network.provider.send("evm_mine");或者恢复自动挖矿: await network.provider.send("evm_setAutomine", [true]);原因 2:nonce 冲突 之前发过一个低 gas 交易占住了某个 nonce,后面的交易全排在它后面: cast nonce 0x<address> --rpc-url http://127.0.0.1:8545解法:发一个相同 nonce、更高 gas 的交易顶掉旧的(cancelTx 模式)。 原因 3:fork 主网时 RPC 挂了 npx hardhat node --fork https://eth-mainnet.g.alchemy.com/v2/xxxAlchemy 超限、RPC 断线、网络超时,Hardhat 表现得就像卡住。开 debug 日志看: DEBUG=hardhat* npx hardhat node会看到"Waiting for chain state"之类的提示。 原因 4:不是链卡,是前端缓存了 Hardhat 每次重启链状态会重置,但前端还拿着旧合约地址、旧 ABI。清理编译产物: rm -rf cache artifacts npx hardhat compile npx hardhat run scripts/deploy.js --network localhost原因 5:evm_mine 返回 0 不等于失败 有人看到: await network.provider.send("evm_mine"); // '0'以为报错了。其实 '0' 是 RPC 的正常返回值,表示成功。区块号会 +1,验证一下: await ethers.provider.getBlockNumber();兜底:一键重置 想快速回到初始状态而不重启进程: await network.provider.request({ method: "hardhat_reset", params: [], });链状态、快照、内存池全清。跑测试之间用这个特别方便。 或者最粗暴——Ctrl+C 结束 npx hardhat node,重新起一个。 一句话总结 Hardhat "卡链" 九成是 auto: false 关了自动挖矿。先 evm_setAutomine [true] 或手动 evm_mine 一下,绝大多数情况就通了。
