来康康CommonJS模块化规范?🧐

541 阅读3分钟

前端模块化规范是一个老生常谈的话题,今天我们了解一下 CommonJS 模块化规范。

CommonJS出现的原因

在古代前端开发中,通过script标签管理前端代码模块。但script的问题也显而易见

  • 脚本多变,需要手动管理加载顺序
    • 模块之间的依赖关系难以管理
  • 不同脚本之间逻辑调用,需要通过全局变量的方式
    • 容易造成全局变量污染
    • 容易产生命名冲突
  • 只能在html中使用

所谓乱世出英雄,CommonJS 规范应运而生。该规范约定,一个文件就是一个模块,每个模块都有单独的作用域。通过 exportsmdoule.exports 导出模块,通过 require 引入模块。

CommonJS同步的方式加载模块

浏览器端使用 即时编译 的方式执行代码,即:代码的每一行都在运行时编译和执行。 如果在浏览器端使用同步的方式加载模块,就会引起大量的同步请求,导致应用运行效率低下。所以 浏览器端不适合使用CommonJS

服务器端使用 提前编译 的方式执行代码,即:在编译阶段,在程序执行之前,源代码已被转换为机器代码。 Node.js 的模块加载机制也是类似的范式:在启动时加载模块,执行过程中只是使用模块。所以,Node.js 适合使用CommonJS

因此Node.js 系列的应用一般都遵循CommonJS模块化规范,如

  • Webpack
  • Gulp
  • Eletron
  • .......

exports

exports导出的是一个对象,该对象中可以包含任何数据类型,如

  • 字符串
  • 函数
  • 对象
  • ......

代码示例📄

// src/lib.js 
exports.hello = 'hello'
exports.add = function (a, b) {
    return a + b
}
exports.obj = { name: 'a', age: 20 }
setTimeout(() => {
    /* 打印出
      {
        hello: 'hello',
        add: [Function],
        obj: { name: 'a', age: 20 },
        className: '1年级'
  	  }
    */
    console.log(exports);
}, 3000);

// src/index.js
const lib = require('./lib.js')
// 打印出 :  { hello: 'hello', add: [Function], obj: { name: 'a', age: 20 } }
console.log(lib);
lib.className = '1年级'
module.exports = 'a'

exports 导出的对象与外部变量lib是同一个引用。因为外部变量lib可以修改lib.js文件的内容

通过 webpack打包代码分析 exports

手写-模拟打包后的exports代码

其实 moduleexports只是两个单纯的变量,为了更方便理解, 把exports 替换为变量 e,

const webpack_modules = {
    // 如果通过 module.exports 导出, 则有三个参数,并直接给 module赋值
    "./src/index.js": ((m, __unused_webpack_exports, __webpack_require__) => {
        const lib = __webpack_require__(/*! ./lib.js */ "./src/lib.js")
                /* 打印出
                {
                  hello: 'hello',
                  add: [Function],
                  obj: { name: 'a', age: 20 },
                  className: '1年级'
                }
              */
        console.log(lib);
        console.log('====================================');
        lib.className = '1年级'
        m.e = 'a'
    }),
    // 如果通过 exports 导出, 则有两个个参数,并直接给 module.e赋值
    './src/lib.js': ((__unused_webpack_module, e) => {
        e.hello = 'hello'
        e.add = function (a, b) {
            return a + b
        }
        e.obj = { name: 'a', age: 20 }
        setTimeout(() => {
                /* 打印出
                {
                  hello: 'hello',
                  add: [Function],
                  obj: { name: 'a', age: 20 },
                  className: '1年级'
                }
              */
            console.log(e);
            console.log('====================================');
        }, 3000);
    })
}

const webpack_module_cache = {};

function webpack_require(moduleId) {
    // 加载缓存
    if (webpack_module_cache[moduleId]) {
        return webpack_module_cache[moduleId].e;
    }
    // 创建 moudle.e 对象
    var module = webpack_module_cache[moduleId] = {
        e: {}
    };
    // 执行此函数为 module或moudle.e赋值
    webpack_modules[moduleId](module, module.e, webpack_require);
    // 赋值结束,返回module.e
    return module.e;
}

webpack_require("./src/index.js");

mdoule.exports,exports同时存在

// src/lib.js
exports.hello = 'hello'
exports.add = function (a, b) {
    return a + b
}
exports.obj = { name: 'a', age: 20 }
setTimeout(() => {
    console.log('============打印出 { hello: 'hello', add: [Function], obj: { name: 'a', age: 20 } }========================');
    console.log(exports);
    console.log('====================================');
}, 3000);

module.exports = function min(a, b) {
    return a - b
}

// src/index.js
const lib = require('./lib.js')
console.log('==========打印出 [Function: min]==========================');
console.log(lib);
console.log('====================================');
lib.className = '1年级'
module.exports = 'a'

mdoule.exports,exports 同时存在, mdoule.exports 会覆盖 exports, 原理结合上图 webpack打包后的代码手写-模拟打包后的exports代码 就可以理解。

因此 在真实场景中,推荐使用 mdoule.exports

以上就是 CommonJS 的一点个人浅解,有不足的地方多谢各位看官补充😁