深入理解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是一个对象,我们可以在这个对象中添加很多属性,添加的属性会被导出
image-20210430204943740

另外一个文件可以通过require()函数导入
image-20210430205253091

这里的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

image-20210430210120520

image-20210430210139476

require细节

require是一个函数,可以帮助我们引入一个文件(模块)中导入的对象, 导入格式如下require(X)

  1. X是一个核心模块,直接返回该模块并且停止查找
  2. X是一个以 ./ 或者 ../ 或者 / 开头的,如果有后缀名,按照后缀名的格式查找对应的文件,如果没有就直接查找文件X再按照 js json node 顺序添加后缀名查找,如果没有找到就将X作为目录进行往下查找,还是没有就报错
  3. 直接是一个X但是不是核心模块,就会按照目录一层一层往上找

模块的加载过程

  1. 模块在被第一次引入时,模块中的js代码会被运行一次
  2. 模块被多次引入时,会缓存,最终只加载(运行)一次
  3. 如果有循环引入,如下图展示

    image-20210430211044484

我们可以看出上图的引入结构就是一个图结构,再结合node的源码我们可以得出node采用的深度优先算法引入的文件
所以引入的顺序就是main - aaa - ccc - ddd - eee - bbb
源码如下:
image-20210430211448919

CJS规范的缺点

CJS加载模块是同步加载的,在服务器端倒没啥影响,但是在浏览器端就会造成卡顿,所以我们在浏览器端通常是不使用CJS规范的

AMD规范

AMD规范是Asynchronous Module Definition(异步模块定义)的缩写,它采用的异步加载的模式,AMD实现的比较常用的库是require.js和curl.js这里我们使用require.js来简单的演示下

首先在需要引入的html页面导入入口文件,导入方式如下图
data-main属性的作用是在加载完src的文件后会加载执行该文件

image-20210430211900637

入口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关键字,采用编译期的静态分析,并且也加入了动态引用的方式

  1. export负责将模块内的内容导出
  2. import负责从其他模块导入内容
  3. 采用ES Module将自动采用严格模式:use strict

注意一个问题在浏览器演示es6模块化
image-20210430212838560

如果直接将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() 函数来动态加载

image-20210430215959119

当然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以上我们可以:

  1. 在package.json中配置 type: module
  2. 文件以.mjs结尾

在运行的时候可能结果但是会报一个警告
image-20210430220929679

当然这个了解即可

通常情况下,CommonJS不能加载ES Module

因为CommonJS是同步加载的,但是ES Module必须经过静态分析等,无法在这个时候执行JavaScript代码,但是这个并非绝对的,某些平台在实现的时候可以对代码进行针对性的解析,也可能会支持,Node当中是不支持的

多数情况下,ES Module可以加载CommonJS

ES Module在加载CommonJS时,会将其module.exports导出的内容作为default导出方式来使用,这个依然需要看具体的实现,比如webpack中是支持的、Node最新的Current版本也是支持的,但是在最新的LTS版本中就不支持

Last modification:December 27, 2021
如果觉得我的文章对你有用,请随意赞赏