深入了解 CommonJs 如何在浏览器运行

前面对比了前端模块化规范的优缺点前端模块化:commonJS、AMD、CMD、UMD、ES module,接下来深入探讨下 CommonJs 如何在浏览器运行

概述

目前 npm 包大部分是按照 Node.js 环境设计的,遵循 CommonJs 规范,浏览器没办法直接运行。要在浏览器上运行,需要进行格式的转化

那我们依赖根据CommonJs开发的页面是怎么在浏览器运行的呢?

原理

浏览器不兼容 CommonJS 主要有两大原因

同步加载

CommonJS 模块是同步加载的,也就是说,当遇到require语句时,系统会停下来等待模块加载完成并执行完整个文件,然后才会继续执行后面的操作。

  • 在服务器端,资源文件(模块)通常都是存放在本地磁盘,IO 操作速度相对较快,这样的设计是可以接受的。

  • 但是对于浏览器来说,模块文件需要从服务器获取,等待的时间取决于网络速度,如果直接采用这种模式,可能会导致页面的加载和执行被阻塞,从而影响用户体验。

环境变量

Node.js 环境主要提供一下环境变量:

  • module
  • exports
  • require
  • global

实现思路

  • 对于同步加载,我们只需要把依赖的文件全部打包进来,避免同步加载阻塞的问题;(这里也体现了 ES Module 静态化的优势,tree-shaking 只会引入文件中使用到的模块)
  • 对于全局变量,我们只要能提供对应变量,浏览器就能处理 CommonJS 模块 简单示例:
javascript 复制代码
const module = {
  exports: {},
};
const require = () => {};

(function (module, exports, require) {
  exports.add = function (a, b) {
    return a + b;
  };
})(module, module.exports);

const sum = module.exports.add;
sum(1, 2); // 3

目前 Webpack,Browserify 等工具都能很好将 CommonJS 语法转换为浏览器能理解的语法

下面将通过一个简单的 webpack 构建产物来深入理解处理后的 CommonJs

webpack5 构建 CommonJs 代码解析

javascript 复制代码
// 将js封装打包成一个立即执行函数(IIFE)
(() => {})();
  • 问题一:解决同步加载问题,将所有模块都打包进来
javascript 复制代码
(() => {
  // __webpack_modules__ 是一个包含所有模块的对象。
  var __webpack_modules__ = {
    // "./main.js" 是一个模块。
    "./main.js": (module, exports, __webpack_require__) => {
      // 使用 eval 执行模块内部的代码。
      eval(
        'const { add } = __webpack_require__(/*! ./add.js */ "./add.js");\n\nconst sum = add(1, 2);\n\nconsole.log(sum);\n\n\n//# sourceURL=webpack://webpack-demo/./main.js?'
      );
    },
    // 同样 "./add.js" 也是一个模块。
    "./add.js": (module, exports, __webpack_require__) => {
      // 使用 eval 执行模块内部的代码。
      eval(
        "function add(a, b) {\n  return a + b;\n}\nmodule.exports = {\n  add: add,\n};\n\n\n//# sourceURL=webpack://webpack-demo/./add.js?"
      );
    },
  };
})();
  • 问题二:解决全局变量问题,核心实现是通过 require 方法,注入变量
javascript 复制代码
// 缓存已加载的模块,减少多次加载 和 循环引用问题
var __webpack_module_cache__ = {};

// 核心方法:require导入
function __webpack_require__(moduleId) {
  // 检查是否有缓存,有缓存则直接导出
  var cachedModule = __webpack_module_cache__[moduleId];
  if (cachedModule !== undefined) {
    return cachedModule.exports;
  }
  // 如果没有缓存过,对应key创建一个缓存对象
  var module = (__webpack_module_cache__[moduleId] = {
    exports: {},
  });

  // 执行模块函数,并传入module, module.exports, __webpack_require__等变量
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

  // 返回模块的exports,所以返回是module内的一个对象模块
  return module.exports;
}

下面是完整的构建后代码

javascript 复制代码
(() => {
  // __webpack_modules__ 是一个包含所有模块的对象。
  var __webpack_modules__ = {
    // "./main.js" 是一个模块。
    "./main.js": (module, exports, __webpack_require__) => {
      // 使用 eval 执行模块内部的代码。
      eval(
        'const { add } = __webpack_require__(/*! ./add.js */ "./add.js");\n\nconst sum = add(1, 2);\n\nconsole.log(sum);\n\n\n//# sourceURL=webpack://webpack-demo/./main.js?'
      );
    },
    // 同样 "./add.js" 也是一个模块。
    "./add.js": (module, exports, __webpack_require__) => {
      // 使用 eval 执行模块内部的代码。
      eval(
        "function add(a, b) {\n  return a + b;\n}\nmodule.exports = {\n  add: add,\n};\n\n\n//# sourceURL=webpack://webpack-demo/./add.js?"
      );
    },
  };

  // 缓存已加载的模块,减少多次加载 和 循环引用问题
  var __webpack_module_cache__ = {};

  // 核心方法:require导入
  function __webpack_require__(moduleId) {
    // 检查是否有缓存,有缓存则直接导出
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    // 如果没有缓存过,对应key创建一个缓存对象
    var module = (__webpack_module_cache__[moduleId] = {
      exports: {},
    });

    // 执行模块函数,并传入module, module.exports, __webpack_require__等变量
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

    // 返回模块的exports,所以返回是module内的一个对象模块
    return module.exports;
  }

  // 加载模块并导出
  var __webpack_exports__ = __webpack_require__("./main.js");
})();

至此,通过以上代码,我们也能更好理解 module, exports, require 方法之间的关系

实际返回的是module.exports:所以直接赋值exports = xxx或者module = xxx,都没办法拿到值,类似如下操作:

javascript 复制代码
const module = {
  exports: 0,
};
function test(module, exports) {
  module = {
    exports: 1,
  };
  exports = 1;
}
test(module, module.exports);
console.log(module, module.exports); // {exports: 0} 0

所以应该通过 module.exports 修改

javascript 复制代码
const module = {
  exports: 0,
};

function test(module, exports) {
  module.exports = 1;
}
test(module, module.exports);
console.log(module, module.exports); // {exports: 1} 1

参考文献

浏览器加载 CommonJS 模块的原理与实现

相关推荐
benbenAItalk6 小时前
数字人口播视频的批量生产实践:素材规范、任务编排与质量验收
java·前端·音视频
2601_962885726 小时前
如何用 Python 扫描 A 股跳空缺口并统计缺口回补概率?
java·前端·python
西瓜太郎12347 小时前
Claude Code、Codex CLI、Gemini CLI 能否共用一枚 Key?先看协议选择矩阵
前端·api 网关·claude code·gemini cli·codex cli
多加点辣也没关系7 小时前
JavaScript|第31章:表单与控件
开发语言·javascript·ecmascript
怕浪猫7 小时前
长会话不崩盘:DeepSeek Harness 的上下文压缩与目标管理策略
面试·github·agent
ocean21037 小时前
2025-2026年Python面试高频知识点洞察
开发语言·python·面试·python八股文
陈随易9 小时前
在Finch用了62亿词元,我认为这是新一代Agent工具之神
前端·人工智能·后端
letisgo59 小时前
JAVA 高级进阶07篇《@Transactional失效的8个场景:代理机制到传播行为》
java·面试·transactional·spring事务·aop动态代理
水域安全老周9 小时前
水趣钓鱼救生衣专利拆解:两级锁紧如何解决落水人衣分离
java·前端·网络
计算机魔术师10 小时前
Anthropic CEO突然喊踩刹车,OpenAI罕见力挺:AI这辆车不能只踩油门了
前端