node概念
- 给js提供了一个运行环境,而不是一门语言
- js两大环境:
- 浏览器环境:BOM、DOM、window
- node环境:提供一些模块、global
概念性知识
- node是单线程,只有一个主线程,但有异步事件循环机制
- 虽然有事件循环,但多个异步任务还是按照优先级顺序进行执行的,并不是立即执行
- 优点:占用内存少
多线程概念
- 当多个请求过来时,可以同时处理,适合压缩、计算等
- 如果多个线程同时处理一个资源会出现不可控情况,所以会有锁的概念
阻塞概念
- 同步阻塞:a请求b,在b那里需要1分钟来获取数据,所以告诉a先等下,那么a在等待过程中会造成1分钟的阻塞
- node异步非阻塞:a请求b,然后停止,b获取数据后,通过回调的方式通知a,在这个过程中a可以做其他的事
可以做什么
- 前端学习成本小
- 文件的读写操作,浏览器因为安全问题,在每次读写用户模块会有用户进行操作(上传弹窗),node可以通过一些api操作自己的文件
- 做工具:webpack、gulp、rollup、vite
- 服务端渲染(ssr),解析js,生成字符串,然后返回
- 中间层,解决跨域问题,对后端数据进行代理、数据聚合等(多个数据合并)
- 向后端发展的过渡
提示
- vscode没给node加声明,所以没有提示
- 我们可以npm i @types/node自己下载下
调试
- 点击vscode的左侧小爬虫(Run an Debug)
- 点击 (创建launch.json文件),选择Node,根目录下会出现(.vscode>launch.json)
- program为入口文件地址
- 给左侧加上断点,点击小臭虫上面的绿色三角icon,开始调试(这时候可以返回目录,不影响)
模块
- commonjs规范,使用modules以及require
- 内置一些核心包,不用安装,可以省略(绝对||相对)路径
- 模块化的好处:解决命名冲突、方便复用(组件化基于模块化,所谓的高内聚低耦合)
- node_modules内,存放第三方包,会逐级向上查找
commonjs的实现
流程
- 传入路径
- 处理路径为绝对路径、且补全后缀
- 拿到文件内容(字符串,还没有执行)
- 根据路径判断在缓存映射(一个object)中查找,存在直接返回,否则初始化实例,然后放到缓存映射中
- 执行实例方法,将模块所需参数传递进去,然后给返回实例(module)的exports,默认为{}
const fs = require("fs");
const vm = require("vm");
const path = require("path");
function Module(id) {
this.id = id;
this.exports = {};
}
Module._cache = {};
Module._resolveFilename = function (filename) {
let absPath = path.resolve(__dirname, filename);
let isExists = fs.existsSync(absPath);
if (isExists) {
return absPath;
} else {
let keys = Object.keys(Module._extensions);
for (let i = 0; i < keys.length; i++) {
let newPath = absPath + keys[i];
let flag = fs.existsSync(newPath);
if (flag) {
return newPath;
}
}
throw new Error("module not exists");
}
};
Module._extensions = {
".js"(module) {
let content = fs.readFileSync(module.id, "utf8");
fn = vm.compileFunction(content, [
"exports",
"require",
"module",
"__filename",
"__dirname",
]);
let exports = (thisvalue = module.exports);
let filename = module.id;
let dirname = path.dirname(module.id);
Reflect.apply(fn, thisvalue, [exports, req, module, filename, dirname]);
},
".json"(module) {
let content = fs.readFileSync(module.id, "utf8");
module.exports = JSON.parse(content);
},
};
Module.prototype.load = function () {
let extName = path.extname(this.id);
Module._extensions[extName](this);
};
function req(id) {
let filename = Module._resolveFilename(id);
let cacheModule = Module._cache[filename];
if (cacheModule) {
return cacheModule.exports;
}
let module = new Module(filename);
Module._cache[filename] = module;
module.load();
return module.exports;
}
console.log(req("./b"));
console.log(req("./b"));
为什么module.exports可以赋值基本类型,而exports不可以
- 因为是通过module.exports来赋值的,所以它改变实例的值
- exports只是一个引用地址,所以把它改变为基本类型的话,外界拿不到,因为只是私有改变,外界没变
内置核心模块
- fs 文件的读写操作
- path 路径的处理
- vm 运行代码
path
- node中路径不唯一,所以需要使用path处理
- 属性:
__dirname 代表当前文件所在目录,只读,绝对路径
api
- join(路径, 路径) 按照操作系统将路径合并,可以有n个路径,类似数组的join
- resolve(路径, 路径) 根据执行脚本的路径,拼接路径,如果使用了/那么就回到根路径
- extname(路径Or文件名) 获取文件后缀
- dirname(路径) 获取倒数第二级路径
const path = require('path');
path.dirname('a/b');
path.dirname('a/b/c');
path.extname('a/a');
path.extname('a.a');
path.extname('a/a.a');
fs
- api分两类,带Sync为同步,不带为异步
- 同步api
- 异步api
- 会将错误对象传递给回调
- node中的回调大多采用错误优先
api
- readFileSync('路径','文件格式') 一次性读取文件,格式可省略,默认为buffer,同步
- readFile('路径','文件格式',回调) 一次性读取文件
- existsSync(路径) 判断路径对应文件是否存在,返回布尔
- writeFile(路径,内容,回调) 覆盖式写文件,文件不存在会自动创建
- open('路径',权限符,回调) 打开文件
- 常用权限符:
- 回调(err,fd)
- fd为标识符,每个操作系统不一样,但都是number,作用是记录下本次操作
- mac以20开头,每操作一次会+1
- read(标识符,赋值给谁,从第几个赋值,赋值几个,从第几个开始读,function(err,实际读了几个){}) 读取操作
- 前置条件,文件已打开open
- 赋值个数不能超过赋值容器的总数,会抛出异常
注意:如果赋值个数为0,function的第二个参数一定为0,因为没有地方写了
- write(标识位,内容(buffer),从内容的第几个开始读,读几个,写到文件的哪个位置,function(err,写入了几个){}) 写入部分文件
const buf = Buffer.alloc(3);
fs.open(path.resolve(__dirname, "1.txt"), "r", function (err, rfd) {
fs.open(path.resolve(__dirname, "2.txt"), "w", function (err, wfd) {
let roffset = 0;
let woffset = 0;
function close() {
fs.close(rfd);
fs.close(wfd);
}
function next() {
fs.read(rfd, buf, 0, 3, roffset, function (err, bytesRead) {
if (bytesRead === 0) return close();
fs.write(wfd, buf, 0, bytesRead, woffset, function (err, written) {
roffset += bytesRead;
woffset += written;
next();
});
});
}
next();
});
});
process
- 进程信息
- 是模块,可以导入
- 挂载在global上,所以也可以直接访问
属性
- platform
- 返回平台信息,window是win32、mac是darwin、linux是linux
- env
- 环境变量,object
- 可以直接通过process.env设置,也可以在命令行设置,mac为export 变量名=值
- argv
- 用户运行时,传递的参数,以空格划分,返回array
- 第一项默认为node地址,copy后放命令行等同于node执行
方法
- cwd()
- 返回node运行地址,跟path.resolve()返回一致
- chdir(路径)
- nextTick(callback)
- stadin
- on('data',(chunk)=>{}),可以监听用户在命令行测操作,并且给到回调
- write(内容),进程的输出,会在命令行显示
const path = require("path");
console.log(process.cwd());
console.log(path.resolve());
process.chdir("../");
console.log(process.cwd());
console.log(path.resolve());
console.log(process.platform);
console.log(process.env.a);
console.log(process.argv);
process.stdin.write('先给你个内容');
process.stdin.on("data", function (chunk) {
process.stdin.write(chunk.toString());
});
process.stdin.pipe(process.stdout);
events
on
- 事件绑定(注册、订阅)
- 有一个需要特殊记忆,当注册的事件为newListener的时候,后续每次调用on注册事件,都会触发newListener的回调,且将event传递给它
- newListener触发时机在注册事件前,如果想让它在注册后,可使用异步任务派发
emit
off
once
例子
const EventEmitter = require("events");
function Girl() {}
Object.setPrototypeOf(Girl.prototype, EventEmitter.prototype);
const girl = new Girl();
const weep = function (data) {
console.log("哭", data);
};
const eat = function (data) {
console.log("吃饭", data);
};
const sleep = function (data) {
console.log("睡觉", data);
};
girl.on("newListener", function (type) {
console.log("绑定", type);
girl.emit("放假", "newListener");
});
girl.on("放假", eat);
girl.on("放假", sleep);
girl.once("放假", weep);
girl.emit("放假", "第二次");
实现
function EventEmitter() {
this._events = {};
}
EventEmitter.prototype.on = function (event, listener) {
if (!this._events) {
this._events = {};
}
let listeners = this._events[event];
if (!listeners) {
this._events[event] = listeners = [];
}
if (event !== "newListener") {
this.emit("newListener", event);
}
listeners.push(listener);
};
EventEmitter.prototype.emit = function (event, ...args) {
if (!this._events) {
this._events = {};
}
let listeners = this._events[event];
listeners &&
listeners.forEach((listener) => {
listener(...args);
});
};
EventEmitter.prototype.off = function (event, listener) {
if (!this._events) {
this._events = {};
}
let listeners = this._events[event];
this._events[event] = listeners.filter((l) => {
return l !== listener && listener.l !== l;
});
};
EventEmitter.prototype.once = function (event, listener) {
const wrapper = (...args) => {
listener(...args);
this.off(event, wrapper);
};
listener.l = wrapper;
this.on(event, wrapper);
};
module.exports = EventEmitter;
Buffer
- node中为了支持二进制数据搞了一个数据类型Buffer( 也可以称为:缓冲区、内存),可以与字符串进行相互的转换
- 文件的读写,读取的内容都是buffer类型,我们会设置格式将其转换,如utf-8
- buffer在node中的展现形式是16进制
- 中文编码utf8 (3个字节 = 一个汉字)
- 一个字节由8个bit组成,
11111111 每位都是1的话,最大为255
进制知识补充
特殊记忆
0b11 2进制,0b开头
0o11 8进制,0o开头
0x11 16进制,0x开头
进制计算
111 1*2 + 1*2^1 + 1*2^2
console.log(0b111);
111 1*8 + 1*8^1 + 1*8^2
console.log(0o111);
parseInt
parseInt('111', 2);
toString
- 以()包裹,将一个值转为指定进制
- 返回string
(7).toString(2);
encodeURIComponent
- 转码
- 中文的话,会转为16进制,一个汉字占3字节,以%分割
encodeURIComponent('崔')
'%E5%B4%94'
encodeURIComponent('崔崔')
'%E5%B4%94%E5%B4%94'
encodeURIComponent('1')
'1'
encodeURIComponent('f')
'f'
base64
- 编码
- 好处:替换链接,不用发请求,快些
- 缺点:会比原来的大3分之1,所以大文件就不合适了,比如有个3g的文件,转完就是4g
什么是base64
- base64由64个字符组成,索引以0开始,到63结束
- 按照索引从字符表中找到对应的内容
- 一个字节默认为8个bit组成,二进制下8个1最大为255,那么base64就是将前2位变成0,让其最大不超过64
方法
toString
- 一个参数:格式,可省略,默认utf8
- 将buffer转换为其他格式,如字符
alloc
- 一个参数:number
- 创建n个字节的buffer,固定的,不可扩容
from
- 一个参数:array、string
- array时:
- 根据传递的数组,创建对应的buffer
- 只识别number和字符串的number,(如果是string的字符,会先转number),最大为255
- string时:
- 特点:字符采用ascii码,一个字符一个字节,汉字采用utf8,一个汉字三个字节
- 作用:创建一个固定的buffer
- 识别不了的就为00
let buf1 = Buffer.from("崔崔崔");
console.log(buf1);
let buf2 = Buffer.from([1, "1", "255", "256", "崔", 123]);
console.log(buf2);
let buf3 = Buffer.from('1');
console.log(buf3);
let buf4 = Buffer.from(1);
console.log(buf4);
let buf5 = Buffer.from();
console.log(buf5);
copy
- 实例.copy(拷贝到哪里去,从第几位开始放,从第几位开始区,取几位)
- 后两个参数可以省略,不写的话默认为起始到结束
- 不太常用,因为还需要自己计算长度,麻烦,通常使用concat
let buf1 = Buffer.from("小");
let buf2 = Buffer.from("崔");
let buf3 = Buffer.alloc(6);
buf1.copy(buf3, 0, 0, 3);
buf2.copy(buf3, 3, 0, 3);
console.log(buf3, buf3.toString());
concat
- 2个参数:(array,number?)
- 按照先后顺序,拼接,然后返回一个新的buffer
- 第二个参数是截取几个字节,如果不写的话,默认不截取,返回全部的
- 内部是copy,但用起来更方便
let buf3 = Buffer.concat([buf1,buf2]);
let buf4 = Buffer.concat([buf1,buf2], 2);
slice
- 截取,等同于数组的slice
- 不改变原buffer
let buf3 = Buffer.concat([buf1, buf2]);
let buf4 = buf3.slice();
let buf5 = buf3.slice(0, 2);
console.log(buf3, buf4, buf5)
属性
流 stream
- 例1:有一个3G的文件,直接使用readFile读,会占用很大内存,这个时候可以使用流来进行少量多次的读取操作
- 例2:状态码206代表分片传输,需要截取部分内容,整个读取文件然后截取显然不合适
- 特点:少量多次,可控,可暂停和恢复,采用发布订阅的方式
- 分类:读流、写流、双工流、转化流
可读流
一个可读流一定包含data与end
- 可以链式调用,使用on注册
- data 将每次的buffer给到callback参数
- end 读取完毕
- error 异常
- open 打开文件,属于文件独有的
- close 关闭文件,属于文件独有的
createReadStream,ReadStream
- 创建一个可读流,返回一个实例
- 参数:(地址,options?)
- createReadStream内部依赖ReadStream
- 实例.pause() 暂停
- 实例.resume() 继续
const fs = require("fs");
const path = require("path");
const rs = fs.createReadStream(path.resolve(__dirname, "1.txt"), {
flags: "r",
highWaterMark: 3,
start: 0,
end: 5,
});
let arr = [];
rs.on("open", function (fd) {
console.log("打开", fd);
});
rs.on("close", function () {
console.log("关闭");
});
rs.on("data", function (chunk) {
arr.push(chunk);
console.log(chunk);
});
rs.on("error", function (err) {
console.log("异常");
});
rs.on("end", function () {
console.log("结束", Buffer.concat(arr).toString());
});
实现ReadStream
const EventEmitter = require("events");
const fs = require("fs");
class ReadStream extends EventEmitter {
constructor(path, options = {}) {
super();
this.path = path;
this.flags = options.flags || "r";
this.highWaterMark = options.highWaterMark || 64 * 1024;
this.start = options.start || 0;
this.end = options.end;
this.offset = this.start;
this.flowing = false;
this.open();
this.on("newListener", function (type) {
if (type === "data" && !this.flowing) {
this.flowing = true;
this.read();
}
});
}
destory(err) {
if (err) {
this.emit("error", err);
}
if (typeof this.fd === "number") {
fs.close(this.fd, () => this.emit("close"));
}
}
pause() {
this.flowing = false;
}
resume() {
if (!this.flowing) {
this.flowing = true;
this.read();
}
}
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) this.destory(err);
this.fd = fd;
this.emit("open", fd);
});
}
read() {
if (typeof this.fd !== "number") {
this.once("open", () => {
this.read();
});
return;
}
const howMuchToRead = this.end
? Math.min(this.end - this.offset + 1, this.highWaterMark)
: this.highWaterMark;
const buf = Buffer.alloc(howMuchToRead);
fs.read(this.fd, buf, 0, buf.length, this.offset, (err, bytesRead) => {
this.offset += bytesRead;
if (bytesRead) {
this.emit("data", buf.slice(0, bytesRead));
if (this.flowing) this.read();
} else {
this.emit("end");
this.destory();
}
});
}
}
module.exports = ReadStream;
基于stream实现一个可读流
- 声明一个ReadStream类,它继承了stream包的Readable类
- 当每次调用read的时候,会先找Readable的read方法,它内部回去找ReadStream上的_read方法
- _read执行的时候需要给实例push一个buffer类型数据,当push为null代表结束
const { Readable } = require("stream");
class ReadStream extends Readable {
constructor() {
super();
this.i = 0;
}
_read() {
if (this.i === 10) {
this.push(null);
} else {
this.push(this.i++ + "");
}
}
}
let mrs = new ReadStream();
mrs.on("data", function (data) {
console.log(data);
});
mrs.on("end", function () {
console.log("结束");
});
可写流
一个可写流一定包含write与end方法
- write是写,会将第一个参数异步写入,因为是异步,所以有回调,但回调可不传
- write(内容,encoding?,function?)
- 多个write按照先后顺序执行
- write会返回布尔值
- 判断依据:它会在存之前将这几次操作占用的内存与highWaterMark进行比对,比highWaterMark大或相等的话,返回false, 否则就是true,返回值起到一个警示作用,但不会阻碍写入
- write的返回值是同步的,但实际执行是异步的
- 它不是马上去写,而是先存到缓存区,然后批量执行,但第一次会先去写,然后调用clearBuffer来清除缓存区
- 例子:假设highWaterMark为2,第一次存1,占用1字节,小于,那么返回true,第二次再存个2,占用1字节,上次也占了一字节,那就是2字节,大于等于了,这时候就返回false
- end相当于write+end,也是异步操作,会在最后一次写入后,执行关闭
- end(内容?,回调?)
- 将内容写入后,执行回调,close事件会子啊end后执行
end后不可再执行write
createWriteStream,WriteStream
- fs提供的可读流api
- 它的option.highWaterMark与可读流的不同,在这里只起到警示作用,与write的返回值相关,默认16k
- drain事件,会在为write的返回值为false后执行,执行顺序在write的回调前面,如果不满足预期,不会执行(不同node版本之间,执行时机也不同)
- createWriteStream内部就是new了一个WriteStream
const fs = require("fs");
const path = require("path");
const ws = fs.createWriteStream(path.resolve(__dirname, "1.txt"), {
flags: "w",
highWaterMark: 1,
});
ws.on("open", function (fd) {
console.log("打开文件", fd);
});
let f = ws.write("1", function (err, data) {
console.log("1写完了");
});
console.log(f);
setTimeout(() => {
ws.write("22", function (err, data) {
console.log("2写完了");
});
}, 1000);
ws.on("drain", function () {
console.log("draindraindraindrain");
});
setTimeout(() => {
ws.end("结束", function (err, data) {
console.log("最后写个结束,然后关闭文件了");
});
}, 2000);
ws.on("close", function () {
console.log("关闭文件");
});
转化流
- 我们读文件后,有时会想在写之前进行操作,比如压缩、加密等,可以统称为转化
- 特点:输入和输出有关联
pipe
- 集成读写流,可实现分段读写,异步执行
- 适用于大文件读写
- 可读可写
const fs = require("fs");
const path = require("path");
const rs = fs.createReadStream(path.resolve(__dirname, "1.txt"), {
highWaterMark: 4,
});
const ws = fs.createWriteStream(path.resolve(__dirname, "2.txt"), {
highWaterMark: 4,
});
rs.pipe(ws);
const crypto = require("crypto");
const { Transform } = require("stream");
class MyTransform extends Transform {
_transform(chunk, encoding, clearBuffer) {
let r = crypto.createHash("md5").update(chunk).digest("base64");
this.push(r);
clearBuffer();
}
}
const myTransform = new MyTransform();
process.stdin.pipe(myTransform).pipe(process.stdout);
双工流
const { Duplex } = require("stream");
class MyDuplex extends Duplex {
_read() {
console.log("_read");
}
_write() {
console.log("_write");
}
}
let myDuplex = new MyDuplex();
myDuplex.on("data", (chunk) => {
console.log('读');
});
myDuplex.write('ok');