前端模块化

32 阅读4分钟

Script脚本

还早在前端即jquery时代,模块加载都用的是script标签,是最古老的一种模块加载方式。

它的不足:

  • 全局变量:存在命名冲突污染隐患
  • 依赖管理:各脚本的编写和加载有先后要求
  • 串行执行:加载的时候,浏览器会停止网页渲染,加载的文件越多、越大,页面失去响应的时间越长

CommonJS

是由Mozilla工程师于2009年开始的一个项目,目的是为了让除浏览器之外的客户端(如:服务器端或桌面端)对javascript进行模块化开发。

主要适用于服务器端开发,如Node.js,采用同步加载模块策略。

// 关键字
module.exports = {}
require('')

路径加载方式

require('/') 如果以/开头,则是按绝对路径加载;

以./或../,则是相对路径;

不以以上方式加载,如果是非核心模块(node是核心模块优先),则会加载node_modules下模块,如:require('path')

exports对象会被引用方修改吗

思考如下代码:

// lib.js
exports.foo = {
  name: 'lib'
}
setTimeout(() => {
  console.log(exports)  // { foo: { name: 'lib' }, modify: true }
}, 1000)

// index.js下引入lib.js
const lib = require('./lib.js')
lib.modify = true

从打印出的日志可以看出:在引用方修改导出的lib对象,exports对象是被修改的。说明了两个对象指向的是同一块内存地址。

这样会不会有问题,既然我们需要模块化,而外部可以随便修改它。

module.exports

修改上述代码:

// lib.js
exports.foo = {
  name: 'lib'
}

// 新增一个add导出函数
module.exports = function add(a, b) {
  return a + b
}
setTimeout(() => {
  console.log(exports) // { foo: { name: 'lib' } }
  console.log(module.exports) // [Function: add] { modify: true }
}, 1000)

// index.js下引入lib.js
const lib = require('./lib.js')
lib.modify = true
console.log(lib) // [Function: add] { modify: true }

lib.js新增一个module.exports导出函数add,打印出的结果不同,导出的对象变成了module.exports

我们再略作修改:

module.exports = function add(a, b) {
  return a + b
}
// 改成
module.exports.add = function(a, b) {
  return a + b
}
// 三处打印结果相同
// { foo: { name: 'lib' }, add: [Function (anonymous)], modify: true }

这样说明都指向的是同一个对象。

为什么会这样呢?

我们来看看通过webpack5编译之后的代码:

(() => {
  var __webpack_modules__ = ({
    "./index.js":((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
      eval("const lib = __webpack_require__(/*! ./module/lib.js */ \"./module/lib.js\")\r\nlib.modify = true\r\nconsole.log(lib, 'lib index...')\n\n//# sourceURL=webpack://node/./index.js?");
    }),
    "./module/lib.js":((module, exports) => {
      eval("exports.foo = {\r\n  name: 'lib'\r\n}\r\n\r\nmodule.exports = function add(a, b) {\r\n  return a + b\r\n}\r\n\r\nsetTimeout(() => {\r\n  console.log(module.exports, 'lib2...')\r\n  console.log(exports, 'lib...')\r\n}, 1000)\r\n\n\n//# sourceURL=webpack://node/./module/lib.js?");
    })
  });
  var __webpack_module_cache__ = {};
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    // Create a new module (and put it into the cache)
    var module = __webpack_module_cache__[moduleId] = {
      exports: {}
    };
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    return module.exports;
  }
  var __webpack_exports__ = __webpack_require__("./index.js");
})();

注释都去掉了,代码不多。核心的逻辑在于eval函数执行模块本身的代码:

// 第1个入参是导出的module,第2个入参是module.exports的引用
__webpack_modules__[moduleId](module, module.exports, __webpack_require__)

如果lib.js中仅是通过exports的引用来新增参数,那返回的内存地址并没有变化,仅新增了一些值;但如果模块中存在module.exports的赋值,例如函数,则会改变整个module的内存地址。

再简化一下这个函数:

const moduleX = {
  exports: {}
}
function test (moduleX, exports) {
  // exports仅是一个引用地址,若没有函数赋值来改变moduleX.exports的地址,则返回的将是{foo: 123}
  moduleX.exports = function f1 () {}
  exports.foo = 123
  return moduleX.exports
}
console.log(test(moduleX, moduleX.exports)) // [Function: f1]

AMD

全称:Asynchronous Module Definition(异步模块定义)。是从CommonJS讨论中诞生的,优先照顾了浏览器的模块加载场景,使用了异步加载和回调。

// lib/sayModule.js
define(function (){
  return {
    sayHello: function () {
      console.log('hello');
    }
  };
});
define(['./lib/sayModule'], function (say){
  say.sayHello(); 
})

RequireJS

是前端模块化管理工具,遵循AMD规范,通过一个函数来将所需要的或者说所依赖的模块装载进来,然后返回一个新的函数或模块,所有的有关新模块的操作都在内部操作。

CMD

全称:Common Module Definition(公共模块定义)。在CMD中,一个模块就是一个文件。

// 定义JSON
define({"foo": "100"})

// 定义函数时,表示模块的构造方法,执行构造方法便可以得到模块向外提供的接口
define(function(require, exports, module) { 
    // 模块代码
})

AMD和CMD的区别在于:

对依赖模块的执行时机不同,而不是加载模块的时机不同。两者加载模块都是异步的,只不过AMD在主逻辑执行前就已经知道依赖的模块是什么,而CMD是在执行主逻辑代码的时候才知道依赖于谁。

UMD

全称:Universal Module Definition(统一模块定义)。是将AMD和CommonJS合并在一起的一种尝试。

(function(define) {
    define(function () {
        return {
            sayHello: function () {
                console.log('hello');
            }
        };
    });
}(
    typeof module === 'object' && module.exports && typeof define !== 'function' ?
    function (factory) { module.exports = factory(); } :
    define
));

该模式的核心思想是IIFE,会根据环境来判断需要的参数类别。运行时动态编译。

ES6 Module

自动采用严格模式,不管在模块头部是不是加了"use strict"。运行前编译。

a.js
export const a = 1;
// 引入
import a from 'a.js'