ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

commonjs-require-definition 源码解析:Brunch 的轻量级 CommonJS require() 运行时

commonjs-require-definition 源码解析:Brunch 的轻量级 CommonJS require() 运行时 构建工具前端【免费下载链接】brunch Web applications made easy. Since 2011.项目地址https://gitcode.com/gh_mirrors/br/brunch点击查看免费下载导读commonjs-require-definition是 Brunch 构建链中负责“在浏览器端执行 CommonJS 模块加载”的微型运行时。它仅有 116 行源码却完整实现了require()、require.register()、require.list()三件套并预留了热模块替换HMR的接入点。阅读本文后你将掌握该运行时的 API 用法、路径解析与模块缓存机制、别名与扩展名处理策略以及它如何在 Brunch 的构建产物中作为模块定义前缀被注入、如何与 HMR 运行时协同工作。定位一个被注入到 JS 构建产物顶部的运行时在 Brunch 的模块体系中commonjs-require-definition的角色非常清晰它不参与打包逻辑只提供打包完成后浏览器端需要的require()定义。package.json 中版本为0.6.3关键字为commonjs / module / require / define许可证为 MITindex.js 的全部逻辑只有一行用readFileSync将同目录下的 require.js 原文以字符串形式导出Brunch 根 package.json 声明了对commonjs-require-definition^0.6.3的依赖并在 lib/utils/modules.js 中以require(commonjs-require-definition)直接取得这份源码字符串。因此它不是一个“库”而是一段被拼进最终 JS 文件的运行时前缀代码。这也是为什么在 Brunch 构建出的app.js产物里能看到这份 require 定义的完整内联版本例如 test/fixtures/public/javascripts/app.js 中就保留了同构的require.define实现。快速上手原文档的 API 与用法根据 README.md这份 require 定义遵循 CommonJS Modules/1.1 规范公开了三个 APIAPI说明require(name)加载已注册的模块并返回其exportsrequire.register(name, fn)注册新模块fn回调接收(exports, require, module)require.list()列出所有已注册模块的 id在页面顶部引入require.js后即可使用script srcrequire.js/script script require.register(module, (exports, require, module) { // Expose count externally. exports.count 42; }); console.log(require(module).count); /script执行流程是先用require.register把模块定义存入注册表再通过require按名字取回其exports。console.log会输出42。源码剖析模块注册表、缓存与加载主流程1. 幂等挂载与全局对象识别require.js 用一个 IIFE 包裹全部实现var globals typeof global undefined ? self : global; if (typeof globals.require function) return;global对应 Node.js 环境self对应浏览器环境从而保证该脚本既能在 Node 里加载、也能在浏览器window/self上挂载。第二行是关键的自卫逻辑如果全局已经存在require函数直接放弃定义避免与 Node 原生 require 或重复注入的副本冲突。随后初始化三个核心容器var modules {}; // 注册表模块 id - 定义函数 var cache {}; // 缓存模块 id - 模块对象 {id, exports, hot} var aliases {}; // 别名表短名/无扩展名 - 完整模块 id2. 路径规范化expand、dirname与相对路径 require模块 id 内部统一使用/分隔不依赖path模块因为运行时最终要跑在浏览器里。路径处理由三个小函数完成require.jsexpand(root, name)用正则/^\.\.?(\/|$)/判断name是否以.或..开头若是相对路径则拼上root前缀再按/切分遇到..弹出一层、跳过.和空段最后重新 join得到一个绝对化的模块 iddirname(path)取路径的父目录段localRequire(path)返回一个“局部 require”它基于当前模块所在目录解析相对依赖var localRequire function(path) { return function expanded(name) { var absolute expand(dirname(path), name); return globals.require(absolute, path); }; };也就是说模块内部写require(./dep)时会被解析为“以当前模块 id 的目录为基准”的绝对 id再走全局require。这一层正是 CommonJS 相对路径语义的实现。3. 模块初始化initModule首次加载某个已注册模块时进入initModulerequire.jsvar initModule function(name, definition) { var hot hmr hmr.createHot(name); var module {id: name, exports: {}, hot: hot}; cache[name] module; definition(module.exports, localRequire(name), module); return module.exports; };它先创建模块对象{id, exports, hot}并先写入缓存再执行定义函数——这正是 CommonJS 规范要求的循环依赖保护在定义执行期间如果依赖图中有模块回引当前模块能从缓存拿到尚未完成填充的exports。定义函数收到的三个参数与 README 中require.register(name, fn)的签名(exports, require, module)一一对应其中require就是上文localRequire产出的局部 require。4. 加载主流程require(name, loaderPath)全局requirerequire.js按“缓存 → 注册表 → 抛错”三步查找var require function(name, loaderPath) { if (loaderPath null) loaderPath /; var path expandAlias(name); if (has.call(cache, path)) return cache[path].exports; if (has.call(modules, path)) return initModule(path, modules[path]); throw new Error(Cannot find module name from loaderPath ); };注意错误信息会带上loaderPath加载者路径方便在浏览器控制台定位是谁加载了不存在的模块。has {}.hasOwnProperty是防御性写法避免modules原型链污染干扰in判断。5. 别名机制require.alias与addExtensions为了兼容“不带扩展名”和“目录 index”这两种常见的模块引用写法运行时在注册模块时会自动补充别名require.jsvar extRe /\.[^.\/]$/; // 匹配最后一个扩展名 var indexRe /\/index(\.[^\/])?$/; // 匹配 /index[.ext] var addExtensions function(bundle) { if (extRe.test(bundle)) { var alias bundle.replace(extRe, ); if (!has.call(aliases, alias) || aliases[alias].replace(extRe, ) alias /index) { aliases[alias] bundle; } } if (indexRe.test(bundle)) { var iAlias bundle.replace(indexRe, ); if (!has.call(aliases, iAlias)) { aliases[iAlias] bundle; } } };注册app/main.js时app/main会被记录为别名注册app/index.js时app也会指向它。require.alias(from, to)则支持运行时手动登记别名且expandAlias会递归展开别名链require.js防止别名指向另一个别名时解析中断。6.require.register的两种形态require.register与require.define是同一个函数的两个名字require.js支持两种调用批量对象形态传入{id: fn}这样的映射对象逐个递归注册单条形态require.register(name, fn)写入注册表、删除该模块的缓存强制下次重新初始化并调用addExtensions补充扩展名别名。值得一提的是delete cache[bundle]这一行正是它让模块可以被“重新注册”从而支撑了 HMR 场景下的模块替换。Brunch 的 CHANGELOG.md 也记录了“bump commonjs-require-definition 以允许重置模块用于配合更新的 auto-reload-brunch 做 JS 热重载”这一演进。7. 枚举与标记require.list()require.js遍历modules注册表返回所有模块 id 数组可用于调试或服务端判断哪些模块被打入包内require.brunch truerequire.js是一个环境标记用于识别“这是 Brunch 注入的 require 运行时”。HMR 集成点_hmr、require.hmr与_cache运行时在文件末尾主动探测热更新宿主require.jsvar hmr globals._hmr new globals._hmr(_resolve, require, modules, cache); require._cache cache; require.hmr hmr hmr.wrap;若全局存在_hmr构造器则把解析器_resolve、全局 require、模块注册表、缓存四个句柄传给它由 HMR 运行时接管依赖图跟踪与模块热替换_resolve(name, dep)是“别名展开 相对路径展开”的复合解析函数被专门抽出供 HMR 计算模块间依赖关系require.hmr(graph, fn)是热更新入口Brunch 的 lib/utils/hmr.js 在开发模式下会生成形如require.hmr({...依赖图...}, function(require) { ...模块注册... });的代码段把依赖图 JSON 和注册回调一并交给运行时。对照 packages/addons/hmr-brunch/runtime.js 可以看到_hmr构造器正是按( _resolve, require, modules, cache )的签名接收参数并通过this.createHot(name)为每个模块生成带accept / decline / dispose / addDisposeHandler / removeDisposeHandler / data的 hot 对象回填到initModule创建的module.hot上。也就是说HMR 的“钩子”全部由这份 require 定义主动暴露热更新运行时只是寄生在它之上的客户端。在 Brunch 构建管线中的位置definition 与 wrappercommonjs-require-definition的注入由 Brunch 的模块配置驱动。在 lib/utils/modules.js 中exports.normalizeDefinition definition { switch (definition) { case commonjs: return () commonRequireDefinition; case false: return () ; } return definition; };当用户在brunch-config中设置modules.definition: commonjs时normalizeDefinition返回的正是 index.js 读出的 require.js 源码字符串若设为false则产出空字符串不注入运行时。同一文件中的getWrapperFnlib/utils/modules.js则生成了配套的模块包装前缀require.register(moduleName, function(exports, require, module) {即每个编译后的模块都被包进require.register(...)调用。这两个配置最终在 lib/utils/config.js 的normalizeConfig中被合并进config._normalized.modules.wrapper / definition进入构建的 join 流程。整体链条可概括为brunch-config 中 modules.definition: commonjs → normalizeDefinition 取回 require.js 源码字符串 → 作为构建产物顶部的运行时前缀 → 各模块以 require.register(id, function(exports, require, module){...}) 注册 → 浏览器端 require(id) 返回 exportsrequire.hmr(graph, fn) 支撑开发期热更新test/fixtures/public/javascripts/app.js 正是这种产物的真实写照运行时定义 this.require.define({ app: function(exports, require, module) {...} })的注册块二者组合即构成浏览器可直接执行的完整 bundle。适用前提与限制这份运行时面向CommonJS 风格的同步模块加载模块 id 统一使用/路径不处理 AMD、ES Modules 的运行时语法ES 模块在构建期已被编译器转换浏览器端加载必须保证require.js先于任何模块代码执行且整个 bundle 被设计为单文件注入——正如 lib/utils/hmr.js 中针对多文件输出场景给出的警告所示HMR 正常工作也依赖单文件编译产物若全局环境已存在require函数例如在 Node.js 中直接 require 该文件运行时会静默让位不会覆盖宿主实现。结语commonjs-require-definition用不到 120 行代码把 CommonJS Modules/1.1 的require语义搬进了浏览器并为 Brunch 暴露了三条关键接口require.register构建期注入模块、require.alias/扩展名别名加载容错以及require.hmr热更新接入。理解它的内部结构等于同时理解了 Brunch 产物顶部的运行时前缀、模块包装器的由来以及 HMR 运行时如何寄生在这份定义之上完成模块替换。赞分享构建工具前端【免费下载链接】brunch Web applications made easy. Since 2011.项目地址https://gitcode.com/gh_mirrors/br/brunch点击查看免费下载相关推荐webpack CommonJS 打包实战解析从 require 依赖链到 bundle 产物结构与运行原理webpack CommonJS 打包实战解析从 require 依赖链到 bundle 产物结构与运行原理 导读 CommonJS 是 Node.js 生态前端构建开发工具webpack 官方 CommonJS 示例深度解析一条 require 依赖链如何变成可运行的 bundlewebpack 官方 CommonJS 示例深度解析一条 require 依赖链如何变成可运行的 bundle CommonJS 是 webpack 能够开箱前端构建开发工具the-super-tiny-compiler中的CommonJS模块支持require/exports实现the super tiny compiler中的CommonJS模块支持require/exports实现 在前端开发中模块系统是组织代码的重要方式。Co编译器上一篇AndroidVideoCache内存缓存实现ByteArrayCache使用场景下一篇Lime前端交互设计提升用户编辑体验的关键细节创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进