# e-nodejs **Repository Path**: ymcdhr/e-nodejs ## Basic Information - **Project Name**: e-nodejs - **Description**: nodejs基础:文件流的读写、buffer、commonJs规范、nodejs事件循环、 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-08-03 - **Last Updated**: 2021-09-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## Nodejs常用功能 #### Nodejs 架构 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0803/211146_9bcbe882_9130428.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2021/0803/212721_5de5a19d_9130428.png "屏幕截图.png") #### Nodejs 的优势和特点 1. 传统的服务器处理流程 - 多进程、多线程 - 需要考虑阻塞、进程通信 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0803/211515_92e266b0_9130428.png "屏幕截图.png") 2. 基于Nodejs的处理 - Reactor模式:单线程、异步IO、事件驱动 - 适应于IO密集型高并发请求,不适合CPU密集型任务 3. Nodejs IO特点: - IO分为:阻塞IO、非阻塞IO - IO属于操作系统级别,平台都有对应实现 - IO是应用程序的瓶颈:如果使用非阻塞IO,CPU可以执行其它任务但是会启动轮询来查询结构(重复调用IO判断是否结束),这种方式不够完美。 - Nodejs单线程配合事件驱动架构及libuv库实现了异步IO,异步IO结合事件的回调机制可以提高性能,它无需等待结果返回; 4. 事件驱动架构 - 发布订阅 - 观察者 - 事件驱动:类似于发布订阅模式;[相关代码说明](https://gitee.com/ymcdhr/e-code/blob/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/01Base/01-event-driven.js) ![输入图片说明](https://images.gitee.com/uploads/images/2021/0803/212805_aa8bbb43_9130428.png "屏幕截图.png") 5. 单线程 - Nodejs主线程是单线程;Nodejs单线程完成了多线程的工作,但是不适合CPU密集型任务; - libuv库中存在线程池;将异步任务分成3种:网络IO、非网络IO、非IO的异步任务;对于网络IO使用IO接口进行操作,其它两个使用线程池进行任务;???3种4个线程??? ```js const http = require('http') function sleepTime (time) { const sleep = Date.now() + time * 1000 while(Date.now() < sleep) {} return } sleepTime(4) const server = http.createServer((req, res) => { res.end('server starting......') }) server.listen(8080, () => { console.log('服务启动了') }) ``` 6. 应用场景: - IO 密集型高并发 => BFF架构 => Node作为中间件 - 普通基础的服务端 => 操作数据库提供API服务 - 实时聊天系统 - 前端工程化:vite等 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0803/220310_3085a584_9130428.png "屏幕截图.png") ## Nodejs 实现 API 服务示例 #### 在 Nodejs 中使用 TypeScript 1. TypeScript 编译 nodejs 工具:ts-node ``` > npm i ts-node -D ``` 2. 引入第三方库时需要安装对应的类型库,例如express ``` > npm i @types/express -D ``` 3. 启动测试 ``` > ts-node .\api_server.js ``` #### Web服务,[示例代码](https://gitee.com/ymcdhr/e-code/blob/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/node_ts_api/api_server.ts) ``` // 需求:希望有一个服务,可以依据请求的接口内容返回相应的数据 import express from 'express' import { DataStore } from './data' const app = express() app.get('/', (req, res) => { res.json(DataStore.list) }) app.listen(8080, () => { console.log('服务已经开启了') }) ``` ## Nodejs 基本知识点 #### 全局对象global 1. global和浏览器Window不完全相同 2. global上挂载许多属性 3. __filename:返回正在执行脚本文件的绝对路径 4. __dirname:返回正在执行脚本文件的目录 5. timer类函数:执行顺序与事件循环间的关系,setTimeout、setInterval、setImmediate、nextTick 6. process:提供当前进程互动的接口 7. require:模块加载 8. module、exports:导出模块 9. 默认情况下全局this是{},和global并不一样,自执行函数里面的this和global相同 - js 文件中的 this 为 初始化的 exports:{} - (function() {})() 为啥是global? ```js console.log("this:", this) // {} console.log(this == global) // false // 避免和前面的冲突,加上分号 // 为什么? ;(function () { console.log("this:", this) // global console.log(this == global) // true })() ``` ```js // 输出结果: this: {} this: Object [global] { global: [Circular *1], clearInterval: [Function: clearInterval], clearTimeout: [Function: clearTimeout], setInterval: [Function: setInterval], setTimeout: [Function: setTimeout] { [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)] }, queueMicrotask: [Function: queueMicrotask], clearImmediate: [Function: clearImmediate], setImmediate: [Function: setImmediate] { [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)] } } ``` - 编译模块的时候,先是给模块包上了一个函数;注意:content的内容就是fs读取模块内容的字符串。 ``` "(function (exports, require, module, __filename, __dirname) { // content // ... module.exports = xxx })" ``` - 然后调用该函数的时候使用call将exports传递给了模块里面的this;所以this指向{},exports初始化时是{}; ``` // 返回一个方法 let compileFn = vm.runInThisContext(content) // 执行方法,返回module.exports的具体内容 compileFn.call(exports, exports, myRequire, module, filename, dirname) ``` - 对于模块中自执行函数里的this:自执行函数没有调用者,所以this指向全局也就是global; #### process 程序资源操作 1. 获取进程信息 - 获取cpu、内存信息:
![输入图片说明](https://images.gitee.com/uploads/images/2021/0803/225057_244d2066_9130428.png "屏幕截图.png") ``` // 1 资源: cpu 内存 console.log(process.memoryUsage()) console.log(process.cpuUsage()) { rss:常驻内存 heapTotal:申请总内存 heapUsed:实际使用的内存大小 external:扩展内存,C++底层模块使用的内存大小 arrayBuffers:一片独立的空间,不占V8内存 } { user:用户占用cpu system:操作系统占用cpu } ``` - 获取运行环境信息 ``` // 2 运行环境:运行目录、node环境、cpu架构、用户环境、系统平台 console.log(process.cwd()) // 当前的工作目录,命令执行的目录 console.log(process.version) // 当前的版本 // console.log(process.versions) console.log(process.arch) // CPU 架构:x86、x64、amd等 console.log(process.env.NODE_ENV) // 环境参数:production、development // console.log(process.env.PATH) // 系统环境变量 console.log(process.env.USERPROFILE) // HOME 用户管理员目录 console.log(process.platform) // 当前平台 ``` - 获取运行状态信息 ``` // 3 运行状态: 启动参数、PID、运行时间 console.log(process.argv) // 运行参数 console.log(process.argv0) // 第一个参数,execArgv获取--后面的参数 console.log(process.argv[0]) // 第一个参数 console.log(process.pid) // 获取pid setTimeout(() => { console.log(process.uptime()) // 当前js脚本整体运行时间 }, 3000) ``` 2. 执行进程事件监听 - process.on事件是异步的: ``` // 4 事件 // 退出的时候执行 process.on('exit', (code) => { console.log('exit ' + code) }) // 退出前执行 process.on('beforeExit', (code) => { console.log('before exit' + code) }) console.log('end') // 执行结果 end before exit 0 exit 0 ``` - process.on('exit',callback)回调函数里面不支持异步执行代码,例如setTimeout - process.exit()手动执行退出,此时beforeExit事件不会触发(nodejs机制)! - process 标准输入、输出(可以控制命令行的输入输出) ``` // 5 标准输入、输出、错误;可以 console.log = function (data){ process.stdout.write('---' + data + '\n') } console.log(11) console.log(22) // 读文件标准输出 + 管道流输出 const fs = require('fs') fs.createReadStream('test.txt').pipe(process.stdout) // 命令行输入,然后打印 process.stdin.setEncoding('utf-8') // 设置编码防止乱码 process.stdin.on('readable', ()=>{ // on 可以监听哪些事件请查看官方文档 let chunk = process.stdin.read() // 命令行输入内容 if(chunk!==null){ process.stdout.write('data' + chunk) } }) ``` ### Nodejs 核心模块 #### Path 模块 1. 用于处理文件/目录的路径 2. 示例代码: ```js const path = require('path') console.log(__filename) // D:\Tony‘sDocument\documents\e-讲义\05-01-课程资料\Code\02Path\01-path.js // 1 获取路径中的基础名称 /** * 01 返回的就是接收路径当中的最后一部分 * 02 第二个参数表示扩展名,如果说没有设置则返回完整的文件名称带后缀 * 03 第二个参数做为后缀时,如果没有在当前路径中被匹配到,那么就会忽略 * 04 处理目录路径的时候如果说,结尾处有路径分割符,则也会被忽略掉 */ console.log(path.basename(__filename)) // 01-path.js console.log(path.basename(__filename, '.js')) // 01-path => 第二个参数表示扩展名,如果说没有设置则返回完整的文件名称带后缀 console.log(path.basename(__filename, '.css')) // 01-path.js => 第二个参数做为后缀时,如果没有在当前路径中被匹配到,那么就会忽略 console.log(path.basename('/a/b/c')) // c => 返回路径结尾字符串 console.log(path.basename('/a/b/c/')) // c => 自动忽略结尾的斜杠 // 2 获取路径目录名 (路径) /** * 01 返回路径中最后一个部分的上一层目录所在路径 */ console.log(path.dirname(__filename)) // D:\Tony‘sDocument\documents\e-讲义\05-01-课程资料\Code\02Path\ console.log(path.dirname('/a/b/c')) // /a/b console.log(path.dirname('/a/b/c/')) // /a/b // 3 获取路径的扩展名 /** * 01 返回 path路径中相应文件的后缀名 * 02 如果 path 路径当中存在多个点,它匹配的是最后一个点,到结尾的内容 */ console.log(path.extname(__filename)) // .js => 返回最后一个后缀名 console.log(path.extname('/a/b')) // => 返回空字符串 console.log(path.extname('/a/b/index.html.js.css')) // .css => 返回最后一个后缀名 console.log(path.extname('/a/b/index.html.js.')) // . => 返回. // 4 解析路径 /** * 01 接收一个路径,返回一个对象,包含不同的信息 * 02 root dir base ext name */ const obj = path.parse('/a/b/c/index.html') // 返回对象 // => { root: '/', dir: '/a/b/c', base: 'index.html', ext: '.html', name: 'index' } const obj = path.parse('/a/b/c/') // => { root: '/', dir: '/a/b', base: 'c', ext: '', name: 'c' } const obj = path.parse('./a/b/c/') // => { root: '', dir: './a/b', base: 'c', ext: '', name: 'c' } console.log(obj.name) // 5 序列化路径 const obj = path.parse('./a/b/c/') // 返回对象 console.log(path.format(obj)) // 重新组合该对象 // 6 判断当前路径是否为绝对 console.log(path.isAbsolute('foo')) // true 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 拼接路径 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('')) 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 绝对路径 console.log(path.resolve()) // D:\Tony‘sDocument\Code\02Path /** * path.resolve([from], to) from有默认值 * to里面如果是绝对路径,就直接忽略from里面的值 * to里面如果是相对路径,就需要链接from里面的值 */ console.log(path.resolve('a', '/b')) // \b console.log(path.resolve('/a', '/b')) // \b console.log(path.resolve('/a', 'b')) // \a\b console.log(path.resolve('a', 'b')) // D:\Tony‘sDocument\Code\02Path\a\b console.log(path.resolve('/a', '../b')) // \b console.log(path.resolve('index.html')) // D:\Tony‘sDocument\Code\02Path\index.html => 最常用的写法 ``` #### Buffer 模块 1. Buffer的特点: - 无需require的全局变量 - Buffer 不占用V8引擎中的空间,它是内存中的一块空间 - 内存的使用由Node控制,由V8的GC回收 - 一般配合Stream流使用,充当数据缓冲区(例如:二进制文件读取操作) - 数字占1个字节,中文占3个字节 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0804/121501_63cc3a85_9130428.png "屏幕截图.png") 2. Buffer 基本使用,[参考文档](https://gitee.com/ymcdhr/e-code/blob/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/03Buffer/01-buffer.js) ```js // 1、 分配空间:alloc、allocUnsafe const b1 = Buffer.alloc(10) // 创建指定字节大小的buffer const b2 = Buffer.allocUnsafe(10) // 创建指定子节大小的buffer(不安全),只要有空余空间就会使用 console.log(b1) // console.log(b2) // // 2、接收数据:from const b1 = Buffer.from('中') // 接收数据,创建buffer;转成了16进制; console.log(b1) // => 一个汉字占用3个子节 // 3、数组 // 3.1 数组中不能直接使用中文,需要先转成十六进制 const b1 = Buffer.from([0xe4, 0xb8, 0xad]) // const b1 = Buffer.from([0x60, 0b1001, 12]) console.log(b1) // console.log(b1.toString()) // 中 => 转换成中文 // const b1 = Buffer.from('中') // console.log(b1) // console.log(b1.toString()) const b1 = Buffer.alloc(3) const b2 = Buffer.from(b1) console.log(b1) // console.log(b2) // // 3.2 Buffer的操作类似于数组 b1[0] = 1 console.log(b1) // console.log(b2) // ``` 3. Buffer实例方法:[示例代码](https://gitee.com/ymcdhr/e-code/blob/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/03Buffer/02-buffer-api-1.js) - fill - write - toString - slice - indexOf - copy 4. Buffer静态方法:[示例代码](https://gitee.com/ymcdhr/e-code/blob/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/03Buffer/02-buffer-api-2.js) - concat - isBuffer 5. Buffer的拆分:[示例代码](https://gitee.com/ymcdhr/e-code/blob/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/03Buffer/04-buffer-split.js) #### FS 文件操作模块 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0804/135943_d2871e29_9130428.png "屏幕截图.png") 1. FS 基本操作类 2. FS 常用API 3. FS 文件API: 同步、异步;[示例代码](https://gitee.com/ymcdhr/e-code/blob/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/04FS/01-file-api-01.js) - readFile - writeFile - appendFile - copyFile - watchFile ```js // readFile fs.readFile(path.resolve('data1.txt'), 'utf-8', (err, data) => { console.log(err) if (!null) { console.log(data) } }) // writeFile // 参数3可省略,默认覆盖写操作 fs.writeFile('data.txt', '123', { mode: 438, // 权限类 flag: 'w+',// 可配置是否覆盖,清空;可选:r+、w+等 encoding: 'utf-8' }, (err) => { if (!err) { fs.readFile('data.txt', 'utf-8', (err, data) => { console.log(data) }) } }) // appendFile:实现追加操作 fs.appendFile('data.txt', 'hello node.js',{}, (err) => { console.log('写入成功') }) // copyFile:将data.txt拷贝到test.txt fs.copyFile('data.txt', 'test.txt', () => { console.log('拷贝成功') }) // watchFile:监控文件修改 fs.watchFile('data.txt', {interval: 20}, (curr, prev) => { if (curr.mtime !== prev.mtime) { console.log('文件被修改了') fs.unwatchFile('data.txt') } }) ``` 4. nodejs [将markdown文档转换成html文件示例](https://gitee.com/ymcdhr/e-code/blob/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/04FS/02-md-to-html.js) - 读取md文件 - 读取css文件 - 将md内容转换成html内容 - 将源文档中{{content}}替换成html内容 - 将源文档中{{style}}替换成css内容 - 写入html文件 - 执行命令:node .\02-md-to-html.js index.md 5. nodejs 实现边读边写文件的功能 - 文件打开与关闭 ```js const fs = require('fs') const path = require('path') // open fs.open(path.resolve('data.txt'), 'r', (err, fd) => { console.log(fd) }) // close fs.open('data.txt', 'r', (err, fd) => { console.log(fd) fs.close(fd, err => { console.log('关闭成功') }) }) ``` - 文件边读边写 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0804/160656_83b8e5aa_9130428.png "屏幕截图.png") ``` const fs = require('fs') // read : 所谓的读操作就是将数据从磁盘文件中写入到 buffer 中 let buf = Buffer.alloc(10) /** * fd 定位当前被打开的文件 * buf 用于表示当前缓冲区 * offset 表示当前从 buf 的哪个位置开始执行写入:起始索引位置 * length 表示当前次写入的长度 * position 表示当前从文件的哪个位置开始读取 */ fs.open('data.txt', 'r', (err, rfd) => { console.log(rfd) // 从rfd中读数据,写到Buffer中 // 从rfd中的3开始读取,从Buffer的1开始写长度为4字节的内容 fs.read(rfd, buf, 1, 4, 3, (err, readBytes, data) => { console.log(readBytes) console.log(data) console.log(data.toString()) }) }) // // write 将缓冲区里的内容写入到磁盘文件中 // buf = Buffer.from('1234567890') // fs.open('b.txt', 'w', (err, wfd) => { // fs.write(wfd, buf, 2, 4, 0, (err, written, buffer) => { // console.log(written, '----') // fs.close(wfd) // }) // }) ``` 6. 自定义实现文件拷贝(非流式方法),[示例代码](https://gitee.com/ymcdhr/e-code/blob/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/04FS/05-copy-file.js) - 01 打开 a 文件,利用 read 将数据保存到 buffer 暂存起来 - 02 打开 b 文件,利用 write 将 buffer 中数据写入到 b 文件中 ``` let buf = Buffer.alloc(10) // 实现文件的边读边写功能: // Buffer大小为10,那么就读取10个字节写10个字节 const BUFFER_SIZE = buf.length // Buffer大小 let readOffset = 0 // 偏移量,从哪里开始读 fs.open('a.txt', 'r', (err, rfd) => { fs.open('b.txt', 'w', (err, wfd) => { function next () { // 从rfd文件中读出来,存到buf中 fs.read(rfd, buf, 0, BUFFER_SIZE, readOffset, (err, readBytes) => { if (!readBytes) { // 如果条件成立,说明内容已经读取完毕 fs.close(rfd, ()=> {}) fs.close(wfd, ()=> {}) console.log('拷贝完成') return } readOffset += readBytes // 从buf中读出来,写到文件wfd中 fs.write(wfd, buf, 0, readBytes, (err, written) => { next() }) }) } next() }) }) ``` #### 目录操作模块 1. 常见API,[参考代码](https://gitee.com/ymcdhr/e-code/blob/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/04FS/06-fs-dir-api.js) - access 权限查询操作 - stat 目录信息:statObj.isDirectory()判断是否目录 - mkdir 创建目录 - rmdir 删除目录 - readdir 读取目录 - unlink 文件删除 2. 创建目录(mkdir 自定义实现)[参考代码](https://gitee.com/ymcdhr/e-code/blob/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/04FS) - 同步创建 - 异步创建 3. 删除目录 ### CommonJS 规范 #### CommonJS 规范(模块化规范 + 其它定义) 1. 模块引用:require,使用require导入其他模块 2. 模块定义:module.exports,任意一个文件就是一个模块,具有独立作用域 3. 模块标识:模块id,将模块ID传入require实现目标模块定位 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0804/194818_c2c10130_9130428.png "屏幕截图.png") #### CommonJS 规范属性说明 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0804/183003_5fe4d18e_9130428.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2021/0804/194430_76cf9667_9130428.png "屏幕截图.png") #### CommonJS 规范基本使用,[参考源码](https://gitee.com/ymcdhr/e-code/tree/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/05Module) 1. 任何 js 文件都是一个 module,在文件中直接输出 module,它里面存放了 id 等基本信息 ``` Module { id: 'D:\\Tony‘sDocument\\documents\\e-讲义\\05-01-课程资 料\\Code\\05Module\\m.js', path: 'D:\\Tony‘sDocument\\documents\\e-讲义\\05-01-课程 资料\\Code\\05Module', exports: 1111, parent: Module { id: '.', path: 'D:\\Tony‘sDocument\\documents\\e-讲义\\05-01-课程资料\\Code\\05Module', exports: {}, parent: null, filename: 'D:\\Tony‘sDocument\\documents\\e-讲义\\05-01-课程资料\\Code\\05Module\\01-nodejs-commonjs.js', loaded: false, children: [ [Circular *1] ], paths: [ 'D:\\Tony‘sDocument\\documents\\e-讲义\\05-01-课程资料\\Code\\05Module\\node_modules', 'D:\\Tony‘sDocument\\documents\\e-讲义\\05-01-课程资料\\Code\\node_modules', 'D:\\Tony‘sDocument\\documents\\e-讲义\\05-01-课程资料\\node_modules', 'D:\\Tony‘sDocument\\documents\\e-讲义\\node_modules', 'D:\\Tony‘sDocument\\documents\\node_modules', 'D:\\Tony‘sDocument\\node_modules', 'D:\\node_modules' ] }, filename: 'D:\\Tony‘sDocument\\documents\\e-讲义\\05-01-课程资料\\Code\\05Module\\m.js', loaded: false, children: [], paths: [ 'D:\\Tony‘sDocument\\documents\\e-讲义\\05-01-课程资料\\Code\\05Module\\node_modules', 'D:\\Tony‘sDocument\\documents\\e-讲义\\05-01-课程资料\\Code\\node_modules', 'D:\\Tony‘sDocument\\documents\\e-讲义\\05-01-课程资料\\node_modules', 'D:\\Tony‘sDocument\\documents\\e-讲义\\node_modules', 'D:\\Tony‘sDocument\\documents\\node_modules', 'D:\\Tony‘sDocument\\node_modules', 'D:\\node_modules' ] } ``` 2. exports 指向 module.exports,所以他们都能导出模块,但是 exports 不能直接赋值! 3. CommonJS 是同步加载模块,因为 Nodejs 使用的模块基本都是服务器磁盘上的模块。 #### CommonJS 规范相关问题: 1. module.exports 和 exports 的区别?? - exports 和 module.exports 一样都能导出模块; - exports 指向的就是 module.exports,所以不能对 exports 进行赋值,否则指向变更后就不能导出了;但是可以给exports 的属性赋值! ```js exports.name = 'zce' // OK exports = { // Not OK 不应该给exports直接赋值 name: 'syy', age: 18 } ``` ![输入图片说明](https://images.gitee.com/uploads/images/2021/0804/183102_7c89b5d8_9130428.png "屏幕截图.png") 2. CommonJS 和 ES Module 的区别?[对比文档](https://gitee.com/ymcdhr/e-task/wikis?sort_id=4087755) 3. 如何判断当前执行的代码是不是改代码所在的js文件: - require.main => 指向调用/加载该 js 文件的模块 - module => 指向 js 文件本身对应的模块 ``` 假如: // m.js let name = 'lg' module.exports = name console.log(require.main == module) // false // index.js let obj = require('./m') console.log('01.js代码执行了') ``` ### Nodejs 模块加载 #### Nodejs 模块分类 1. 核心模块:Node源码编译时写入到二进制文件中,加载速度快些 2. 文件模块:代码运行时,动态加载,加载速度相对更慢 #### Nodejs 模块加载流程;[可参考模拟实现源码深入理解](https://gitee.com/ymcdhr/e-code/blob/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/05Module/04-imitate-require.js) 1. **路径分析:** 依据标识符确定模块位置,在模块初始化时module里面生成了路径数组:module.path - 路径标识符,例如:require('./src/components/title') - 非路径标识符,例如:require('fs')、require('path');nodejs 会根据路径数组去查找,路径数组就是 module.paths(存的各种node_modules的路径) 2. **文件定位:** 确定目标模块中具体的文件及文件类型,因为有可能文件名没有后缀; - 可以省略文件扩展名,按照.js => .json => .node => 目录的顺序; - 如果是目录,就查找package.json文件,使用JSON.parse()解析; - 然后查找package.json中的main入口,还是按照.js => .json => .node的顺序查找。 - 如果还没找到,就将index作为目标模块中的具体文件名称。 - 还是没找到就报错。 3. **编译执行:** 采用对应的方式完成文件的编译执行 - 将某个具体类型的文件按照响应的方式进行编译和执行,每个类型的文件编译方式不同! ``` js文件: 1. 使用fs模块同步读入目标文件内容; 2. 对内容进行语法包装,生成可执行JS函数;函数里面就是我们写的模块代码,包括:module.exports=xxx。 3. 调用函数时传入exports、module、require等属性值; Json文件: 1. 将读取到的内容通过JSON.parse()进行解析 ``` - 创建新对象,按路径载入,完成编译执行; ``` // 可执行的 JS 函数包装的样子,大概是这样: "(function (exports, require, module, __filename, __dirname) { // ... module.exports = xxx })" ``` 4. **缓存优先原则:** - 依据标识符加载模块的时候首先查看是否存在缓存,提高模块加载速度 - 如果模块不存在,就经历一次完整的加载流程 - 模块加载完成后,使用路径作为索引进行缓存 #### Nodejs 模块加载流程源码分析(注意区别 webpack 打包后的模块加载流程) 1. 在require的地方打断点即可调试:const obj = require('./m') 2. return mod.require(path); => Module._load(id, this, /* isMain */ false); => #### Nodejs 模块加载流程相关问题 1. module 模块(js文件)中包含那几个可用的变量?他们是如何能用的? - exports、require、module、__filename、__dirname - 他们在编译的时候调用:module._compile,先将模块中的js函数以字符串的形式读出来,正式编译时将其放到函数自调用结构里面,传入了这5个变量作为参数: 2. 关于this的指向:默认情况下全局this是{},和global并不一样,自执行函数里面的this和global相同;为什么?参考:Nodejs基本知识点,第一节; ``` console.log(this == global) (function () { console.log(this == global) })() ``` 3. **Nodejs中的Module.exports导出的模块是一个浅拷贝,它是怎样做到的?** - Nodejs使用fs读取模块文件,读出来的代码是字符串; - 然后将这些代码用一个函数包裹起来: ``` "(function (exports, require, module, __filename, __dirname) { // ... module.exports = xxx })" ``` - 然后使用VM模块的方法vm.runInThisContext(content)执行该字符串(返回exports),并且将 module 传递进去; ``` // 返回一个方法 let compileFn = vm.runInThisContext(content) // 执行方法,返回module.exports的具体内容 compileFn.call(exports, exports, myRequire, module, filename, dirname) ``` - vm.runInThisContext(content)有两个特点: - (1)content 里面不能使用外面的局部变量,所以对content起到了封装的作用。 - (2)content 里面可以使用全局变量; - 因为将module作为参数传给了方法compileFn(),所以将模块导出的内容赋值到了module.exports上;最后在require函数返回了exports对象; - (1)其它js文件require()了模该块之后,相当于浅拷贝了模块文件中的module.exports返回的对象(其实归根结底就是个等号赋值的浅拷贝);如果module.exports是一个基本类型的值,那require()之后就相当于赋值。 - (2)模块文件中使用的变量不能是外部的,只能使用它本文件的变量;因为vm.runInThisContext(content)方法有隔离的作用。 - 测试代码:参考项目源码 #### VM 模块(字符串 => 编译成 Function) 1. Nodejs 在编译的时候,是怎样将字符串函数转换成的 function 的? - eval:没有办法隔离text.txt中的变量 - new Function:操作比较麻烦,需要拆分源文件内容 - VM:提供了api 2. VM 模块提供了api:runInThisContext - vm.runInThisContext(content) 里面没有办法使用外面的局部变量 - vm.runInThisContext(content) 里面可以使用外面的全局变量 ``` const fs = require('fs') const vm = require('vm') number = 10 let age = 33 let content = fs.readFileSync('test.txt', 'utf-8') // 将字符串转换成function的方法: // 1、 eval() ,没有办法隔离text.txt中的变量 // eval // eval(content) // 2、new Function,操作比较麻烦,需要拆分源文件内容 // 参数1:函数名 // 参数2:函数体 // console.log(age) // let fn = new Function('age', "return age + 1") // console.log(fn(age)) vm.runInThisContext(content) vm.runInThisContext("number += 10") // 20 => 可使用全局变量 vm.runInThisContext("age += 10") // 报错 => 不能使用局部变量 console.log(age) ``` #### Nodejs 模块加载[模拟实现](https://gitee.com/ymcdhr/e-code/blob/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/05Module/04-imitate-require.js) 1. 路径分析 2. 缓存优先 3. 文件定位 4. 编译执行 #### 事件模块,[模拟实现](https://gitee.com/ymcdhr/e-code/tree/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/06Events) 1. 内置events模块,提供了EventEmitter类 2. 很多核心模块都继承了EventEmitter类 3. on:添加事件 4. emit:触发事件 5. once:添加事件(首次触发) 6. off:删除时间 ```js const EventEmitter = require('events') const ev = new EventEmitter() // on 事件监听 ev.on('事件1', () => { console.log('事件1执行了---2') }) ev.on('事件1', () => { console.log('事件1执行了') }) // emit 触发事件 ev.emit('事件1') ev.emit('事件1') // once 首次监听 ev.once('事件1', () => { console.log('事件1执行了') }) ev.once('事件1', () => { console.log('事件1执行了--2') }) ev.emit('事件1') ev.emit('事件1') // off 删除事件 let cbFn = (...args) => { console.log(args) } ev.on('事件1', cbFn) // ev.emit('事件1') // ev.off('事件1', cbFn) // 给事件传参 ev.emit('事件1', 1, 2, 3) ev.on('事件1', function () { console.log(this) // EventEmitter 对象 }) ev.on('事件1', function () { console.log(2222) }) ev.on('事件2', function () { console.log(333) }) ev.emit('事件1') // 内置的核心模块继承了EventEmitter,例如: const fs = require('fs') const crt = fs.createReadStream() crt.on('data') ``` #### 事件循环 1. 浏览器的事件循环,[相关文档](https://gitee.com/ymcdhr/e-task/tree/master/part1/fed-e-task-01-01/code#%E4%B8%89%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E5%92%8C%E5%AE%8F%E4%BB%BB%E5%8A%A1%E5%BE%AE%E4%BB%BB%E5%8A%A1) 2. Nodejs的事件循环,不止有宏任务与微任务,有6个类型的宏任务回调 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0805/143411_7b7862e2_9130428.png "屏幕截图.png") - **timers** :setTimeout、setInterval - pending callbacks:系统操作的回调,例如:tcp、udp(不需要关心) - idle、prepare:只在系统内部进行使用(不需要关心) - **poll** :执行与IO相关的回调 - **check** :执行setImmediate中的回调 - close callbacks:执行close事件的回调(不需要关心) 3. Nodejs的任务分类(需要关心的3类,按顺序执行) - 宏任务:timer(setTimeout、setInterval) => poll(readFile) => check(setImmediate) - 微任务:process.nextTick() => 优先级高于 promise 4. Nodejs的完整事件循环 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0805/143851_df9121be_9130428.png "屏幕截图.png") 5. 示例代码:
![输入图片说明](https://images.gitee.com/uploads/images/2021/0805/144602_c3f5caf0_9130428.png "屏幕截图.png") ```js // 宏任务 setTimeout(() => { console.log('s1') }) // 微任务 Promise.resolve().then(() => { console.log('p1') }) console.log('start') // 微任务 process.nextTick(() => { console.log('tick') }) // 宏任务 setImmediate(() => { console.log('setimmediate') }) console.log('end') // start, end, tick, p1, s1, st ``` 6. **Nodejs 与浏览器事件循环的区别(重点,注意)** - 1. 任务队列数不同:浏览器只有宏任务和微任务,Nodejs除了微任务还有6个队列; - 2. 微任务执行时机不同:虽然微任务的执行优先级更高,但是: - (1)浏览器平台在执行完一个宏任务后会 **立即执行微任务**! - (2)Nodejs会在执行完当前宏任务队列之后才会切换到微任务!(**注意:最新版本的 Nodejs 和浏览器保持一致**) - 3. 任务优先级:浏览器微任务只需要先进先出,Nodejs中process.nextTick优先级高于promise;宏任务中setTimeout和setImmediate的优先级不稳定; ```js setTimeout(() => { console.log('s1') Promise.resolve().then(() => { console.log('p1') }) process.nextTick(() => { console.log('t1') }) }) Promise.resolve().then(() => { console.log('p2') }) console.log('start') setTimeout(() => { console.log('s2') Promise.resolve().then(() => { console.log('p3') }) process.nextTick(() => { console.log('t2') }) }) console.log('end') // 以前的Nodejs: // start, end, p2, s1, s2 , t1, t2, p1, p3 // 新版的Nodejs: // start, end, p2, s1, t1, p1, s2, t2, p3 ``` 7. Nodejs 事件循环常见问题 - setTimeout 和 setImmediate 的优先级不稳定? - setTimeout 和 setImmediate 如果放到某一个事件队列中,就会按照固定顺序来执行 ``` // 问题1:setTimeout 和 setImmediate 的执行是随机的; setTimeout(() => { console.log('timeout') }) setImmediate(() => { console.log('immdieate') }) // setTimeout需要设置时间,如果不写就默认是0 // setImmediate不需要设置时间 setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immdieate') }) // 问题2:setTimeout 和 setImmediate 如果放到某一个事件队列中,就会按照固定顺序来执行 // 首先:队列的执行顺序是:timer => poll => check // 其次:readFile 的回调函数会添加到 poll 的队列,setTimeout => timer,setImmediate => check // 所以:poll 的任务结束后会按照顺序执行 check,然后再重新事件循环到 timer const fs = require('fs') fs.readFile('./m1.js', () => { setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immdieate') }) }) ``` ### Stream 流处理模块([参考文档](https://www.jianshu.com/p/ca1903332fc7)) #### 1. **为什么要使用流?** 如果资源文件太大,一次性读取占用内存太大,占用网络时间太长; - 时间效率:流的分段可以同时操作多个数据chunk。 - 空间效率:同一时间流无需占据大内存空间; - 使用方便:流配合管道,扩展程序功能更简单; ![输入图片说明](https://images.gitee.com/uploads/images/2021/0805/155612_38f6886e_9130428.png "屏幕截图.png") #### 2. Nodejs 流的分类: - Readable:可读流 - writeable:可写流 - duplex:双工流,例如:sockect - transform:转换流 #### 3. Stream 的使用: - Strem 对上诉4个类的流都有封装 - 所有的流都继承了 EventEmitter 类 ```js const fs = require('fs') let rs = fs.createReadStream('./test.txt') let ws = fs.createWriteStream('./test1.txt') rs.pipe(ws) ``` #### 4. Stream 可读流自定义实现 - 可读流的两种模式:1、流动模式、2、暂停模式;(二者可以切换) - 可读流的工作原理?读取底层数据 => 存储到缓冲区 => 触发readable/data事件等待消费 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0805/163207_0719e43c_9130428.png "屏幕截图.png") - 可读流两个主要事件(readable需要调用read()方法读取数据) ![输入图片说明](https://images.gitee.com/uploads/images/2021/0805/164016_fbf021e3_9130428.png "屏幕截图.png") ```js // 自定义可读流 const {Readable} = require('stream') // 模拟底层数据,直接定义数组 let source = ['lg', 'zce', 'syy'] // 自定义类继承 Readable class MyReadable extends Readable{ constructor(source) { super() this.source = source } // 读取底层数据,null标记读取结束 _read() { let data = this.source.shift() || null this.push(data) } } // 实例化 let myReadable = new MyReadable(source) // 暂停模式:监听 readable 事件,等待消费数据 // read() 方法应该是 Readable 类里面定义的方法 myReadable.on('readable', () => { let data = null while((data = myReadable.read(2)) != null) { console.log(data.toString()) } }) // 流动模式:监听 data 事件,等待消费数据 myReadable.on('data', (chunk) => { console.log(chunk.toString()) }) ``` #### 5. Stream 可写流自定义实现 - 可写流两个主要事件 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0805/165205_fd21aa7f_9130428.png "屏幕截图.png") ``` // 自定义可写流 const {Writable} = require('stream') class MyWriteable extends Writable{ constructor() { super() } // 覆盖原始方法 _write(chunk, en, done) { process.stdout.write(chunk.toString() + '<----') process.nextTick(done) } } let myWriteable = new MyWriteable() myWriteable.write('拉勾教育', 'utf-8', () => { console.log('end') }) ``` #### 6. Stream 双工流自定义实现 - duplex - transform ```js let {Duplex} = require('stream') class MyDuplex extends Duplex{ constructor(source) { super() this.source = source } _read() { let data = this.source.shift() || null this.push(data) } _write(chunk, encoding, next) { process.stdout.write(chunk) process.nextTick(next) } } let source = ['a', 'b', 'c'] let myDuplex = new MyDuplex(source) // myDuplex.on('data', (chunk) => { // console.log(chunk.toString()) // }) myDuplex.write('拉勾教育', () => { console.log(1111) }) ``` ```js let {Transform} = require('stream') class MyTransform extends Transform{ constructor() { super() } _transform(chunk, encoding, callback) { this.push(chunk.toString().toUpperCase()) callback(null) } } let t = new MyTransform() t.write('a') t.on('data', (chunk) => { console.log(chunk.toString()) }) ``` #### 7. 文件可读流的使用;[模拟实现文件可读流](https://gitee.com/ymcdhr/e-code/tree/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/08FileStream): - fs.createReadStream() 读取文件数据,data、readable 事件消费数据; - 事件触发顺序:open => data => end => close - 如果需要完整的使用数据,可以在 end 里面进行处理; - 文件可读流有一个缓存区域,默认大小 64kb; ```js const fs = require('fs') // 文件可读流 // test.txt 中为:0123456789,每次读4个 let rs = fs.createReadStream('test.txt', { flags: 'r', // 操作方式,可读模式 encoding: null, // 编码格式,null对应Buffer fd: null, // 文件标识符/句柄 mode: 438, // 权限类,十进制438=>八进制0566 autoClose: true,// 自动关闭文件 start: 0, // 底层数据从哪儿开始读取 // end: 3, // 默认读出所有数据 highWaterMark: 4// 每次读取多少字节的数据到缓冲区 }) // 1、data:读取数据出来,流动模式 => 切换到暂停模式 => 再输出剩余数据 rs.on('data', (chunk) => { console.log(chunk.toString()) rs.pause() // 加载 pause() 就切换成暂停模式 setTimeout(() => {// 1s中之后继续输出剩余数据 rs.resume() }, 1000) }) // 2、readable:读取数据出来,需要使用read(num)方法,参数num表示取多少数据 rs.on('readable', () => { // let data = rs.read() // console.log(data) let data while((data = rs.read(1)) !== null) { console.log(data.toString()) console.log('----------', rs._readableState.length)// 打印缓冲区中剩余数据字节长度。 } }) // 3、事件触发顺序:open => data => end => close // fs.createReadStream 执行的时候会触发 rs.on rs.on('open', (fd) => { console.log(fd, '文件打开了') }) // 默认不触发,数据消费结束之后关闭文件的时候 rs.on('close', () => { console.log('文件关闭了') }) // 数据消费的时候触发 let bufferArr = [] rs.on('data', (chunk) => { bufferArr.push(chunk) }) // 数据被清空之后触发,文件关闭之前 rs.on('end', () => { console.log(Buffer.concat(bufferArr).toString()) console.log('当数据被清空之后') }) rs.on('error', (err) => { console.log('出错了') }) ``` #### 8. 文件可写流的使用;[模拟实现文件可写流](https://gitee.com/ymcdhr/e-code/tree/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/10FileStream) - fs.createWriteStream() 写入文件 - 文件可写流有一个缓存区域,默认大小16kb; - 文件可写流使用单向链表实现了一个队列,写操作时向队列添加任务; - 事件触发顺序:open => write => end => close - end 之后不能再调用 write - end 本身可以写入数据 ```js const fs = require('fs') // 文件可写流 const ws = fs.createWriteStream('test.txt', { flags: 'w', mode: 438, fd: null, encoding: "utf-8", start: 0, highWaterMark: 3 }) let buf = Buffer.from('abc') // // ws.write() 是异步函数 // // 写入参数:可以是字符串 或者 buffer ===> fs rs // ws.write(buf, () => { // console.log('ok2') // }) // // 写入结束后执行回调 // ws.write('拉勾教育', () => { // console.log('ok1') // }) // 也会触发open ws.on('open', (fd) => { console.log('open', fd) }) ws.write("2") // close 是在数据写入操作全部完成之后再执行 // end 执行之后触发 close ws.on('close', () => { console.log('文件关闭了') }) // end 执行之后就意味着数据写入操作完成 ws.end('拉勾教育') // error ws.on('error', (err) => { console.log('出错了') }) ``` #### 9. 文件可读流的执行流程,缓存区溢出的处理; - let flag = write() 方法的返回值,并不表示数据写入成功与否;而是表示缓存区是否溢出。 - 文件可读流、写入流缓存区默认大小:64kb、16kb;htghWaterMark配置?如果缓存区溢出,可能会导致内存溢出、GC频繁调用、其它进程变慢等情况出现。 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0805/181616_635372fd_9130428.png "屏幕截图.png") - 第一次调用write方法时将数据写入文件中; - 第二次调用write方法时将数据放入缓存区中; - 对于缓存区的数据,生产速度和消费速度是不一样的,一般情况下生产速度要比消费速度快很多; - 当flag为false之后并不意味着当前的数据不能被写入,但是我们应该告知数据的生产者,当前的消费速度已经跟不上生产速度了,所以这个时候,一般我们会将可读流的模式修改为暂停模式; - 当数据生产者暂停之后,消费者会慢慢消化它内部缓存中的数据,直到可以再次被执行写入操作; - 当缓冲区可以继续写入数据时如何让生产者知道?通过触发 drain 事件 ```js const fs = require('fs') let ws = fs.createWriteStream('test.txt', { highWaterMark: 3 // 配置缓存区的大小为3字节,默认为16kb }) // 执行写入 let flag = ws.write('1') console.log(flag) // true flag = ws.write('2') console.log(flag) // true flag = ws.write('3') console.log(flag) // false // 如果 flag 为 false 并不是说明当前数据不能被执行写入 ws.on('drain', () => { console.log('drain') }) ``` #### 10. drain 事件与写入速度 - 主要的作用:针对大文件处理,在缓存区溢出之前进行限速; - pipe() 方法可以替代 drain 方案 ```js /** * 需求:“拉勾教育” 写入指定的文件 * 01 一次性写入 * 02 分批写入 * 对比:大内存的文件分批写入更好 */ let fs = require('fs') let ws = fs.createWriteStream('test.txt', { highWaterMark: 3 }) // ws.write('拉勾教育') let source = "拉勾教育".split('') let num = 0 let flag = true function executeWrite () { flag = true while(num !== 4 && flag) { flag = ws.write(source[num]) num++ } } executeWrite() ws.on('drain', () => { console.log('drain 执行了') executeWrite() }) ``` #### 11. pipe() 方法与背压机制 - 作用:实现文件拷贝 - 可读流: ![输入图片说明](https://images.gitee.com/uploads/images/2021/0805/212344_307ce90b_9130428.png "屏幕截图.png") - 可写流: ![输入图片说明](https://images.gitee.com/uploads/images/2021/0805/212447_f40f3556_9130428.png "屏幕截图.png") - 什么是背压机制?数据读写可能存在的问题:如果缓冲区溢出,可能会导致内存溢出、GC频繁调用、其它进程变慢等情况出现。需要一种机制来避免缓冲区溢出; - pipe() 方法的使用: ```js let fs = require('fs') // 可读流和可写流默认缓冲区大小为4:1,64kb:16kb let rs = fs.createReadStream('test.txt', { highWaterMark: 4 }) let ws = fs.createWriteStream('test1.txt', { highWaterMark: 1 }) let flag = true // 1、传统的 drain 方式实现 // rs.on('data', (chunk) => { // flag = ws.write(chunk, () => { // console.log('写完了') // }) // if (!flag) { // rs.pause() // } // }) // ws.on('drain', () => { // rs.resume() // }) // 2、pipe()方法实现自动限流,原理和drain类似 rs.pipe(ws) ``` - pipe 方法的实现原理: ```js pipe(ws) { this.on('data',(data)=>{ let flag = ws.write(data) if(!flag){ this.pause() } }) this.on('drain',()=>{ this.resume() }) } ``` ### Nodejs 通信模块 #### Net 模块(主要涉及 TCP 处理,了解) 1. 通信事件/TCP通信:利用可读流 + 可写流 + 事件实现 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0806/134703_0a3d80e6_9130428.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2021/0806/134647_a8c9d068_9130428.png "屏幕截图.png") 2. 作为服务端发送数据 ```js const net = require('net') // 创建服务端实例 const server = net.createServer() const PORT = 1234 const HOST = 'localhost' server.listen(PORT, HOST) server.on('listening', () => { console.log(`服务端已经开启在 ${HOST}: ${PORT}`) }) // 接收消息 回写消息 // 双工流,可读可写 server.on('connection', (socket) => { socket.on('data', (chunk) => { const msg = chunk.toString() console.log(msg) // 回数据 socket.write(Buffer.from('您好' + msg)) }) }) server.on('close', () => { console.log('服务端关闭了') }) server.on('error', (err) => { if (err.code == 'EADDRINUSE') { console.log('地址正在被使用') }else{ console.log(err) } }) ``` 3. 作为客户端请求数据 ```js const net = require('net') const client = net.createConnection({ port: 1234, host: '127.0.0.1' }) client.on('connect', () => { client.write('拉勾教育') }) client.on('data', (chunk) => { console.log(chunk.toString()) }) client.on('error', (err) => { console.log(err) }) client.on('close', () => { console.log('客户端断开连接') }) ``` 4. 什么是数据的粘包问题? - 粘包现象:多次发送数据时,数据堆叠到一条数据里面; - 解决方案:把每次数据发送用时间隔开;[参考源码](https://gitee.com/ymcdhr/e-code/tree/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/11TCP/02) - 封包拆包:[参考源码](https://gitee.com/ymcdhr/e-code/tree/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/11TCP) ![输入图片说明](https://images.gitee.com/uploads/images/2021/0806/140116_7060ded7_9130428.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2021/0806/140209_aa3f8d2d_9130428.png "屏幕截图.png") - Buffer 数据读写 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0806/140234_369e4a2b_9130428.png "屏幕截图.png") #### HTTP 模块 1. 使用 HTTP 模块创建服务 ``` const http = require('http') // 创建服务端 let server = http.createServer((req, res) => { // 针对于请求和响应完成各自的操作 console.log('1111') }) server.listen(1234, () => { console.log('server is running......') }) ``` 2. 获取 HTTP 请求信息 ```js const http = require('http') const url = require('url') // 创建服务 const server = http.createServer((req, res) => { console.log('请求进来了') // 请求路径,true表示格式化输出 let { pathname, query } = url.parse(req.url, true) console.log(pathname, '----', query) // 请求方式 console.log(req.method) // 版本号 console.log(req.httpVersion) // 请求头 console.log(req.headers) // 请求体数据获取 // 其实是一个可读流,通过事件取得数据 let arr = [] req.on('data', (data) => { arr.push(data) }) req.on('end', () => { console.log(Buffer.concat(arr).toString()) }) }) // 监听端口 server.listen(1234, () => { console.log('server is start......') }) ``` 3. 设置响应数据 ```js const http = require('http') const server = http.createServer((req, res) => { console.log('有请求进来了') // res // res.write('ok') // res.end() // res.end('test ok') res.statusCode = 302 // 回写中文需要设置utf-8编码 res.setHeader('Content-type', 'text/html;charset=utf-8') // 必须要调用end方法结束服务 res.end('拉勾教育') }) server.listen(1234, () => { console.log('server is start.....') }) ``` 4. 请求数据: - http.get - http.post - http.request ```js // server.js const http = require('http') const url = require('url') const querystring = require('querystring') const server = http.createServer((req, res) => { // console.log('请求进行来了') let { pathname, query } = url.parse(req.url) console.log(pathname, '----', query) // post let arr = [] req.on('data', (data) => { arr.push(data) }) req.on('end', () => { let obj = Buffer.concat(arr).toString() // console.log(Buffer.concat(arr).toString()) // console.log(req.headers['content-type']) // json格式的数据 if (req.headers['content-type'] == 'application/json') { let a = JSON.parse(obj) a.add = '互联网人的大学' res.end(JSON.stringify(a)) // 序列化数据 }else if(req.headers['content-type'] == 'application/x-www-form-urlencoded') { let ret = querystring.parse(obj) res.end(JSON.stringify(ret)) } }) }) server.listen(1234, () => { console.log('server is running') }) ``` ```js // client.js const http = require('http') let options = { host: 'localhost', port: 1234, path: '/', method: 'POST' } let server = http.createServer((request, response) => { let req = http.request(options, (res) => { let arr = [] res.on('data', (data) => { arr.push(data) }) res.on('end', () => { // console.log(Buffer.concat(arr).toString()) let ret = Buffer.concat(arr).toString() response.setHeader('Content-type', 'text/html;charset=utf-8') response.end(ret) }) }) req.end('拉勾教育') }) server.listen(2345, () => { console.log('本地的服务端启动了') }) ``` 5. 处理跨域问题;[完整代码](https://gitee.com/ymcdhr/e-code/tree/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/agent-apply) ```js const http = require('http') // 代理到的服务器配置,应该读取文件配置更好 let options = { host: 'localhost', port: 1234, path: '/', method: 'POST' } // 创建http服务,接收请求 let server = http.createServer((request, response) => { // 创建请求,请求代理到的服务器 let req = http.request(options, (res) => { let arr = [] res.on('data', (data) => { arr.push(data) }) res.on('end', () => { // console.log(Buffer.concat(arr).toString()) // 返回数据给浏览器 let ret = Buffer.concat(arr).toString() response.setHeader('Content-type', 'text/html;charset=utf-8') response.end(ret) }) }) req.end('拉勾教育') }) server.listen(2345, () => { console.log('本地的服务端启动了') }) ``` 6. HTTP 实现静态服;[示例源码](https://gitee.com/ymcdhr/e-code/tree/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/static-server) #### 实现一个完整的命令行工具:[源码实现](https://gitee.com/ymcdhr/e-code/tree/master/05-01-%E8%AF%BE%E7%A8%8B%E8%B5%84%E6%96%99/Code/lgserve) - bin/www.js:用于处理帮助命令的显示,以及命令行参数的处理; - ### 附录: #### 将常用api转换成promise: ``` const {promisify} = require('util') const access = promisify(fs.access) const mkdir = promisify(fs.mkdir) ``` #### Nodejs 如何调试(VSCode) 1. 启动调试:Ctrl + Shift + D ![输入图片说明](https://images.gitee.com/uploads/images/2021/0804/203515_708a06db_9130428.png "屏幕截图.png") 2. 完整填写 program 路径;关闭跳过源码调试:launch.json 中注释掉以下一段: ```js { // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "pwa-node", "request": "launch", "name": "Launch Program", // "skipFiles": [ // "/**" // ], "program": "${workspaceFolder}\\Code\\05Module\\02-require-load.js" } ] } ``` 3. 打好断点 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0804/204324_38a34a15_9130428.png "屏幕截图.png") 4. 启动调试
![输入图片说明](https://images.gitee.com/uploads/images/2021/0804/204432_5cbc0e79_9130428.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2021/0804/204505_ee58f8a9_9130428.png "屏幕截图.png") #### Nodejs 常用的工具模块 1. url 2. util 3. fs 4. path 5. mime: ```js const mime = require('mime') // 获取文件类型 // mime.getType(absPath) 获取文件类型 fs.readFile(absPath, (err, data) => { res.setHeader('Content-type', mime.getType(absPath) + ';charset=utf-8') res.end(data) }) ``` 6. commander 7. #### Stream、fs、http、EventEmitter 等模块之间的关系 #### Nodejs 文件写入,为什么要限流?控制速度? #### EventEmitter 里面有哪些常用的事件? - newListener - data - end - open #### BFF 架构中的高并发量请求时如何实现高性能运行的? #### 为什么是三次握手和四次挥手?因为客户端请求断开链接的时候,服务端可能还没有结束发送数据,等结束了再去请求客户端说要断开链接。 #### HTTP 模块的原理?可读流 + 可写流 + 事件(发布订阅模式) #### 如何判断是文件还是路径? ```js fs.stat(absPath, (err, statObj) => { if (statObj.isFile()) { ... ```