1. Node基本概念
1. Node是什么?
Node.js是一个基于Chrome V8引擎的JavaScript运行环境(runtime).Node不是一门语言是让js运行在后端的运行时,并且不包括javascript全集因为在服务端中不包含DOM和BOM,Node也提供了一些新的模块例如http,fs模块等。Nodejs 使用了事件驱动、非阻塞式I/O的模型,使其轻量又高效并且Nodejs的包管理器npm,是全球最大的开源库生态系统。事件驱动与非塞IO后面我们会——介绍。到此我们已经对node有了简单的概念。
2. Node解决了哪些问题?
Node在处理高并发I/O密集场景有明显的性能优势
- 高并发是指在同一时间并发访问服务器
- I/O密集指的是文件操作、网络操作、数据库,相对的有CPU密集,CPU密集指的是逻辑处理运算、压缩、
解压、加密、解密
Web主要场景就是接收客户端的请求读取静态资源和渲染界面,所以Node非常适合Web应用的开发。前后分离
3. JS单线程
javascript在最初设计时设计成了单线程为什么不是多线程呢?如果多个线程同时操作DOM那岂不会很混乱?这里所谓的单线程指的是主线程是单线程的,所以在Node中主线程依旧是单线程的。但是Node可以开启多个子线程
优点:
- 单线程特点是节约了内存,并且不需要在切换执行上下文
- 而且单线程不需要管锁的问题.
4. 同步异步和阻塞非阻塞
- 阻塞和非阻塞 针对的是调用方
- 我调用了一个方法之后的状态 fs.readFile
- 同步异步 针对的是被调用方
- 我调用了一个方法 ,这个方法会给我说他是同步的还是异步的
- 异步非阻塞 (我调用了一个方法,这个方法是异步的,我不想要等待这个方法执行完毕)
- http 第一个请求 要计算 100万个数相加 第二个请求来了,需要等待第一个人计算完成
5. Node中的Event Loop

Node官网说明https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/
2. global
node和前端的区别 前端里面有dom bom 服务端中没有widnow,
服务端中有global属性 全局对象 global有很多属性, 当访问global属性时不需要添加global.
因为默认会在global中查找(作用域链)
node中有一个模块化系统,是以文件为单位的,每个文件都是一个模块,模块中的this被更改了{}
- process 进程(重要)
- Buffer 处理二进制文件
- clearInterval / clearTimeout
- setInterval / setTimeout
- setImmediate / clearImmedate 宏任务
- 还要很多没有被枚举出来的属性 可用console.log(global, {showHidden: true})
宏任务:
- script
- ui
- setTiemout
- setInterval
- requestFrameAnimation
- setImmediate
- MessageChannel
- 异步的 click
- ajax ```javascript // 可以用这个属性来判断当前执行的系统环境 win32 darwin console.log(process.platform);
// 1.node.exe 2.node当前执行的文件 (解析用户自己传递的参数) // 执行node文件 node 文件名 a b c d (webpack —mode —config —port —progress) console.log(process.argv); // let args = process.argv.slice(2); // [ ‘—port’, ‘3000’, ‘—color’, ‘red’, ‘—config’, ‘a.js’ ]
// 当用户在哪执行node命令时 就去哪找配置文件 webpack // 当前用户的工作目录 current working directory (这个目录可以更改,用户自己切换即可 ) console.log(process.cwd());
// 当前文件的所在的目录 这个目录是不能手动修改的 console.log(__dirname);
// 环境变量 可以根据环境变量实现不同的功能 // window set key=value mac export key=value 这样设置的环境变量是临时的变量 console.log(process.env.b); let domain = process.env.NODE_ENV === ‘production’? ‘localhost’:’zfpx.com’;
// node中自己实现的微任务 nextTick / queueMicrotask console.log(process.nextTick);
// node中setImmediate 宏任务 不需要传入时间 setImmediate(() => { console.log(‘setImmediate’) setTimeout(() => { // 进入事件环时 setTimeout 有可能没有完成 console.log(‘timeout’) }, 1000); });
<a name="Nhfz8"></a>## 3. 模块的概念<a name="DLzEq"></a>#### 1. 模块化规范Node中的模块化规范 commonjs规范(node自己实现的), es6Module(import export), umd 统一模块规范 (如果浏览器不支持commonjs requirejs,直接将变量放到window上),amd规范 requirejs cmd规范 seajs<br />目前主流的两个规范commonjs / ESM<a name="MXGLL"></a>#### 2. **commonjs规范(模块的概念)**1. 可以把复杂的代码拆分成小的模块,方便管理代码和维护1. 每个模块之间的内容都是相互独立的,互不影响的 (解决变量冲突的问题) 单例模式(不能完全解决,命名空间) 使用自执行函数来解决1. 规范的定义1. 每个文件都是一个模块1. 如果你希望模块中的变量被别人使用,可以使用module.exports 导出这个变量1. 如果另一个模块想使用这个模块导出的结果 需要使用require语法来引用 (同步)<a name="IuKjZ"></a>#### **4. 模块的分类**1. 核心模块/包 , 内置模块1. 不是自己写的,也不是安装来的是node中自己提供的,可以直接使用1. require('fs') , path, http, vm, ......2. 第三方模块1. 别人写的模块,通过npm install 安装过来的, 不需要有路径1. require('commander'); webpack,.....3. 自定义模块1. 自己写的模块 引用时需要增加路径(相对路径,绝对路径)1. require('./utils')<a name="BnTAn"></a>#### 5. **核心模块**1. fs(fileSystem处理文件的)1. fs.readFileSync() 同步读取文件1. fs.existsSync() 判断路径文件是否存在1. fs.mkdir() 创建文件夹1. ...2. path(处理路径)1. path.resolve() 默认连接CWD路径再拼接, 如果末尾遇到/ 会回到根目录1. path.join() 仅仅路径拼接1. path.extname() 获取扩展名1. ...3. vm(虚拟机模块 沙箱环境)1. vm.runInThisContext() 运行在当前环境栈4. ...还有很多<a name="xqTJL"></a>## 4. commander```javascript// 在npm上的模块都需要先安装在使用 (模块内部也提供了几个属性,也可以在模块中直接访问 - 参数)const program = require('commander');program.version('1.0.0').command('create').action(()=>{console.log('创建项目')}).name('node').usage('my-server').option('-p,--port <v>', 'set your port').option('-c,--config <v>', 'set your config file').parse(process.argv); // -- 开头的是key 不带--是值
5. 模板引擎
模板引擎就是传入参数, 根据特定语法转换为真实数据
沙箱: 虚拟机模块, 干净的环境, 跟外界没有任何关联
模板引擎的实现原理: with语法 + 字符串拼接 + new Function/vm.runInThisContext()
模板引擎一般都是操作字符串逻辑(拼接后的字符串代码),
那么如何让字符串执行?
eval 弊端默认会取当前作用域下的变量, 环境污染
var a = 1eval('console.log(a)') // 1 污染了环境
new Function 可以创建一个沙箱环境,让字符串执行
var a = 1let fn = new Function('c','b','d',`let a =1;console.log(a)`);console.log(fn()); // a is not defined
vm 虚拟机模块 可以建立沙箱环境
const vm = require('vm'); // 虚拟机模块 可以创建沙箱环境var a = 1vm.runInThisContext(`console.log(a)`); // a is not defined
1. 实现ejs模板引擎
- template.html
定义了name , age , arr 循环
<!--template.html--><!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title></head><body><%=name%> <%=age%><%arr.forEach(item=>{%><li><%=item%></li><%})%></body></html>
- template.js 模板引擎, with + 字符串拼接 + new Function
可使用ejs模板引擎处理,可自己实现一个简单模板引擎
// 实现自定义的模板引擎// const ejs = require('ejs'); // 第三方模块const path = require('path');const fs = require('fs');const renderFile = (filePath,obj,cb) =>{fs.readFile(filePath,'utf8',function (err,html) {if(err){return cb(err,html);}// arguments[0] 就是匹配到的原字符串 arguments[1] 就是第一个原来括号html = html.replace(/\{\{([^}]+)\}\}/g,function () { // RegExp.$1let key = arguments[1].trim();return '${'+key+'}' // {{name}} => ${name}});let head = `let str = '';\r\n with(obj){\r\n`;head += 'str+=`'html = html.replace(/\{\%([^%]+)\%\}/g,function () {return '`\r\n'+arguments[1] + '\r\nstr+=`\r\n'})let tail = '`}\r\n return str;'let fn = new Function('obj',head + html + tail)cb(err,fn(obj));});}// ejs实现// ejs.renderFile(path.resolve(__dirname,'my-template.html'),{name:'zf',age:11,arr:[1,2,3]},function (err,data) {// console.log(data);// });// 自己实现renderFile(path.resolve(__dirname,'my-template.html'),{name:'zf',age:11,arr:[1,2,3]},function (err,data) {console.log(data);});
6. 模块断点调试
https://nodejs.org/en/docs/inspector
- 直接在vscode / webstorm中调试
- 可以在chrome中进行调试方案调试node—inspect-brk执行的文件
7. 手写node require方法
- 会默认调用require语法
- Module.prototype.require 模块的原型上有require方法
- Module._load 调用模块的加载方法 最终返回的是module.exports
- Module._resolveFilename 解析文件名 将文件名变成绝对路径 默认尝试添加 .js / .json /.node 策略模式
- Module._cache 默认会判断是否存在缓存
- new Module 创建模块(对象) id ,exports
- 把模块缓存起来,方便下次使用
__ 根据文件名(绝对路径) 创建一个模块 - tryModuleLoad 尝试加载模块 module.load
- module.paths 第三方模块查找的路径
- 获取当前模块的扩展名 根据扩展名调用对应的方法Module._extensions 策略模式
- 获取文件的内容
- 调用module._compile 方法 vm.runInThisContext
- 将用户的内容 包裹到一个函数中 (function (exports, require, module, filename, dirname) {}) ```javascript // 最终返回的是module.exports 用户会给这个module.exports进行赋值 const path = require(‘path’); const fs = require(‘fs’); const vm = require(‘vm’);
function Module(id) { this.id = id; this.exports = {}; } Module.wrap = function (script) { let arr = [ ‘ (function (exports, require, module, filename, dirname) {‘, script, ‘})’ ] return arr.join(‘’) } Module._extensions = { ‘.js’: function(module) { let content = fs.readFileSync(module.id,’utf8’); let fnStr = Module.wrap(content); let fn = vm.runInThisContext(fnStr); let exports = module.exports; let require = myRequire; let filename = module.id; let dirname = path.dirname(module.id); // 这里的this 就是exports对象 fn.call(exports,exports,require,module,filename,dirname); // 用户会给module.exports 赋值 }, ‘.json’: function(module) { let content = fs.readFileSync(module.id,’utf8’); // 读取出来的是字符串 module.exports = JSON.parse(content); } } Module._resolveFilename = function(filepath) { // 根据当前路径实现解析 let filePath = path.resolve(__dirname, filepath); // 判断当前面文件是否存在 let exists = fs.existsSync(filePath); if (exists) return filePath; // 如果存在直接返回路径
// 尝试添加后缀let keys = Object.keys(Module._extensions);for (let i = 0; i < keys.length; i++) {let currentPath = filePath + keys[i];if (fs.existsSync(currentPath)) { // 尝试添加后缀查找return currentPath;}}throw new Error('模块不存在')
} Module.prototype.load = function(filename) { // 获取文件的后缀来进行加载 let extname = path.extname(filename); Module._extensionsextname; // 根据对应的后缀名进行加载 } Module.cache = {}; Module._load = function(filepath) { // 将路径转化成绝对路径 let filename = Module._resolveFilename(filepath);
// 获取路径后不要立即创建模块,先看一眼能否找到以前加载过的模块let cacheModule = Module.cache[filename];console.log(cacheModule);if(cacheModule){return cacheModule.exports; // 直接返回上一次require的结果}// 保证每个模块的唯一性,需要通过唯一路径进行查找let module = new Module(filename); // id,exports对应的就是当前模块的结果Module.cache[filename] = modulemodule.load(filename);return module.exports;
} function myRequire(filepath) { // 根据路径加载这个模块 return Module._load(filepath) }
let a = req(‘./a.js’) console.log(a)
<a name="ZDeZR"></a>## 8. Node event 手写观察者模式 会有两个类,观察者会被存到被观察者中,如果被观察者状态变化,会主动通知观察者,调用观察者的更新方法观察者模式和发布订阅的场景 区分<br />发布订阅模式```javascriptfunction EventEmitter (){this._events = {}; // 默认给EventEmitter准备的}EventEmitter.prototype.on = function (eventName,callback) {if(!this._events) this._events = {};// 如果不是newListener 那就需要触发newListener的回调if(eventName !== 'newListener'){this.emit('newListener',eventName)}(this._events[eventName] || (this._events[eventName] = [])).push(callback)}EventEmitter.prototype.emit = function (eventName,...args) {if(this._events && this._events[eventName]){this._events[eventName].forEach(event =>event(...args));}}EventEmitter.prototype.off = function (eventName,callback) {// 先找到对应的数组if(this._events && this._events[eventName]){// 1.可以使用数组自带的filter方法 直接过滤,找到索引采用splice删除// 删除时获取once中的l属性和callback 比较,如果相等则删除this._events[eventName] = this._events[eventName].filter(cb=>(cb != callback)&&(cb.l !== callback))}}EventEmitter.prototype.once = function (eventName,callback) {const once =(...args)=> {callback(...args);this.off(eventName,once)}once.l = callback; // 给once增加callback的标识this.on(eventName,once); // 先绑定一个一次性事件,稍后触发时,再将事件清空}module.exports = EventEmitter;
9. npm
概述: npm 是node的包管理器, 管理都是node的模块
1. 什么是3N模块
- nrm node中的源管理工具包 (主要切换npm配置的源registry)
- nvm node中的版本管理工具 (切换node的版本)
- npm 包管理工具, 官网存放着成千上万个不同功能的包供开源使用
2. 第三方模块分类
- 全局模块
- 只能在命令行中使用 任何路径都可以
- 本地模块
- 开发或者上线使用的
- 分为开发依赖 devDependencies, 生产依赖 dependencies
3. 包的初始化工作
npm init -y // -y 是指使用默认配置npm init // 自定义配置信息
4. 包的管理
// 全局安装模块npm install nrm -gnrm的使用 `nrm ls` `nrm use` `nrm current` 再操作配置文件// 安装到本地 生产依赖 dependenciesnpm install vue// 安装到本地 开发依赖devDependencies (--save-dev || -D)npm install mockjs -D
5. 编写一个全局包调试
先创建bin的配置
// package.json{"name": "my-pack", // 对应的名称"bin": {"computed": "./bin/computed.js" // 脚本}}
! /usr/bin/env node 以什么方式来运行
#! /usr/bin/env nodeconst total = process.argv.slice(2).reduce((memo, current) =>memo += +current, 0)console.log(total)
放到npm全局中 (上传后下载-g , 直接临时拷贝过去)
- 上传npm
- 先登录npm addUser
- npm publish发布
// 在my-pack 项目下 运行npm link
在其他项目npm link my-pack 或者命令行 computed 1 2 3
6. 版本问题
major(破坏性更新).minor(增加功能 修订大版本中的功能).patch(小的bug)
特殊版本
- alpha预览版(内部测试的版本)
- beta(公测版本)
- rc (最终测试版)可上线
版本符号
默认运行npm run 时会将node_modules下的.bin目录放到全局下所有可以使用当前文件夹下的命令
npx 命令 npm 5.2之后提供的(这个命令没有npm run好管理) npx可以去下载包 下载完毕后执行,执行后删除
10. Buffer
node中的fs模块读取文件默认是返回Buffer
// 在node中需要进行文件读取,node中操作的内容默认会存在内存中,// 内存中的表现形式肯定是二进制的,二进制转换16进制来展现 11111111 ffconst fs = require('fs');let r = fs.readFileSync('./note.md');// Buffer可以和字符串直接相互转化console.log(r.toString());
1. 常用的进制
- 10进制, 2进制, 8进制, 16进制
- 进制的互相转化 js中提供了进制转换的方法 parseInt
- 一个字节是由8bit位(二进制)
- 编码过程中都是以字节为单位 常见的一个字节组成一个字符 双字节组成汉字 三个字节组成一个汉字 (gbk utf8)
- 把任意进制转换成10进制 需要用当前位所在的值 * 当前进制^第几位
把10进制转换成任意进制可以采用取余的方法
// 将任意进制转换成10进制console.log(parseInt('11',2))// 可以将任意进制转换成任意进制console.log((0x16).toString(16))
2. 编码的发展史
了解就好
ASCII 编码 一些常用的符合和字母 进行了一个排号 127 只会占用一个字节大小
- GB2312 用两个字节 27000
- GBK 扩展 4000+
- GB18030 少数民族的汉字
- Unicode 组织
- UTF8 (在utf8编码中 一个字符占用一个字节 一个汉字占用三个字节)
3. blob类型
blob是前端 二进制文件类型,
比如我们input:type=file 读取回来就是一个blob类型
blob的应用
- 前端实现下载功能 前端实现下载 将字符串包装成二进制类型
- 利用new Blob()
- 利用URL.createObjectUrl(blob)
``javascript let str =hello world`; // 包装后的文件类型不能直接修改 const blob = new Blob([str],{ type:’text/html’ }); const a = document.createElement(‘a’);珠峰
a.setAttribute(‘download’,’index.html’); a.href = URL.createObjectURL(blob); a.click();
2. 前端实现预览功能1. 读取二进制中内容 fileReader```javascript// html=> <input type="file" id="file">file.addEventListener('change', (e) => {let file = e.target.files[0]; //二进制文件类型let fileReader = new FileReader();fileReader.onload = function() {let img = document.createElement('img');img.src = fileReader.result;document.body.appendChild(img)}fileReader.readAsDataURL(file);})
- 利用URL.createObjectUrl(blob)
// html=> <input type="file" id="file">file.addEventListener('change', (e) => {let file = e.target.files[0]; //二进制文件类型let img = document.createElement('img');let url = URL.createObjectURL(file);img.src = url;document.body.appendChild(img);// 删除内存中的blob类型Url// URL.revokeObjectURL(url);})
4. Buffer的应用
- 服务端可以操作二进制 Buffer 可以和字符串进行相互转化
Buffer代表的都是二进制数据 内存 (buffer不能扩容) java 数组不能扩容 动态数组, 在生成一个新的内存 拷贝过去
buffer的三种声明方式 ```javascript // 开发中数字都是字节为单位 const buffer = Buffer.alloc(5);
// 根据汉字来转化成buffer const buffer1 = Buffer.from(‘珠峰’);
// 通过数组来指定存放的内容 const buffer2 = Buffer.from([0x16,0x32]);
console.log(buffer, ‘buffer ‘) //
2. buffer常用操作```javascript// slicelet buf = buffer.slice(0,1); // slice 方法也是“浅拷贝”// isBufferconst bool = Buffer.isBuffer(buffer)// lengthconst len = buf.length
- buffer方法封装 copy concat
```javascript
Buffer.prototype.copy = function (targetBuffer, targetStart, sourceStart =0 , sourceEnd = this.length) {
for(let i = sourceStart; i < sourceEnd;i++){
} } // buf1.copy(bigBuf,0,0,6) // buf2.copy(bigBuf,6) // console.log(bigBuf.toString())targetBuffer[targetStart++] = this[i];
Buffer.concat = function (bufferList,length= bufferList.reduce((a,b)=>a+b.length,0)) { let buffer = Buffer.alloc(length); let offset = 0; bufferList.forEach(buf=>{ buf.copy(buffer,offset); offset += buf.length; }); return buffer.slice(0,offset); }
const newBuf = Buffer.concat([buf1,buf2,buf1,buf2])
4. 大文件分批上传思路1. _文件上传 -》 分片上传 =》 “二进制” => buffer 来拼接_<a name="gndGc"></a>#### 5. 流 stream原理: 读取一点写入一点1. 拷贝文件, 小文件可以使用fs.writeFile / fs.copyFile1. 如果文件比内存还要大, 那就GG了, 可以采用流的方式读写文件```javascriptconst fs = require('fs')// 读取默认不指定编码都是buffer类型let r = fs.readFileSync('./name.txt');// 默认会将二进制转化成字符串写到文件中 虽然看到的是字符串但是 内部存储的都是二进制fs.writeFileSync('./age.txt',r);// 会默认把药拷贝的文件“整个”读取一遍 。// 特点不能读取比内存大的文件 (会占用很多可用内存)// stream 边读边写 (采用 分块读取写入的方式 来实现拷贝)fs.copyFile(source, target)
- 大文件copy
- 使用fs.createReadStream
6. 手写一个可读流方法
原理: 利用发布订阅模式 + fs.open + fs.read + fs.write
// 流 有方向的 读 => 写 node中实现了stream模块// 文件也想实现流 ,所以内部文件系统集成了stream模块const path = require('path');const ReadStream = require('./readStream');// 创建一个可读流(可读流对象) 这个方法默认并不会读取内容// fs.open fs.read fs.closelet rs = new ReadStream(path.resolve(__dirname, 'name.txt'), {flags: 'r', // r代表的是读取encoding: null, // 默认buffermode: 0o666, // 模式 可读可写autoClose: true, // fs.closestart: 2, // 2 - 8 包前又包后end: 8,highWaterMark: 3 // 每次读取的个数 3 3 1});// 为了多个异步方法可以解耦, 发布订阅模式// 可读流继承了events模块,这里的名字必须叫data rs.emit('data'), 如果监听了data 内部会拼命读取文件的内容 ,触发对应的回调// Buffer.concat()let bufferArr = []rs.on('open', (fd) => {console.log(fd,'---')})// 读和写 先读取 =》 像文件中写入 pause() resume()rs.on('data', (data) => { // 默认会直到文件读取完毕// rs.pause(); // 让可读流暂停触发data事件// // console.log('暂停')// setTimeout(() => {// rs.resume(); // 再次触发data事件// }, 1000);console.log('触发')bufferArr.push(data);});rs.on('end', () => {console.log(Buffer.concat(bufferArr).toString(),'end');});rs.on('error', (err) => {console.log(err)});rs.on('close',()=>{console.log('close')})// on('data') on('end') pause() resume()// 文件流有两个特殊的事件 ,不是文件流 是普通的流 就没有这两个事件
const EventEmitter = require('events');const fs = require('fs');class ReadStream extends EventEmitter {constructor(path, opts = {}) {super()this.path = path;this.flags = opts.flags || 'r';this.mode = opts.mode || 0o666;this.autoClose = opts.autoClose || true;this.start = opts.start || 0;this.end = opts.end;// 读取的数量默认是64k 如果文件大于64k 就可以采用流的方式this.highWaterMark = opts.highWaterMark || 64 * 1024;// 记录读取的偏移量this.pos = this.start;// 默认创建一个可读流 是非流动模式 不会触发data事件,如果用户监听了data事件后 需要变为流动模式this.flowing = false; // 是否是流动模式this.open(); // 打开文件 fs.openthis.on('newListener', (type) => {if (type === 'data') {// 用户监听了datathis.flowing = true;this.read(); // fs.read}});}open() {fs.open(this.path, this.flags, this.mode, (err, fd) => {if (err) {return this.emit('error', err)}this.fd = fd; // 保存到实例上,用于稍后的读取操作this.emit('open', fd)})}read() {// 读取必须要等待文件打开完毕, 如果打开了会触发open事件if (typeof this.fd !== 'number') {return this.once('open', () => this.read());}// 在这之后 文件肯定已经打开了 可以开始进行读取操作const buffer = Buffer.alloc(this.highWaterMark);// 3 3 1// start 0 end 6// 0 1 2 3 4 5 6// 0 3// 3 3// 6 1// 每次理论上应该读取highWaterMark个 但是用户能指定了读取的位置let howMuchToRead = this.end ? Math.min(this.end - this.pos + 1, this.highWaterMark) : this.highWaterMark; // 应该读取几个fs.read(this.fd,buffer,0,howMuchToRead,this.pos,(err,bytesRead)=>{if(bytesRead){this.pos += bytesRead; // 每次读取到后累加this.emit('data',buffer.slice(0,bytesRead));if(this.flowing){this.read();}}else{this.emit('end');if(this.autoClose){fs.close(this.fd,()=>{this.emit('close');});}}})}pause(){this.flowing = false;}resume(){this.flowing = true;this.read();}}module.exports = ReadStream;


