Node高级
Fs模块
读取文件的三种方式
1.同步读取
const res1 = fs.readFileSync("./abc.txt");
console.log(res1);//<Buffer 31 31 31 31 31>
const res1 = fs.readFileSync("./abc.txt", { encoding: "utf8" });
console.log(res1); //11111
2.异步:回调函数
const res2 = fs.readFile("./abc.txt", { encoding: "utf-8" }, (err, data) => {
if (err) return;
console.log(data);
});
console.log("后续代码");
//后续代码
//11111
3.异步:Promise
fs.promises
.readFile("./abc.txt", { encoding: "utf8" })
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
//11111
fs获取文件描述符
fs.open("./abc.txt", (err, fd) => {
if (err) {
console.log(err);
return;
}
//获取文件描述符
console.log(fd);
//获取文件信息:修改时间,创建时间,大小等...
fs.fstat(fd, (err, stats) => {
if (err) return;
console.log(stats);
});
// fs.fstatSync();
// fs.promises.fstatSync();
fs.close(fd);//分配新的文件描述符
});
Stats {
dev: 2021145378,
mode: 33206,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: 4096,
ino: 844424930605069,
size: 5,
blocks: 0,
atimeMs: 1677071679788.5024,
mtimeMs: 1677071087528.7202,
ctimeMs: 1677071087528.7202,
birthtimeMs: 1677071066021.2,
atime: 2023-02-22T13:14:39.789Z,
mtime: 2023-02-22T13:04:47.529Z,
ctime: 2023-02-22T13:04:47.529Z,
birthtime: 2023-02-22T13:04:26.021Z
}
读写
const content = "111";
fs.writeFile("./abc.txt", content, { encoding: "utf8", flag: "a+" }, (err) => {
if (err) {
return;
} else {
console.log("写入成功");
}
});
flag:文件系统标志 | Node.js API 文档 (nodejs.cn)
-
'a': 打开文件进行追加。 如果文件不存在,则创建该文件。 -
'ax': 类似于'a'但如果路径存在则失败。 -
'a+': 打开文件进行读取和追加。 如果文件不存在,则创建该文件。 -
'ax+': 类似于'a+'但如果路径存在则失败。 -
'as': 以同步模式打开文件进行追加。 如果文件不存在,则创建该文件。 -
'as+': 以同步模式打开文件进行读取和追加。 如果文件不存在,则创建该文件。 -
'r': 打开文件进行读取。 如果文件不存在,则会发生异常。 -
'r+': 打开文件进行读写。 如果文件不存在,则会发生异常。 -
'rs+': 以同步模式打开文件进行读写。 指示操作系统绕过本地文件系统缓存。这主要用于在 NFS 挂载上打开文件,因为它允许跳过可能过时的本地缓存。 它对 I/O 性能有非常实际的影响,因此除非需要,否则不建议使用此标志。
这不会将
fs.open()或fsPromises.open()变成同步阻塞调用。 如果需要同步操作,应该使用类似fs.openSync()的东西。 -
'w': 打开文件进行写入。 创建(如果它不存在)或截断(如果它存在)该文件。 -
'wx': 类似于'w'但如果路径存在则失败。 -
'w+': 打开文件进行读写。 创建(如果它不存在)或截断(如果它存在)该文件。 -
'wx+': 类似于'w+'但如果路径存在则失败。
文件夹操作
创建文件夹
// 创建文件夹
fs.mkdir("./test", (err) => {});
// fs.mkdirSync();
读取文件夹
// 读取文件夹
fs.readdir("./test", (err, files) => {});
读取文件夹,并获取文件夹中文件的信息
//读取文件夹,并获取文件夹中文件的信息
fs.readdir("./test", { withFileTypes: true }, (err, files) => {
files.forEach((file) => {
if (file.isDirectory()) {
console.log("这是一个文件夹", file.name);
} else {
console.log("是一个文件", file.name);
}
});
});
递归读取文件夹内所有文件
//递归读取文件夹内所有文件
function readDir(path) {
fs.readdir(path, { withFileTypes: true }, (err, files) => {
files.forEach((item) => {
if (item.isDirectory()) {
readDir(`${path}/${item.name}`);
} else {
console.log("获取到的文件", item.name);
}
});
});
}
文件夹重命名(同样适用于文件重命名)
fs.rename("./test", "./newName", (err) => {});
Event模块
const EventEmitter = require("events");
const emitter = new EventEmitter();
function handleFn(args) {
console.log("监听why的事件", args);
}
//监听事件
emitter.on("why", handleFn);
//发射事件
setTimeout(() => {
emitter.emit("why", 111);
}, 2000);
//取消监听
emitter.off("why", handleFn);
emit可以传递参数: emitter.emit("why", 111);
常见方法
-
emitter.eventNames(); 返回当前EventEmitter对象注册的事件字符串数组
-
emitter.getMaxListeners(); 返回当前EventEmitter对象最大监听数量(默认10个),可通过**setMaxListeners()**修改
-
emitter.listenerCount("zxs"); 返回当前对象某个事件名称的监听器个数
-
emitter.listeners("zxs"); 返回当前对象某个事件监听器上所有监听器数组
-
emitter.once("zxs",()=>{}) 只监听一次
-
emitter.prependListener() 将事件添加到最前面
-
emitter.prependOnceListener(); 同上
-
emitter.removeAllListeners() 移除所有名称的监听器,带参数则移除特点名称的监听器
Buffer模块
在计算机中,很少情况我们会直接操作一位二进制,因为一位二进制存储的数据非常有限
通常 会将8位合在一起作为一个单元,称为一个字节(byte)
1byte=8bit 1kb=1024byte
rgb值为0-255,本质上用一个字节存储
使用
const buf2 = Buffer.from("hello");
console.log(buf2);//<Buffer 68 65 6c 6c 6f>
console.log(buf2.toString());//hello
其他操作
alloc默认开辟8kb空间
//1.开辟8个字节大小的空间
const buf3 = Buffer.alloc(8);
//2.
console.log(buf3[0]);
console.log(buf3[1]);
buf3[0] = 100;
buf3[1] = 0x66;
console.log(buf3);
从文件中读取buffer
文件读取,不传第二个参数 { encoding: "utf8" },则data默认为buffer
一般用于处理图片
fromStringFast
先判断剩余的空间是否足够填充这个字符串,
如果不够,则通过createPool()开辟新空间
够则直接使用,但之后要进行poolOffset的偏移变化
Stream
流
问题:可以直接通过readFile...等方式读写文件,为什么还需要流?
readFile方式无法控制 :从什么位置开始读,读到什么位置,一次性读取多少字节,读到某个位置后,暂停读取,某个时刻继续读取等
或当文件非常大时,如一个视频文件,一次性全部读取不合适。
http模块的req和res也是基于流实现的
所有流都是EventEmitter的实例
四种基本流类型
1.Writable:可以向其写入数据 的流
const writeStream = fs.createWriteStream("./test.txt", {
flags: "r+",
start: 6,
});
writeStream.write("i love you", (err) => {
console.log("写入完成", err);
});
writeStream.on("close", () => {
console.log("文件被关闭");
});
//写入完成时,需要手动调用close方法
writeStream.close();
//将最后的内容写入文件,并关闭文件
writeStream.end();
2.Readable:可以从中读取数据 的流
const fs = require("fs");
// 1.可读流
// start:起始位置
//end:结束位置
//highWaterMark:每次读取几个字节,每读完一次,作一次回调,默认64kb
const readStream = fs.createReadStream("./test.txt", {
start: 6,
end: 20,
highWaterMark: 3,
});
readStream.on("data", (data) => {
console.log(data);
console.log("执行了一次回调");
readStream.pause(); //暂停读取
//暂停1秒后继续读取
setTimeout(() => {
readStream.resume(); //继续读取
}, 1000);
});
其他事件:
- "open":监听打开文件事件,有参数fd
- "end":读取到了end位置的事件,会自动关闭文件
- "close":文件读取结束
3.Duplex:同时为Writable和Readable,如net.Socket
4.Transform:可以在写入和读取数据时 修改或转换数据 的流,如zlib.createDeflate()
Pipe()方法
pipe:直接将可读流的数据流入到可写流
拷贝文件示例:
const readStream = fs.createReadStream("./test.txt");
const writeStream = fs.createWriteStream("./test_copy.txt");
// 方式1.
readStream.on("data", (data) => {
writeStream.write(data);
});
readStream.on("end", () => {
writeStream.close();
});
// 方式2.
readStream.pipe(writeStream);
Http模块
const http = require('http')
const path = require('path')
const fs = require('fs');
const url=require("url")
const server = http.createServer();
//req是请求对象 ,包含了客户端相关的数据与属性
server.on('request', (req, res) => {
console.log("request事件被触发了\n");
const url = req.url;
const method = req.method;
const header=req.headers
let {pathname, query} = url.parse(req.url)
const fpath = path.join(__dirname, url)
const str = `your url为: ${url} 请求方法为${method}`
console.log(str);
// 设置响应头
res.setHeader('content-type', 'text/html;charset=utf-8');
fs.readFile(fpath, 'utf8', function (err, data) {
if (err) return res.end("404");
res.end(data)
})
// 将内容发送给服务端
res.end(content);
})
//启动服务器
server.listen(8080, () => {
console.log("服务器运行中 http://127.0.0.1:8080");
})
createServer底层:new 一个Server对象
function createServer(options, reqListener) {
return new Server(options, reqListener);
}
小问题:为什么浏览器访问服务器,服务器会响应两次?
原因:因为浏览器默认会请求一次localhost:8000/favicon.ico,还有localhost:8000
nodemon
npm i nodemon -g
req对象本质是一个readable可读流,res对象本质为writeable可写流
const server = http.createServer((req, res) => {
//获取body参数
// req对象本质是一个readable可读流
req.setEncoding("utf-8");
req.on("data", (data) => {
const body = JSON.parse(data);
});
req.on("end", () => {
res.end("");
});
res.write("hello world")
res.writeHead(401)
res.end()
});
server.listen(8000);
-
res.setHeader("Content-type:"application/json;charset=utf8"): 一次写入一个头部信息
-
res.writeHead(200,{"Content-type:"application/json;charset=utf8"}): 同时写入header和status
Http发送网络请求和axios
文件上传
上传的文件内容是需要通过 boundary 来标明分割线的,正确的Content-Type: mutlipart/form-data; boundary = -----xxxx这种形式。
原生nodejs 处理文件上传 - 掘金 (juejin.cn)
前端上传的时候只需要设置content-type为multipart/form-data。然后服务端拿到headers 里面的boundary 在根据这个去处理得到文件内容即可。以上是nodejs 原生处理文件上传流程,略显复杂。现在koa 结合koa-body 处理文件上传很方便
Express
当匹配到第一个符合要求的中间件时,则执行。是否执行下一个中间件,则取决当前中间件是否调用了next(),是 则执行下一个中间件。
const express = require('express');
const app = express();
//中间件本质是一个带next参数的函数(类似于vue路由守卫)
//定义多个全局中间件 会按照定义的先后顺序依次调用
const mw = function (req, res, next) {
const time = Date.now();
console.log("这是中间件函数");
req.startTime = time;
//把流转关系,转交给下一个中间件或路由
next();
}
//定义局部中间件 next()必须在结尾 连续多个中间件,之间共享req,res对象
const mw1 = (req, res, next) => {
console.log("局部中间件1");
next();
}
const mw2 = (req, res, next) => {
console.log("局部中间件2");
next();
}
//中间件分类
//1.错误级别的中间件(必须注册在所有路由后)
// const mwerr=(err, req, res,next) => {
// console.log("出现错误"+err.message);
// res.send("中间件捕获到错误"+err.message);
// // next();
// }
// 2.express内置中间件
// 2.1 express.json解析json格式请求
app.use(express.json())
app.get('/json', (req, res) => {
console.log(req.body); //通过路由里的req.body访问解析后的json数据
res.send('OK')
})
// 2.2 express.static
// 2.3 express.urlencoded
app.use(express.urlencoded({
extended: false//是否使用node内置的querystring模块,false为使用,true则使用qs模块
}));
app.get('/urlencode', (req, res) => {
console.log(req.body);
res.send('OK');
})
//3 第三方中间件
//body-parser
//4 自定义中间件
app.use((req, res, next) => {
let str = '';
req.on('data', (data) => {
str += data;
})
req.on('end', () => {
console.log(str);
next();
})
})
//注册全局中间件
app.use(mw)
//使用多个局部中间件(一定要在路由之前注册中间件)
app.get('/mid', [mw1, mw2], (req, res) => {
res.send('home' + req.startTime);
console.log("使用了局部中间件");
})
app.get('/', (req, res) => {
res.send('home' + req.startTime);
})
app.get('/user', (req, res) => {
res.send('user' + req.startTime)
})
// 1.错误级别的中间件(必须注册在所有路由后)
const mwerr = (err, req, res, next) => {
console.log("出现错误" + err.message);
res.send("中间件捕获到错误" + err.message);
//无next()
}
app.listen(80, () => {
console.log(" 打开 http://127.0.0.1");
})
express文件上传
multer库
npm i multer
const upload=multer({
dest:'./upload'//文件储存到的位置
})
//单文件上传
app.post('/upload',upload.single('fileName'),(req,res,next)=>{
req.file//文件内容
res.end()
})
//多文件上传
const upload=multer({
storage:multer.diskStorage({
destination(req,file,callback){
callback(null,'./upload')
},
filename(req,file,callback){
callback(null,"自定义文件的名称")
}
})
})
app.post('/upload',upload.array('fileName'),(req,res,next)=>{
req.files//多个文件的内容,数组
res.end()
})
Koa
ctx.req
Node 的 request 对象.
ctx.res
Node 的 response 对象.
ctx.request
koa 的 Request 对象.
ctx.response
koa 的 Response 对象.
response.body
将响应体设置为以下之一:
string写入Buffer写入Stream管道Object||ArrayJSON-字符串化null无内容响应
如果下一个中间件是一个异步函数,那next默认不会等到中间件的结果,就会直接执行下一步操作。
期望:等到中间件产生结果,再执行下一步操作。
解决办法:所有中间件加async,await next()
express与koa同步执行中间件顺序相同,异步不同
express中 中间件加async,await next()毫无意义,因为next返回值为void,而不是promise