深入理解Node.js (二)---JavaScript模块化
模块化
将程序划分为一个个小的解构,每个结构都有自己的作用域,不会影响到其他的解结构,这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用,也可以通过某种方式,导入另外结构中的变量、函数、对象等。
CommonJS和Node
CommonJS(CJS)是一个模块化规范,node就是CJS的一个具有代表性的实现,Browserify是CommonJS在浏览器中的一种实现,webpack打包工具具备对CommonJS的支持和转换。
在node中每个JS文件都是一个模块(实际上是一个函数)我们可以在文件中输出以下语句来验证
console.log(arguments.callee + "")
这个模块包括CJS规范的核心变量exports、module.exports、require
exports和module.exports可以负责对模块中的内容进行导出
require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容
exports导出
exports是一个对象,我们可以在这个对象中添加很多属性,添加的属性会被导出
另外一个文件可以通过require()函数导入
这里的bar就等于exports,bar对象是exports对象的浅拷贝(引用赋值)
module.exports
为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是 module,
所以在Node中真正用于导出的其实根本不是exports,而是module.exports,内部中做了如下的操作
module.exports = exports//该代码是顶层运行
所以当我们执行module.exports = {},exports就和module.exports没有了关系,另外如果我们对export = {},我们在另一个文件就不能获取到导出的文件,所以我们导出一般就使用module.exports
require细节
require是一个函数,可以帮助我们引入一个文件(模块)中导入的对象, 导入格式如下require(X)
- X是一个核心模块,直接返回该模块并且停止查找
- X是一个以 ./ 或者 ../ 或者 / 开头的,如果有后缀名,按照后缀名的格式查找对应的文件,如果没有就直接查找文件X再按照 js json node 顺序添加后缀名查找,如果没有找到就将X作为目录进行往下查找,还是没有就报错
- 直接是一个X但是不是核心模块,就会按照目录一层一层往上找
模块的加载过程
- 模块在被第一次引入时,模块中的js代码会被运行一次
- 模块被多次引入时,会缓存,最终只加载(运行)一次
如果有循环引入,如下图展示
我们可以看出上图的引入结构就是一个图结构,再结合node的源码我们可以得出node采用的深度优先算法引入的文件
所以引入的顺序就是main - aaa - ccc - ddd - eee - bbb
源码如下:
CJS规范的缺点
CJS加载模块是同步加载的,在服务器端倒没啥影响,但是在浏览器端就会造成卡顿,所以我们在浏览器端通常是不使用CJS规范的
AMD规范
AMD规范是Asynchronous Module Definition(异步模块定义)的缩写,它采用的异步加载的模式,AMD实现的比较常用的库是require.js和curl.js这里我们使用require.js来简单的演示下
首先在需要引入的html页面导入入口文件,导入方式如下图
data-main属性的作用是在加载完src的文件后会加载执行该文件
入口JS文件设置映射关系并导入
(function() {
require.config({
paths: {
"bar": "./bar",//映射关系,不要加上后缀名
"foo": "./foo"
}
})
require(['bar'], function (bar) {
})
})();
bar.js
define(['foo'], function (foo) {
console.log(foo.name);
foo.sayHello();
})
foo.js
define(function () {
const name = "zhangheng";
const sayHello = function() {
console.log("hello world");
}
return {
name,
sayHello
}
})
CMD规范
CMD 是Common Module Definition(通用模块定义)的缩写,也采用了异步加载模块,但是它将CommonJS的优点吸收了过来,SeaJS就是采用的CMD规范
html文件引入入口文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>cmd</title>
<script src="04_CMD/lib/sea.js"></script>
<script>
seajs.use('./cmd.js')
</script>
</head>
<body>
</body>
</html>
cmd.js
define(function (require, exports, module) {
const cmd2 = require('./cmd2');
console.log(cmd2);
})
cmd2.js
define(function (require, exports, module) {
const name ="cmd";
module.exports = {
name
}
})
CMD和AMD规范现在几乎不怎末使用了
ES Module
这是JS官方推出的模块化系统,ES Module使用import和export关键字,采用编译期的静态分析,并且也加入了动态引用的方式
- export负责将模块内的内容导出
- import负责从其他模块导入内容
- 采用ES Module将自动采用严格模式:use strict
注意一个问题在浏览器演示es6模块化
如果直接将html文件以本地目录打开会报错误CORS,我们需要通过服务器方式打开
export 关键字
方式一
在语句声明的前面直接加上export关键字
export const name = xxx;
export const say = function () {}
方式二
在所有需要导出的标识符放到export后面的 {},注意这个{} 不是对象,不是对象,不是对象,所以export {name: name},是错误的写法
export {} //这里{}不是对象,是防要导出变量的引用列表
export {
name,//也不要认为是es6对象的增强写法
age
}
方式三
导出的时候给标识符添加一个别名
export {
name as NewName,
age
}
import 关键字
方式一
import {标识符列表} from '模块'
import {age, name} from './modules/bar.js' 也是要注意{}不是对象,也不是啥对象解构赋值
方式二
导入的时候也可以起别名
import {age, name as Fname} from './modules/bar.js' 也是要注意{}不是对象,也不是啥对象解构赋值
方式三
全部导入
import * as foo from 'xxxx'
export 和 import结合使用
//如果我们要将bar中的东西导入在导出:
/*
* import {name} from 'xxx'
*
* export {
* name
* }
*/
//使用import 和 export的结合方法代替上面的方式
export {name} from 'xxx'
这样做的场景,在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中,方便指定统一的接口规范,也方便阅读
default用法
在导出的时候我们都指定了名字,导入的时候我们也要知道具体的名字,默认导出export时可以不需要指定名字,在导入的时候不需要使用{}
//a文件
export default function() {
...
}
============
//b文件
import 自定义标识符 from './a.js'
默认导出只能在文件中出现一次
import函数
通过import加载模块是不能放到逻辑代码中的如下
if(true){
import xxx from './xxx/js'
}
//会报错的
因为ES Module在被引擎解析的时候(parsing阶段)就要完成模块依赖关系,这个时候仅仅是对代码进行词法分析和语法分析,生成AST树,而这时候JS代码是没有执行的,所以我们再执行JS代码遇到导入语句是无法识别的。
解决方式
如果我们确实需要动态加载模块可以使用 import() 函数来动态加载
当然CJS的导入实质上也是函数来执行,所以也可以放置在逻辑代码中来执行
ES Module 加载过程
前面提到ES Module加载是编译时加载,异步进行,也就是说设置了 type=module 的代码,相当于在script标签上也加上了 async 属性
export在导出一个变量时,js引擎会解析这个语法,并且创建模块环境记录(module environment record),模块环境记录会和变量进行绑定(binding),并且这个绑定是实时的,如果在导出的模块中修改了变化,那么导入的地方可以实时获取最新的变量,但是注意在导入的地方不可以修改变量,因为它只是被绑定到了这个变量上(其实是一个常量),就可以类似理解为该变量使通过const声明的,不能进行直接赋值修改,当然每次在导出的模块进行修改都相当于先删除这个变量再重新const声明这个变量以实现更新。如果这个变量是个对象,我们对其属性进行修改这个两边是同步变化的,就仅仅使引用类型特性了。
import { age } from "./modules/bar.mjs";
age = 20;//会报错
console.log(age);
Node中对ES Module的支持
在老版本的node中是不支持ES Module规范的,在比较新的版本比如14.13.1以上我们可以:
- 在package.json中配置 type: module
- 文件以.mjs结尾
在运行的时候可能结果但是会报一个警告
当然这个了解即可
通常情况下,CommonJS
不能加载ES Module
因为CommonJS
是同步加载的,但是ES Module必须经过静态分析等,无法在这个时候执行JavaScript代码,但是这个并非绝对的,某些平台在实现的时候可以对代码进行针对性的解析,也可能会支持,Node当中是不支持的
多数情况下,ES Module可以加载CommonJS
ES Module在加载CommonJS
时,会将其module.exports
导出的内容作为default导出方式来使用,这个依然需要看具体的实现,比如webpack中是支持的、Node最新的Current版本也是支持的,但是在最新的LTS版本中就不支持