一、内置模块path
const path = require('path');
// 1、获取路径中的基础名称 basename: 第一个参数是接受路径 第二个参数是后缀如果有后缀返回后缀前的文件名称
// 返回路径中的最后一个部分不管事文件还是目录, 如果最后有目录分割符会直接忽略 path.basename()
console.log(__filename)
console.log(path.basename(__filename, '.css'))
console.log(path.basename('/a/b/c'))
// 2、获取路径目录名 path.dirname()
/**
* 01、返回最后一个部分的上一级目录所在路径
* 02、如果末尾出现路径分隔符不会被处理
*/
console.log(path.dirname(__filename))
console.log(path.dirname('/a/b/c'))
console.log(path.dirname('/a/b/c/'))
// 3、获取路径的扩展名 path.extname()
/**
* 01、返回path路径中响应文件的后缀名
* 02、如果path路径中存在多个. 他匹配的是最后一个.到结尾的内容
*/
console.log(path.extname(__filename))
console.log(path.extname('/a/b/c'))
console.log(path.extname('/a/b/index.html.js.css'))
console.log(path.extname('/a/b/index.html.js.'))
// 4、解析路径
/**
* 01、path.parse 接受一个路径,返回一个对象对象中包含着路径信息 root dir base ext name
* 02、 会忽略结尾的路径分隔符
* 03、 如果是相对路径root是空 如果是绝对路径 root是根/
*/
const obj = path.parse('./a/b/c')
console.log(obj)
// 5、序列化路径
/**
* 与解析路径相反
*/
const obj = path.parse('./a/b/c')
console.log(obj)
console.log(path.format(obj))
// 6、判断路径是否是绝对路径
console.log(path.isAbsolute('foo')) // false
console.log(path.isAbsolute('/foo')) // true
console.log(path.isAbsolute('///foo')) //true
console.log(path.isAbsolute('')) //false
console.log(path.isAbsolute('.')) // false
console.log(path.isAbsolute('./bar')) //false
// 7、拼接路径
/**
* 01、 拼接给定的路径片段生成完整路径
* 02、 可以识别../ ./ 等操作
* 03、 忽略为空的路径片段 如果只有空路径 那么返回一个. 代表当前目录
*/
console.log(path.join('a/b', 'c', 'index.html')) // a\b\c\index.html
console.log(path.join('/a/b', 'c', 'index.html')) // \a\b\c\index.html
console.log(path.join('/a/b', 'c', '../' ,'index.html')) // \a\b\index.html
console.log(path.join('/a/b', 'c', './' ,'index.html')) // \a\b\c\index.html
console.log(path.join('/a/b', 'c', '' ,'index.html')) // \a\b\c\index.html
console.log(path.join('')) // .
// 8、规范化路径
console.log(path.normalize('/a/b/c/d')) // \a\b\c\d
console.log(path.normalize('/a///b/c../d')) // \a\b\c..\d
console.log(path.normalize('/a/\\b/c\\/d')) // \a\b\c\d
console.log(path.normalize('/a//\b/c\\/d')) // \a\c\d
// 9、返回绝对路径
/**
* resolve([form], to) from和to是一个相对的关系 取决于第二个参数是怎么传的
* 如果传参中没有绝对路径那么返回的前缀是cwd
*/
console.log(path.resolve()) // D:\项目\node学习
console.log(path.resolve('a','b')) // D:\项目\node学习\a\b
console.log(path.resolve('a','/b')) // D:\b
console.log(path.resolve('/a','b')) // D:\a\b
console.log(path.resolve('/a','/b')) // D:\b
console.log(path.resolve('index.html')) // D:\项目\node学习\index.html
二、全局变量buffer
Buffer让JavaScript可以操作二进制
IO行为操作的就是二进制
数据的端到端传输会有生产者和消费者,生产和消费往往存在等待,产生等待时数据就存放在Buffer缓冲区,Buffer一般配合Stream流使用 充当数据缓冲区
Nodejs中Buffer是一片内存空间,不占据V8的堆内存大小;内存的使用由Node来控制,由V8的GC回收
创建Buffer:
/**
* 创建buffer方法
* alloc: 创建指定字节大小的buffer
* allocUnsafe: 创建指定大小的buffer(不安全) 有空闲内存就会拿来用
* from:接收数据,创建数据对应buffer
*/
const b1 = Buffer.alloc(10);
console.log(b1) // <Buffer 00 00 00 00 00 00 00 00 00 00>
const b2 = Buffer.allocUnsafe(10);
console.log(b2) // <Buffer 00 00 00 00 00 00 00 00 00 00>
const b3 = Buffer.from('中') // 第一个参数是传入的数据可传入字符串数组和本身是buffer的类型数据 第二个参数是编码方式默认utf-8
console.log(b3) // <Buffer e4 b8 ad>
const b4 = Buffer.from([1,2,3])
console.log(b4) // // <Buffer 01 02 03>
console.log(b3.toString()) // 中
const b5 = Buffer.from([0xe4, 0xb8, 0xad], 'utf-8')
console.log(b5) // <Buffer e4 b8 ad>
console.log(b5.toString()) // 中
// 利用老的buffer空间创建新的buffer空间,其实是根据老空间的大小长度数据进行的拷贝,修改老空间数据不会影响新空间数据
const b6 = Buffer.alloc(3)
const b7 = Buffer.from(b6)
console.log(b6, b7) // <Buffer 00 00 00> <Buffer 00 00 00>
b6[0] = 1
console.log(b6, b7,11) // <Buffer 01 00 00> <Buffer 00 00 00> 11
为什么 allocUnsafe 不安全?
关键在于给 Buffer 申请分配的内存是否被初始化。被初始化的内存即填充了默认的数据,例如 0。没被初始化的内存可能包含敏感的旧数据,这是不安全的。
Buffer.alloc() 分配的内存是初始化过的内存(被 0 填满覆写),这种创建方式虽然慢但被认为是安全的。
Buffer.allocUnsafe() 分配的内存没有被初始化,所以分配速度相当快,但内存中可能存在敏感旧数据,在 Buffer 可读的情况下,可能会泄露数据,这种方式被认为是不安全的。一般会建议手动通过 buf.fill(0) 初始化或写满这个 Buffer。
虽然在使用 Buffer.allocUnsafe() 时有明显的性能优势,但必须额外小心,以避免给应用程序引入安全漏洞。
为什么不使用 new 创建 Buffer?
在 Nodejs 的 v6 版本之前可以直接通过 new 实例化 Buffer 对象。
但是这种方式提供给 Buffer 实例对象的操作权限实在是太大了,所以在后续的版本中对它进行了一些处理。
主要还是使用这种方式分配的内存是没有初始化过的,包含敏感数据,Node.js 认为在分配内存的安全性上需要更加明确的区分,所以建议使用 Buffer 类的静态方法创建,而不是通过 new 实例化。
Buffer的实例方法
/** Buffer实例方法
* fill: 使用数据填充buffer
* write: 向buffer中写入数据
* toString: 从buffer中提取数据
* slice: 截取buffer
* indexOf: 在buffer中查找数据
* copy: 拷贝buffer中的数据
*/
// fill
/**
* 第一个参数是要填充的数据如果长度比buffer长度小会被反复填充满为止,如果比buffer长度长那么就填充到buffer的长度
* 第二个参数是从buffer的哪个下标开始填充
* 第三个参数是填充到哪里 经典顾头不顾腚
*/
let buf = Buffer.alloc(6)
buf.fill(123)
console.log(buf, buf.toString())
//write
/**
* 第一个参数要写入的数据,他不会和fill一样填充
* 第二个参数是从哪里开始写
* 第三个参数是写入的长度
*/
buf.write('123',1,4)
console.log(buf, buf.toString())
// toSting:根据编码转换buffer数据
/**
* 第一个参数要转换成的编码格式
* 第二个参数截取数据的起始位置
* 第三个参数截取数据的结束为止 顾头不顾腚
*/
buf = Buffer.from('啊哈哈)
console.log(buf,buf.toString('utf-8', 3, 6))
//slice:像操作数组一样对buffer进行截取,然后返回
/**
* 第一个参数,第二个参数截取的开始和结束 顾头不顾尾
* 如果想从后往前就传入负数
*/
buf = Buffer.from('啊哈哈')
const b1 = buf.slice(-3)
console.log(b1,b1.toString())
// indexOf
/**
* 传入目标字符 找到第一次出现的下标,如果没有是-1
*/
buf = Buffer.from('云顶,铲铲,云铲铲')
console.log(buf.indexOf('哈'))
// copy
/**
* b2是数据源 b1是拷贝的容器
* 第二个参数是 从容器的哪个位置去进行写入操作
* 第三个参数 从原buffer的哪个位置进行的读取操作
* 第四个参数是从原buffer的哪个位置结束 顾头不顾腚
*/
const b1 = Buffer.alloc(6)
const b2 = Buffer.from('啊哈')
b2.copy(b1, 3, 3, 6)
console.log(b1.toString(), b2.toString())
自定义Buffer方法之split
Buffer.prototype.split = function (sep) {
let len = Buffer.from(sep).length // 获取分割符字节长度
let ret = [] // 返回的分割后的数组
let start = 0 // 分割起始位置
let offset = 0 // 偏移量
while ( (offset = this.indexOf(sep, start)) !== -1) {
ret.push(this.slice(start, offset))
start = offset + len
}
// 追加尾部
ret.push(this.slice(start))
return ret
}
const buf = Buffer.from('怎么说呢,什么情况,怎么会这样')
console.log(buf.split(',').map(v => v.toString()))
三、FS模块
fs是内置核心模块,提供文件系统操作的API
fs模块结构:
1、fs基本操作类:实现文件信息的获取,如判断当前是目录还是文件、文件的可读流和可写流的操作、文件的监听行为等
2、fs常用API:如文件的打开/关闭、文件的增删改查等
关于系统和文件的前置知识
权限位
这里的“权限”指在当前操作系统内,不同的用户角色对于当前文件所具备的不同权限操作。
文件的权限操作分为三种:
r
:read 读权限,读取/查看w
:write 写权限,修改文件x
:execute 执行权限,执行文件-
:无权限
以上权限都不能指定文件的删除权限,删除权限由是否拥有对该文件所在目录的写权限决定。
用八进制数字进行表示:
r
:4w
:2x
:1无权限
:0
例如:rw
表示拥有读写权限,数字表示就是 6
(4+2
)
操作系统中将用户分为三类:
文件的所有者:一般指的是当前用户
文件的所属组:当前用户所在组
其它用户:例如访客用户
文件的权限由以上三类用户依次组成,每类用户的权限由 r、w、x 依次组成,或用一个八进制数字表示。
例如:
774或rwxrwxr--:当前用户和当前用户所属组拥有最大权限,其它用户只拥有读权限
777或rwxrwxrwx 表示为任意用户分配对当前文件的最大权限。
windows 系统下,文件的权限一般是可读可写但不可执行,即 rw-rw-rw-
(666
)。
文件标志符
文件系统标志符 flag 表示文件打开的方式。
常见标识符有很多,这里只列举一部分:
a:打开文件进行追加,如果文件不存在,则创建文件
r:打开文件进行读取,如果文件不存在,则抛出异常
w:打开文件进行写入,如果文件不存在,则创建
s:表示以同步模式操作,配合 a、r、w 进行使用
x:表示排它操作,如果路径存在则失败,配合 a、w 使用
+:表示附加操作,配合 a、w、r 使用,a和w包含写入操作,所以 a+和 w+附加 r 读取操作,而 r+ 附加 w 写入操作。
r+ 和 w+ 的区别是:
如果文件不存在,前者抛出异常,后者会创建文件。
w+ 会将文件内容清空,再写入;r+ 会读取文件内容,从开头开始覆盖每个字节的内容,不会清空
文件描述符
文件描述符 fd 就是操作系统分配给被打开文件的数字标识。
这个标识用于识别和跟踪每个特定文件。
windows 系统采用了不同但概念类似于文件描述符的机制追踪资源,为了方便用户,Nodejs 抽象了操作系统之间的差异,并为所有打开的文件分类的一个数字文件描述符。
在 Nodejs 中每操作一个文件,文件描述符就会递增一次,并且这个描述符一般是从 3 开始,因为 0、1、2 被标准输入、标准输出、标准错误占用了。
程序首次使用 fs.open() 打开一个文件的时候会获得一个 fd,它就是这个文件的描述符,并且从 3 开始。
fs 总结
fs 是 Nodejs 中内置的核心模块,所有与文件相关的操作都要通过它的 API 完成
代码层面上 fs 分为基本操作类和常用 API
文件操作有三个常用概念:权限位、文件标志符、文件描述符
文件读写与拷贝操作
文件操作API
readFile:从指定文件中读取数据
writeFile: 向指定文件中写入数据
appendFile:追加的方式向指定文件中写入数据
copyFile:将某个文件中的数据拷贝至另一文件
watchFile:对指定文件进行监控
readFile
、writeFile
、appendFile
、copyFile
都是一次性的操作,例如copyFile
会将文件内容一次性获取并放到内存中,然后再一次性写入另一个文件。这些都不适用于大内存的文件操作。
Nodejs 中几乎所有文件 API 操作都有同步和异步两种方式,同步 API 名称比异步 API 名称多个 Sync
,如 readFile 对应的同步 API 是 readFileSync,更多可以查看 Nodejs 文档
const fs = require('fs')
const path = require('path')
// readFile
/**
* Nodejs回调函数为错误优先,即第一个参数是错误信息,后面才是数据
* readFile第一个参数为文件路径建议绝对路径,第二个参数是以什么方式编码,第三个为回调函数
*/
fs.readFile(path.resolve('data.txt'), 'utf-8' , (err, data) => {
console.log(err)
console.log(data)
})
// writeFile
/**
* writeFile第一个参数为文件路径,第二个参数是需要写入的内容,第三个是一个对象 mode是权限位 flag是文档标记符 encoding是编码方式
* writeFile默认的是覆盖写如操作会覆盖原文档的内容
* 如果写入的路径不存在,那么他会创建这个文件
*/
fs.writeFile(path.resolve('data.txt'), '啊', {
mode: 0o666, // 八进制0o666 十进制438
flag: 'r+', // 默认w
encoding: 'utf8'
}, (err) => {
if (!err) {
fs.readFile('data.txt','utf-8',(err, data) => {
console.log(err)
console.log(data)
})
}
})
// appendFile
/**
* 第一个参数文件路径,第二个参数写入内容
*/
fs.appendFile(path.resolve('data.txt'), '今天整一个', (err) => {
console.log('写入成功')
})
// copyFile
/**
* 第一个参数源文件,第二个参数目标文件
*/
fs.copyFile('data.txt', 'test.txt', (err) => {
console.log('拷贝成功')
})
// watchFile
/**
* 监听文件变化
* 第一个参数是文件路径,第二个参数的interval属性代表多长时间监听一次
* 回调函数中 curr是监听修改后的文件对象, prev是修改前的文件对象(mTime是修改时间)
*/
// unwatchFile 结束监听
fs.watchFile('data.txt', { interval:20 }, (curr,prev) => {
// 判断是否已经进行过修改
if (curr.mtime !== prev.mtime) {
console.log('文件被修改了')
fs.unwatchFile('data.txt')
}
})
文件操作实现md转html
需要依赖
marked:把md文件转换成html的格式
browserSync:开启一个web服务,打开html文件并实时更新
const fs = require('fs')
const path = require('path')
const marked = require('marked')
const browserSync = require('browser-sync')
// 01 读取 markdown 和 css 的内容
// 02 将上述读取的内容,替换模板中的占位符,生成最终需要展示的 html 字符串
// 03 将最终 html 字符串写入到指定的 html 文件中
// 04 监听 markdown 文件内容的变化,实时更新 html 内容
// 05 实时显示 html 内容
// html 模板
const temp = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
{{style}}
</style>
</head>
<body>{{content}}</body>
</html>
`
// markdown 文件路径
// 启动的时候 node index.js md文件名 所以md文件的路径就是目录名dirname加启动参数的第三个
const mdPath = path.join(__dirname, process.argv[2])
// css 文件路径
const cssPath = path.resolve('github.css')
// html 最终转化的文件路径
// 指定为与 markdown 文件同目录下的同名 html 文件
const htmlPath = mdPath.replace(path.extname(mdPath), '.html')
// 监听 markdown 文件
fs.watchFile(mdPath, (current, previous) => {
if (current.mtime !== previous.mtime) {
// 读取 markdown 内容
fs.readFile(mdPath, 'utf8', (err, data) => {
// 将 markdown 转化为 html
const content = marked(data)
// 读取 css 内容
fs.readFile(cssPath, 'utf8', (err, style) => {
// 替换内容
const html = temp.replace('{{style}}', style).replace('{{content}}', content)
// 写入指定 html 文件
fs.writeFile(htmlPath, html, err => {
console.log('写入成功')
})
})
})
}
})
// 开启服务 显示 html 内容
browserSync.init({
server: {
baseDir: __dirname, // 服务的根目录
index: path.basename(htmlPath) // 指定首页的文件名
},
watch: true // 监听更新
})
文件的打开与关闭
前面的 API 是将文件中的数据一次性的读取/写入到内存中,这种方式对于大体积的文件来说,显然不合理。
所以需要实现一个可以边读边写或边写边读的操作方式,这就需要将文件的打开、读取、写入、关闭看作各自独立的环节。
const fs = require('fs');
const path = require('path');
// open
/**
* 第一个参数是打开的文件路径
* 第二个参数是文件标志符,进行什么样的操作,
* 回调中的第二个参数是文件描述符,用于追踪文件从3开始 0 1 2被标准输入、输出、错误占用
*/
fs.open(path.resolve('data.txt'), 'r', (err, fd) => {
console.log(fd)
})
// close
fs.open(path.resolve('data.txt'), 'r', (err, fd) => {
console.log(fd)
// 第一个参数传入打开文件的文件描述符追踪关闭哪一个文件
fs.close(fd, (err) => {
console.log('关闭成功')
})
})
大文件读写操作
A文件中的数据要想拷贝到B文件中,默认情况下需要内存作为中转。
如果是一次性的操作,就会存在内存占满并且溢出的潜在问题。
因此我们更期望有一个中间暂存区,一点一点的读取,然后一点一点的写入。
而这个中间暂存区就是 Buffer。
const fs = require('fs');
const path = require('path');
// read:所谓的读操作就是将文件从磁盘中读取然后写入到buffer中
let buf = Buffer.alloc(10)
/**
* 第一个参数是fd 也就是文件标识符,标记是读取哪一个文件
* 第二个参数是缓冲区,指明写入哪一个缓冲区
* 第三个参数是偏移量,表示从buffer的哪一个位置开始执行写入
* 第四个参数是length,表示当前次写入的长度
* 第五个参数是position,表示从文件的哪一个位置开始进行读取操作
* 缓冲区的大小必须大于文件内容大小不然会报错
*/
fs.open(path.resolve('data.txt'), 'r', (err, rfd) => {
fs.read(rfd, buf, 0 ,4, 3, (err,readBytes,data) => {
console.log(readBytes,data,data.toString())
fs.close(rfd)
})
})
// write:将缓冲区里的内容写入到磁盘文件中,所谓的写也就是读
/**
* 第一个参数fd,标记写入哪一个文件
* 第二个参数缓冲区 指明写入哪一个缓冲区
* 第三个参数是偏移量,表示从buffer的哪一个位置开始执行读取
* 第四个参数是length,表示当前次读取的长度
* 第五个参数是position,表示从文件的哪一个位置开始进行读取写入 一般都是0不动
*/
buf = Buffer.from('1234567890')
fs.open(path.resolve('b.txt'), 'w', (err,wfd) => {
fs.write(wfd, buf, 0, 4, 0, (err,writeBytes,buffer) => {
// buffer指向写入的数据源
console.log(writeBytes)
console.log(buffer)
console.log(buffer.toString)
fs.close(wfd) // 用完之后关掉 不然每open一次文件标识符都会+1 会造成资源浪费
})
})
文件拷贝自定义实现
fs的copyFile是针对readFile和writeFile进行实现的,他们都是对文件的一次性操作,对大体积文件来说非常不合适,
基于文件读写对大体积文件进行拷贝api的实现
// 将A文件拷贝到B文件 用read读数据存入buffer 从buffer中取数据写入B文件
const fs = require('fs')
const buf = Buffer.alloc(10)
// 数据完全拷贝
const BUFFER_SIZE = buf.length // 每次读取数据的字节数
let readOffset = 0
fs.open('copyA.txt', 'r', ( err, rfd ) => {
fs.open('copyB.txt', 'w', ( err, wfd ) => {
function next() {
// position指定为null 自动更新读取文件的起始位置
fs.read(rfd, buf, 0, BUFFER_SIZE, readOffset, (err, readBytes, buffer) => {
if (!readBytes) {
fs.close(rfd);
fs.close(wfd);
console.log('拷贝完成')
return
}
readOffset += BUFFER_SIZE
fs.write(wfd, buf, 0, readBytes, (err, writeBytes, buffer) => {
next()
})
});
}
next()
})
})
对于大文件的拷贝,在开发中最优的实现方式是流操作
目录操作API
access:判断用户是否具有当前文件或目录的操作权限
stat:获取目录及文件信息
mkdir:创建目录 make directory
rmdir:删除目录 remove directory
readdir:读取目录中的内容
unlink:删除文件
rm:删除文件和目录 新增于 v14.14.0,rmdir 递归删除的替代推荐 ;Nodejs v14.14.0 推荐使用 fs.rm 代替 fs.rmdir 的 recursive 选项;Nodejs v16.0.0 弃用 fs.rmdir 的 recursive 选项,使用将导致错误
const fs = require('fs');
// access 判断有无操作权限,用于判断目录是否存在
fs.access('data.txt', err => {
if (err) {
console.log(err);
} else {
console.log('有操作权限')
}
})
// stat
/**
* 回调函数第二个参数是文件的信息
* size:内容字节数
* isFile:判断是否是文件
* isDirectory:判断是否是目录文件夹
*/
fs.stat('data.txt', (err,statObj) => {
console.log(statObj.size)
console.log(statObj.isFile())
console.log(statObj.isDirectory())
})
// mkdir
/**
* 默认情况下创建的是路径最后部分,创建前提是保证父级目录全部存在
* 下面是创建c如果a,b文件夹妹有那么会创建失败
* 第二个参数如果传入 recursive是true那么他就会递归创建,传入的路径会依次创建
*/
fs.mkdir('a/b/c', err => {
if(err){
console.log(err)
} else {
console.log('创建成功')
}
})
fs.mkdir('a/b/c', { recursive: true}, err => {
if(err){
console.log(err)
} else {
console.log('创建成功')
}
})
// rmdir
/**
* 默认情况下删除的是路径最后的部分
* 如果删除的不是目录或者路径不存在就会报错
* 如果要删除的目录不是非空目录会报错
* 同mkdir一样 第二个参数传入recursive 可以递归删除用于删除非空的目录
* 如果递归删除目录下有文件类型文件也会删除掉
*/
fs.rmdir('a', err => {
if (err) {
console.log(err)
} else {
console.log('删除成功')
}
})
fs.rmdir('a', { recursive: true}, err => {
if (err) {
console.log( err )
} else {
console.log('删除成功')
}
})
// readdir
/**
* 仅读取当前目录的下一层文件列表,不会递归读取
* 回调中的files是文件名组成的数组
*/
fs.readdir('a', (err, files) => {
console.log(files)
})
// unlink 删除文件
/**
* 删除文件类型的 如果是目录会报错
* 删除的是路径中最后的部分,如果文件不存在就会报错
*/
fs.unlink('a', err => {
if (err) {
console.log(err)
} else {
console.log('文件删除成功')
}
})
同步模拟递归创建目录
const fs = require('fs');
const path = require('path');
const makeDir = (dirPath) => {
const items = dirPath.split(path.sep); // 获取当前平台路径分隔符
for (let i = 1; i <= items.length; i++) {
const dir = items.slice(0, i).join(path.sep);
try {
// 判断文件的可操作权限 (文件是否存在)
fs.accessSync(dir)
} catch (e) {
fs.mkdirSync(dir)
}
}
}
makeDir(path.resolve('a/b/c'))
创建目录异步实现
const makeDir = (dirPath, cb) => {
const items = dirPath.split(path.sep); // 获取当前平台路径分隔符
let index = 1
const next = () => {
if (index > items.length) return cb && cb()
const dir = items.slice(0, index++).join(path.sep)
fs.access(dir, err => {
if (err) {
fs.mkdir(dir, err => {
if (err) {
console.log(err)
} else {
next()
}
})
} else {
next()
}
})
}
next()
}
makeDir(path.resolve('a/b/c'), () => {
console.log('创建成功')
})