ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Puerts 模块化编程指南:从 Eval 到 ESM 模块与 TypeScript

Puerts 模块化编程指南:从 Eval 到 ESM 模块与 TypeScript Puerts 模块化编程指南从 Eval 到 ESM 模块与 TypeScript【免费下载链接】puertsPUER(普洱) Typescript. Lets write your game in UE or Unity with TypeScript.项目地址: https://gitcode.com/GitHub_Trending/pu/puerts本指南聚焦 Puerts 在 Unity 环境下的 JavaScript/TypeScript 模块化加载体系核心解决“如何组织、加载、复用 JS/TS 代码”的问题。文中先从 Eval 的缺陷与立即执行函数IIFE的隔离思想入手再完整演示基于 ESM 规范、以JsEnv.ExecuteModule加载独立模块文件的推荐实践并结合仓库源码剖析DefaultLoader的加载机制与模块解析细节。读完本文你将掌握在 Puerts 中正确组织脚本代码、脱离 Eval 构建可维护项目并平滑过渡到 TypeScript 开发的完整方案。一、为什么应避免大量使用 Eval在 Puerts 中JsEnv.Eval是执行 JavaScript 代码最直观的入口例如文档中的这段示例void Start() { Puerts.JsEnv env new Puerts.JsEnv(); env.Eval(const a 3); ////// much later env.Eval(const a function () {}); }这段代码其实无法正常运行第一次Eval(const a 3)之后const声明的变量a已经存在于当前作用域第二次Eval(const a function () {})再次声明同名const a会直接触发“重复声明”的语法错误。这个例子恰恰揭示了 Eval 的典型问题——它以字符串形式反复注入代码每次执行都共享同一个全局/顶层作用域变量名极易冲突代码难以拆分与复用。从源码结构看JsEnv.Eval是对底层引擎求值能力的直接包装定义于 JsEnv.cspublic void Eval(string chunk, string chunkName chunk) { env.Eval(chunk, chunkName); } public TResult EvalTResult(string chunk, string chunkName chunk) { return env.EvalTResult(chunk, chunkName); }它可以带泛型返回值使用例如文档中的“求值并取回结果”的写法void Start() { Puerts.JsEnv env new Puerts.JsEnv(); int a env.Evalint( (function() { const a 3 return a; })() ); // a 3 }注意这里包裹的(function(){ ... })()正是立即执行函数表达式IIFE。IIFE 是 JavaScript 中与“模块”思想最接近的原生结构它有独立的作用域内部变量不会泄漏到外部通过return显式声明对外输出。正因为如此它非常适合临时封装一段功能逻辑这也是文档建议的模式——“用 IIFE 包裹 Eval 的内容而不是直接把裸代码丢进 Eval”。但 IIFE 仍存在明显局限跨文件复用依然困难模块之间的依赖关系仍需手工管理。二、ESM 模块PuerTS 官方推荐的加载方式从 IIFE 出发JavaScript 生态逐步演化出多种模块规范其中最流行且成为官方 JS 标准的是ESMECMAScript Modules。PuerTS 完整支持按 ESM 规范执行模块。2.1 一个完整的 ESM 示例文档给出了最精简的示例。首先在任意Resources目录下添加helloworld.mjsimport { world } from lib.mjs console.log(hello world);再在任意Resources目录下添加lib.mjsconst world puerts export { world }然后在 C# 侧通过JsEnv.ExecuteModule加载入口模块void Start() { Puerts.JsEnv env new Puerts.JsEnv(); env.ExecuteModule(helloworld.mjs) }运行后控制台会输出hello puerts。整个过程完全不经过 Eval入口模块、被依赖模块均由引擎按 ESM 规范解析加载import/export语法天然提供了作用域隔离与依赖声明。2.2 为什么推荐 .mjs 扩展名文档特别强调“将 JS 写在独立文件后就无需再用 Eval实际开发中也不建议使用 Eval”。在文件命名上Puerts 的官方加载器对扩展名有专门处理。见 Loader.cs 中DefaultLoader.PathToUse的实现private string PathToUse(string filepath) { #if !PUERTS_GENERAL if (filepath.EndsWith(.js)) { UnityEngine.Debug.LogWarning(It is not recommended to use *.js in using Puers DefaultLoader because .js is a reserved extension in older Unity3D. Use .mjs or .cjs instead); } #endif return // .cjs asset is only supported in unity2018 #if UNITY_2018_1_OR_NEWER filepath.EndsWith(.cjs) || filepath.EndsWith(.mjs) ? filepath.Substring(0, filepath.Length - 4) : #endif filepath; }这里蕴含两个关键点.js扩展名不推荐在旧版 Unity3D 中.js是引擎保留扩展名直接用会被加载器告警加载器建议使用.mjsESM 模块或.cjsCommonJS 模块。扩展名剥离逻辑Unity 2018 环境下.mjs/.cjs文件在加载时会被去掉末尾 4 个字符即去掉.mjs/.cjs因为UnityEngine.Resources.Load并不认识这些扩展名需要按去掉扩展名后的路径查找 TextAsset 资源。2.3 模块入口的完整 APIJsEnv.ExecuteModule在 JsEnv.cs 中有两个重载分别对应“只执行、不取返回值”和“执行并读取具名导出”两种场景public T ExecuteModuleT(string specifier, string exportee) { if (exportee typeof(T) ! typeof(ScriptObject)) { throw new Exception(T must be Puerts.ScriptObject when getting the module namespace); } ScriptObject jso env.ExecuteModule(specifier); if (exportee ) return (T)(object)jso; return jso.GetT(exportee); } public ScriptObject ExecuteModule(string specifier) { return env.ExecuteModule(specifier); }用法说明env.ExecuteModule(helloworld.mjs)仅执行模块不关心导出控制台日志照常输出env.ExecuteModulestring(a_mjs.mjs, default)执行模块并取出名为default的导出值并转换为stringenv.ExecuteModule(a_mjs.mjs).Getstring(str)先拿到模块命名空间对象ScriptObject再通过GetT读取具名导出若传空字符串exportee且泛型不是ScriptObject会抛出异常提示“获取模块命名空间时 T 必须是Puerts.ScriptObject”。三、底层加载机制ILoader 与 ESM 判定模块文件到底如何被找到、如何区分 ESM 与 CommonJS答案在 Puerts 的加载器抽象中。ILoader是模块读取的核心接口定义于 Loader.cspublic interface ILoader { bool FileExists(string filepath); string ReadFile(string filepath, out string debugpath); } public interface IModuleChecker { bool IsESM(string filepath); } public interface IResolvableLoader { string Resolve(string specifier, string referrer); }三个接口各司其职ILoader.FileExists/ReadFile判断模块是否存在并读取文本内容IModuleChecker.IsESM判定某个文件应按 ESM 还是 CommonJS 解析IResolvableLoader.Resolve在支持“从相对/非相对路径解析模块”时将specifier结合referrer引用者解析为最终路径。DefaultLoader同时实现了ILoader与IModuleChecker其 ESM 判定逻辑非常直接Loader.cspublic bool IsESM(string filepath) { return filepath.Length 4 !filepath.EndsWith(.cjs); }即除.cjsCommonJS之外的文件一律按 ESM 处理。在 Unity 编辑器中DefaultLoader通过UnityEngine.Resources.Load从任意Resources目录加载.mjs/.cjs/.txt资源Loader.cs因此文档中“把 .mjs 文件放进任意 Resources 目录”即可被加载到在非 Unity 的通用模式PUERTS_GENERAL下则直接走System.IO.File按磁盘路径读取。与此同时Puerts 自身的内置库log.mjs、csharp.mjs、timer.mjs、promises.mjs等也是通过ExecuteModule加载的见 BackendJs.cs这说明模块加载机制是整个 Puerts 运行时的地基设施而非仅为用户脚本提供的旁路功能。四、模块加载行为与错误场景来自测试用例的印证仓库的单元测试EvalTest.cs围绕模块加载覆盖了大量边界场景可以作为实际开发中排查问题的对照清单场景测试用例预期行为入口模块不存在ESModuleNotFound抛异常异常信息包含缺失的模块路径入口模块编译错误ESModuleCompileError抛异常信息包含语法错误关键字如export入口模块执行期报错ESModuleEvaluateError抛异常信息包含运行时错误如not a function依赖模块不存在ESModuleImportNotFound抛异常信息包含被导入文件路径依赖模块编译错误ESModuleImportCompileError抛异常依赖模块执行报错ESModuleImportEvaluateError抛异常信息包含not a function相对路径导入ESModuleImportRelative支持import { str } from ../b/whatever.mjs并正确取值循环依赖ESModuleImportCircular两个模块互相import可正常执行非相对路径导入ESModuleImportNotRelative支持不带./前缀的模块标识符包式导入ESModuleImportPackageTestExecuteModule(import-package)直接加载目录并解析其index.jsESM 导入 CommonJSESModuleExecuteCJS.mjs可通过import引用.cjs模块import.meta.urlESModuleImportMeta返回形如puer:import-meta/entry.mjs的模块标识动态import()ESDynamicModule*支持按需异步加载错误走 Promisecatch其中循环依赖、包式导入等测试的示例模块就存放在仓库的 unity/test/Src/Resources 目录下例如import-circular/main.mjs、import-package/index.js.txt、a_mjs.mjs通过import str from a_cjs.cjs演示 ESM 引用 CommonJS。值得注意的细节是import-package目录内的模块文件实际以index.js.txt、lib.js.txt存放这正对应DefaultLoader中.mjs/.cjs扩展名剥离逻辑的应用——以Resources资源形式存在的 JS 文件在旧引擎版本下可用.txt后缀规避保留扩展名问题加载器在路径处理时按需还原。从实现事实看Puerts 的 ESM 解析引导逻辑集中在 esm_bootstrap.cjs其中包含来自 Node.js 源码的 POSIX 路径规范化实现posixNormalize用于处理模块标识符中的.、..、连续斜杠等路径归一化这是相对路径导入如../b/whatever.mjs能够正确解析的底层保障。4.1 一个可直接运行的取导出示例结合测试与 API一个“执行模块并取回导出值”的典型 C# 代码如下void Start() { Puerts.JsEnv env new Puerts.JsEnv(); // 假设 Resources 下存在 math.mjsexport const version 1.0 string version env.ExecuteModulestring(math.mjs, version); // 或者读取多个具名导出 var ns env.ExecuteModule(math.mjs); string v ns.Getstring(version); }五、从模块到 TypeScriptPuerts 的核心关注点当脚本被组织为独立文件并通过ExecuteModule加载后下一步自然是回归 Puerts 的重点方向——TypeScriptTS。文档在结尾指出模块化是 TS 工程化的前置条件因为 TS 最终会被编译为 ESM 模块的 JS 代码Puerts 的模块加载机制会直接消费编译产物。TS 代码经编译后通常产出// 编译产物示例TS 的 import/export 被保留为 ESM 语法 import { world } from lib.mjs; console.log(hello world);这与本文第二节的 ESM 示例在加载方式上完全一致在 Unity 中同样通过Resources目录存放编译产物用ExecuteModule加载入口文件。更进一步的类型声明.d.ts、代码生成等工作属于 Puerts 的 TS 能力范畴可继续阅读同一目录下的 TypeScript 指南 深入了解。小结本文围绕“模块化”这一主题梳理了 Puerts 在 Unity 中的完整代码组织链路从 Eval 的局限性认识到 IIFE 的作用域隔离思想再到官方 ESM 模块加载方案ExecuteModule以及其背后的ILoader/DefaultLoader实现与测试用例验证的各类边界场景。实际开发中请遵循以下原则生产代码避免 Eval改用ExecuteModule加载.mjs文件获得作用域隔离与依赖管理模块文件放入Resources目录并通过DefaultLoader的扩展名剥离规则正确命名.mjs/.cjs必要时.txt利用ExecuteModule的返回值通过具名导出在 C# 与 JS/TS 之间传递数据以 ESM 模块为底座平滑接入 TypeScript让编译产物直接复用同一套加载机制。【免费下载链接】puertsPUER(普洱) Typescript. Lets write your game in UE or Unity with TypeScript.项目地址: https://gitcode.com/GitHub_Trending/pu/puerts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表