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

相关推荐
前端小趴菜0539 分钟前
React - createPortal
前端·vue.js·react.js
晓13131 小时前
JavaScript加强篇——第四章 日期对象与DOM节点(基础)
开发语言·前端·javascript
倔强青铜三1 小时前
苦练Python第18天:Python异常处理锦囊
人工智能·python·面试
菜包eo1 小时前
如何设置直播间的观看门槛,让直播间安全有效地运行?
前端·安全·音视频
倔强青铜三2 小时前
苦练Python第17天:你必须掌握的Python内置函数
人工智能·python·面试
烛阴2 小时前
JavaScript函数参数完全指南:从基础到高级技巧,一网打尽!
前端·javascript
军军君012 小时前
基于Springboot+UniApp+Ai实现模拟面试小工具四:后端项目基础框架搭建下
spring boot·spring·面试·elementui·typescript·uni-app·mybatis
chao_7893 小时前
frame 与新窗口切换操作【selenium 】
前端·javascript·css·selenium·测试工具·自动化·html
天蓝色的鱼鱼3 小时前
从零实现浏览器摄像头控制与视频录制:基于原生 JavaScript 的完整指南
前端·javascript
三原3 小时前
7000块帮朋友做了2个小程序加一个后台管理系统,值不值?
前端·vue.js·微信小程序