总结自 Coderwhy的nodejs课程。
Express总结: juejin.cn/post/701653…
Koa总结:juejin.cn/post/701658…
nodejs官网:nodejs.org/dist/latest…
Nodejs是什么
Node.js是一个基于V8 JavaScript引擎的JavaScript运行时环境。
V8引擎原理
Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;
- 如果函数没有被调用,那么是不会被转换成AST的;
- Parse的V8官方文档:v8.dev/blog/scanne…Ignition是一个解释器,会将AST转换成ByteCode(字节码)
- 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
- 如果函数只调用一次,Ignition会执行解释执行ByteCode;
- Ignition的V8官方文档:v8.dev/blog/igniti…TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;
- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
- 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
- TurboFan的V8官方文档:v8.dev/blog/turbof…上面是JavaScript代码的执行过程,事实上V8的内存回收也是其强大的另外一个原因,不过这里暂时先不展开讨论:
- Orinoco模块,负责垃圾回收,将程序中不需要的内存回收;
- Orinoco的V8官方文档:v8.dev/blog/trash-…
Node.js架构
- 我们编写的JavaScript代码会经过V8引擎,再通过Node.js的Bindings,将任务放到Libuv的事件循环中;
- libuv(Unicorn Velociraptor—独角伶盗龙)是使用C语言编写的库;
- libuv提供了事件循环、文件系统读写、网络IO、线程池等等内容;
Nodejs的全局对象
一些有用的全局对象。
__dirname
:获取当前文件所在的文件夹的绝对路径
__filename
:获取当前文件的绝对路径:
process
对象:
- cwd(): 获取当前终端运行的文件夹绝对路径。
- argv:获取在终端执行时传入的参数。
- env:获取当前程序的环境变量。
Node.js模块化
Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
- 在Node中每一个js文件都是一个单独的模块;
- 这个模块中包括CommonJS规范的核心变量:
exports、module.exports、require
;
exports
和module.exports
可以负责对模块中的内容进行导出;
require
函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容; 下面我们将来介绍exports、module.exports、require
的使用。
- exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出;
- 我们也可以通过module.exports直接导出一个对象。
- 我们通过
require()
函数导入一个文件。并且该文件导出的变量。 下面来详细介绍一个module.exports
。
- CommonJS中是没有module.exports的概念的;
- 但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是module;
- 所以在Node中真正用于导出的其实根本不是exports,而是module.exports;
- 因为module才是导出的真正实现者;
- 并且内部将
exports
赋值给module.exports
。 该方式的导入导出有一个特点。
具体请访问:juejin.cn/post/701299…
- Node中的文件都运行在一个函数中。可以通过打印
console.log(arguments.callee + "")
来验证。
- 导入导出是值的引用,如果导出的是一个基本数据类型值,那么导出文件改变该值,然后导入文件该变量的值也不会变。
- 他是通过require 函数来导入的,只有在执行js代码才会知道模块的依赖关系。
- 代码是同步执行的。
- 模块多次引入,只会加载一次。每个module内部会存在一个loaded来确定是否被加载过
- 代码循环引入的时候,深度优先来加载模块。然后再广度优先。
path模块
只介绍一些常用的API
由于不同操作系统可能使用不同的路径分隔符,所以join方法就非常有用。
- resolve: 拼接路径。这个会根据传入的第一个路径前面是否有
/,../,./
来查找本地的完整目录,然后再拼接传入的其他路径。如果第一个传入的路径/
开头的,那么将把路径拼接到当前执行文件的绝对路径后面。如果最后一个传入的路径通过/
开头,那么我们将直接忽略前面传入的路径参数。
- join:路径拼接。这个就是传入的是啥拼接的就是啥。傻瓜式拼接。但是也会根据
../
来拼接上一级
const { resolve, join } = require("path") const first = '/zhang'; const second = "./hao" || '../hao' const third = './llm' || '/llm' console.log(resolve( first, second, third)) //C:\zhang\hao\llm || C:\hao\llm || C:\llm console.log(join(first, second, third)) // \zhang\hao\llm || \hao\llm || \zhang\hao\llm
- dirname:获取文件的父文件夹;
- basename:获取文件名;
- extname:获取文件扩展名;
const path = require("path") const url = "c:/zh/study/nodejs/index.js"; console.log(path.dirname(url))// c:/zh/study/nodejs console.log(path.basename(url))// index.js console.log(path.extname(url))//.js
fs模块
fs模块大部分API的实现都有三种方法。
- 方式一:同步操作文件:代码会被阻塞,不会继续执行;
- 方式二:异步回调函数操作文件:代码不会被阻塞,需要传入回调函数,当获取到结果时,回调函数被执行;
- 方式三:异步Promise操作文件:代码不会被阻塞,通过
fs.promises
调用方法操作,会返回一个Promise,可以通过then、catch进行处理;
方式一:同步操作文件
// 1.方式一: 同步读取文件 const state = fs.statSync('../foo.txt'); console.log(state); console.log('后续代码执行');
方式二:异步回调函数操作文件
// 2.方式二: 异步读取 fs.stat("../foo.txt", (err, state) => { if (err) { console.log(err); return; } console.log(state); }) console.log("后续代码执行");
方式三:异步Promise操作文件
// 3.方式三: Promise方式 fs.promises.stat("../foo.txt").then(state => { console.log(state); }).catch(err => { console.log(err); }) console.log("后续代码执行");
获取文件描述信息
const fs = require("fs"); fs.stat("./01.hello.txt", (err, info) => { console.log(info) console.log(info.isFile())// 判断他是否为文件 console.log(info.isDirectory())// 判断他是否为文件夹 })
文件描述符
在 POSIX 系统上,对于每个进程,内核都维护着一张当前打开着的文件和资源的表格。
每个打开的文件都分配了一个称为文件描述符的简单的数字标识符。
在系统层,所有文件系统操作都使用这些文件描述符来标识和跟踪每个特定的文件。
Windows 系统使用了一个虽然不同但概念上类似的机制来跟踪资源。
为了简化用户的工作,Node.js 抽象出操作系统之间的特定差异,并为所有打开的文件分配一个数字型的文件描述符
const fs = require("fs"); fs.open("./01.hello.txt", (err, fd) => { if (err) { console.log(err) } else { console.log(fd) // fd是一个数字。 fs.promise.readFile(fd).then(res => { console.log(res) }) } })
文件读写
fs.readFile(path[, options], callback)
:读取文件的内容;
fs.writeFile(file, data[, options], callback)
:在文件中写入内容;
const fs = require("fs"); fs.writeFile("./01.hello.txt", "加入文件", { encoding: 'utf-8', flag: 'a+' }, (err, res) => { if (err) { console.log(err) } else { console.log(res) } })
调用读写API很简单就可以操作文件,下面我们来说明一些options选项。
encoding
:指定编码格式。注意如果读取文件和写入文件的编码格式不同,将会乱码。一般都采用utf-8
编码。如果文件读取不指定编码格式,那么它将返回Buffer数据。
flag
: 用于指定文件写入的方式。带有+的flag都可以读写。除了r+文件不存在,读取和写入会报出异常,其他的+flag都可以自动创建。
文件夹操作
- 使用fs.mkdir()或fs.mkdirSync()创建一个新文件夹。
const dirname = './zh'; if (!fs.existsSync(dirname)) { // 判断文件夹是否存在 fs.mkdir(dirname, err => { console.log(err); }); }
fs.readdir(), fs.readdirSync()
读取文件夹中的所有文件和文件夹
fs.readdir(dirname, (err, files) => { console.log(files); });
rename()
重命名文件夹
fs.rename("./zh", "./llm", err => { console.log(err); })
兄弟们,熟练掌握文件操作,可以写很多方便的脚本。