理解
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
好处
- 避免命名冲突(减少命名空间污染)
- 更好的分离, 按需加载
- 更高复用性
- 高可维护性
模块化规范
CommonJS
主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD、CMD解决方案
CommonJS包规范是理论,npm是其中一种实践。一个JS就是一个模块。每一个JS文件中的JS代码都是独立运行在一个函数中,所以模块的变量和函数在其他模块中无法访问。当Node在执行模块中的代码时,它会自动把手写代码包在以下函数中。
funciton(exports, require, module, __filename, __dirname){
// exports: 函数,导出外部。
// require:函数,用来引入外部模块。
// module:代表当前模块本身。
// __filename:当前模块的完整路径。
// __dirname:当前模块所在文件夹的完整路径。
// exports == module.exports
}
引入:require()。
相对路径必须以 ./ 或 ../ 或 /(电脑根目录)开头
查找情况:
导出:exports 、 module.exports的区别
exports.x = 1 通过exports只能使用 . 的方式
module.exports 既可以通过 . 的方式,也可以直接赋值
例如:
// a(exports)和 obj.a(module.exports)指向的是同一个对象
var obj = {};
obj.a = {};
var a = obj.a;
Node模块的加载过程
结论:
- 模块在被第一次引入时,模块中的js代码会被运行一次。
- 模块被多次引入时,会缓存,最终只加载(运行)一次。
Q:为什么只会加载运行一次?
A:因为每个模块对象module都有一个属性:loaded,为false表示未加载,为true表示已加载。
- 循环引入的加载顺序:深度优先算法
CommonJS缺点
- CommonJS加载模块是同步的
同步意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行。
这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快。
- 如果将它应用于浏览器呢?
浏览器加载js文件需要先从服务器将文件下载下来,之后在加载运行;
那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作。
- 所以在浏览器中,我们通常不使用CommonJS规范
当然在webpack中使用CommonJS是可以的,因为它会将我们的代码转化成浏览器可以直接执行的代码。
- 在早期为了可以在浏览器中使用模块化,通常采用AMD或CMD
但是目前现在的浏览已经支持ES5,还有借助于webpack等工具可以实现对CommonJS或ES代码的转化,AMD和CMD已经使用非常少了。
AMD
在浏览器(现在很少用,了解)环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
AMD常用的库是require.js和curl.js
CMD
CMD规范与AMD规范很相似,也是在浏览器(现在很少用,了解)环境中异步加载模块,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重。
CMD常用的库是SeaJS
ES Module
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
- 使用import和export关键字(关键字:不是对象也不是函数)
- 采用编译期的静态分析,并且也加入了动态引用的方式
- 采用ES模块化将自动采用严格模式:use strict
使用ES Module
// 方法一:type='module'开启ES Module
<script src='./index.js' type='module'></script>
// 方法二:package.json配置type:module
// 方法三:后缀名.js全部改为.mjs
引入:import
// 方法一
import {name, age} from './index.js';
// 方法二:导出变量后起别名
import {name as na, age as a} from './index.js';
// 方法三:* as
import * as index from './index.js';
import加载模块,不可以放在逻辑代码中,如:
// ES模块在被js引擎解析时,就必须知道他的依赖关系,所以下列写法错误
if(flag){
import {name, age} from './index.js';
};
执行import(),动态加载
// 本质上是一个promise函数
import(path).then(function(){
})
导出:export
// 方法一
export const name = 'wjh';
export const age= '18';
// 方法二:{}大括号,但不是一个对象,就是一个语法
export {
name,
age
}
// 方法三:{}导出时,给变量起别名
export {
name as na,
age as a
}
默认导出:default
- 默认到处export时可以不需要指定名字。
- 在导入时也不需要使用{},且可以自己指定名字。
- 方便我们和现有的CommonJS等规范相互操作。
- 在一个模块中,只能有一个默认导出。
import和export可以结合使用
export {name, age} from './index.js'; // 先导入index.js然后导出name,age
Q:为什么这么做?
A:在开发和封装一个功能库时,通常希望将暴露的所有接口放在一个文件中方便指定统一的接口规范也方便阅读,这个时候就可以使用import和export的结合。
ES模块的加载过程
ES模块加载过程是编译(解析)时加载,且异步,意味着import不能和运行时相关的内容放在一起使用。
例如:
原理:
- export在导出一个变量时,js引擎会解析这个语法,并且创建模块环境记录。
- 模块环境记录会和变量进行绑定(bindings),且是实时的。
- 在导入的地方,可以实现获取到绑定的最新值
参考链接:https://juejin.im/post/6844903744518389768
还有coderwhy老师的Node视频讲解:https://ke.qq.com/course/3025600(付费课程)