JavaScript中的模块

  • 出现原由
  1. 在早期编写JavaScript时,我们只需在 script标签 内写入JavaScript的代码就可以满足我们对页面交互的需要了。
    但随着时间的推移,时代的发展,原本的那种简单粗暴的编写方式所带来的诸如逻辑混乱,页面复杂,可维护性差,全局变量暴露等问题接踵而至 ,前辈们为了解决这些问题提出了很种的解决方案,其中之一就是JavaScript模块化编程

模块通常是指编程语言所提供的代码组织机制,利用此机制可将程序拆解为独立且通用的代码单元。
一个模块就是实现特定功能的文件,有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块

  • 可维护性
    因为模块是独立的,一个设计良好的模块会让外面的代码对自己的依赖越少越好,这样自己就可以独立去更新和改进。
  • 命名空间
    在 JavaScript 里面,如果一个变量在最顶级的函数之外声明,它就直接变成全局可用。因此,常常不小心出现命名冲突的情况。使用模块化开发来封装变量,可以避免污染全局环境
  • 重用代码
    我们有时候会喜欢从之前写过的项目中拷贝代码到新的项目,这没有问题,但是更好的方法是,通过模块引用的方式,来避免重复的代码库

✨所谓模块化主要是解决代码分割、作用域隔离、模块之间的依赖管理以及发布到生产环境时的自动化打包与处理等多个方面。

  •  JavaScript模块化编程的优点:
  • 解决项目中的全局变量污染的问题
  • 开发效率高,有利于多人协同开发
  • 职责单一,方便代码复用和维护
  • 解决文件依赖问题,无需关注引用文件的顺序

🖌模块开发需要遵循一定的规范,否则就都乱套了,因此,才有了后来大家熟悉的AMD规范,CMD规范

1、AMD规范

AMDAsynchronous Module Definition,中文名是“异步模块定义”的意思
它采用异步方式加载模块,模块的加载不影响它后面语句的运行,所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行
一般来说,AMD是 RequireJS 在推广过程中对模块定义的规范化的产出,因为平时在开发中比较常用的是require.js进行模块的定义和加载,一般是使用define来定义模块,使用require来加载模块

  • 定义模块
  • AMD规范只定义了一个函数define,它是全局变量,我们可以用它来定义一个模块
define(id?, dependencies?, factory);

id 是定义中模块的名字,这个参数是可选的,如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字,如果提供了该参数,模块名必须是“顶级”的和绝对的

dependencies 是定义的模块中所依赖模块的数组,依赖模块必须根据模块的工厂方法优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂方法中

factory 是模块初始化要执行的函数或对象,如果为函数,它应该只被执行一次,如果是对象,此对象应该为模块的输出值

例子:

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

定义了一个alpha的模块,这个模块依赖require,exports,beta,因此需要先加载它们,再执行后面的factory
  • 加载模块
  • require.js中采用require()语句加载模块,在定义好了模块后,我们可以使用require进行模块的加载
require([module], callback);

require要传入两个参数,
第一个参数[module],是一个数组,里面的成员就是要加载的模块
第二个参数callback,则是加载成功之后的回调函数

例子:

require([increment'], function (increment) {
    increment.add(1);
});

比如我们现在已经定义了一个模块,名字为increment,里面有一个add方法,我们现在需要用到里面的方法,只要像上面一样将模块加载进来,然后调用方法就可以了

在使用require.js时,可以通过define()定义模块
这时候里面的模块的方法和变量外部是无法访问到的,只有通过return
然后再加载这个模块,才可以进行访问

define('math',['jquery'], function ($) {//引入jQuery模块
    return {
        add: function(x,y){
            return x + y;
        }
    };
});
上面的代码定义了一个math模块,返回了一个add方法,要使用这个模块的方法,我们需要向下面这样进行访问

require(['jquery','math'], function ($,math) {
    console.log(math.add(10,100));//110
});
通过require,我们加载了math模块,这样就可以使用math模块里面的add方法了

2、CMD

  • CMDCommon Module Definition通用模块定义,CMD规范是国内发展出来的,同时,CMD是在SeaaJS推广的过程中形成的,CMD和AMD要解决的都是同个问题,在使用上也都很像,只不过两者在模块定义方式和模块加载时机上有所不同

  • 在 CMD 规范中,一个模块就是一个文件,通过define()进行定义
define(factory);

define接受factory参数,factory可以是一个函数,也可以是一个对象或字符串
factory为对象、字符串时,表示模块的接口就是该对象、字符串,
比如可以如下定义一个 JSON 数据模块:
      define({ "foo": "bar" });

也可以通过字符串定义模板模块:
    define('I am a template. My name is {{name}}.');
factory为函数时,表示是模块的构造方法,执行该构造方法,可以得到模块向外提供的接口
factory方法在执行时,默认会传入三个参数:require,exports和 module
define(function(require, exports, module) {
  // 模块代码
});

其中,require用来加载其它模块,而exports可以用来实现向外提供模块接口:
define(function(require, exports) {
  // 对外提供 foo 属性
  exports.foo = 'bar';

  // 对外提供 doSomething 方法
  exports.doSomething = function() {};
});

module是一个对象,上面存储了与当前模块相关联的一些属性和方法,传给factory构造方法的exports参数是module.exports对象的一个引用,只通过exports参数来提供接口,
有时无法满足开发者的所有需求,比如当模块的接口是某个类的实例时,需要通过module.exports来实现
define(function(require, exports, module) {
  // exports 是 module.exports 的一个引用
  console.log(module.exports === exports); // true

  // 重新给 module.exports 赋值
  module.exports = new SomeClass();

  // exports 不再等于 module.exports
  console.log(module.exports === exports); // false
});

  • 在前面定义模块时,我们说过,当factory为函数时,require会作为默认参数传递进去,而require可以实现模块的加载
  • require是一个方法,接受模块标识作为唯一参数,用来获取其他模块提供的接口
define(function(require, exports) {
  // 获取模块 a 的接口
  var a = require('./a');

  // 调用模块 a 的方法
  a.doSomething();
});

🔗

// CMD
define(function(require, exports, module) {
  var a = require('./a')
  a.doSomething()
  // 此处略去 100 行
  var b = require('./b') // 依赖可以就近书写
  b.doSomething()
  // ... 
})

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
  a.doSomething()
  // 此处略去 100 行
  b.doSomething()
  ...
}) 

AMD和CMD都是通过define()定义模块

AMD需要把依赖的模块先写出来,可以通过return暴露接口,CMD在定义模块需要传入require,exports和module这几个参数
要加载某个模块时,使用require进行加载
要暴露接口时,可以通过exports,module.exports和return

🔗

  • 从上面定义模块和加载模块的方式上,可以看出AMD和CMD主要有下面几个不同:
    (1)AMD是RequireJS在推广过程中对模块定义的规范化产出,CMD是SeaJS在推广过程中对模块定义的规范化产出
    (2)对于依赖的模块,AMD是提前执行,CMD是延迟执行
    (3)对于依赖的模块,AMD推崇依赖前置,CMD推崇依赖就近

  • CMD是SeaJS在推广过程中对模块定义的规范化产出,因此一般在实际开发中,我们都是通过SeaJS进行模块的定义和加载
// 定义模块  myModule.js
define(function(require, exports, module) {
  var $ = require('jquery.js')
  $('div').addClass('active');
  exports.data = 1;
------});

// 加载模块
seajs.use(['myModule.js'], function(my){
    var star= my.data;
    console.log(star);  //1
});

上面的代码中定义了myModule.js模块,因为该模块依赖于jquery.js

因此在需要使用该模块时可以使用require进行模块的加载,然后通过exports暴露出接口,
通过SeaJS的use方法我们可以加载该模块,并且使用该模块暴露出的接口

ES6中的模块化

  • (在es6没有出来之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种,前者用于服务器,后者用于浏览器,ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案)
  • es6中的模块化有一个比较大的特点,就是实现尽量的静态化
比如说在CommonJS中我们要加载fs中的几个方法,需要这样写

// CommonJS模块
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

上面的代码其实是加载了fs中的所有方法,生成一个对象,再从这个对象上读取方法,这种加载其实叫做**运行时加载**,也就是只有运行时才能得到这个对象,不能实现在编译时实现静态优化
  • ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入
// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载,这种加载称为**“编译时加载”**或者**静态加载**,
即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高,当然,这也导致了没法引用 ES6 模块本身,因为它不是对象

1、export

  • 模块功能主要由两个命令构成:export和import,export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能
  • 一般来说,一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取,如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 2021;

如果要输出函数,可以像下面这样定义:
function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
上面的代码中,我们使用了as对函数的对外接口进行了重命名

2、import

  • 使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块
// main.js
import {firstName, lastName, year} from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

import命令接受一对大括号,里面指定要从其他模块导入的变量名。
大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同
我们也可以对加载的模块进行重命名:

import { lastName as surname } from './profile.js';
  • 除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面

例子:

// circle.js

export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}
整体加载的写法如下:

import * as circle from './circle';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
  • 这里有一个地方需要注意,模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变,下面的写法都是不允许的
import * as circle from './circle';

// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};

后续看到相关内容会及时增改,如有不正之处,欢迎指正!万分感谢!🙇