深入了解 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 模块的原理与实现

相关推荐
passerby60611 分钟前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 分钟前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅11 分钟前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅33 分钟前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊1 小时前
jwt介绍
前端
爱敲代码的小鱼1 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc