前端模块化

154 阅读6分钟

前言

模块化编程就是通过组合一些__相对独立可复用的模块__来进行功能的实现,其最核心的两部分是__定义模块__和__引入模块__;

  • 定义模块时,每个模块内部的执行逻辑是不被外部感知的,只是导出(暴露)出部分方法和数据;
  • 引入模块时,同步 / 异步去加载待引入的代码,执行并获取到其暴露的方法和数据;

CommonJS

CommonJS是NodeJs采用的模块化规范,其核心主要是四个环境变量: module、exports、require、global

global

在CommonJS中global同浏览器环境下的window是一样的角色,主要是用来定义跨模块的、全局的变量

	global.env = 'production'
    global.env = 'devlopment'

像上面这样定义一个env变量,之后就可以在所有模块中通过这个变量来判断当前的是生产环境还是开发环境

module、exports

module对象是每个模块的引用,其中包含着模块的一些相关信息,每一次引入一个模块文件都会生成一个module对象作为变量然后传入模块中,执行完模块中的代码再返回module.exports对象。

Module {
  id: "D:\\myFile\\nodejs\\moudle.js",
  exports: {},
  parent: {
    id: ".",
    exports: {},
    parent: null,
    filename: "D:\\myFile\\nodejs\\index.js",
    loaded: false,
    children: [[Circular]],
    paths: [
      "D:\\myFile\\nodejs\\node_modules",
      "D:\\myFile\\node_modules",
      "D:\\node_modules",
    ],
  },
  filename: "D:\\myFile\\nodejs\\moudle.js",
  loaded: false,
  children: [],
  paths: [
    "D:\\myFile\\nodejs\\node_modules",
    "D:\\myFile\\node_modules",
    "D:\\node_modules",
  ],
};

其主要属性如下:

  • id: 模块的标识符。 通常是完全解析后的文件名
  • parent: 调用该模块的模块
  • filename: 模块的完全解析后的文件名
  • loaded: 模块是否已经加载完成,或正在加载中
  • children: 被该模块引用的模块对象
  • paths: 模块的搜索路径
  • exports: 导出的值

module.exports、exports

在一个模块内,exports 是 module.exports 的引用

	module.exports === exports (true)

exports赋值即给module.exports赋值,因此 module.exports.f = ... 可以更简洁地写成 exports.f = ...,但是只能给exports增加新的属性而不能将exports赋予新值,赋予新值后增加的新属性将不能导出,一言以蔽之,模块最终导出的只是module.exports

  exports.hello = 1;
  module.exports.hello  (1)
  
  exports = {}
  module.exports === exports  (false)

require

通过require引入模块

  // moudle.js
  exports.a = 1
  module.exports.hello = 'hello'
  const moudle = require('./moudle')
  
  // { a: 1, hello: 'hello' }

特点

  • CommonJS规范加载模块是同步的,在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。也就是说,只有加载完成,才能执行后面的操作。
  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。

AMD & CMD

因为CommonJS是同步请求,不适用于浏览器端的模块化,所以诞生了AMD & CMD,解决了在浏览器端的异步模块化编程的需求,其最核心的原理是通过动态加载 script 和事件监听的方式来异步加载模块,通过动态加载文件然后分析其中的引用依赖关系以此递归完成所有依赖文件的加载,通过事件通知,当一个文件的所有依赖完成加载后才来执行当前文件。实现AMD、 CMD规范的著名的库分别是requireJSSeaJS

AMD

规范只定义了一个函数 "define",它是全局变量。函数的描述为:

define(id?, dependencies?, factory);

  • 第一个参数,id,是个字符串。它指的是定义中模块的名字,这个参数是可选的
  • 第二个参数,dependencies,是个定义中模块所依赖模块的数组。依赖模块必须根据模块的工厂方法优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂方法中。 本规范定义了三种特殊的依赖关键字。如果"require","exports", 或 "module"出现在依赖列表中,参数应该按照CommonJS模块规范自由变量去解析。
  • 第三个参数,factory,为模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。

例子

创建一个名为"alpha"的模块,使用了require,exports,和名为"beta"的模块:

	define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
       exports.verb = function() {
           return beta.verb();
           //Or:
           return require("beta").verb();
       }
   });

一个返回对象的匿名模块:

   define(["alpha"], function (alpha) {
       return {
         verb: function(){
           return alpha.verb() + 2;
         }
       };
   });

一个没有依赖性的模块可以直接定义对象:

   define({
     add: function(x, y){
       return x + y;
     }
   });

一个使用了简单CommonJS转换的模块定义:

   define(function (require, exports, module) {
     var a = require('a'),
         b = require('b');

     exports.action = function () {};
   });

CMD

在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:

  define(factory);

CMDAMD基本相似,其主要区别如下:

  1. AMD推崇依赖前置即将依赖放在define的dependencies参数中,CMD推崇就近依赖即放在工厂函数中,这种区别各有优劣,只是语法上的差距,而且requireJS和SeaJS都支持对方的写法
  2. 执行工厂函数的时机不同:AMD是加载完立即执行,CMD是延迟执行,CMD只有在require的时候才会执行

例子

define(function(require) {

  // 通过 return 直接提供接口
  return {
    foo: 'bar',
    doSomething: function() {}
  };

});
  define(function(require, exports, module) {
    var a = require('./a')
    var req;
    
    if(a.some) {
      req = require('./b')
    }
    else {
      req = require('./c')
    }
    
    module.exports = {
      foo: req.some,
      doSomething: function() {}
    };
  });

require.async 方法用来在模块内部异步加载模块,并在加载完成后执行指定回调。callback 参数可选。

define(function(require, exports, module) {
  // 异步加载一个模块,在加载完成时,执行回调
  require.async('./b', function(b) {
    b.doSomething();
  });

  // 异步加载多个模块,在加载完成时,执行回调
  require.async(['./c', './d'], function(c, d) {
    c.doSomething();
    d.doSomething();
  });

});

UMD

umd是一种思想,就是一种兼容 commonjs,AMD,CMD 的兼容写法,define.amd / define.cmd / module 等判断当前支持什么方式, UMD先判断支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。都不行就挂载到 window 全局对象上面去

(function (global, factory) {
  if (typeof exports === 'object') {   
    module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
    define(factory);
  } else {
    this.eventUtil = factory();
  }
})(this, function (exports) {
  // Define Module
  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.default = 42;
});

ES6

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };

/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

参考文章

juejin.cn/post/684490…

juejin.cn/post/685731…

juejin.cn/post/684490…