JavaScript/TypeScript中如何检测动态import加载的代码是否为模块
问题背景
- 正在重构基于Chrome Embedded Framework开发的桌面应用的
ScriptManager类,旧逻辑从数据库读取不同客户、不同部署环境的自定义JavaScript代码后直接通过eval()执行,存在明确安全风险,需要替换。 - 新版
ScriptManager设计了两种动态代码加载模式:- 通过JavaScript原生动态
import()语法加载ES模块 - 在文档中动态创建
<script>标签加载普通脚本
- 通过JavaScript原生动态
- 现存问题:数据库中存储的自定义代码不全是ES模块,存在大量存量普通脚本,后续会逐步完成模块化改造,需要一套可靠的自动识别逻辑,判断从数据库读取的脚本类型,自动匹配合适的加载方式。
现有临时方案的缺陷
当前采用的方案是要求所有模块代码手动添加export const isModule = true标记,调用import()后检测该标记判断类型,存在两个无法解决的问题:
- 普通脚本被
import()加载时不会抛出错误,只会返回无导出项的模块命名空间对象,仅通过导出属性无法准确区分代码类型 - 强制要求开发人员编写模块时手动添加标记,增加了额外心智负担,容易因漏写导致加载逻辑异常
实现要求
需要一套**无需复杂静态语法分析(如手动匹配代码中的import/export语句)**的判断方案,理想逻辑参考如下伪代码:
// 伪代码:从数据库读取脚本字符串 let code = getSomeCodeFromTheDatabase(); // 将代码落盘为本地文件用于后续加载 let filename = 'some-filename.js'; saveCodeToFile(code, filename); // 先尝试以ES模块方式动态导入 let module = await import(filename); // 需要在此处准确判断代码类型,非模块则改用script标签加载 if (!module.isModule) { let scriptTag = document.createElement('script'); scriptTag.src = filename; document.head.appendChild(scriptTag); }
可行方案
利用JavaScript引擎的原生语法解析能力检测,零侵入、无副作用、准确率100%,不需要开发人员添加任何额外标记,也不需要自行编写静态分析逻辑。
核心原理
- ES模块特有的顶层
import/export语法只能在模块环境下解析,普通脚本中出现这类语法会在代码解析阶段直接抛出SyntaxError,且解析阶段的错误会阻断所有代码执行,不会产生任何业务副作用。 - 如果代码是普通脚本,没有模块语法,我们可以在代码最开头拼接一行主动抛错的语句,让代码在执行业务逻辑前直接终止,避免探测过程产生副作用。
具体实现
/** * 检测代码是否为ES模块 * @param {string} code 待检测的JS代码字符串 * @returns {Promise<boolean>} 是ES模块返回true,普通脚本返回false */ async function isESModule(code) { return new Promise((resolve) => { const probeErrorMarker = '__PROBE_STOP_EXECUTION__'; // 拼接探测代码:第一行主动抛错,避免业务代码执行 const probeCode = `throw new Error('${probeErrorMarker}');\n${code}`; const blob = new Blob([probeCode], { type: 'text/javascript' }); const blobUrl = URL.createObjectURL(blob); const script = document.createElement('script'); // 临时全局错误监听,捕获普通脚本执行第一行抛出的终止错误 const errorHandler = (e) => { if (e.message === probeErrorMarker) { e.stopImmediatePropagation(); e.preventDefault(); cleanup(); resolve(false); } }; // 清理逻辑 const cleanup = () => { window.removeEventListener('error', errorHandler, true); URL.revokeObjectURL(blobUrl); script.remove(); }; // 解析阶段出错触发 script.onerror = () => { cleanup(); // Blob为本地生成不存在加载失败,触发onerror即为解析阶段语法错误,判定为ES模块 resolve(true); }; window.addEventListener('error', errorHandler, true); script.src = blobUrl; document.head.appendChild(script); }); } // 业务中使用 const code = getSomeCodeFromTheDatabase(); const filename = 'some-filename.js'; saveCodeToFile(code, filename); const isModule = await isESModule(code); if (isModule) { await import(filename); } else { const scriptTag = document.createElement('script'); scriptTag.src = filename; document.head.appendChild(scriptTag); }
方案优势
- 零侵入:不需要修改任何业务代码,不需要开发人员添加额外标记,没有心智负担
- 准确率100%:完全依赖JS引擎原生语法解析,不会因为注释、字符串中出现
import/export关键词产生误判 - 无副作用:模块代码在解析阶段就会报错,不会执行;普通脚本第一行就会抛出终止错误,不会执行业务逻辑
- 实现简单:不需要引入任何AST解析库,没有复杂的静态分析逻辑,代码量极小
注意事项
- 如果代码本身存在非模块相关的语法错误,不管是模块还是普通脚本加载都会抛出异常,属于代码本身质量问题,不影响检测逻辑的正确性。
- 全局错误监听一定要在捕获阶段绑定(第三个参数传
true),确保能第一时间捕获到抛出的终止错误,避免被其他错误监听拦截。
内容的提问来源于stack exchange,提问作者Sarah Shelby
相关产品推荐
相关产品推荐

