前端的模块化是什么?

前端模块化的进化史,CommonJS,AMD,CMD,ES6规范

Posted by AzirKxs on 2022-07-14
Estimated Reading Time 9 Minutes
Words 2.5k In Total
Viewed Times

参考

作者:木头房子 链接:https://juejin.cn/post/6844903817272623117
作者:浪里行舟 链接:https://juejin.cn/post/6844903744518389768

ps:我只是通过自己学习这几篇文章进行了一个总结,另外根据自己的理解进行了拓展,仅仅是一个学习笔记,感谢大佬们写的优质文章,建议去阅读原文

前言

模块化概述?

  • 什么是模块?

    * 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起; * 块的内部数据/实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信; * 一个模块由内部的数据和方法组成
  • 非模块化会造成的问题

    1. 难以维护 2. 依赖模糊 3. 请求过多 4. 命名冲突 5. 污染全局变量
  • 模块化的好处

    * 更好地分离:避免一个页面中放置多个script标签,而只需加载一个需要的整体模块即可,这样对于HTML和JavaScript分离很有好处; * 更好的代码组织方式:有利于后期更好的维护代码; * 按需加载:提高使用性能,和下载速度,按需求加载需要的模块 * 避免命名冲突:JavaScript本身是没有命名空间,经常会有命名冲突,模块化就能使模块内的任何形式的命名都不会再和其他模块有冲突。 * 更好的依赖处理:使用模块化,只需要在模块内部申明好依赖的就行,增加删除都直接修改模块即可,在调用的时候也不用管该模块依赖了哪些其他模块。

模块化的进化史

全局function模式

直接将不同的功能封装成不同的全局函数

1
2
3
4
5
6
function m1(){
//...
}
function m2(){
//...
}

问题:

污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系

namespace模式/对象封装模式

将方法和数据封装到一个对象中成为模块,减少了全局变量,解决了命名冲突

1
2
3
4
5
6
7
8
9
10
11
12
let myModule = {
data: 'www.baidu.com',
foo() {
console.log(`foo() ${this.data}`)
},
bar() {
console.log(`bar() ${this.data}`)
}
}
myModule.data = 'other data' //外部可以直接修改内部数据
myModule.foo() // foo() other data

问题:

数据不安全(外部可以直接修改模块内部的数据)

LIFE模式/立即执行函数

将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口,数据是私有的,解决了数据不安全的问题,只能通过外部暴露的方法进行操作

问题:

无法处理依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function(window) {
let data = 'www.baidu.com'
//操作数据的函数
function foo() {
//用于暴露有函数
console.log(`foo() ${data}`)
}
function bar() {
//用于暴露有函数
console.log(`bar() ${data}`)
otherFun() //内部调用
}
function otherFun() {
//内部私有的函数
console.log('otherFun()')
}
//暴露行为
window.myModule = { foo, bar } //ES6写法
})(window)

LIFE模式增强:引入依赖

如果当前的模块依赖jquery这个模块怎么办呢?可以用以下的写法。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显,这是现代模块化的基石。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function(window, $) {
let data = 'www.baidu.com'
//操作数据的函数
function foo() {
//用于暴露有函数
console.log(`foo() ${data}`)
$('body').css('background', 'red')
}
function bar() {
//用于暴露有函数
console.log(`bar() ${data}`)
otherFun() //内部调用
}
function otherFun() {
//内部私有的函数
console.log('otherFun()')
}
//暴露行为
window.myModule = { foo, bar }
})(window, jQuery)


CommonJS规范

概述

node.js是CommonJS的主要实践者。Node由应用模块组成,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理(在引入之前已经完成了处理,运行的是打包生成的js文件)

特点

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。
  • CommonJS 模块输出的是一个值的拷贝。

基本语法

module,exports(暴露接口),require(加载模块),global

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义模块math.js
var basicNum = 0;
var value = 10
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);

规范概述

  • Node内部提供一个Module构建函数。所有模块都是Module的实例。
  • 每个模块内部,都有一个module对象,代表当前模块。
  • module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
  • Node为每个模块提供一个exports变量,指向module.exports。
  • 如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。

更多的内容在学习node的时候在进一步了解。

AMD规范

  • CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
  • AMD规范则是非同步加载模块,允许指定回调函数,可以实现异步加载依赖模块,并且会提前加载;
  • 由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。
  • 如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。

AMD规范需要搭配require.js使用

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

CMD规范

CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。搭配sea.js使用。

1
2
3
4
5
6
7
8
9
10
11
define(function (require, exports, module) {
//内部变量数据
var data = 'azirkxs'
//内部函数
function show() {
console.log('module1 show() ' + data)
}
//向外暴露
exports.show = show
})

ES6规范

概述

  • 模块化的规范:CommonJS和AMD两种。前者用于服务器,后者用于浏览器。
  • 而ES6 中提供了简单的模块系统,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。
  • ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

基本语法

export

export用于规定模块对外的接口,export不止可以导出函数,还可以导出,对象、类、字符串等等;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//test.js文件
//1.暴露一个变量和方法
export const name = 'azir';
export function find(){
console.log(name);
}
//2.或者一起暴露
let a = 'a';
let b = 'a';
let c = 'a';
export {a,b,c}
//3.通过as改变输出名称

export {a as newName}

//4.通过通配符暴露其他引入的模块
export * from './test.js'

//5.指定默认输出,import无需知道变量名就可以直接使用
export default function(){
console.log('azirkxs');
}
import say from 'test.js';
say(); //通过默认暴露无需知道函数名称直接使用

import

1
2
3
4
//导入一个模块
import {name,find} from './test.js';
//或者全部导入
import * as test from './test';

ES6 模块与 CommonJS 模块的差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
1
2
3
4
5
6
7
8
9
10
11
12

// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // ES6模块输出4,node模块输出3

总结

  • CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
  • AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
  • CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

如果这篇文章对你有帮助,可以bilibili关注一波 ~ !此外,如果你觉得本人的文章侵犯了你的著作权,请联系我删除~谢谢!