// 前端和Server端开发的区别
1. 服务稳定性
server端可能会遭受各种恶意攻击和误操作
单个客户端可以意外挂掉,但是服务端不能
pm2进程守护
2. 考虑CPU和内存 // 优化、扩展
客户端独占一个浏览器,内存和CPU都不是问题
server端要承载很多请求,CPU和内存都是稀缺资源
stream写日志,使用redis存session
3. 日志记录
前端也会参与写日志,但只是日志的发起方,不关心后续
server端要记录日志、存储日志、分析日志,前端不关心
4. 安全
server端要随时准备接收各种恶意攻击,前端则少很多
如: 越权操作,数据库攻击等
语法xss攻击和sql注入
5. 集群和服务拆分
产品发展速度快,流量可能会迅速增加
如何通过扩展机器和服务拆分来承载大流量
----------------------------------------------------------------------------------------------
// 什么是nodejs
1. nodejs不是一门语言,不是库,不是框架,是一个js运行时环境,可以解析和执行js代码
2. nodejs是javascript的运行时,构建在chrome的v8引擎,nodejs用了事件驱动,非阻塞I/O模型
3. 阻塞: I/O时进程休眠等待I/O完成后进行下一步
4. 非阻塞: I/O时函数立即返回,进程不等待I/O完成
5. I/O操作很慢,是操作系统底层,读写硬盘数据
6. 事件驱动: I/O等异步操作结束后的通知,观察者模式
7. cpu密集: 计算、逻辑判断、压缩、解压、加密、解密
8. I/O密集: 存储设备、网络设施的读取、文件操作、数据库
9. 进程: 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,简单理解是一个运行的程序
10. 多进程: 启动多个进程,多个进程可以一块执行多个任务,原理是快速切换
11. 线程: 进程内一个相对独立的、可调度的执行单元,与同属一个进程的线程共享进程的资源
12. 多线程: 启动一个进程,在一个进程内启动多个线程,这样多个线程也可以一块执行多个任务
13. nodejs的单线程: 单线程只是针对主进程,I/O操作系统底层是多线程调度
14. 单线程不是单进程
15. 浏览器是多进程的
16. Node 是一个基于 V8 引擎的 Javascript 运行环境,它使得 Javascript 可以运行在服务端,直接与操作系统进行交互,与文件控制、网络交互、进程控制等
Chrome 浏览器同样是集成了 V8 引擎的 Javascript 运行环境,与 Node 不同的是他们向 Javascript 注入的内容不同,
Chrome 向 Javascript 注入了 window 对象,Node 注入的是 global,这使得两者应用场景完全不同,
Chrome 的 Javascript 所有指令都需要通过 Chrome 浏览器作为中介实现
17. 浏览器中的 Web Worker 开启多进程只是把 js 放到这个多开的进程中的线程中去执行,然后再和原来的进程进行通信交互
18. 在 Node 中开启多进程
1. child_process.fork 开启子进程
// compute.js
function getSum() {
let sum = 0
for (let i = 0; i < 1000; i++) {
sum +=i
}
return sum
}
process.on('message', data => {
console.log('子进程 id', process.pid)
console.log('子进程接收到的信息', data)
const sum = getSum()
process.send(sum) // 发送消息给主进程
})
// http.js
const http = require('http')
const fork = require('child_process').fork
const server = http.createServer((req, res) => {
if (req.url === 'get-sum') {
console.log('主进程 id', process.pid)
// 开启子进程
const computeProcess = fork('./compute.js')
computeProcess.send('开始计算') // 和子进程交互,告诉子进程开始工作
computeProcess.on('message', data => {
console.log('主进程接收到的信息', data)
res.end('sum is' + data)
})
computeProcess.on('close', () => {
console.log('子进程因报错而退出')
computeProcess.kill()
res.end('error')
})
}
})
2. cluster.fork 开启集群 // 类似 pm2
const http = require('http')
const cpuCoreLength = require('os').cpus().length
const cluster = require('cluster')
// 当前是否是主进程
if (cluster.isMaster) {
for (let i = 0; i < cpuCoreLength; i++) {
cluster.fork() // 开启子进程
}
cluster.on('exit', worker => {
console.log('子进程退出')
cluster.fork() // 进程守护
})
} else {
// 多个子进程会共享一个 TCP 连接,提供一份网络服务
const server = http.createServer((req, res) => {
res.writeHead(200)
res.end('done')
})
server.listen(3000)
}
----------------------------------------------------------------------------------------------
// global变量,使用时不需要require
1. commonjs
2. process // 进程
const { argr, argr0, execArgr, execPath, env } = process // 进程
argr是一个数组,前2个是固定的,1是启动时使用的命令路径,2是当前执行文件的路径
argr0是argr第一个值的引用
execArgr可以拿到写在文件名之前的特殊参数 // node 参数 文件名
execPath调用脚本的路径,相当于argr的第一个值
env是各种配置参数
process.cmd() // 返回process执行的路径
3. timer // setImmediate()、setTimeout()、nextTick()
setImmediate(() => { code }) // 把当前函数放到下一个队列的队首
process.nextTick(() => { code }) // 把当前函数放到当前队列的最后一个
process.nextTick < setTimeout < setImmediate // process.nextTick最快,setImmediate最慢,一般用setImmediate
4. Buffer // 二进制
----------------------------------------------------------------------------------------------
// CommonJs
1. 每个文件是一个模块,有自己的作用域
2. 在模块内部module变量代表模块本身
3. module.exports属性代表模块对外接口
// require规则
1. /表示绝对路径
2. ./表示相对路径
3. 支持js、json、node扩展名,不写则依次尝试
4. 不写路径直接写名字引入则认为是自带的模块或者各级node_modules内的第三方模块
// require特性
1. module被加载引用时会执行,加载后缓存,只执行一次
2. 一旦出现某个模块被循环加载,就只输出已经执行的部分,还未执行的部分不会输出
3. exports = module.exports,不能改变exports的指向
4. exports = { a: 1, b: 2 } // 错误
5. module.exports = { a: 1, b: 2 } // 正确
6. module.exports.fn1 = s1
const mod = require('路径')
mod.fn1
7. module.exports = {
s1,
s2
}
const { s1, s2 } = require('路径')
----------------------------------------------------------------------------------------------
// debug
1. 在命令行执行或在 package.json 的 scripts 中添加命令,node --inspect-brk index
在代码中输入 debugger,在浏览器中输入'chrome://inspect',点击选择你运行的文件
这样就能在浏览器中调试 node 代码
2. 在vscode中打断点,可以打条件断点
----------------------------------------------------------------------------------------------
// 基础API——Path路径,操作系统不同,方法不同
1. const path = require('path')
2. path.normalize('路径') // 自动修复路径的错误
3. path.join('路径1', '路径2', ...) // 拼接路径,包含path.normalize()
4. path.resolve('相对路径') // 把相对路径解析成绝对路径
5. path.basename('路径') // 返回文件名
6. path.dirname('路径') // 返回所在的绝对路径
7. path.extname('路径') // 返回扩展名
8. path.parse('路径') // 把一个路径拆分成一个对象包括5、6、7、root、name
9. path.format('对象') // 把一个对象解析成一个路径,参数是一个对象
10. path.sep // 返回路径的分隔符,'/'
11. path.delimiter // 返回path的分隔符,':'
12. path.win32 // 操作系统,可以通过这2个属性调用与自己操作系统不同的方法和属性
13. path.posix // 操作系统,可以通过这2个属性调用与自己操作系统不同的方法和属性
14. __dirname、__filename总是返回文件的绝对路径 // require时自带的
15. process.cmd() // 总是返回执行node命令所在的文件夹路径
16. 在require方法中使用'./'相对的是当前文件所在的文件夹,而在其他地方和process.cmd()一样,相对的是node启动的文件夹
----------------------------------------------------------------------------------------------
// 基础API——Buffer,处理二进制数据流,是global对象,不需要require
1. Buffer用于处理二进制数据流,默认为十六进制表示,(0-255)的数字
2. 实例类似整数数组,大小固定不可改变
3. 使用的堆内存是c++代码,在v8堆外分配物理内存
// Buffer实例化
1. Buffer.alloc(10) // 创建长度为10的Buffer,默认用0填充
2. Buffer.alloc(10, 1) // 创建长度为10的Buffer,用1填充(十六进制)
3. Buffer.allocUnsafe() // 不安全的Buffer,可能包含旧数据
4. Buffer.from([1, 2, 3]) // 创建长度为3的Buffer
5. Buffer.from('text', 'base64') // 第二个参数不传,默认是utf-8
// 静态方法
1. Buffer.bytelength('测试') // 返回字节的长度,一个中文对应三个字节,返回6,英文是一个对应一个
2. Buffer.isBuffer({}) // 判断是否是Buffer对象, 返回布尔值
3. Buffer.concat([b1, b2]) // 拼接Buffer
// 实例方法
1. buf.length // 返回对应的字节数,和内容无关
2. buf.toStirng() // 可传入base64,默认是utf-8
3. buf.fill(10, 2, 6) // 从下标2开始到6填充10
4. buf.equals(buf) // 判断buf内容是否一致,返回布尔值
5. buf.indexOf() // 与js数组一样
6. buf.copy(b, 0, i) // copy到b,从0开始,i结束
// 处理中文乱码
1. const stringDecoder = require('string-decoder').stringDecoder
const decoder = new stringDecoder('utf-8')
decoder.write('字符')
----------------------------------------------------------------------------------------------
// 基础API——event,事件对象,继承了EventEmitter类(events)
1. const EventEmitter = require('events') // 引入
class CustomEvent extend EventEmitter { code } // 继承
const ce = new CustomEvent() // 实例化
ce.on('test', () => { code }) // 绑定,也可用once绑定,只触发一次
ce.emit('test', '参数') // 触发,后面可以用逗号隔开传参,个数不限
ce.removeListener('test1', 'test2', () => { code }) // 移除,后面可以用逗号隔开移除多个事件
ce.removeAllListeners() // 移除全部
----------------------------------------------------------------------------------------------
// 基础API——fs,文件有关,有回调函数,第一个参数保留给异常,当err是null或undefined时则是成功,可操作二进制数据
1. 所有的fs的API都有同步方法,在方法名后加Sync // 同步: fs.readFileSync(),异步: fs.readFile()
2. const fs = require('fs')
3. fs.readFile('文件路径', 'utf-8', (err, data) => { code }) // 读文件,默认是Buffer对象,可用toString()转成字符串,也可以在文件路径后面传第二个参数'utf-8'
4. fs.writeFile('文件路径', '写入内容', { encoding: 'utf-8' }, err => { code }) // 写文件, 可追加可覆盖需在第三参数传flags, 'a'表追加, 'w'表覆盖
5. fs.stat('文件路径', (err, stat) => { stat对象 }) // 文件信息
stat.isFile() // 判断是否是文件
stat.isDirectory() // 判断是否是文件夹
6. fs.rename('文件路径', '新名字', err => { code }) // 重命名
7. fs.unlink('文件路径', err => { code }) // 删除
8. fs.readdir('文件夹路径', (err, files) => { code }) // 读文件夹
9. fs.mkdir('新建的文件名', err => { code }) // 创建文件夹
10. fs.rmdir('文件夹路径', err => { code }) // 删除文件夹
11. fs.watch('监听的路径', { recursive: true }, (event, filename) => { code }) // 监听, recursive是递归监听,event是发生了什么变化,filename发生变化的文件名
12. const rs = fs.createReadStream('路径') // 读流,有方向的数据,读一点给一点,而不是全部读完再一起给
rs.pipe(process.stdout) // 输出到控制台,pipe是输出,process.stdout是控制台
13. const ws = fs.createWriteStream('路径') // 写流,有方向的数据,写一点给一点,而不是全部写完再一起给
ws.write('流') // 写
rs.pipe(ws) // 把读到的数据用流的方式传递到写的地方, 可用于复制文件
rs.on('data', chunk => {}) // 每次写的时候都会触发data事件
rs.on('end', () => {}) // 当写完会触发end事件
在http处理中也可以直接使用 rs.pipe(res) 即执行了 res.end('数据流')
14. process.stdin.pipe(process.stdout) // 标准流的输入输出, 可在node环境测试
----------------------------------------------------------------------------------------------
// promisify把异步函数转成promise解决回调地狱
1. const promisify = require('util').promisify
const read = promisify(require('fs').readFile)
read('路径').then(data => { code }).catch(err => { code })
2. 也可以通过async和await方式,在同步中try/catch相当于异步中的.catch
----------------------------------------------------------------------------------------------
// 登录流程
1. 当用户请求 API 时, 会设置一个id到 cookie 中, 后端获取到 id 后判断是否存在匹配的信息
2. session原理是把每个请求的客户端都生成一个对应的id如userId, 然后通过Set-Cookie把userId存储到客户端的cookie中
3. 生成的id是可以加密的, 然后再把用户信息等内容存储到id对应的变量中去, 实现绑定, 原理和cookie中直接存储用户信息差不多
4. 把session存储到redis中, 只是把上面3中的那些原本在变量中的数据存储到redis数据库中而已
5. 在不使用connect-redis这个库时, 存储到redis中需要自己set进去, 而用了就只要配置文件就可以
6. 在所有接口调用前, 可以加一个中间件验证是否有登录或权限等
----------------------------------------------------------------------------------------------
// 日志
1. IO 操作的性能瓶颈
1. IO 包括网络IO和文件IO
2. 相比于 CPU 计算和内存读写,IO 的突出特点就是:慢!
3. 如何在有限的硬件资源下提高 IO 的操作效率?
1. stream
2. 日志记录访问服务器的各种东西 // accesslog访问日志、eventlog自义定日志、errorlog错误日志
const fs = require('fs')
const path = require('path')
// 写日志
function writeLog(writeStream, log) {
writeStream.write(log + '\n') // 关键代码
}
// 生成 write Stream
function createWriteStream(fileName) {
const fullFileName = path.join(__dirname, '../', '../', 'logs', fileName)
const writeStream = fs.createWriteStream(fullFileName, {
flags: 'a'
})
return writeStream
}
// 写访问日志
const accessWriteStream = createWriteStream('access.log')
// 还可以扩展其他日志的写入,方式都一样
function access(log) {
writeLog(accessWriteStream, log)
}
module.exports = {
access
}
在 app.js 中使用
access(`${req.method} -- ${req.url} -- ${req.headers['user-agent']} -- ${Date.now()}`)
3. 通过linux的crontab命令即(定时任务)执行sh脚本拆分日志文件,日志前可加上时间节点用于拆分
可设置时间每天或每小时、分钟等把日志文件 'access.log' 拷贝并重命名为 '2020-09-17.access.log'
然后清空'access.log'文件,继续积累日志
crontab -e // 编辑crontab文件任务
1. 进入后执行 sh 脚本命令,下面的脚本一般是放在项目中的,就是一个 .sh 文件
#!/bin/sh
# cd /Users/wfp/Project/video-tutorial/node-tutorial/code-demo/blog-1/logs
# cp access.log $(date +%Y-%m-%d).access.log
# echo "" > access.log
2. 进入定时任务后编辑 * 0 * * * sh 路径.sh
1. 第一个 * 表示分钟
2. 第二个 * 表示小时
3. 第三个 * 表示日期
4. 第四个 * 表示月份
5. 第五个 * 表示星期
6. 后面表示执行的命令
crontab -l // 查看有什么定时任务
4. 日志是按行存储的,一行就是一条日志,使用readline逐行读取文件,分析日志内容 // readline基于stream
因为 createReadStream 读出来的不一定是一行,而是一点,所以需要 readline 来读行
主要用于日志分析,比如分析 chrome 的占比等,读一行就+1
const readline = require('readline')
const fs = require('fs')
const path = require('path')
const fileName = path.join(__dirname, access.log)
const readStream = fs.createReadStream(fileName) // 创建读流
// 创建 readline 对象
const rl = readline.createInterface({
input: readStream // 读取这个文件
})
rl.on('line', () => {}) // 逐行读取触发
rl.on('close', () => {}) // 读取完成触发
----------------------------------------------------------------------------------------------
// 安全
1. sql注入: 通过在表单输入sql代码给服务器执行 // 在username中输入 chenj -- 就能实现免密登录
select username, realname from users where username = 'zhangsan' and password = '123'
当上面的用户名 'zhangsan' 变成 'zhangsan' -- 时,后面的 sql 就会被注释掉,从而不需要验证密码
除了注释后面的 sql 以外,还能加入任何其他的 sql 代码,比如删除数据等操作
比如 'zhangsan'; delete from users -- 这样就会把数据表给删除掉
解决方法: 使用mysql的escape函数处理输入内容 // 在拼接 sql 时,变量使用 escape 包裹执行
原理: 把输入的单引号进行转义,在sql中'--'是注释后面代码的意思,应用在所有能拼接sql语句的地方
在 controller 执行 sql 的时候,需要使用 mysql.escape 方法进行转义拼接的参数,然后再执行 sql 语句
username = mysql.escape(username)
password = mysql.escape(password)
`select username, realname from users where username=${username} and password=${password}`
2. xss攻击: 在页面展示内容掺杂js代码,也就是说需要用户输入,并且会在页面上展示的内容,就需要进行处理转义
<script>alert('里面的代码在页面渲染的时候会执行')</script>
解决方法: 把左右的尖括号转义或使用npm install xss,是一个函数
3. 密码加密: 数据库信息泄漏,获取到用户名和密码,再去尝试登录其他系统
解决方案:将密码加密,即便拿到密码也不知道明文,node自带加密库,crypto
const crypto = require('crypto')
// 密钥
const SECRET_KEY = 'chenj666'
// md5加密
function md5(content) {
let md5 = crypto.createHash('md5')
return md5.update(content).digest('hex') // 16进制
}
// 加密函数
function genPassword(password) {
const str = `password=${password}&key=${SECRET_KEY}` // 这里字符串的拼接规则是可以改变的,只要最后是字符串就行
return md5(str)
}
module.expotrs ={
genPassword
}
----------------------------------------------------------------------------------------------
// 自动重启node,相当于webpack的devServer
1. npm install supervisor
supervisor 文件名
2. 当文件发生变化时,不用重新运行命令,相当于监听
3. npm install nodemon
nodemon 文件名
----------------------------------------------------------------------------------------------
// Node 项目开发
1. node项目入口一般在'/bin/www.js'文件中
2. 在开发环境中可以使用 nodemon 包来自动重启node项目,否则每次修改都需要重启
3. 可以使用 corss-env 包来在package.json的script中获取env
"dev": "corss-env NODE_ENV=dev nodemon ./bin/www.js"
4. 在项目架构时需要合理的拆分
'bin/www.js' -> 'app.js' -> 'router' -> 'controller' -> 'db' -> 'model'
5. 路由和API的关系
API是前端和后端或不同端之间对接的术语,包括url路由、req输入、res输出
路由是API的一部分
6. 后端可以通过设置cookie
res.setHeader('Set-Cookie', 'username=cj; path=/; httpOnly; expires=过期时间必须是UTC格式')
当后端设置了 username 的 cookie 后并且加了 httpOnly,前端是不能改的,也获取不到的
但是前端也可以加 username 的 cookie,也就是说会存在两个 username 的 cookie,一个前端设置,一个后端
在请求的时候,两个都会传给服务端,但是服务端设置的会排在后面,根据服务端的解析规则,后面的会覆盖前面的
也就是说服务端获取到的就是前面服务端设置的,而不是前端设置的,这个基于解析规则,如果规则变了
获取到的可能是前端设置的,但是一般解析规则是后面覆盖前面的逻辑
7. require 加载资源类型
1. .js ==> module.exports/exports
2. .json ==> JSON.parse
3. any ==> 当做 .js 文件处理
----------------------------------------------------------------------------------------------
// 版本号x.y.z,如0.0.1
1. x: 不保证兼容,全新, 1.0.0,x为偶数时是稳定版,x为积数时是不稳定版
2. y: 有新增的功能同时兼容以前的功能,0.1.0
3. z: 有bug修改了, 0.0.2
4. 1.2.*表示z位最新不固定,和~1.2.0一样
5. 2.x表示y和z位最新不固定,和^2.0.0一样
6. alpha:内部测试版本,除非是内部测试人员,否则不推荐使用,有很多 bug
7. beta:公测版本,消除了严重错误,还是会有缺陷,这个阶段还会持续加入新的功能
8. rc:Release Candidate,发行候选版本,这个版本不会加入新的功能,主要是排错,修改 bug
9. release:正式版本
----------------------------------------------------------------------------------------------
// ESlint
1. env: 脚本的运行环境
2. globals: 额外的全局变量
3. rules: 启用的规则
4. off === 0 // 关闭
5. warn === 1 // 开启,使用警告级别的错误,不会让程序退出
6. error === 2 // 开启,使用错误级别的错误,程序会退出
7. 在官网中的规则前面有√符号的表示推荐使用,前面有小扳手的表示可以通过命令行进行修复 // eslint --fix .
8. /* eslint-disable */ code /* eslint-enable */ // 中间的代码不受全部规则验证
9. /* eslint-disable no-alert, no-console */ code /* eslint-enable no-alert, no-console */ // 中间的代码不受no-alert, no-console验证
10. code // eslint-disable-line 这行不受全部规则验证
11. // eslint-disable-next-line 下一行不受全部规则验证
12. eslint --init // 创建一个配置规则
13. 可以通过npm install pre-commit这个包让代码如果有错误时,不让上传到git中去
----------------------------------------------------------------------------------------------
// npm参数
1. --save-dev === -D // 安装到devDependencies中(开发环境)
2. --save === -S === 不写 // 安装到dependencies中(生产环境)
3. -g // 全局安装
4. 全局安装的npm不会出现在package.json中,可以直接使用命令方式调用,如果不是全局安装则要用npx启动,或者在package.json的script中配置命令才能使用npm run的方式
5. 没有init时不加-S或-D时会安装到项目目录下,但不会写进package.json中
----------------------------------------------------------------------------------------------
// npm init生成的文件目录
1. README.md // 项目介绍
2. .gitignore // 不上传到git仓库的文件
匹配模式前加'/'表示项目根目录
匹配模式最后加'/'表示是目录,而不是文件
匹配模式前加'!'表示取反
'*'表示任意个字符
'?'匹配任意一个字符
'**'匹配多级目录
3. .npmignore // 不上传到npm仓库的文件,如果没有这个文件,会自动匹配.gitignore里面的配置
4. .editorConfig // 约定代码风格
5. .eslintrc.js // eslint代码校验
----------------------------------------------------------------------------------------------
// 常用的 npm 命令
1. 清除缓存
1. npm cache clean --force
2. yarn cache clean
2. 快速删除 node_modules 文件夹
1. npm install rimraf -g
2. rimraf node_modules
3. 查看所有全局安装的 npm 模块
1. npm list -g --depth=0
----------------------------------------------------------------------------------------------
// nvm 配置
1. 更换下载源,打开安装目录下的 settings.txt 文件进入编辑,新增以下配置
1. `node_mirror: https://npmmirror.com/mirrors/node/`
2. `npm_mirror: https://npmmirror.com/mirrors/npm/`
3. 注意不能使用 `https://npm.taobao.org` 相关的域名,此域名已经切换至 `https://npmmirror.com`
2. 查看远程 node 版本命令:nvm ls available
----------------------------------------------------------------------------------------------
// 包如何到达 node_modules 当中的
当执行 npm install 包名 时,npm 会通过以下6步来进行包的安装:
1. 解析依赖树: NPM 首先会根据 package.json 文件中的依赖列表,以及可能存在的锁定文件
(如 package-lock.json )来解析项目的依赖树。从而确定需要安装的包及其版本要求。
2. 检查本地缓存: NPM 会检查本地缓存(位于用户主目录下的.npm 目录)是否已经存在所需的包。
如果包已经存在于缓存中且版本符合要求,则 NPM 会直接从缓存中复制到 node_modules目录,跳过后续步骤。
3. 请求包并下载:如果包不在本地缓存中,NPM 将向远程软件包注册表发送请求,以获取包的元数据和下载链接。
NPM 默认使用官方的 NPM 注册表,也可以配置使用其他私有或定制的注册表。
4. 下载和解压包: NPM 根据获取到的下载链接,将包的压缩文件下载到临时目录中。
然后,NPM4会解压缩该文件,并将解压后的文件移动到 node_modules 目录中。
5. 安装包的依赖项: 对于每个安装的包,NPM 会检查它们的 package.json 文件,查找并安装它们的依赖项。
这个过程会递归进行,直到所有的依赖项都被安装到 node_modules 目录中。
6. 更新锁定文件(可选):如果使用了锁定文件(如package-lock.json),
NPM会更新锁定文件以记录确切安装的版本,以便在将来重现相同的依赖关系。
----------------------------------------------------------------------------------------------
// 整个 npm 的包管理规则,大致分为5点:
// 在 npm2.0 中是树形结构,后面才改成扁平式结构
1. 扁平化结构: NPM 尽可能地将依赖包安装在 node_modules 的顶层,以实现扁平化的结构。
这意味着,即使多个依赖项都需要同一个包的不同版本,也会安装每个版本的单独副本,
而不会形成嵌套的依赖关系。这样可以避免版本冲突和依赖关系的复杂性。
2. 依赖解析: 当一个模块引用另一个模块时, NPM 会根据 require 语句中的模块标识符来解析依赖关系。
它会首先在当前模块的 node_modules 目录下查找被引用的模块。
如果找不到,它会逐级向上查找,直到顶层的 node_modules 目录。
3. 包版本冲突解决:在某些情况下,不同依赖项可能需要同一个包的不同版本,导致版本冲突。
NPM 使用"依赖树"和"依赖优先级"的概念来解决这个问题。当多个依赖项需要不同版本的包时,
NPM 会根据依赖树的结构选择合适的版本,并确保在运行时正确加载。
4. 锁定文件: NPM 的锁定文件(如 package-lock.json)记录了确切安装的包及其版本。
锁定文件可以确保在重复安装相同依赖关系时,使用相同的版本,从而保持一致性。
5. 嵌套依赖: 尽管 NPM 鼓励扁平化结构,但某些情况下会出现嵌套依赖。
当一个依赖项依赖于特5定版本的另一个依赖项时, NPM 会在 node.modules 目录内创建子目录,
将被依赖项安装在子目录下,以维护版本的独立性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463