CommonJS模块
  CommonJS是一种规范,它定义了JavaScript 在服务端运行所必备的基础能力,比如:模块化、IO、进程管理等。其中,模块化方案影响深远,其对模块的定义如下:

    1,模块引用:使用require() 方法引用模块,它接受模块标识作为参数,将一个模块引入到当前运行环境中。

    2,模块定义:使用exports对象,导出当前模块的方法或者变量,并且它是唯一的导出出口。

    3,模块标识:就是模块的名字,传递给 require() 方法的参数。

  如果JS文件中存在 exports 或 require,该 JS文件就是一个模块,模块内的所有代码均为 隐藏代码,包括变量、函数,对其他文件不可见,也不会对全局变量造成污染。如果一个模块需要暴露一些API给外部使用,需要通过exports
导出,exports 是一个空对象,你可以为该对象添加任何需要导出的内容。如果一个模块需要导入其他模块,通过require
实现,require 是一个函数,传入模块的路径即可返回该模块导出的整个内容。

  Node.js 实现了CommonJS 模块,它主要做了三件事情,路径的解析,文件的查找,编译执行,就是当require一个模块标识时,怎么才能找到模块,并把exports对象获取到,引入当前运行环境中。模块标识是一个字符串,它主要有两种情况,以'/,'./' 或'../' 为主的路径和没有路径标识的字符串。如果是路径,就直接查找路径对应的模块。如果不是路径,Node.js先查找是不是核心模块,比如fs,http,如果不是,就在当前目录下的 node_modules 目录查找,如果没有,在父级目录的 node_modules 查找,如果没有,在父级目录的父级目录的 node_modules 中查找。沿着路径向上递归,直到根目录下的 node_modules 目录。

  找到路径,它可以是一个文件,它还可以是一个文件夹。如果是一个文件,还会看有没有后缀,如果有,它就会直接加载文件。如果没有后比缀,它就会先查找.js,再查找.json,最后查找.node文件。 如果路径指的是一个文件夹,它先查找有没有package.json文件,如果有,就会找 package.json 下 main 属性指向的文件,如果没有 package.json ,在 node 环境下会以index.js ,index.json ,index.node。也就是说,在Node.js中,模块可以是一个文件,也可以是一个文件夹,还可以是一个包。包就是一个文件夹中包含package.json。只要使用require方法引入的,都称为模块。

  找到了要加载的文件,为了隐藏模块中的代码,同时提供export 和require方法,nodejs 执行模块时,会将模块中JS代包括到一个函数中。

(function (exports, require, module, __filename, __dirname) {
// 模块中的js代码
})

  当然,为了高效的执行,Node.js在CommonJS模块上做了一些改进,

  1,运行时加载:Node.js 执行到require函数时才会加载模块并执行,然后将模块的exports对象返回。加载模块是同步的,只有当模块加载完成后,才能执行后面的操作。加载,执行,返回exports对象, require就像一个普通的函数调用,把返回值exports对象赋值给一个变量。比如number.js

let num = 1
function add() {
num++
}
exports.num = num
exports.add = add

  在index.js中引入

const number = require('./number.js')

console.log(number.num)

  当require('./number.js')时,Node.js执行number.js,exports.num = 1; exports.add = add, 执行完毕,然后把exports对象返回,相当于把{num:1, add: add} 对象赋值给index.js中的number,require函数执行完华,number变量和模块就没有关系了。这时即使调用number.add() 也不会必改变number 对象中num的值,因为add函数引用的是它自己作用域中的num,而不是index.js中number对象的属性。相反,你可以把修改number变量中num属性的值。

number.add()
console.log(number.num) //1 number.num = 3
console.log(number.num) //3

  2,缓存:当require一个模块时,会先将模块缓存,然后执行代码,以后再加载该模块时,就直接从缓存中读取该模块。比如a.js

console.log('a 模块加载')
exports.a = 'a'

  b.js

console.log('b 模块加载')
const moduleA = require('./a.js'); exports.b = 'b'

  index.js

const a = require('./a')
const b = require('./b')

  执行index.js,可以发现a模块只加载了一次。当b.js中再require a.js时,a.js已经缓存了,所以就没有加载,执行了。模块的缓存也有助于解决循环依赖。a.js改为

const { getMes } = require('./b')

console.log('我是 a 文件')

exports.say = function () {
const message = getMes()
console.log(message)
}

  b.js

const say = require('./a');

console.log('我是 b 文件')
exports.getMes = function(){return "Hello"}

   执行index.js,先加载a.js 放入缓存中,然后执行a.js,它又加载 b.js,b.js放入缓存,并执行,它又引入a.js,因为a模块已经在缓存中,所以直接读取缓存中的a, 实现上缓存中的a模块,只是一个空对象,加载完之后,b.js继续执行,控制台输出"我是b文件"。b.js执行完以后,再执行a.js,输出 “我是b文件”

  当然,也可以删除缓存,缓存是按照路径的形式将模块进行缓存,放到 require.cache对象上。通过delete require.cache[modulePath]将缓存的模块删除。需要注意的是modulePath是绝对路径。delete require.cache[path.resolve('./myclass')];

  3,exports 和 module.exports。CommonJS模块化只规定exports对象向外导出。想要导出什么,只给exports对象,添加属性,但只想导出方法,对象就有点麻烦,所以Node.js 可以直接使用module.exports 进行导出。

(function(module){
module.exports = {};
var exports = module.exports
// a.js 写入的代码
exports.a = 'a'; return module.exports;
})()

  到了commonjs2,module.exports也可以导出。exports只是module.exports的引用,相当于exports = modules.exports ={},整个模块只暴露modules.exports指向的一个对象出去,exports只能用来添加属性,所以exports 不能再被赋值给任何对象,即使赋值了,它就不能指向module.exports了,打破了引用,也就不能export出去(module.exports 是真正暴露出的对象),要想export一个对象出去,只能给module.exports赋值。exports.myFun 就是module.exports.myFunc.

  ES模块

  ES模块是ECMAScript官方推出的模块化解决方案,它吸取CommonJS的优点,一个JS文件就是一个独立的模块,模块内部的变量都是私有化的,其他模块无法访问;要想让其它模块进行访问,就要暴露出去,其它模块需要引入才能使用。但语法更简洁

  1,使用export 导出模块,不仅可以export对象,还可以export 变量,函数等,其实,在ES模块下,可以导出任意的标识符

export const a = 'a';
export function sayHello() { console.log('hello , world') }

  export导出的是标识符,也就是内存地址,而不是值,所下面两种是错误的写法:

// 报错,是个值=
export 1; var m = 1;
export m;  

  2,使用 import并配合 from关键词进行导入。注意,from后面的文件名要加后缀。

import { a, sayHello } from'./a.js' //引入的文件要加后缀名

  import 导入的就是变量名, 相当于内存地址,因此,导入的是只读引用,不能修改a 和sayHello 的值。也正因为是import的是变量名,导出模块内部的变量一旦发生变化,对应导入模块的变量也会跟随变化。

  3,以上的import 和export 称为有名字的import和 export。ES模块还定义default export和import。就是导出的时候,使用 export default,

export default class Logger {
log (message) {
console.log(`${message}`)
}
}

  导出是default, 而不是Logger,导出的内容被注册到default上,所以后面的logger 被忽略了, 正是由于导出的是default,所以export default 后面跟的是值,而不是变量, default在某种意义上来说,可以称为变量声明了,所以需要提供值。

export default 1; // 正确
export default const a = 1; // 错误

  导出default 还有一个影响, 就是,一个模块中只能有一个默认的导出。默认导出的import 是

import MyLogger from './logger.js'

  导入的时候,不加{}, 并且可以随意命名变量。实际上在内部,模块导出的就是default

import * as loggerModule from './logger.js'
console.log(loggerModule) // [Module] { default: [Function: Logger] }

  但你不能是用 import {default} from './logger.js',语法错误,default是关键字。  

  4,模块标识符(要import的模块的位置):

    相对路径标识符,就是 ‘./a.js’, '../a.js'

    绝对路径标识符: file:// 本地url, 比如"file:///opt/nodejs/config.js" , ES 模块绝对路径标识符,只有这一种格式,直接使用/ 或// 作为绝对路径不起作用

    无路径标识符,就是node 核心模块和node_modules中的第三方包,比如 fs, http 和fastify

    深度import 标识符,比如fastify/lib/logger.js。node_modules中fastify下的lib下的logger.js

  5,ES模块的加载方式是静态化的,只有在编译时加载,不会在执行时加载,也就是说引入模块的语句必须在模块的最顶层,不能在任何控制语句中。并且引入的模块名称也只能是常量字符串,不能是需要运行期动态求值的表达式,因为编译不会运算求值。静态化加载,也有好处,比如也可以是异步的。如果想要动态加载模块,只能调用import()函数,它接受模块标识符作为参数,返回一个promise, promise  resolve之后,就是模块对象。

  Node 14中实现了ES 模块,来看一下它是怎么解析和执行ES模块的? 解析入口文件,生成模块记录(Module Record),知道import模块,寻找模块,再解析成Module Record,深度优先遍历,构建整个项目的模块依赖图(dependency graph),根据module Record 构建Module instance,建立各个模块之间的依赖关系。 这个过程又分为三个阶段

  1,构建或解析阶段:根据路径,加载模块,解析成Module Record。加载入口文件,生成Module Record,识别它的依赖,根据依赖路径,加载依赖模块,再生成Module Record,再加载依赖,层层递进,深度优先,直到依赖没有依赖为止。

  加载依赖的过程中,会把加载完成的依赖(Module Record)缓存起来,放到module map中, 如果以后再加载相同的依赖,就不执行加载操作,所以每一个模块只会加载一次,

  最终形成完整的module record的依赖树。

  2, 实例化阶段:从依赖树的最底端module record 开始,JS引擎会为每一个module record 创建模块环境记录(module environment record) 管理里面的变量,同时,为export出去的变量的内存中找一块空间,沿着依赖树向上,module record中 有import 也有export, 先为export变量在内存中找空间,然后再为import 的依赖建立联系。由于import 的模块在依赖树的底层,我们是从是树底层向上走的,所以import的依赖都已经export 出去了,只要为import 找到export 就可以了,import和export都指向内存的同一位置。这一个过程是一个树的后序遍历的过程。

  3,执行阶段:执行代码,也是按照后序遍历的顺序,从下向上,依次执行每一个module instance中的代码,得到的值放到export 指向的内存中的位置,每一个模块的代码只执行一次。此时,可以从入口文件开始执行代码,整个项目开始执行。

  这三个阶段相互分离,在构建完整个依赖图之前,没有代码会执行,因此模块的导入或导出都是静态的。

  理解了模块的加载,看一下ES模块是怎么处理循环依赖的?

// a.js
import * as bModule from './b.js'
export let loaded = false
export const b = bModule
loaded = true // b.js
import * as aModule from './a.js'
export let loaded = false
export const a = aModule
loaded = true // main.js
import * as a from './a.js'
import * as b from './b.js'
console.log('a ->', a)
console.log('b ->', b)

  解析阶段:node main.js,main.js就是入口文件。main.js 加载a.js, a.js加载b.js,b.js再加载a.js,因为,a.js已经加载过了,就不加载了,它也没有其它import,直接返回到a.js,a.js也没有其它import,就返回到main.js,main.js再加载b.js,由于b.js已经加载过了,也就不加载了,注意,这里只执行inport 加载,不执行代码。

  2, 实例化阶段,根据第一阶段得到的依赖树,从下到上遍历,对于每一个模块,解释器找到所有export出来的属性,然后,再建立build out a map of the exported names
in memory:

从b.js开始,它export出了loaded 和a, 再到a.js,它也export出了loaded 和b,最后到main.js, 它没有export什么。注意,图中exports 映射只track export出来的名字,值没有初始化。再link the exported names to the modules importing them

  所有的值仍然是未初始化的。

  执行阶段,每一个文件中的所有代码最终执行。执行顺序,也是沿着依赖图从下到上执行。b.js先执行,loaded设为false,a指向代表a.js模块的aModule, loaded再调为true. 至此b.js执行完了,再执行a.js, loaded设为false,b指向模块b.js,loaded重置为御true, a.js也执行完了,再执行main.js, 所有export出来的属性都执行完了,引入的模块a, b 都是引用,直接找到a, b 进行输出。在ES 模块中,每一个模块都能引用到其它模块实时更新的或最新的状态。

  模块使用

  Node.js 中同时存在两种模块机制,要怎么使用呢?.js文件默认是CommonJS模块(语法),不能使用ESM语法,否则报错。要想使用ESM语法,可以把文件命名为.mjs,或者文件名是.js, 但在项目的package.json中加个type字段, 值为"module", "type": "module",为了后者进行对应,CommonJS解析也进行了相应的变化,1,文件以.cjs结尾,2,如果有package.json, package.json中没有type 字段或type 字段设为comonjs, 以 .js结尾的文件以CommonJS 解析。因此,Node.js 团队强烈建议包的作者在package.json文件中注明type 字段

{
  "type": "module"
}

  当使用CommonJS时,Node向模块中注入了__dirname, __firename 等全局对象。但ES模块是通过 import/export关键词来实现,没有对应的函数包裹,所以在 ES模块中,没法使用这些CommonJS对象。但可能通过import.meta来获取到当前文件的URL的引用。import.meta是 ECMA 实现的一个包含模块元数据的特定对象,主要用于存放模块的 url,而 node 中只支持加载本地模块,所以 url 都是使用 file:协议。import.meta.url is a reference to the current module
file in a format similar to file:///path/to/current_module.js. This value can be
used to reconstruct __filename and __dirname in the form of absolute paths:

import { fileURLToPath } from 'url';
import {dirname} from 'path'; const __firname = fileURLToPath(import.meta.url);
const __dirname = dirname(__firname);

  在ES模块文件中,可以引用CommonJS模块的内容,使用default import可以import进来CommonJS模块exports出来的整个对象, 使用name import 可以单独import 进来CommonJS模块exports出来的某个属性。假设cmj.js中 exports.a = 3; 在m.mjs 中,

import aa from './cmj.js' // 整个对象 {a: 3}
import {a} from './cmj.js' // 单个属性 a, 3

  在CommonJS模块文件中,也可以引用ES 模块中的内容, 不过,不能使用require, 而是使用import()函数,动态加载。

import('./m.mjs').then(data => {
console.log(data) // [Module] { a: 3 }
})

  有些包还包含子包,除了直接引用整个包外,Node.js还允许我们引用包里的某个模块。这时require 或import接受的文件标识符参数,就变成了包名/引用的模块。以mine包为例,你可以 require('mine') 引用整个包,也可以引用require('mine/lite'). import 就是import 'mine' 或import 'mine/lite'。如果遇到这样的模块标识符, Node.js先在node_moudules中找到主包,在这里是mine。然后再根据模块标识符找主包下面的文件。模块标识符也标识出了路径,mine目录下面的lite(主包mine也是一个目录),lite可以是lite.js, 也可以是lite目录,它里面包含index.js。

  像这种深入引用的模块标识符,包的作者也可以在package.json中定义,而不用使用上面的路径对应的方式。

{
"exports": {
"./cjsmodule": "./src/cjs-module.js",
"./es6module": "./src/es6-module.mjs"
}
}

  require('module-name/cjsmodule') or  import 'module-name/es6module' , 就可以加载相应的子模块或子包。

Node.js中的模块的更多相关文章

  1. node.js中express模块创建服务器和http模块客户端发请求

    首先下载express模块,命令行输入 npm install express 1.node.js中express模块创建服务端 在js代码同文件位置新建一个文件夹(www_root),里面存放网页文 ...

  2. node.js中ws模块创建服务端和客户端,网页WebSocket客户端

    首先下载websocket模块,命令行输入 npm install ws 1.node.js中ws模块创建服务端 // 加载node上websocket模块 ws; var ws = require( ...

  3. node.js中net模块创建服务器和客户端(TCP)

    node.js中net模块创建服务器和客户端 1.node.js中net模块创建服务器(net.createServer) // 将net模块 引入进来 var net = require(" ...

  4. node.js中module模块的理解

    node.js中使用CommonJS规范实现模块功能,一个单独的文件就是一个单独的模块.通过require方法实现模块间的依赖管理. 通过require加载模块,是同步操作. 加载流程如下: 1.找到 ...

  5. 在 Node.js 中引入模块:你所需要知道的一切都在这里

    本文作者:Jacob Beltran 编译:胡子大哈 翻译原文:http://huziketang.com/blog/posts/detail?postId=58eaf471a58c240ae35bb ...

  6. Web 前端模块出现的原因,以及 Node.js 中的模块

    模块出现原因 简单概述 随着 Web 2.0 时代的到来,JavaScript 不再是以前的小脚本程序了,它在前端担任了更多的职责,也逐渐地被广泛运用在了更加复杂的应用开发的级别上. 但是 JavaS ...

  7. Node.js中的模块接口module.exports浅析

    在写node.js代码时,我们经常需要自己写模块(module).同时还需要在模块最后写好模块接口,声明这个模块对外暴露什么内容.实际上,node.js的模块接口有多种不同写法.这里作者对此做了个简单 ...

  8. Node.js中的模块接口module.exports

    在写node.js代码时,我们经常需要自己写模块(module).同时还需要在模块最后写好模块接口,声明这个模块对外暴露什么内容.实际上,node.js的模块接口有多种不同写法.在此做了个简单的总结. ...

  9. node.js中通过dgram数据报模块创建UDP服务器和客户端

    node.js中 dgram 模块提供了udp数据包的socket实现,可以方便的创建udp服务器和客户端. 一.创建UDP服务器和客户端 服务端: const dgram = require('dg ...

  10. node.js中net网络模块TCP服务端与客户端的使用

    node.js中net模块为我们提供了TCP服务器和客户端通信的各种接口. 一.创建服务器并监听端口 const net = require('net'); //创建一个tcp服务 //参数一表示创建 ...

随机推荐

  1. Python第三方库的安装和导入

    目录 一.Python第三方库的安装 1. 使用pip命令行安装 2. 使用PyCharm进行安装 3. 下载第三方库文件到本地进行安装 4. 通过国内源进行安装 二.Python第三方库的导入 1. ...

  2. linux文本三剑客之awk详解

    linux文本三剑客之awk详解 目录 linux文本三剑客之awk详解 1.awk命令详解 1.1 awk的处理流程 1.2 awk中的变量 1.2.1 内置变量 1.2.2 自定义变量 1.3 a ...

  3. PageOffice 6 保存数据区域数据同时保存文档

    在实际应用中,例如在线签订合同的时候,合同的签订日期,合同号等等这些信息既要保存到数据库,合同签订后又要将整个合同文件保存起来.这时候就需要用到PageOffice的保存数据区域数据的同时保存整个文件 ...

  4. feign入门

    .net core: feign.net是一个spring cloud feign组件的c#移植版 https://github.com/daixinkai/feign.net 在.net core ...

  5. CSS——基本选择器

    例子: <head> <meta charset="UTF-8"> <title>Title</title> <style&g ...

  6. 授权调用: 介绍 Transformers 智能体 2.0

    简要概述 我们推出了 Transformers 智能体 2.0! ⇒ 在现有智能体类型的基础上,我们新增了两种能够 根据历史观察解决复杂任务的智能体. ⇒ 我们致力于让代码 清晰.模块化,并确保最终提 ...

  7. 在Windows上运行Rainbond,10分钟快速安装

    前言 Windows 桌面运行 Rainbond,Windows 开发者的新选择. 经过适配Mac以后,Windows的适配也是成为了近期的小目标,经过不断地测试,不断地研究.最后也是达成了完美运行的 ...

  8. vue Ref 动态组件 keeplive

    ref被用来给元素或子组件注册引用信息.引用信息将会注册在父组件的 $refs 对象上.如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素:如果用在子组件上,引用就指向组件实例 # 普通d ...

  9. kettle从入门到精通 第六十七课 ETL之kettle 再谈kettle阻塞,阻塞多个分支的多个步骤

    场景:ETL沟通交流群内有小伙伴反馈,如何多个分支处理完毕之后记录下同步结果呢?或者是调用后续步骤.存储过程.三方接口等. 解决:使用步骤Blocking step进行阻塞处理即可. 1. 如下流程图 ...

  10. FlashDuty Changelog 2023-09-21 | 自定义字段和开发者中心

    FlashDuty:一站式告警响应平台,前往此地址免费体验! 自定义字段 FlashDuty 已支持接入大部分常见的告警系统,我们将推送内容中的大部分信息放到了 Lables 进行展示.尽管如此,我们 ...