JS模块化

162 阅读14分钟

js模块化

背景

在js刚刚出现的时候,是为了实现一些简单的功能,但随着浏览器的不断发展,js越来越被重视起来,可以实现较为复杂的功能。这个时候开发者为了维护方便,会把不同功能的模块抽离出来写入单独的js文件,但是当项目更为复杂的时候,html可能会引入很多个js文件,而这个时候就会出现命名冲突,污染作用域等一系列问题,这个时候模块化的概念及实现方法应运而生。

模块化开发是一种管理方式,一种生产方式,一种解决问题的方案。一个模块就是实现某个特定功能的文件,我们可以很方便的使用别人的代码,想要什么模块,就引入那个模块。但是模块开发要遵循一定的规范,后面就出现了我们所熟悉的AMD和CMD规范。

image.png

立即执行函数

在早期,使用立即执行函数实现模块化是最常见的手段,通过函数作用域解决了命名冲突、污染全局的问题,那么立即执行函数也是一种模块化的实现方式,但并非是一种解决方案。举个例子:

(function (a) {
    // 在这里面声明各种变量、函数都不会污染全局作用域
})(a)

AMD

AMD(Asynchronous Module Definition,异步模块定义)

AMD即是“异步模块定义”,它采用异步方式加载模块,模块的加载不影响后面语句的运行,所有依赖整个模块的语句,都定义在一个回调函数中,等到加载完成后,整个回调函数才会运行。

AMD规范中,我们使用define定义模块,使用require加载模块。

定义模块

define(id?, dependencies?, factory);
  • id是定义的模块名,这个参数是可选的,如果没有定义该参数,模块名字应该默认为模块加载器请求的指定脚本的名字,如果有该参数,模块名必须是顶级的绝对的。
  • dependencies是定义的模块中所依赖的模块数组,依赖模块优先级执行,并且执行结果按照数组中的排序依次以参数的形式传入factory。
  • actory是模块初始化要执行的函数或对象,只被执行依次,如果是对象,则为该模块的输出值。 下面来看一个例子:
define("OrderModel", ["Header", "Pay"], function (Header, Pay) {   
    var OrderModel = function () {
        this.headerData = Header.getHeaderData();
        this.payData = Pay.getPayData();
    }
    return OrderModel;
})

加载模块

require([module], callback); 

require要传入两个参数,

  • 第一个是[module],是一个数组,就是要加载的模块
  • 第二个callback是加载成功之后的回调函数

下面举个例子:

// 在定义模块中已经定义过OrderModel模块了,下面只需要加载并使用它
require(["OrderModel"], function (OrderModel) {
    console.log(OrderModel.headerData);    
    console.log(OrderModel.payData);
})

说明

requireJS的使用:www.jianshu.com/p/c90fff39c…

CMD

CMD规范:全称是Common Module Definition,即通用模块定义。

CMD即是“通用模块定义”,CMD规范是国内发展出来的,CMDAMD都是要解决同一个问题,只不过两者在模块定义方式和模块加载时机上有所不同罢了。

定义模块

CMD中一个模块就是一个文件,通过define()进行定义。

define接收factory参数,它可以是一个函数,也可以是一个对象一个字符串。

  • factory是一个对象或者一个字符串时,表示该模块的接口就是这个对象或者字符串。
  • factory是一个函数时,表示是该模块的构造方法。执行该构造方法,可以得到模块向外提供的接口,factory在执行时,默认传入三个参数:requireexportsmodule
    • 其中require用来加载其它模块。exports用来实现向外提供的模块接口。
    • module是一个对象,存储着与当前模块相关联的一些属性和方法,传给factory构造方法的exportsmodule.exports对象的一个引用,至通过exports参数来提供对外的接口,有时无法满足所有需求,比如当模块的接口是某个类的实例时,这个时候就需要通过module.exports来实现。

下面举个例子:

// 定义模块OrderModel.js
define(function (require, exports, module) {
    
    var Header = require('./Header'); // require用来加载其它模块    
    exports.headerData = Header.getHeaderData(); // 对外提供headerData属性    
    // exports是module.exports的一个引用
    console.log(exports === modele.exports); // true
 
    var Pay = require('./Pay'); // 依赖可以就近加载
    exports.payData = Pay.getPayData(); // 对外提供payData属性    
    exports.payFun = function() {
        console.log('payFun log something');
    }; // 对外提供payFun方法
 
})

加载模块

通过SeaJs的use方法我们可以加载模块

举个例子:

// 上面我们已经定义了OrderModel模块了,直接加载即可
seajs.use(["OrderModel.js"], function (orderModel) {
    var headerData = orderModel.headerData;
    var payData = orderModel.payData;
    orderModel.payFun(); // 可以直接使用,输出 payFun log something
})

说明

SeaJs使用:www.jianshu.com/p/ebdf2233e…

AMD与CMD的不同

AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。

异步加载:不阻碍后面代码加载执行

(1) 依赖处理方式

  • AMD依赖前置,需要在定义模块时明确声明所有依赖,模块加载器会先加载完所有依赖再执行模块代码。示例(以 RequireJS 为例):
// 定义模块时声明依赖(依赖前置)
define(['dep1', 'dep2'], function(dep1, dep2) {
  return { /* 模块内容 */ };
});
  • CMD依赖就近,允许在模块代码中按需引入依赖,加载器会在代码执行到依赖处时才去加载对应模块。示例(以 SeaJS 为例)
// 定义模块时无需提前声明依赖,使用时通过require引入(依赖就近)
define(function(require, exports, module) {
  var dep1 = require('dep1'); // 执行到此处才加载dep1
  var dep2 = require('dep2'); // 执行到此处才加载dep2
  // ...
});

(2)加载时机与执行顺序

  • AMD:加载器在解析模块定义时,会立即加载所有声明的依赖,依赖加载完成后再执行当前模块的回调函数,因此执行顺序与依赖声明顺序一致。
  • CMD:加载器先加载当前模块,执行模块代码时遇到require才会加载对应的依赖,依赖加载完成后继续执行后续代码,更接近同步代码的执行逻辑。

(3)设计理念

  • AMD:更强调异步加载,适合浏览器环境中需要快速响应、避免阻塞的场景(如 RequireJS),通过提前加载依赖保证模块执行时依赖已就绪。
  • CMD:更接近CommonJS 的同步加载风格(如 Node.js 的require),但仍保持异步特性,注重代码的自然书写方式,依赖引入与使用位置更接近(如 SeaJS)。

(4)模块输出方式

  • AMD:通过define的回调函数返回模块输出。
  • CMD:除了通过return返回,还可通过module.exportsexports对象定义输出,更贴近 CommonJS 规范。

随着 ES6 Module(import/export)成为标准,AMD 和 CMD 的使用场景已逐渐减少,但它们在 JavaScript 模块化发展中起到了重要的过渡作用。

CommonJS

CommonJS规范主要应用于Node,每个文件就是一个模块,有自己的作用域,即在一个文件中定义的变量、函数、类都是私有的,对其他文件不可见。

定义模块

(上面说了每个文件就是一个模块,所以不存在定义的概念,只是为了承接上下文,更好理解罢了,文章后面不再说明。)

CommonJs规范规定,每个模块内部有两个变量可以使用:requiremodule

  • require用来加载某个需要的模块。
  • module代表的是当前模块,是一个对象,存储着当前模块的相关联的属性和方法。

exportsmodule上的一个属性。该属性表示当前模块对外输出的接口,其它文件加载该模块,实际上就是读取module.exports变量。(在实际开发中如果区分不了exports和module.exports的话,那就直接使用module.exports即可,那个exports就别管、别用了。

eg:

暴露方式1

// orderModel.js
var Header = require('./Header'); // require用来加载其它模块
var Pay = require('./Pay');
 
var payFun = function () {
    console.log('payFun log something');
}
 
module.exports = { // 对外提供以下三个属性
    headerData: Header.getHeaderData(),
    payData: Pay.getPayData(),
    payFun: payFun
}

暴露方式2

// orderModel.js
var Header = require('./Header'); // require用来加载其它模块
var Pay = require('./Pay');
 
var payFun = function () {
    console.log('payFun log something');
}
 

// 对外提供以下三个属性
exports.headerData  = Header.getHeaderData()
exports.payData  =  Pay.getPayData()
exports.payFun  = payFun

加载模块

其实在上面的代码中,即orderModel.js中已经写出了加载模块的方法了。

下面是加载并使用orderModel.js的例子:

var orderModel = require('./orderModel');
 
var headerData = orderModel.headerData;
var payData = orderModel.payData;
orderModel.payFun(); // 输出 payFun log something

需要注意的是,CommonJS规范规定,模块可以多次加载,但是只会在第一次加载时运行一次,运行结果就会被缓存下来,以后再加载就直接读取缓存结果,如果想让模块再次运行,必须清除缓存。

eg:

require('./orderModel');
require('./orderModel').message = 'hello world';
require('./orderModel').message;
// hello world

清除缓存例子:

// 删除指定模块的缓存,这里删除orderModel.js,也可以写多个进行批量删除。
delete require.cache[require.resolve('./orderModel')];
 
// 删除所有模块的缓存,大范围攻击
Object.keys(require.cache).forEach(function (key) {
    delete require.cache[key];
})

说明

Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:moduleexportsrequireglobal。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。

// 定义模块math.js
var basicNum = 0;
function add(a, b) {
  return a + b;
}
module.exports = { //在这里写上需要向外暴露的函数、变量
  add: add,
  basicNum: basicNum
}

// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5);

// 引用核心模块时,不需要带路径
var http = require('http');
http.createService(...).listen(3000);

commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。

ES Moudule

在ES6没出来之前,模块加载方案主要使用CommonJS和AMD两种,前者用于服务器,后者用于浏览器。ES6在语言标准层面上实现了模块功能,而且使用起来相当简单。

定义模块

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import用于引入其它模块提供的功能。

一般来说,一个模块对应的就是一个文件,该文件内部的变量外部无法获取,如果你希望外部能够读取到某个变量,就需要使用export关键字输出该变量。

举个例子:

统一暴露

// user.js
let name = "张三";
let age = 20;
 
const getSex = (s) => {
    return s === 1 ? "男" : "女";
}
// 通用写法,如果不想了解,就这样写就完事了
export {
    name,
    age,
    getSex
}

分别暴露

// test.js
export const data = '测试'
export const msg = '信息'

export function showData(){
    
}

export function showMsg(){

}

默认暴露--- 只能暴露一次

// module4.js
export default {
    name:'cc',
    age:18
}

混合暴露--- 多种暴露方式结合在一个文件中

// module5.js

// 分别暴露
export const teacher1 = {name:'t1',age:24}
export const teacher2 = {name:'t2',age:26}

// 统一暴露
const stu1 = {name:'s1',age:12}
const stu2 = {name:'s2',age:13}
export {stu1,stu2}

// 默认暴露
export default {
    name:'ff',
    age:18
}

加载模块

上面已经使用export命令定义了模块对外的接口后,其它的JS文件就可以通过import命令加载这个模块。

举个例子:

// main.js
import {name, age, getSex} from './user';
 
console.log(name); // 张三
console.log(age); // 20
console.log(getSex(1)); // 男
// mainTest.js

// 引入【分别暴露】的模块
// 注意这里不是解构赋值哦!!!!
import {data, msg, showData, showMsg} from './test';
 
// 引入【分别暴露】的模块 + 重命名
import {data as data2} from './test';

// 引入【分别暴露】的模块 + 打包引入
// 要引入的东西很多,可以用如下方式
import * as module1 from './test';



// 引入【统一暴露】的模块
// 统一暴露和分别暴露,最后引入的方式都是一样的
import {name, age} from './user';



// 引入【默认暴露】的模块
import module4 from './module4';


// 引入【混合暴露】的模块
import module5,{teacher1,teacher2,stu1,stu2} from './module5';

import接受一对大括号,里面指定的是要从其它模块导入的变量名。大括号内的变量名,必须与导入模块对外接口的名称相同。但是注意这不是解构赋值。

再次强调!!!

对加载模块进行重命名,如下写法:

// main.js
import {name as otherName} from './user'; // 使用as进行重命名
console.log(otherName); // 张三

对模块的整体加载,如下写法:

// main.js
import * as user from './user'; // 使用 * 号指定一个对象,所有输出值都加载在这个对象上面
 
console.log(user.name); // 张三
console.log(user.age); // 20
console.log(user.getSex(1)); // 男
 
// 需要注意的是 user是静态分析的,不允许运行时改变
// 下面的写法是不允许的
user.height = 180;
user.setOld = function () {};

export写法

/** 定义模块 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);
}

使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。

export default写法

/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
    ele.textContent = math.add(99 + math.basicNum);
}

ES6的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。

与commomJS的区别

  • ES Module不支持动态导入,但已提案,指日可待。
  • ES Module是异步导入,因为用于浏览器,需要下载文件,如果采用同步导入对渲染有很大影响。CommonJS是同步导入,因为用于服务端,文件都在本地,同步导入即使卡主主线程影响也不大。
  • ES Module导出的是值的引用,导入导出值都指向同一个内存地址,所以导入值会跟随导出值变化。而CommonJS在导出时都是值的拷贝,就算导出的值变了,导入的值也不会改变,所以想要更新值,必须重新导入一次。

ES6转ES5语法,用babel,但是,Promise babel不支持,用webpack即可

babel将ES6语法转为ES5语法,即ES6模块化转为Commonjs,但是浏览器还不支持,所以还需借助Browserify翻译

Browserify 能翻译CommonJs代码,在浏览器上运行

Browserify 可以让你使用类似于 node 的 require () 的方式来组织浏览器端的 Javascript 代码

通过预编译让前端 Javascript 可以直接使用 Node NPM 安装的一些库。

AMD CMD CommonJS ES6

MD、CMD、CommonJS 和 ES6 Module 都是 JavaScript 中用于模块化开发的规范,它们的出现是为了解决代码复用、作用域隔离等问题,但适用场景和设计理念有所不同。以下是它们的关系和区别:

核心定位与背景

  • CommonJS:主要为 服务器端(Node.js)  设计,同步加载模块(因为服务器文件存储在本地,同步加载性能影响小)。
  • AMD(Asynchronous Module Definition) :为 浏览器端 设计,异步加载模块(解决浏览器加载时的阻塞问题),代表库是 RequireJS。
  • CMD(Common Module Definition) :同样为 浏览器端 设计,异步加载但更强调 “按需加载”,代表库是 Sea.js(国内产物,受 CommonJS 和 AMD 影响)。
  • ES6 Module(ESM) :ES6 官方标准化的模块系统,同时支持浏览器和服务器端,设计上借鉴了前三者的优点,成为现代 JavaScript 模块化的标准。

关键区别对比

特性CommonJSAMD(RequireJS)CMD(Sea.js)ES6 Module(ESM)
加载方式同步(运行时加载)异步(提前声明依赖)异步(就近声明依赖)静态分析(编译时加载)
适用场景Node.js 为主浏览器端浏览器端(已淘汰)浏览器 / Node.js(通用)
语法require()/module.exportsdefine(deps, factory)define(factory)(内部requireimport/export
依赖处理运行时动态加载依赖前置(提前加载)依赖就近(按需加载)编译时确定依赖
模块输出拷贝值(非引用)引用传递引用传递引用传递(只读)

关系总结

  • 发展脉络:CommonJS(服务器)→ AMD/CMD(浏览器异步方案)→ ES6 Module(统一标准)。
  • 替代关系:ES6 Module 是最终标准化方案,逐渐取代 AMD/CMD;CommonJS 仍在 Node.js 中广泛使用,但 Node.js 也已支持 ES6 Module。
  • 设计理念:ES6 Module 综合了 CommonJS 的静态分析能力和 AMD 的异步加载优势,成为现代 JavaScript 模块化的首选。