JS中的几种模块化规范(CommonJS、AMD、CMD、ES6 Module)

引入问题:为什么要进行模块化?

1)模块化就是将系统分离成独立功能的模块,这样我们需要什么功能,就加载什么功能
2)模块化的好处:

  • 避免命名空间的冲突(减少命名空间的污染)
  • 更好的分离,实现按需加载
  • 提高可代码的复用性
  • 提高了代码的维护性

JS模块化大致发展过程

CommonJS(服务端)=》AMD(浏览器)=》CMD=》ES6 Module模块化

模块化规范的种类

在这里插入图片描述

模块化规范的发展趋势

在这里插入图片描述
终极:现代工具webpack

  • webpack自己实现了一套模块机制,无论是CommonJs模块的require语法还是ES6模块的import语法,都能够被解析并转换成指定的环境的可运行代码。(随着webpack打包工具的流行,ES6语法广泛手中,后来开发者对于AMD CMD的感知越来越少)

1.CommonJS规范

1.1说明:

  • 每个文件都可以作为一个模块(这里的文件指的是js文件)
  • 在服务器端:模块的加载是运行时同步加载的
  • 在浏览去端:模块需要提前编译打包处理,不然浏览器不能识别require语法

1.2 使用

主要分为定义模块和引入模块两个步骤
定义模块语法:

1)module.exports=value
2export.xxx=value

引入模块语法

var xxx=require(url)

模块标识:
模块标识就是require()函数的参数,规范是这样的:

  • 必须时字符串
  • 可以时以./ …/开头的相对路径
  • 可以时绝对路径
  • 可以省略后缀名

其中,当引入的模块为自定义的模块时,那么url则是该模块所在的路径
当引入的模块是第三方模块时,则url为其具体包名
标准内容:

  • 模块通过exports来向外暴露API,exports只能是一个对象,暴露的API需作为此对象的属性
  • 定义全局函数require,通过传入模块标识来引入其他模块,执行的结果即为别的模块暴露出来的API
  • 如果被reqiure函数引入的模块中也包含依赖,那么以此加载这些依赖

特点

  • 模块加载是一项阻塞操作,也就是同步加载
1.2.1模块定义与使用

举例1:

  • 方式①(module1.js)
module.exports={
    
    
	var msg="i am msg"
	foo(){
    
    
		console.log("i am ①");
	}
}
  • 方式②(module2.js)
//这里暴露处的是一个函数
//当引入的方式为
//var module2=require('./module2')时,
//便可以直接使用 module2()调用即可

//当引入的方式为
//var fun=require('./module2')时,
//便可以直接使用 fun()调用即可
module.exports=function(){
    
    
	return ...
}

如:
module.exports=function(){
    
    
	console.log('i am ② ');
}
  • 方式③(module3.js)
module.exports.xxx=function(){
    
    
	....
}
如:
module.exports.foo=function(){
    
    
	console.log('i am ③');
}

module.exports.bar=function(){
    
    
	console.log('i am bar from ③');
}

  • 引入(一般在主模块中如main.js)
/*说明:
    ./module1是该模块所在的路径
*/
const module1=require('./module1');
const module2=require('./module2');
const module3=require('./module3');

//引入第三方模块
var http = require('http');

//使用
module1.foo();
module2();
module3.foo();
module3.bar();

举例2:

// file greeting.js 定义一个模块
var helloInLang = {
    
    
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};

var sayHello = function (lang) {
    
    
    return helloInLang[lang];
}
// 对外输出
module.exports.sayHello = sayHello;

// file hello.js  引入一个模块
var sayHello = require('./lib/greeting').sayHello;
var phrase = sayHello('en');
console.log(phrase);

举例3:

// a.js
module.exports = {
    
    
  moduleFunc: function() {
    
    
    return true;
  };
}
// 或
exports.moduleFunc = function() {
    
    
  return true;
};

// 在 b.js 中引用
var moduleA = require('a.js');
// 或
var moduleFunc = require('a.js').moduleFunc;

console.log(moduleA.moduleFunc());
console.log(moduleFunc())

2.AMD规范

2.1说明

  • CommonJS规范出现后,在Node开发中产生了非常好的效果,开发者希望借鉴这个经验来解决浏览器JS的模块化
  • 但是大部分人认为浏览器和服务器的环境差别太大,毕竟浏览器JS时通过网络动态以此加载的,而服务器的JS是保存在本地磁盘中。因此浏览器需要实现异步加载,模块在定义的时候就必须先知名它所需要依赖的模块,然后把本模块的代码写在回调函数中执行,最终衍生出了AMD规范
  • AMD的主要思想时异步模块,主逻辑在函数回调中执行

2.2 标准内容

在这里插入图片描述

1.定义没有依赖的模块
module1.js

define(function(require,exports.module){
    
    
	return 模块
}

2.定义具有依赖的模块
module2.js

define(['module1','module2'],function(m1,m2){
    
    
	return 模块
}

3.引入模块
main.js

require(['module1','module2'],function(m1,m2){
    
    
	
})

在这里插入图片描述

2.3 举例使用:

//module.js
define(function (require, exports, module) {
    
    
  console.log('module.js')
  exports.name = "jack" //暴露
})


//module2.js

define(function (require, exoprts, module) {
    
    
  console.log('module2.js');
  exports.desc = "hello world"
})


//main.js
require(['module1', 'module2'], function (m1, m2) {
    
    

  console.log('main.js');
  console.log(m1.name + ',' + m2.desc); //引入module1和module2之后,直接使用其暴露的属性
})


// 执行顺序:
// module1.js
// module2.js
// main.js

人无完人,AMD/RequireJS 也存在饱受诟病的缺点。按照 AMD 的规范,在定义模块的时候需要把所有依赖模块都罗列一遍(前置依赖),而且在使用时还需要在 factory 中作为形参传进去。
CMD/RequireJS模块化的顺序是这样的:模块化加载=》全部模块预执行=》主逻辑中调用模块
所以是依赖加载完成后会先预先将模块执行一遍,这种方式会使得程序效率低;

define(['a', 'b', 'c', 'd', 'e', 'f', 'g'], function(a, b, c, d, e, f, g){
    
     ..... });

3.CMD规范

3.1 说明

  • AMD/RequireJS的JS模块实现有很多不优雅的地方,主要原因不能以一种更好的管理模块的依赖加载和执行;
    那么就出现了SeaJS,SeaJs遵循的是CMD规范,CMD规范在AMD的基础上改进的一种规范,解决了AMD对依赖模块的执行时机的问题;
  • SeaJS模块化的顺序是:模块化预加载=》主逻辑调用模块时才执行模块中的代码
  • SeaJS的用法和AMD基本相同,并且融合了
    CommonJS的写法:

3.2 使用

(对于模块的引入,具有同步和异步两中方式)

//module1.js
define(function (require, exports, module) {
    
    
  console.log('module1.js')
  // module.exports = value;
  // exports.xxx = value
  exports.name="i am module1"
})


//main.js
define(function (require, exports, module) {
    
    
  //引入依赖模块(同步)
  var module2 = require('./module2');
  console.log(module2.name)

  //引入依赖模块(异步1)
  require.async('./module3', function (m3) {
    
    
    //这里m3对应module3

  })
  //引入依赖模块(异步2)
  var module4=require.async('./module4');
  console.log(module4.name)
})

总结:
SeaJS的出现,是CommonJS在浏览器的践行者,并吸收了RequireJS的优点

4. ES6中的Module模块

4.1 标准内容

  • 模块功能主要由两个命令构成:exportimport
  • export用于暴露接口,import用于引入模块

4.2 模块的定义

有如下3中方式

  • 方式1-----分别暴露

//module1.js
export var m=1
export var arr=[1,2,4]
export function fun(){
    
    
  console.log('i am a fun')
}

//引入与使用(结构引入):
import {
    
    m,arr,fun} from './module1'
console.log(m);
fun()
  • 方式2—统一暴露
//module2.js
var m=1;
var arr=[1,2,4]
function fun() {
    
    
  console.log('i am a fun')
}

export {
    
    m,arr,fun}


//引入与使用(结构引入):
import {
    
    m,arr,fun} from '/module2.js'
console.log(arr);
fun()
  • 方式3----默认暴露
//这里默认暴露对象
//module3.js
//export default其实是导出一个叫做default的变量,所以其后面不能跟变量声明语句。
//错误
export default var a = 1;

//正确
export default {
    
    
  m:1,
  fun(){
    
    
    console.log('i am a fun from export defalut')
  }
}

//person.js
export default function getName(){
    
    
 ...
}


//引入和使用(module为自定义任意名字)
import module from './module3.js'
module.fun()

在这里插入图片描述

4.4 模块的引入

// 解构引入
import {
    
     firstName, lastName, year } from 'a-module';
// 为输入的变量重新命名
import {
    
     lastName as surname } from 'a-module';
// 引出模块对象(引入所有)
import * as ModuleA from 'a-module';

  • 在使用 ES Module 值得注意的是:import 和 export 命令只能在模块的顶层,在代码块中将会报错,这是因为 ES Module 需要在编译时期进行模块静态优化,import 和 export 命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行,这种设计有利于编译器提高效率,但也导致无法在运行时加载模块(动态加载)。

对于这个缺点,TC39 有了一个新的提案 – Dynamic Import,提案的内容是建议引入 import()方法,实现模块动态加载。

// specifier: 指定所要加载的模块的位置
import(specifier)

  • import()方法返回一个Promise对象
import('b-module')
  .then(module => {
    
    
    module.helloWorld();
  })
  .catch(err => {
    
    
    console.log(err.message);
  });

PS:

  • import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。
    它是运行时执行,也就是说,什么时候运行到这句话,就会加载到指定的模块。另外,import()函数所加载的模块没有静态链接关系,这点也是与import语法不同
  • 注意的时ES6 的Module语法有些浏览器是不支持的,因此需要Babel先进性转码,将import和export命令转成ES5语法才能被浏览器解析。


这里举例之前做过的一个小项目,就是使用了ES6语法中的module模块化思想结合axios库,将需要向服务端发送请求的操作封装到一个模块中,然后对于不同的请求数据操作,直接导入该模块调用即可
1)封装ajax请求函数(使用的时默认暴露)

//首先向外暴露一个函数
//默认为GET请求
import axios from 'axios'
export default function ajax(url,data={
    
    },type='GET') {
    
    
  return new Promise(function (resolve, reject) {
    
    
    //这里的return主要时结果的回调函数,即成功了则回调sesolve函数,失败了则回调reject函数
    //即异步返回的数据是response.data
    let promise
    if (type === 'GET') {
    
    
      //这里的目的主要是想将参数拼接成url?username=xx&password=XX
      let dataString = '';
      Object.keys(data).forEach(key => {
    
    
        dataString += key + '=' + data[key] + '&'
      })
      if (dataString != null) {
    
    
        dataString = dataString.substring(0, dataString.length - 1);
        url = url + '?' + dataString;
      }
      //使用axios发送get请求
      promise = axios.get(url)
      ///console.log(promise)
    } else {
    
    
      //使用axios发送post请求
      //post请求不用
      promise = axios.post(url, data)
    }
    //response是axios发送请求后得到的promise对象中的response,
    //最后因为响应体的数据杂多,这里只取response中的data
    promise.then(function (response) {
    
    
      //成功了则调用resolve
      resolve(response.data);
    }).catch(function (error) {
    
    
      //失败了则调用reject
      reject(error)
    })
  })
}

2)封装接口请求函数----使用的时分别暴露
(这个接口函数需要导入上面封装的ajax请求函数,调用,发送请求)

import ajax from './ajax'
const BASE_URL='/api'
//注册接口
//注册时即向后台传送username与password---->一个user对象---参数
//需要ajax为桥梁发送请求,ajax返回的结果就是现所需要的结果
//再ajax中需要指定参数
  //url---只需要指定后面部分

//2.获取食品分类列表
export const reqFootCategory=()=>ajax(BASE_URL+'/index_category')
//3.商店数组对象
export const reqShops=(longitude,latitude)=>ajax(BASE_URL+'/shops',{
    
    longitude,latitude})
//1.根据经纬度获取地址详情
export const reqAddress=(geohash)=>ajax(`${
      
      BASE_URL}/position/${
      
      geohash}`)


//4.根据经纬度和关键字搜索商铺列表
export const reqSearchShop = (geohash, keyword) => ajax(BASE_URL+'/search_shops', {
    
    geohash, keyword})

//5.获取一次性验证码
export const reqGetcaptcha=()=>ajax(BASE_URL+'/captcha')
//6.用户名密码登录
export const reqPwdLogin=({
    
    name,pwd,captcha})=>ajax(BASE_URL+'/login_pwd',{
    
    name,pwd,captcha},'POST')
//7.发送短信验证码
export const reqSendCode=(phone)=>ajax(BASE_URL+'/sendcode',{
    
    phone})
//8.手机号验证码登录
export const reqSmsLogin=(phone,code)=>ajax(BASE_URL+'/login_sms',{
    
    phone,code},'POST')
//18813216310
//9.根据会话获取用户信息
export const reqUserInfo=()=>ajax(BASE_URL+'/userinfo')
//10.用户登出
export const reqLogout=()=>ajax(BASE_URL+'logout')

3)当其他模块需要调用发送请求时,直接使用import结构导入封装的接口函数即可

5.CommonJS、AMD、CMD、ES6 Module的区别

5.1 AMD与CMD区别

1)模块定义时对依赖的处理不同

AMD推崇迁至以来,在定义模块时就要声明其依赖的模块;而CMD推从就近依赖,只有在用到某个模块时再使用require导入;
AMD:

difine(['module1','module2'],function(m1,m2){
    
    
})

CMD:

define(function(require,exports,module){
    
    
	const module1=require('./module1');
})

2)对依赖模块的处理机制不同

  • 首先AMD和CMD对模块的加载方式都是异步的
  • 不过区别在于AMD当加载了依赖模块之后立即执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致;
  • 而CMD加载完依赖模块之后,并不会立即执行,等所有的依赖模块都加载好之后,进入回到函数逻辑,遇到require语句的时候,才执行对应的模块,这样模块的执行顺序就和我们书写的时候一致了

5.2 ES6模块与CommonJS模块加载的区别

  • CommonJS时运行时加载,因为ComminJS加载是先加载整个模块,生成一个对象(这个对象包含了path这个模块的所有API),然后再从这个对象上面读取方法-----运行时加载
  • ES6是编译时加载,ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态定义阶段就会生成-----编译时加载
//ES6模块
import {
    
     basename, dirname, parse } from 'path';

//CommonJS模块
let {
    
     basename, dirname, parse } = require('path');

以上这种写法与CommonJS的模块加载有什么不同?

Guess you like

Origin blog.csdn.net/weixin_46872121/article/details/111772429