Node.js中的模块
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中的模块的更多相关文章
- node.js中express模块创建服务器和http模块客户端发请求
首先下载express模块,命令行输入 npm install express 1.node.js中express模块创建服务端 在js代码同文件位置新建一个文件夹(www_root),里面存放网页文 ...
- node.js中ws模块创建服务端和客户端,网页WebSocket客户端
首先下载websocket模块,命令行输入 npm install ws 1.node.js中ws模块创建服务端 // 加载node上websocket模块 ws; var ws = require( ...
- node.js中net模块创建服务器和客户端(TCP)
node.js中net模块创建服务器和客户端 1.node.js中net模块创建服务器(net.createServer) // 将net模块 引入进来 var net = require(" ...
- node.js中module模块的理解
node.js中使用CommonJS规范实现模块功能,一个单独的文件就是一个单独的模块.通过require方法实现模块间的依赖管理. 通过require加载模块,是同步操作. 加载流程如下: 1.找到 ...
- 在 Node.js 中引入模块:你所需要知道的一切都在这里
本文作者:Jacob Beltran 编译:胡子大哈 翻译原文:http://huziketang.com/blog/posts/detail?postId=58eaf471a58c240ae35bb ...
- Web 前端模块出现的原因,以及 Node.js 中的模块
模块出现原因 简单概述 随着 Web 2.0 时代的到来,JavaScript 不再是以前的小脚本程序了,它在前端担任了更多的职责,也逐渐地被广泛运用在了更加复杂的应用开发的级别上. 但是 JavaS ...
- Node.js中的模块接口module.exports浅析
在写node.js代码时,我们经常需要自己写模块(module).同时还需要在模块最后写好模块接口,声明这个模块对外暴露什么内容.实际上,node.js的模块接口有多种不同写法.这里作者对此做了个简单 ...
- Node.js中的模块接口module.exports
在写node.js代码时,我们经常需要自己写模块(module).同时还需要在模块最后写好模块接口,声明这个模块对外暴露什么内容.实际上,node.js的模块接口有多种不同写法.在此做了个简单的总结. ...
- node.js中通过dgram数据报模块创建UDP服务器和客户端
node.js中 dgram 模块提供了udp数据包的socket实现,可以方便的创建udp服务器和客户端. 一.创建UDP服务器和客户端 服务端: const dgram = require('dg ...
- node.js中net网络模块TCP服务端与客户端的使用
node.js中net模块为我们提供了TCP服务器和客户端通信的各种接口. 一.创建服务器并监听端口 const net = require('net'); //创建一个tcp服务 //参数一表示创建 ...
随机推荐
- SWAG反向代理Jellyfin媒体服务器流量教程
目录 1. 简介 1.1 Jellyfin媒体服务器 1.2 SWAG服务器 2. 设置Jellyfin开启HTTPS访问 3. 安装并配置SWAG服务器反向代理Jellyfin流量 3.1 安装SW ...
- Golang validate验证器
目录 自定义验证规 单条验证 多条批量验证 其它验证包: gookit/validate 手册地址: https://godoc.org/gopkg.in/go-playground/validato ...
- Mysql8.0在windows系统安装一直卡在Starting the server的解决方案
报错:Beginning configuration step: Starting Server Attempting to start service MySQL80 一直卡在这里,手动启动服务也起 ...
- 3种方法实现图片瀑布流的效果(纯JS,Jquery,CSS)
最近在慕课网上听如何实现瀑布流的效果:介绍了3种方法. 1.纯JS代码实现: HTML代码部分: <!DOCTYPE html> <html> <head> < ...
- CSS布局概念与技术教程
以下是一份CSS布局学习大纲,它涵盖了基本到高级的CSS布局概念和技术 引言 欢迎来到CSS教程!如果你已经掌握了HTML的基础知识,那么你即将进入一个全新的世界,通过学习CSS(Cascading ...
- selenium遇到手机验证码怎么解决
完整代码在: selenium使用案例 解决思路,点击发送送验证码,程序用input方法去和人进行交互,手动输入验证码,按回车键,这样程序就接收到手机验证码了,再把验证码赋值给验证码框,继续往下操作 ...
- kubernetes 之 Rolling Update 滚动升级
滚动升级 1.错误的yml文件 [machangwei@mcwk8s-master ~]$ cat mcwHttpd.yml apiVersion: apps/v1 kind: Deployment ...
- 基于webapi的websocket聊天室(番外一)
上一篇我已经实现了聊天室,并且在协议中实现了4种类型的消息传输.其实还可以添加video,audio,live等等类型. 不过假如把目前的协议看作RCP1.0版的话,这个版本就只支持有限的4种消息.精 ...
- 【BI 可视化插件】怎么做? 手把手教你实现
背景 对于现在的用户来说,插件已经成为一个熟悉的概念.无论是在使用软件. IDE 还是浏览器时,插件都是为了在原有产品基础上提供更多更便利的操作.在 BI 领域,图表的丰富性和对接各种场景的自定义是最 ...
- WPF摄像头使用(WPFMediaKit)
添加WPFMediaKit引用 使用WPFMediaKit操作摄像头需要安装WPFMediaKit相关的Nuget包.选中需要进行摄像头操作的项目,然后通过Nuget安装即可. 页面代码 引入命名空间 ...