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

相关推荐
MickeyCV1 小时前
Nginx学习笔记:常用命令&端口占用报错解决&Nginx核心配置文件解读
前端·nginx
祈澈菇凉1 小时前
webpack和grunt以及gulp有什么不同?
前端·webpack·gulp
十步杀一人_千里不留行1 小时前
React Native 下拉选择组件首次点击失效问题的深入分析与解决
javascript·react native·react.js
zy0101011 小时前
HTML列表,表格和表单
前端·html
初辰ge1 小时前
【p-camera-h5】 一款开箱即用的H5相机插件,支持拍照、录像、动态水印与样式高度定制化。
前端·相机
HugeYLH2 小时前
解决npm问题:错误的代理设置
前端·npm·node.js
三天不学习2 小时前
Redis面试宝典【刷题系列】
数据库·redis·面试
六个点2 小时前
DNS与获取页面白屏时间
前端·面试·dns
道不尽世间的沧桑2 小时前
第9篇:插槽(Slots)的使用
前端·javascript·vue.js
bin91532 小时前
DeepSeek 助力 Vue 开发:打造丝滑的滑块(Slider)
前端·javascript·vue.js·前端框架·ecmascript·deepseek