前端模块化的理解(AMD,CMD,CommonJs,ES6)

前言

初期的web端交互还是很简单,不需要太多的js就能实现。随着时代的的发展,用户对Web浏览器的性能也提出了越来越高的要求,浏览器也越来越多的承担了更多的交互,不再是寥寥数语的js就能解决的,那么就造成了前端代码的日益膨胀,js之间的相互依赖也会越来越多,此时就需要使用一定的规范来管理js之间的依赖。
本文主要是什么是模块化,为什么需要模块化以及现下流行的模块化规范:AMD,CMD,CommonJs,ES6。

什么是模块化

要想理解模块化是什么可以先理解模块是什么?
模块:能够独立命名并且能够独立完成一定功能的集合。
因此在js中就可以理解为模块就是能够实现特定功能独立的一个个js文件。
模块化:就可以简单的理解为将原来繁重复杂的整个js文件按功能或者按模块拆成一个个单独的js文件,然后将每一个js文件中的某些方法抛出去,给别的js文件去引用和依赖。

为什么需要模块化

一、模块化的进程

1、全局function模式:将不同的功能封装为不同的函数
缺点:污染全局命名空间,容易引起命名冲突,看不出模块间的依赖
2、namespace模式:封装为对象模式
作用:减少全局变量,解决命名冲突
缺点:数据不安全(外部函数可以修改模块内的数据),看不出模块之间的依赖

const module = {
    data:1,
  getData(){console.log(this.data)}
}
module.data = 2; //这样会直接修改模块内部的数据

3、IIFE模式:匿名函数自调用(闭包)
作用:解决了数据安全,数据是私有的,外部只能调用暴露的信息
缺点:需要绑定到一个全局变量上例如window向外暴露,这样也会有命名冲突的问题
4、IIFE增强模式:引入依赖
作用:解决了模块直接的依赖问题
缺点:引入js的时候需要注意引入的顺序,并且当依赖很多的时候也会有弊端

// IIFE模式:匿名函数自调用(闭包)
(function(window){
    let data = '这是IIFE模式';
  getData(){
    console.log(data);
  }
  window.module = { getData }
})(window)
// IIFE增强模式
(function(window,$){
    let data = '这是IIFE模式';
  getData(){
    console.log(data);
    $('body').css('background', 'red')
  }
  window.module = { getData }
})(window,jQuery);
//index.html
//需要注意引入的顺序



//当


  1. 输出结果


    image

CMD

CMD(Common Module Definition):通用模块定义。用于浏览器端,是除AMD以外的另一种模块组织规范。结合了AMD与CommonJs(后面会讲到)的特点。也是异步加载模块。
与AMD不同的是:AMD推崇的是依赖前置,而CMD是依赖就近,延迟执行。
依赖前置&&依赖就近,延迟执行

//依赖前置:AMD
require(['module1','module2'],function(m1,m2){
    //依赖的模块首先加载,无论后续是否会用到
})
//依赖就近,延迟执行
define(funciton(require){
    const module1 = require('./module1'); //用到的时候再申明,不需要就不用申明,也就不会加载进来       
})
CMD具体使用步骤
  1. 引入sea.js
    a. 官网:https://seajs.github.io/seajs/docs/#downloads
    b. github:https://github.com/seajs/seajs
  2. 模块定义
//module1.js
define(function(require,exports) {
    const msg='这是模块1';
    // const module2 = require('./module2');
    exports.msg = msg; // 注意这里是用的exports
    // exports.module2 = module2; // 如果是多个就得这么写,所以如果暴露多个接口不建议用exports
});
//module2.js
define(function(require,exports,module) {
    const msg="这是模块2";
    module.exports = { //这里用的是module.exports,跟exports作用一样
        msg
    }
});
//module3.js
define(function(require,exports,module) {
    const msg="这是模块3";
    const module2 = require('./module2');
    module.exports = { //这里用的是module.exports,跟exports作用一样
        msg,
        module2
    }
});
//main.js
define(function(require, exports,module) {
    const module1 = require('./module1');
    const module3 = require('./module3');
    console.log(module1);
    console.log(module3);
});
console.log('模块加载');
  1. index.html引入



    
    
    Document


    
    


  1. 输出结果
    image

CommonJS

Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。
CommonJs有4个毕竟重要的变量:modulerequireexportsglobal

CommonJs的特点:
  1. 所有代码都运行在模块作用域,不会污染全局作用域;如果想要多个模块共享一个变量,需要给global添加属性(不建议这么用)
//module.js
global.data = '共享变量'; //添加到global的属性是多个文件可以共享的
let x = {
    a:5
}
let b=0;
const add = function(val){
    x.a+=x.a;
  b=++b;
}
module.exports.x=x; //只有对外暴露了变量,在外部引用的时候才能获取到,否则x,b就是模块内部的私有变量
module.exports.add = add;
  1. 模块可以多次加载,但是只有再第一次加载的时候才会执行,后续的加载都是使用的缓存结果;如果想要再次加载需要清除缓存,或者对外暴露一个函数,加载之后执行暴露的函数
// test1.js
let msg="测试缓存";
console.log(msg);
exports.msg=msg;
// test2.js
const msg = require('./test1');
const msg2 = require('./test1');
// 上面的输出结果是:测试缓存
// 会发现只打印了一次结果,以此可以证明只有第一次加载的时候才会执行,第二次加载的时候使用的是缓存的结果
// 删除指定模块的缓存
delete require.cache[moduleName] // moduleName必须是绝对路径
// 删除所有模块缓存
Object.keys(require.cache).forEach(function(key){
    delete require.cache[key]
})
  1. 模块加载的顺序是按照再代码中书写的顺序加载的,同步加载模块。
  2. 引入的值其实是输出值的拷贝(浅拷贝)。也就是说一旦值输出之后,模块内的改变就不会影响这个值的改变(引用类型除外)
// example.js
let x= {
    a:5
};
let b=0;
const add = function(val){
    x.a+=x.a;
    b=++b;
    // return x.a++;
}
module.exports.x = x;
module.exports.b=b;
module.exports.add = add;
// main.js
const example = require('./example');
const add = require('./example').add;
example.add();
console.log(example.x) // {a:10}
console.log(example.b) // b:0 
CommonJs的语法
//对外暴露接口
module.exports
exports
//引入模块
require(模块的路径) //模块的路径有多种写法,后续补充

module.exports&&exports
module.exports: 每个模块内部,module代表了当前这个模块,module是一个对象,对外暴露的就是exports这个属性,加载某个模块其实就相当于加载的module.exports这个属性
exports: 其实就是module.exports的引用。为了使用方便,node为每个模块创建了一个exports变量,这个变量就指向了module.exports。因此以下两种做法是错误的。

//错误一
exports='msg'; // 此时相当于改变了exports的指向,失去了与module.exports的联系,也就失去了对外暴露接口的能力
//错误二
exports.msg='msg';
module.exports = 'Hello world'; // 上面的msg是无法对外暴露的,因为module.exports被重新赋值了;此时对外暴露的就是『Hello world』

ES6

ES modules(ESM)是 JavaScript 官方的标准化模块系统。ES6模块设计的思想是尽量的静态化,使得编译时就能知道模块的依赖关系,以及输入和输出的变量。有两个主要的命令:export和import。export用于对外暴露接口,import用于引入其他模块。

ES6模块的特点:
  • 严格模式:ES6 的模块自动采用严格模式
  • import read-only特性: import的属性是只读的,不能赋值,类似于const的特性
  • export/import提升: import/export必须位于模块顶级,不能位于作用域内;其次对于模块内的import/export会提升到模块顶部,这是在编译阶段完成的
  • 兼容在node环境下运行
  • ES modules 输出的是值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝
//ES6模块值的引用 .mjs 主要是为了能用node环境运行:node --experimental-modules
// example.mjs
let x= {
    a:5
};
let b=0;
const add = function(val){
    x.a+=x.a;
    b=++b;
}
export {x,b,add}
// main.mjs
import { x,b, add} from './example.mjs';
add();
console.log(x,b); // {a:10} 1 

export&&import用法
export:用于向外暴露接口
import:用于引入外部接口

// 方法一:
//export单个向外暴露接口
export const x = 1;
export const y = {a:1}
export const add = function(){console.log(123)}
//export一起向外暴露接口
const x=1;
const y={a:1};
const add = function(){console.log(123)};
export {x,y,add}
//import引入外部接口
//针对以上两种方式import可以写成如下两种情况
import {x,y,add} from './exmaple'; //使用哪个就引入哪个
import * as moduleName from './exmaple'; // 全部引入,使用的时候使用moduleName.x
// 方法二:
//除了使用export 向外暴露接口外还可以使用export default向外暴露接口:同一个模块中export可以有多个,但是export default只能有一个
const x=1;
const y={a:1};
export default { x,y}
// 对应的import
import moduleName from './exmaple'; //全部引入,使用方式moduleName.x

总结

  • AMD:异步加载模块,允许指定回调函数。AMD规范是依赖前置的。一般浏览器端会采用AMD规范。但是开发成本高,代码阅读和书写比较困难。
  • CMD:异步加载模块。CMD规范是依赖就近,延迟加载。一般也是用于浏览器端。
  • CommonJs:同步加载模块,一般用于服务器端。对外暴露的接口是值的拷贝
  • ES6:实现简单。对外暴露的接口是值的引用。可以用于浏览器端和服务端。

后记

模块化的一次有一次的变更,让系统模块化变得也越来越好,但是响应的也引起了一些问题。例如使用模块的时候,发现有些模块引入了但是并没有真正的使用到,这样就造成了代码的冗余,多了一些不必要的代码。这些模块有时通过检查是很难发现的,因此就要想如何能够快速的去除这些无用的模块呢,此时Tree Shaking就出现了;又比如如何能够在开发代码的时候比较便捷,然后在生产中又有高强度的兼容性呢,此时就出现了babel;又比如如何预处理模块,此时出现了webpack……
出现了解决问题的办法,就得继续学习啦……

参考文章

前端模块化的十年征程
彻底理清 AMD,CommonJS,CMD,UMD,ES6 modules
前端模块化—CommonJS、CMD、AMD、UMD和ESM
前端模块化详解(完整版)

你可能感兴趣的:(前端模块化的理解(AMD,CMD,CommonJs,ES6))