什么是模块化呢?
- 事实上模块化开发最终的目的是将程序划分成
一个个小的结构
; - 这个结构中编写属于
自己的逻辑代码
,有自己的作用域
,不会影响到其他的结构; - 这个结构可以将自己希望暴露的
变量
、函数
、对象
等导出给其结构使用; - 也可以通过某种方式,导入另外结构中的
变量
、函数
、对象
等
没有模块化带来很多的问题
- 如命名冲突
解决:立即函数调用表达式 - 没有规范
各公司实现不相同
CommonJS和Node
CommonJS
是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了
体现它的广泛性,修改为CommonJS
,平时我们也会简称为CJS
。
- Node是CommonJS在服务器端一个具有代表性的实现;
- Browserify是CommonJS在浏览器中的一种实现;
- webpack打包工具具备对CommonJS的支持和转换;
所以,Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
- 在Node中每一个js文件都是一个单独的模块;
- 这个模块中包括CommonJS规范的核心变量:exports、module.exports、require;
- 我们可以使用这些变量来方便的进行模块化开发;
模块化的核心是导出和导入,Node中对其进行了实现:
- exports和module.exports可以负责对模块中的内容进行导出;
- require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
理解对象的引用赋值
let obj = { name: 'kobe', age: 18 }
let info = obj
module.exports和exports
导入和导出的原理就是对象的引用。
在node中,每个js文件都是一个new Module的实例,该实例名字赋值为module
,所以在模块内能访问它,模块内的导出操作都是由module.exports
来负责的,为什么exports
也能实现导出操作呢?node默认做了一个操作module.exports = exports
,一旦模块内将module.exports
重新赋值,exports
就失效了。
require细节
require是一个函数,可以帮助我们引入一个文件(模块)中导入的对象。和es6的import是不一样的。
查找规则
导入格式如下:require(X)
情况一:X是一个核心模块,比如path、http
直接返回核心模块,并且停止查找
情况二:X是以 ./ 或 ../ 或 /(根目录)开头的
- 第一步:将X当做一个文件在对应的目录下查找;
1.如果有后缀名,按照后缀名的格式查找对应的文件
2.如果没有后缀名,会按照如下顺序:
1> 直接查找文件X
2> 查找X.js文件
3> 查找X.json文件
4> 查找X.node文件 - 第二步:没有找到对应的文件,将X作为一个目录
查找目录下面的index文件
1> 查找X/index.js文件
2> 查找X/index.json文件
3> 查找X/index.node文件
如果没有找到,那么报错:not found
情况三:直接是一个X(没有路径),并且X不是一个核心模块
先在该文件所在目录的平级目录查找node_modules里有没有该包,有返回,无往上一级找node_modules,以此类推,直到找到硬盘的根路径为止,找不到直接报错 not found
模块的加载过程
node的require函数加载是同步
的。
- 结论一:模块在被第一次引入时,模块中的js代码会被运行一次
- 结论二:模块被多次引入时,会缓存,最终只加载(运行)一次
为什么只会加载运行一次呢?
这是因为每个模块对象module都有一个属性:loaded。为false表示还没有加载,为true表示已经加载。 -
结论三:如果有循环引入,那么加载顺序是什么?
如果出现下图模块的引用关系,那么加载顺序是什么呢?
这个其实是一种数据结构:图结构;图结构在遍历的过程中,有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first
search);
Node采用的是深度优先算法
:main -> aaa -> ccc -> ddd -> eee ->bbb
ES Module
不能以file协议打开es module的代码,会报错,用http协议访问,如vscode可以用live-server插件运行。
在浏览器环境下运行es module代码:
<script src="main.js" type="module"></script>
加入一个属性type="module"
,就表示该文件以module方式运行,会自动加入一个async
属性,即es module是异步的。
关键字export
表示导出
const name = "why";
const age = 18;
const sayHello = function(name) {
console.log("你好" + name);
}
// 1.方式一:
export const name = "why";
export const age = 18;
export const sayHello = function(name) {
console.log("你好" + name);
}
// 2.方式二:
export {
name,
age,
sayHello
}
// 3.方式三: {} 导出时, 可以给变量起名别
export {
name as fName,
age as fAge,
sayHello as fSayHello
}
注意:export后面跟着的花括号并不是对象,这是一个新语法,虽然看上去有点像es6对象的简写。即import引入的也不是对象。
关键字import
表示导入
// 方式一: import {} from '路径';
import { name, age, sayHello } from './modules/bar.js';
// 方式二: 导出变量之后可以起别名
import { name as wName, age as wAge, sayHello as wSayHello } from './modules/foo.js';
// 方式三: * as foo
import * as foo from './modules/foo.js';
Export和import结合使用
export和import可以结合使用:
export {name, age, sayHello} from './foo.js';
为什么要这样做呢?
- 在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中;
- 这样方便指定统一的接口规范,也方便阅读;
- 这个时候,我们就可以使用export和import结合使用;
default用法
还有一种导出叫做默认导出(default export)
- 默认导出export时可以不需要指定名字
- 在导入时不需要使用 {},并且可以自己来指定名字;
- 它也方便我们和现有的CommonJS等规范相互操作;
import函数
通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:
if (flag) {
import xxx from xxx
}
这样写会直接报错,原因请看后面的es module加载过程。
但是某些情况下,我们确确实实希望动态的来加载某一个模块,es module提供了一个import函数
,返回一个promise
let flag = true;
if (flag) {
// require的本质是一个函数
// require('')
// 执行函数
// 如果是webpack的环境下: 模块化打包工具: es CommonJS require()
// 纯ES Module环境下面: import()
// 脚手架 -> webpack: import()
import('./modules/foo.js').then(res => {
console.log("在then中的打印");
console.log(res.name);
console.log(res.age);
}).catch(err => {
})
}
CommonJS的加载过程
CommonJS模块加载js文件的过程是运行时加载的,并且是同步的。
CommonJS通过module.exports导出的是一个对象。
- 导出的是一个对象意味着可以将这个对象的引用在其他模块中赋值给其他变量
- 但是最终他们指向的都是同一个对象,那么一个变量修改了对象的属性,所有的地方都会被修改
ES Module加载过程
ES Module加载js文件的过程是编译(解析)时加载的,并且是异步的。
- 编译时(解析)时加载,意味着
import
关键字不能和运行时相关的内容放在一起使用: - 比如from后面的路径需要动态获取;
- 比如不能将import放到if等语句的代码块中;
- 所以我们有时候也称ES Module是静态解析的,而不是动态或者运行时解析的;
异步的意味着:JS引擎在遇到import时会去获取这个js文件,但是这个获取的过程是异步的,并不会阻塞主线程继
续执行;
- 也就是说设置了 type=module 的代码,相当于在script标签上也加上了 async 属性
- 如果我们后面有普通的script标签以及对应的代码,那么ES Module对应的js文件和代码不会阻塞它们的执行
ES Module通过export导出的是变量本身的引用:
- export在导出一个变量时,js引擎会解析这个语法,并且创建模块环境记录(module environment
record); - 模块环境记录会和变量进行 绑定(binding),并且这个绑定是实时的;
- 而在导入的地方,我们是可以实时的获取到绑定的最新值的;
所以,如果在导出的模块中修改了变化,那么导入的地方可以实时获取最新的变量。
注意:在导入的地方不可以修改变量,因为它只是被绑定到了这个变量上(其实是一个常量)。
思考:如果bar.js中导出的是一个对象,那么main.js中是否可以修改对象中的属性呢?
答案是可以的,因为他们指向同一块内存空间。
Node对ES Module的支持
在最新的Current版本(v14.13.1)中,支持es module我们需要进行如下操作:
- 方式一:在package.json中配置 type: module(后续学习,我们现在还没有讲到package.json文件的作用)
- 方式二:文件以 .mjs 结尾,表示使用的是ES Module
在最新的LST版本(v12.19.0)中,我们也是可以正常运行的,但是会报一个警告。
CommonJS和ES Module交互
通常情况下,CommonJS不能加载ES Module
- 因为CommonJS是同步加载的,但是ES Module必须经过静态分析等,无法在这个时候执行JavaScript代码;
- 但是这个并非绝对的,某些平台(如:webpack)在实现的时候可以对代码进行针对性的解析,也可能会支持;
- Node当中是不支持的;
多数情况下,ES Module可以加载CommonJS
- ES Module在加载CommonJS时,会将其module.exports导出的内容作为default导出方式来使用;
- 这个依然需要看具体的实现,比如webpack中是支持的、Node最新的Current版本也是支持的;
- 在最新的LTS版本中也支持