node学习笔记(二)
模块化
- 模块化开发最终的目的是将程序划分成一个个小的结构;
- 这个结构中编写属于自己的逻辑代码,有自己的作用域,定义变量名词时不会影响到其他的结构;
- 这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用;
- 也可以通过某种方式,导入另外结构中的变量、函数、对象等;
历史
js是作为一门简单的脚本语言诞生的,早期的页面很简单,在html页面使用js代码使用script标签即可。
但是随着前端任务量的逐渐增加,js变得越来越复杂,所以js急需一个模块化的方案。
在ESModule方案以及社区的CommonJS未推出之前,开发者使用立即执行函数来使用不同函数作用域下的同名变量。
立即执行函数的弊端
- 我必须记得每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用;
- 代码写起来混乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写;
- 在没有合适的规范情况下,每个人、每个公司都可能会任意命名、甚至出现模块名称相同的情况;
模块化方案
CommonJS(CJS)
CommonJS(原名ServerJS)属于社区提出的一种模块化的方案。
其中,Nodejs是CommonJS在服务端的一个代表性的实现;Browserify(已过时)是CommonJS在浏览器中的一种实现;
webpack打包工具具备对CommonJS的支持和转换;
接下来,基于Node对CommonJS进行简单总结:
在Node中每一个js文件都是一个单独的模块;
文个模块中包括CommonJS规范的核心变量: exports、module.exports、require;
exports module.exports require
1 | // a.js |
exports和module.exports指向同一个引用:
1 | console.log(module.exports === exports); |
所以,a.js
文件的导入也可以是这样:
1 | exports.num = 10; |
但是,如果在其之后加上module.exports
语句,则会被覆盖(因为指向同一个引用):
1 | exports.num = 10; |
1 | // b.js |
说明:exports是属于规范中的,module.exports属于Node的实现的。
CommonJS中是没有module.exportst的
但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是module;
所以在Node中真正用于导出的其实根本不是exports,而是module.exports;
因为module才是导出的真正实现者;
require
本质:引用赋值。
1 | // a.js |
1 | // 在b.js引入变量后,修改a_data的值,在a.js中使用 |
require查找细节
1 | const a_data = require('X'); |
情况一:如果X是node的内置模块,则直接返回;
情况二:如果X是相对或者绝对路径:
第一步:将X当做一个文件在对应的目录下查找;
- 如果有后缀名,按照后缀名的格式查找对应的
- 如果没有后缀名,会按照如下顺序:
- 直接查找文件X
- 查找X.js文件
- 查找X.json文件
- 查找X.node文件
第二步:没有找到对应的文件,将X作为一个目录查找目录下面的index文件
- 查找X/index.js文件
- 查找X/index.json文件
- 查找X/index.node文件
如果没找到,报错
情况三:不是路径,不是核心模块,则从node_modules相关的模块中package.json中定义的入口文件(main属性
,如果没有指定,默认是index.js文件)中寻找。
如果没有,去上层目录的node_modules文件夹中寻找,会一直往上,一直到根目录都没有找到,报错。
模块的加载过程
模块在被第一次引入时,模块中的js代码会被运行一次(即导入的文件按遇到require语句,发生阻塞)
模块被多次引入时,会缓存,最终只加载(运行)一次
- 这是因为每个模块对象module都有一个属性:loaded。为false表示还没有加载,为true表示已经加载;
循环引入如何解决?
Node的依赖引用采用的图遍历的深度优先算法
上图的引入顺序:main–>a–>d–>c–>b
弊端
- 变量的导入者可以修改导出者导出的变量(大忌);
- require引入是同步加载,即:在文件A引入文件B,文件B的所有代码(包括require,业务逻辑操作均会被执行一次);若遇到一个文件存在大量的业务逻辑操作,该文件被其他文件引入,会造成阻塞。
- 这也是浏览器不实现CommonJS的一个原因。
- 虽然在webpack仍然可以使用CommonJS和ESModule进行开发,这是因为webpack将CommonJS进行了转化,变成浏览器可以执行的代码。
AMD
AMD(Asynchronous Module Definition):采用异步加载模块
AMD规范早于CommonJS,现在已经不常用了。
AMD实现相关的库:require.js和curl.js
CMD
CMD(Common Module Definition)
采用异步加载模块,继承了AMD的优点,也不常用了。
相关的库:SeaJS
ESModule
ES2015官方推出的模块化方案。
导入者不能修改导出者导出的变量
浏览器使用ESModule,需要给script标签加上type
属性,同时需要开启本地服务(live server插件)
1 | <script src="./b.js" type="module"></script> |
1 | // a.js |
导出起别名
1 | // 导出时使用别名 |
导入起别名
1 | // 导入时使用别名 |
整体导入
1 | // 导出 |
导出导入可以同时起别名
1 | // 导出 |
定义时导出
1 | // 此导出无法定义别名 |
默认导出(default)
1 | export default { |
注意:在一个文件(模块)中,只能有一个默认导出。
import和export同时使用
新建统一的出口文件index.js
1 | // b.js |
import函数
import和export只能在文件(模块)的顶层使用
原因:
- 这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系;
- 由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况;
- 甚至拼接路径的写法也是错误的:因为我们必须到运行时能确定path的值;
如果想要在逻辑操作满足一定条件时再导入,可使用import函数,这个import函数返回一个promise
1 | // a.js |
应用场景:VUE组件异步加载
import meta
import.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的URL;
在ES11 (ES2020)中新增的特性;
1 | //b.js |
ESModule的解析流程
- 阶段一:构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record) ;
- 阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
- 阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中;