node 基础知识

310 阅读11分钟

node 文件查找规则

先查找同名文件,如果没有添加js,json 查找,还查找不到,找package.json 查找main对应文件,查找不到,找index.js文件,再查找不到,就报错

浏览器event loop 和 node event loop

  • 浏览器的event loop 总体来说,先执行宏任务,在执行微任务,script 执行过程中,遇到回调任务,会先放入队列里面,这里分,宏任务队列和微任务队列。每一个宏任务,都有其对应的微任务,script 执行完毕之后,先查看微任务里是否有任务,有的话,就清空微任务队列,然后从宏任务队列中取出一个宏任务执行,执行的时候,发现这个宏任务产生了微任务,就把微任务放入微任务队列,宏任务执行完成之后,再次查看微任务队列,清空微任务,浏览器的事件循环机制,一直这样循环执行执行

  • node 的 event loop 的大致执行规则和浏览器的很相似,但是node是多个宏任务队列的, 里面的process.nextTick 在执行栈的下方,在整个script 执行完成之后,会最先执行process.nextTick,且,node里面执行的顺序按照,script->timer(setTimeout)->poll(i/o回调)->check(setImmediate)

eg:

const fs = require('fs')
fs.readFile('./a.txt', 'utf-8', () => {
    setTimeout(() => {
        console.log('--------------------------time 0---');
    }, 0)
    setTimeout(() => {
        console.log('--------------------------time 1---');
    }, 1000)

    setImmediate(() => {
        console.log('---------------------immediate--------');
    })

    process.nextTick(() => {
        console.log('---------------------nexttick--------');
    })

    new Promise((resolve, reject) => {
        resolve('ok')
    }).then((data) => {
        console.log('----------------------promise-------', data);
    })

})

// 打印结果 nexttick promise immediate time 0 time 1
 setTimeout(() => {
    console.log('--------------------------time 0---');
}, 0)
setTimeout(() => {
    console.log('--------------------------time 1---');
}, 1000)

setImmediate(() => {
    console.log('---------------------immediate--------');
})

process.nextTick(() => {
    console.log('---------------------nexttick--------');
})

new Promise((resolve, reject) => {
    resolve('ok')
}).then((data) => {
    console.log('----------------------promise-------', data);
})
// nexttick promise time 0 immediate time 1
// 注意 immediate 和 time 0 的顺序不是固定的,是随机的

promise 实现 promispromisify

const fs = require('fs')
function promisify(fn) {
    return function (...args) {
        return new Promise((resolve, reject) => {
            fn(...args, (err, data) => {
                if (err) return reject(err)
                resolve(data)
            })
        })
    }
}

const readFile = promisify(fs.readFile)
readFile('./1p.js', 'utf-8').then((data) => {
    console.log('-----------------------------', data);
})

继承的方式

  • prototype.proto
function A1(){}
function A2(){}
A2.prototype.__proto__ = A1.prototype  // 继承原型
A2.__proto__ = A1  // 继承静态属性和方法
// A1 继承 A2
  • Object.setPrototypeOf(obj, prototype)

pipe 的原理

依靠on('data')/on('drain'), 在底部封装实现

const fs = require('fs');
const ws = fs.createWriteStream('./c.txt', { 
    highWaterMark: 2,
})

let i = 0;
function write() {
    let flag = true;  // 表示规定的内存有没有使用完
    while (i < 10 && flag) {
        flag = ws.write(i++ + '')
    }
}
ws.on('drain', () => {
    console.log('----------------------内存满------');
    write()
})
write()

二叉树

节点小于等于2

二叉搜索树

符合二叉树的特性,切左边节点小于右边

// 二叉搜索树
class Node {
    constructor(element,parent) {
        this.element = element;
        this.parent = parent;
        this.left = null;
        this.right = null;
    }
}


class Tree {
    constructor() {
        this.root = null;
    }

    add(element) {
        if (this.root === null) { // 添加的元素是根元素
            return this.root = new Node(element, null)
        }
        
        // 添加的时候要依据根节点,添加左右节点

        // 节点要依次添加,所以,跟节点要作为循环条件,更新
        let currentNode = this.root
        let parent;
        let compare;

        while (currentNode) {
            parent = currentNode
            compare = currentNode.element < element
            if (compare) {
                currentNode = currentNode.right
            } else {
                currentNode = currentNode.left
            }
        }
        
        let node = new Node(element, parent)
        if (compare) {
            parent.right = node
        } else {
            parent.left = node
        }
    }
}

let tree = new Tree();
[10,8,19,6,15,22,20].forEach(item => {
   tree.add(item)
});
console.dir(tree, {depth: 200});

先序遍历

根左右

 preTraverse() {
    function traverse(node) {
        if (node === null) return 
        console.log('-----------------------------', node.element);
        traverse(node.left)
        traverse(node.right)
    }
    traverse(this.root)
}

中序遍历

左根右

centerTraverse() {
    function traverse(node) {
        if (node === null) return 
        traverse(node.left)
        console.log('-----------------------------', node.element);
        traverse(node.right)
    }
    traverse(this.root)
}

后序遍历

左右根

nextTraverse() {
    function traverse(node) {
        if (node === null) return 
        traverse(node.left)
        traverse(node.right)
        console.log('-----------------------------', node.element);
    }
    traverse(this.root)
}

广度遍历

核心是,把当前的节点放入栈,每次找栈的左右孩子,然后依次的用指针指向下一个节点 [1] 指针先找1,发现1有1,2, 2个孩子,然后把1,2 放入栈,指针加一,指向2 [1,2,3] 然后2 有 4,5孩子,把4,5入栈,指针指向3 [1,2,3,4,5] 直到树遍历完

leverTraverse() {
    let stack = [this.root]  // 存放当前需要遍历的节点
    let index = 0
    let currentNode
    while (currentNode = stack[index++]) {
        console.log('---------currentNode--------------------', currentNode.element);
        if (currentNode.left){
            stack.push(currentNode.left)
        }
        if (currentNode.right){
            stack.push(currentNode.right)
        }
    }
}

二叉树的反转

reverse() {
    let stack = [this.root]  // 存放当前需要遍历的节点
    let index = 0
    let currentNode
    while (currentNode = stack[index++]) {
        console.log('---------currentNode--------------------', currentNode.element);
        let temp = currentNode.left
        currentNode.left = currentNode.right
        currentNode.right = temp
        if (currentNode.left){
            stack.push(currentNode.left)
        }
        if (currentNode.right){
            stack.push(currentNode.right)
        }
    }
}

计算机网络

osi 七层模型

  • 物理层: 网线,光纤传递数据,表现0,1
  • 数据链路层: 设备之间传递数据,建立连接 (mac 地址)数据帧
  • 网络层:寻址, 通过ip寻找mac地址 (ip段)
  • 传输层:因为网络层的传输不可靠,所以要在传输层建立tcp连接,保证数据传送的可靠性 (tcp包)
  • 会话层:建立会话
  • 表示层:数据的表现形式,安全性等
  • 应用层:用户最终访问的接口

实际中会把会话层,表示层,应用层合并成一个层,就形成了5层模型

tcp

  • 传输控制协议:可靠,效率低,数据双向传播
  • 一个数据帧的最大长度是1500
  • 头部长度最大60,头部最小20个字节
  • ip协议的头20个字节
  • 所以一个tcp包最大能传递1460个字节
  • tcp协议的组成
    • 3次握手
    • 4次断开(因为断开的时候,数据可能还在发送,所以不能真正的断开,当数据发送完毕,才能真正的断开)
    • 滑动窗口 接收方的窗口要比发送方大,一旦接受方的窗口使用完,发送方会发送探测包,询问接受方的窗口有没有可使用的
    • 拥塞处理 如果接受方的窗口大小是无限的,接收到数据后就能发送ack包,那么传输数据就主要取决于网络的带宽,但是网络的带宽是有限的 cwnd ssthresh
      • 慢启动
      • 拥塞避免
      • 快重传 出现丢包时,不要立刻退回到慢开始的阶段,接受方3次向发送方确认丢失的包,发送方从新发送丢失的包(重传)
      • 快恢复

http

  • 一个域名下最多同时发起6个http请求(浏览器的不同会有区别)
  • 一个http需要1个tcp 多个http就需要重复的建立3次握手,因为同时发起请求,会有对头阻塞的问题
  • http2为了解决同域名下,http数量限制的问题,只建立一个tcp连接,压缩头部,但是没有解决队头阻塞的问题

http 状态码

  • 2
    • 200 成功
    • 204 成功但没有返回
    • 206 分片传输
  • 3
    • 301 永久重定向
    • 302 临时重定向
    • 304 缓存
  • 4
    • 400 参数错误
    • 401 用户权限(没登录)
    • 403 登陆了,没权限
    • 404 找不到
    • 405 客户端的请求方法,服务端不支持
    • 416 请求范围无效
  • 5
    • 500 服务器内部错误,无法完成请求
    • 502 请求代理的无效响应
    • 504 网关错误,超时,没有响应
    • 505 http版本不支持

http 请求方法

restful风格,实现资源的增删改查,get,post,put,delete

  • get:没有请求体
  • post:理论上数据没有限制大小,相对get安全一点

http报文

请求:请求行,请求头,请求体 响应:响应行,响应头,响应体

http 请求demo

const http = require('http');

const server = http.createServer((req, res) => {
    const url = req.url
    console.log('---------------------url--------', url);
    // get 没有请求体。post有,可以用curl 测试
    // 接收到的结果是buffer

    let chunk = []
    req.on('data', (data) => {
        chunk.push(data)
        console.log('----------------data-------------', data.toString());
    })
    // 传输结束后会触发
    req.on('end', () => {
        let data = Buffer.concat(chunk)
        console.log('---------------------end-data--------', data.toString());
    })

    // 服务端发送信息
    res.setHeader('Content-Type-custom', '1234')
    res.write('server')
    res.end('end')
    
})
server.listen(3000, () => {
    console.log('-----------连接成功------------------');
})

屏幕快照 2021-08-13 下午4.06.19.png

https

https = http+tls+ssl

  • 先建立握手
  • 再交换密钥,
  • 通过密钥开始通信, 通信的内容通过ssl加密

服务端建立https服务需要申请证书,和私钥

正向代理反向代理

  • 正向代理: 局域网通过代理访问外部网络,局域网内是知道外部有什么的但是不能访问,需要代理帮助
  • 反向代理:外部的网络想访问局域网内部的资源,有一个代理可以实现外部的网络请求,但是外部的网络请求,不知道局域网内部都做了什么,也不知道局域网内部是不是只有一个代理

options 请求(跨域)

预检请求,只是在跨域的时候,检测能不能通信,只有在复杂请求并且跨域的时候,才会发送预检请求,get.post默认都是简单请求,但是自己定义了header信息,就会变成复杂请求

常见的跨域解决方案?jsonp,iframe,cors后端配置 (常用), nginx反向代理 (常用),websoket,

发送url到网 解析络展现发生了什么

根据域名->得到ip(dns 域名解析)-> 建立连接(tcp)-> ip寻找mac地址(网络层)-> 设备之间相互传递数据

阶段小总结

主要解决大文件读取,流可以指定读取文件的位置,提高效率

  • 流的模式分类
    • 可读流 on('data') on('end') push()[触发data事件]
    • 可泻流 ws.write() ws.end() 参数只能是string或者buffer
    • 双工流 tcp(可读,可写)socket.write socket.end socket.on('data')
    • 转化流 transform 例如pipe. rs.pipe(ws)

栈/队列/链表/树

  • 栈:先进后出
  • 队列:先进先出
  • 链表:主要有一个头节点,通过next一直不停的指向下一个节点
  • 树:主要是二叉树,核心是一个根节点,然后指向左右节点,最终可以得到整个树的信息

文件操作

  • fs.mkdir 创建目录
  • fs.rmdir 删除目录
  • fs.stat() 文件状态 可以判断是目录还是文件
  • fs.readdir 读取目录
  • fs.unlink() 删除文件

缓存

强缓存

通过浏览器请求资源,第一次的请求,肯定不缓存的,但是后续的处理,都可以缓存,设置了强缓存之后,就不会向浏览器请求资源,而是从浏览器的缓存中取用

Cache-Control no-cache 每次都向服务器发送请求,也会缓存到浏览器,但是会让强缓存失效 Cache-Control no-store 每次都向服务器发送请求,不会缓存到浏览器,让缓存失效

res.setHeader('Cache-Control', 'max-age=10')  // 1.1之后 
res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString()) //1.1之前
const http = require('http');
const path = require('path');
const url = require('url');
const mime = require('mime');
const fs = require('fs');
const fsp = fs.promises
const  {createReadStream} = fs
const server = http.createServer(async (req, res) => {
    const {pathname} = url.parse(req.url)
    const filePath = path.join(__dirname,'public',pathname)
    // 文件情况
    try {
        // 设置强缓存
        res.setHeader('Cache-Control', 'max-age=10')  // 1.1之后 
        res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString()) //1.1之前
        const fileInfo = await fsp.stat(filePath)
        if (fileInfo.isFile()) {
            res.setHeader('Content-Type', mime.getType(filePath)+';charset=utf-8')
            createReadStream(filePath).pipe(res)
        } else {
            // 寻找目录下的index.html
            const indexPath = path.join(filePath,'index.html')
            // 判断文件是否存在
            try {
                await fsp.access(indexPath)
                res.setHeader('Content-Type', 'text/html;charset=utf-8')
                createReadStream(indexPath).pipe(res)
            } catch(e) {
                console.log('-----------------------------', e);
                res.statusCode = 404
                res.end('not found')
            }
        }
    } catch (err) {
        console.log('----------------------------e-', err);
        res.statusCode = 404
        res.end('not found')
    }
})

server.listen(3000, () => {
    console.log('server start');
})

屏幕快照 2021-08-19 下午4.41.21.png

协商缓存

  • 'Cache-Control', 'no-cache' 让强缓存失效,在第一次请求资源的时候,服务端会告诉客户端,资源修改的时间,之后客户端请求资源的时候,会带上上次服务端给的时间,然后服务端比对上次的修改时间和请求所带的时间是否一致,不一致就返回现有的资源,一致就不返回资源,让客户端使用浏览器的缓存资源
const ctime = fileInfo.ctime.toGMTString()
res.setHeader('Last-Modified', ctime)
if (req.headers['if-modified-since'] === ctime) {
    res.statusCode = 304
    res.end()
} else {
    res.setHeader('Content-Type', mime.getType(filePath)+';charset=utf-8')
    createReadStream(filePath).pipe(res)
}
const http = require('http');
const path = require('path');
const url = require('url');
const mime = require('mime');
const fs = require('fs');
const fsp = fs.promises
const  {createReadStream} = fs
const server = http.createServer(async (req, res) => {
   
    const {pathname} = url.parse(req.url)

    const filePath = path.join(__dirname,'public',pathname)
    // 文件情况
    try {
        // 设置强缓存
        res.setHeader('Cache-Control', 'no-cache') 

        const fileInfo = await fsp.stat(filePath)

        const ctime = fileInfo.ctime.toGMTString()
        
        res.setHeader('Last-Modified', ctime)

        if (fileInfo.isFile()) {
            if (req.headers['if-modified-since'] === ctime) {
                res.statusCode = 304
                res.end()
            } else {
                res.setHeader('Content-Type', mime.getType(filePath)+';charset=utf-8')
                createReadStream(filePath).pipe(res)
            }
            
        } else {
            // 寻找目录下的index.html
            const indexPath = path.join(filePath,'index.html')
            // 判断文件是否存在
            try {
                await fsp.access(indexPath)
                if (req.headers['if-modified-since'] === ctime) {
                    res.statusCode = 304
                    res.end()
                } else {
                    res.setHeader('Content-Type', 'text/html;charset=utf-8')
                    createReadStream(indexPath).pipe(res)
                }
            } catch(e) {
                res.statusCode = 404
                res.end('not found')
            }
        }
    } catch (err) {
        console.log('----------------------------e-', err);
        res.statusCode = 404
        res.end('not found')
    }

})

server.listen(3000, () => {
    console.log('server start');
})

屏幕快照 2021-08-19 下午5.38.57.png

Last-Modified 是根据文件的修改时间来确定要不要更新请求资源,当先修改文件,然后再把文件恢复,这样文件的内容没变化,但是时间会变化。这样浏览器请求资源的时候,服务器就会重新发送,就会造成缓存失效。

  • Etag
const http = require('http');
const path = require('path');
const url = require('url');
const mime = require('mime');
const crypto = require('crypto');  // md5摘要/加密
const fs = require('fs');
const fsp = fs.promises
const  {createReadStream} = fs
const server = http.createServer(async (req, res) => {
   
    const {pathname} = url.parse(req.url)

    const filePath = path.join(__dirname,'public',pathname)
    // 文件情况
    try {
        // 设置强缓存
        res.setHeader('Cache-Control', 'no-cache') 

        const fileInfo = await fsp.stat(filePath)

        if (fileInfo.isFile()) {
            
            const fileContent = await fsp.readFile(filePath)
            const etag = crypto.createHash('md5').update(fileContent).copy().digest('base64')
        
            if (req.headers['if-none-match'] === etag) {
                res.statusCode = 304
                res.end()
            } else {
                res.setHeader('Etag', etag)
                res.setHeader('Content-Type', mime.getType(filePath)+';charset=utf-8')
                createReadStream(filePath).pipe(res)
            }
            
        } else {
            // 寻找目录下的index.html
            const indexPath = path.join(filePath,'index.html')
            // 判断文件是否存在
            try {
                const fileContent = await fsp.readFile(indexPath)
                const etag = crypto.createHash('md5').update(fileContent).copy().digest('base64')
    
                await fsp.access(indexPath)
                if (req.headers['if-none-match'] === etag) {
                    res.statusCode = 304
                    res.end()
                }  else {
                    res.setHeader('Etag', etag)
                    res.setHeader('Content-Type', 'text/html;charset=utf-8')
                    createReadStream(indexPath).pipe(res)
                }
            } catch(e) {
                res.statusCode = 404
                res.end('not found')
            }
        }
    } catch (err) {
        console.log('----------------------------e-', err);
        res.statusCode = 404
        res.end('not found')
    }

})

server.listen(3000, () => {
    console.log('server start');
})

是通过比对文件内容的变化来确定文件是否变化的,但是大文件的比对就很消耗资源,所以会比对文件的大小或者抽取一部分内容比对。

cookie session localStorage sessionStorage 区别

  • cookie
    • 服务端设置,客户端存在,每次请求都会自动携带,保密性差
    • cookie的大小有限制,大约4k,太大页面可能会白屏,同时也浪费流量
    • domain 针对指定的域名生效。.baidu.com. a.baidu.com 就可以访问baidu.com的cookie
    • path 什么路径可以访问cookie
    • expires/max-age 存放时间
    • httpOnly 浏览器无法通过代码获取cookie
    • secure 只能在https下生效
  • session
    • 存放在服务端
    • session基于cookie,比cookie安全一些
  • token
    • jwt 不需要服务器存储,没有跨域限制