目录
一、模块化的概念
1.定义
模块化开发最终的目的是将程序划分成一个个小的结构,这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构,这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用,也可以通过某种方式,导入另外结构中的变量、函数、对象等。
上面说提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程
2.没有模块化带来的问题
没有模块容易造成命名冲突,在一个地方改变其他地方的变量,导致一些代码无法运行
解决没有模块化的方案:
使用立即执行函数包裹一些代码,并将需要暴露的就通过返回值返回一个对象,需要暴露的东西就作为这个对象的属性,我们可以获取这个对象从而获取他们的属性并使用
缺点:
- 必须记得每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用
- 代码写起来混乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写
- 在没有合适的规范情况下,每个人、每个公司都可能会任意命名、甚至出现模块名称相同的情况
二、CommonJS规范
1.定义
CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了 体现它的广泛性,修改为CommonJS,平时我们也会简称为CJS。
2.CommonJS规范与Node的关系
Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发
- 在Node中每一个js文件都是一个单独的模块
- 这个模块中包括CommonJS规范的核心变量:exports、module.exports、require;
- 我们可以使用这些变量来方便的进行模块化开发
3.基本使用
模块化的核心是导出和导入,Node中对其进行了实现:
- exports和module.exports可以负责对模块中的内容进行导出
- require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
代码示例:
main.js需要使用foo.js的变量,通过require函数获取
foo.js将其声明的变量暴露出去
main.js:
const { name, age} = require('./foo.js')
console.log(name);
console.log(age);
foo.js:
const name = "zyk"
const age = 18
module.exports = {
name,
age
}
4.node中CommonJS的原理
module.exports指向一个对象,并将这个对象暴露出去,另外一个文件使用require函数,并传入一个文件名的参数,然后内部就会判断这个文件名然后找出需要导出的对象,并通过return返回值返回module.exports对象。也就是说有一个对象,有同时三个变量指向它,这三个变量就是exports、module.exports和通过require函数获取的对象
5.module.exports导出
将当前文件定义的变量或者一些函数暴露出去
代码示例:
const name = 'kk'
function foo() {
}
module.exports = {
name,
foo
}
6.exports导出
原理:
将module.exports等于一个空对象,然后再将module.exports赋值给exports对象
在node内部实现的简单代码:
// exports原理:
module.exports = {}
exports = module.exports
// 如果module.exports 重新赋值一个对象 那么exports就没什么用了
// 记住: 最终能导出的一定是module.exports
基本使用:
const name = 'zyk'
const age = 18
function sum(num1, num2) {
return num1 + num2
}
exports.name = name
exports.age = age
exports.sum = sum
7.require导入
require是一个函数,可以帮助我们引入一个文件(模块)中导出的对象
常见的查找规则:
情况一:X是一个Node核心模块,比如path、http,直接返回核心模块,并且停止查找。
情况二:X是以 ./ 或 ../ 或 /(根目录)开头的:
- 第一步:将X当做一个文件在对应的目录下查找:
有后缀名:按照后缀名的格式查找对应的文件
无后缀名:按照下面的顺序:
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
8.模块的加载过程
- 模块在被第一次引入时,模块中的js代码会被运行一次
- 模块被多次引入时,会缓存,最终只加载(运行)一次。每个模块对象module都有一个属性:loaded,这个属性用来记录是否有被加载过,为false表示还没有加载,为true表示已经加载。
- 如果有循环引入,Node会采用的图结构的深度优先算法
二、ES Module规范
1.介绍
ES Module和CommonJS的模块化区别:
- ES Module使用了import和export关键字
- 采用编译期的静态分析(不会运行代码,只会解析import和export关键字的语句),并且也加入了动态引用的方式
ES Module模块采用export和import关键字来实现模块化:
- export负责将模块内的内容导出
- import负责从其他模块导入内容
2.基本使用
在浏览器中使用ES Module,注意:
- 需要在script标签中加入type属性,并且是module
- 需要在本地服务器中运行
<script src="./main.js" type="module"></script>
main.js:
import导入
import {name, age} from "./foo.js"
console.log(name);
console.log(age);
foo.js:
export导出
export const name = 'zyk'
export const age = 18
3.export关键字
export关键字将一个模块中的变量、函数、类等导出
导出方式:
- 方式一:在语句声明的前面直接加上export关键字
- 方式二:将所有需要导出的标识符,放到export后面的 {}中
- 注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的;export {name: name},是错误的写法
- 方式三:导出时给标识符起一个别名
代码示例:
// 1.导出方式一: export 后面加声明
// export const name = 'zyk'
// export const age = 18
// 2.导出方式二:固定语法 export {} 注意不是对象
// const name = 'zyk'
// const age = 18
// export {
// name,
// age
// }
// 3.导出方式三:起别名
const name = 'zyk'
const age = 18
export {
name as fname,
age as fage
}
4.import关键字
import关键字负责从另外一个模块中导入内容
导入方式:
- 方式一:import {标识符列表} from '模块',这里的{}也不是一个对象,里面只是存放导入的标识符列表内容
- 方式二:导入时给标识符起别名
- 方式三:通过 * 将模块功能放到一个模块功能对象(a module object)上
代码示例:
// 导入方式一:
// import {name, age} from "./foo.js"
// console.log(name);
// console.log(age);
// 导入方式二:
// import {fname, fage} from "./foo.js"
// console.log(fname);
// console.log(fage);
// 导入方式三:用*表示全部
import * as foo from "./foo.js"
console.log(foo.fname);
console.log(foo.fage);
5.import与export结合使用
在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中,这样方便指定统一的接口规范,也方便阅读,这个时候,我们就可以使用export和import结合使用。
代码示例:
// 导出方式一
import {sum, sub} from "./math.js"
import {timeFormat, priceFormat} from "./format.js"
export {sum, sub, timeFormat, priceFormat}
// 导出方式二
export {sum, sub} from "./math.js"
export {timeFormat, priceFormat} from "./format.js"
// 导出方式三
export * from './math.js'
export * from './format.js'
6.default默认导出
- 默认导出export时可以不需要指定名字
- 在导入时不需要使用 {},并且可以自己来指定名字
- 它也方便我们和现有的CommonJS等规范相互操作
注意:在一个模块中,只能有一个默认导出(default export)
代码示例:
默认导出:
const name = "zyk"
const age = 18
function foo() {
console.log("默认导出");
}
// 1.默认导出方式一
export {
name,
age,
// foo as default
}
// 2.默认导出方式二 常见
export default foo
默认导入:
import {name, age} from "./foo.js"
// 默认导入
import zyk from "./foo.js"
console.log(name, age);
zyk()
7.import函数
通过import() 函数来动态加载模块,注意:通过import加载一个模块,是不可以在其放到逻辑代码中的。因为为ES Module在被JS引擎解析时,就必须知道它的依赖关系。由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况
8.ES Module的解析流程
ES Module的解析过程可以划分为三个阶段:构建、实例化、运行
阶段一:
-
浏览器会从服务器获取main.js文件
-
对mian.js文件进行静态分析(并不会运行内部代码,只会分析import和export语句)
-
生成Module Record数据结构,内部有个RequestedModules属性(对依赖模块进行请求),继续请求依赖文件
-
下载counter.js和display.js文件,对它们进行解析,生成它们对应的Module Record
-
内部有个Module Map记录着映射关系,记录那个文件已被下载或者正在下载
阶段二和阶段三:
阶段二:
-
对Module Record进行实例化,分析有没有导出一些东西(count)
-
生成Module Enviroment Record(模块环境记录),然后将导出的count绑定到Bindings,在内存生成一个对象,并记录count的值,为undefined
-
在main.js的Module Record发现有导入count,于是创建Module Enviroment Record绑定导入的值,这时count也为undefined
阶段三:
-
运行count文件里的代码,将之前保存undefined的值改为运行后的值
-
导入的模块就可从内存取出值并使用
注意:只能导出的变量才可以修改值,导入的变量不能修改