2025-03-29 14:35:49 +08:00

920 lines
23 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# NodeJS
> Node 是一个构建于 Chrome V8引擎之上的一个 Javascript 运行环境, 作用是让 js 拥有开发服务端的功能
* 使用事件驱动、非阻塞 IO 模型(异步读写)使得它非常的轻量级和高效
* Node 中绝大多数 API 都是异步(类似于 ajax目的是提高性能
* node. js 官网: https://nodejs.org/en/
* 中文文档: http://nodejs.cn/api/
* 英文文档: https://nodejs.dev/en/api/v19/documentation/
* npm 官网 https://www.npmjs.com
## 安装
```sh
# 查看版本
node -v
```
node 版本管理: 安装 `n`
```sh
npm install -g n
# n --help
```
监控 js 变化并重启服务: `node-dev`
```sh
npm i -g node-dev
node-dev app.js
```
## 客户端 js 和服务端 js
客户端 JavaScript 由三部分组成
* ECMAScript确定 js 的语法规范
* DOMjs 操作网页内容
* BOMjs 操作浏览器窗口
node 中的 JavaScript 组成
* ECMAScript
* 核心模块
* 第三方模块
基本的语法和写法和之前的 js 没有本质的区别
* **在 nodejs 中使用 dom 与 bom 的 api 程序会报错**
* 服务器端没有界面
* 不需要操作浏览器和页面元素
## 运行 node. js 程序
`node [js文件路径]`
生产环境: `pm2`
## 模块化
### CommonJS 规范
> 模块必须通过 ` module.exports={xxx:xxx}` 导出对外的变量或接口,通过 `require() ` 来导入其他模块的输出到当前模块作用域中。
CommonJS模块的特点:
* 所有代码运行在当前模块作用域中,不会污染全局作用域
* 模块同步加载,根据代码中出现的顺序依次加载
* 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
==模块使用前要先导入==
```js
/**
* 模块化规范
* CommonJS: 2015年前社区开发, nodejs官方默认
* - 自定义模块
* - 一个js文件就是一个模块
* - 使用 exports 或者 module.exports 暴露:
* exports.xxx = xxx
* module.exports = {xxx:xxx}
* 不能使用 exports = {xxx:xxx} 这是在给变量赋值, 上面的是修改对象的属性
* - 使用 require("模块的路径") 引入, 用变量来接收: const {xxx} = require()
* - 后缀自动补全: 先找js, 再找json
* - 核心模块
* - require("模块名")
* - require("node:模块名")
* nodejs将以下内容视为CommonJS
* 1. 使用.cjs扩展名
* 2. package.json的type属性为CommonJS,且扩展名为js
* 3. package.json不包含type属性,且扩展名为js
* 4. 扩展名是mjs, cjs, json, node, js以外的值, package.json的type属性不是module
*/
(function(exports, require, module, __filename, __dirname){
// 所有的CommonJS模块都会被包装到一个函数里
// exports, require是作为参数传进来的
console.log(arguments) // 证明
})
```
### ES Modules (ESM)
> export 导出, import 导入
```js
/* ES模块: 原生, 2015年es6标准发布
* - mjs 扩展名
* - package.json的type属性设置为module
*
* - 导出: export let a = xxx
* export const b = xxx
* - 导入: import {} from '路径.mjs'
* 改别名 import {a as b} from './test.mjs'
* 开发时要尽量避免 import * as c from './test.mjs' , 按需引用
* - 设置默认导出: export default function sum(){}
* default 后面跟值, export default let a = 0 这种不行, 后面是语句
* 一个模块只有一个默认导出
* - 默认导入: import sum {a} from './test.mjs'
* import {default as sum, a} from './test.mjs'
* 默认导入可以随便起名
* 默认导入可以和按需导入一起用
* - 通过es模块化导入的, 都是常量
* - es模块都是运行在严格模式下
*/
```
### require() 和 import, import()
#### 简单理解
* require(): 在JS本身不支持模块的情况下模拟出来的模块系统, 本质是立即执行函数
* import: 静态引入,在编译时完成模块加载
* import(): 和require()一样是动态加载不同的是它是异步的返回一个Promise对象而require()是同步的
#### 缓存方式
* require 是浅拷贝
```js
// 2.js
let num = 1;
let obj = {
num: 1
};
function add() {
num += 1;
obj.num += 1;
}
module.exports = { num, obj, add };
// testRequire.js
let a = require('./2.js');
console.log(a.num); // 1
console.log(a.obj.num); // 1
a.add();
console.log(a.num); // 1
console.log(a.obj.num); // 2
```
这里的 num 是基本数据类型, require 引入的是它的副本, add 函数里的 num + 修改的是局部变量, 修改不到副本
* import 并不对输出结果进行拷贝,而是直接指向输出结果的引用
```js
// 1.js
let num = 1;
let obj = {
num: 1
};
function add() {
num += 1;
obj.num += 1;
}
export { num, obj, add };
// testImport.js
import * as a from './1.js';
console.log(a.num); // 1
console.log(a.obj.num); // 1
a.add();
console.log(a.num); // 2
console.log(a.obj.num); // 2
```
* import() 也是引用
```js
// 1.js
let num = 1;
let obj = {
num: 1
};
function add() {
num += 1;
obj.num += 1;
}
export { num, obj, add };
// testImportFunction.js
let a = await import('./1.js');
console.log(a.num); // 1
console.log(a.obj.num); // 1
a.add();
console.log(a.num); // 2
console.log(a.obj.num); // 2
```
#### 相互引用
* require()无法引入ES6模块
* import可以引入CommonJS模块, 是把module.exports对象整体引入类似于对exports default的接收直接用一个变量 default 来接收。
```js
// 2.js
let num = 1;
let obj = {
num: 1
};
module.exports = { num, obj, add };
// testImportFunction.js
let a = await import('./2.js');
console.log(a.default.num); // 1
console.log(a.default.obj.num); // 1
```
* import()整体接收module.exports这个对象并把它放在default属性下
```js
// 2.js
let num = 1;
let obj = {
num: 1
};
module.exports = { num, obj, add };
// testImportFunction.js
let a = await import('./2.js');
console.log(a.default.num); // 1
console.log(a.default.obj.num); // 1
```
## node. js 核心模块
Node 应用是由模块组成的Node 遵循了 `CommonJS`的模块规范,来隔离每个模块的作用域,使每个模块在它自身的命名空间中执行。
### fs文件模块(读写文件)
先导入文件模块
```js
const fs = require('fs')
```
#### readFile异步读取
```js
fs.readFile(path[, options], callback(err,data))
/**
* 第一个参数:文件路径
* 第二个参数:编码格式 可选参数默认为buffer二进制,buffer:数据缓冲区)
* 第三个参数:读取回调操作(异步操作)
* err:如果读取成功, err为null,否则读取失败(一般文件路径错误或者找不到文件)
* data:读取到的数据(字符串|二进制)
*/
```
示例:
```js
fs.readFile('./data/aaa.txt','utf-8',(err,data)=>{
//按utf-8编码读取, 解决中文乱码
if(err){
console.log(err);
//抛出异常throw的作用就是让node程序终止运行方便调试
throw err;
}else{
console.log(data);
};
});
```
同步读取(几乎不用,会阻塞,一般在异步的api后面加上Sync就是同步):
```js
let data = fs.readFileSync('./data/aaa.txt','utf-8')
```
#### writeFile异步写入
```js
fs.writeFile(file, data[, options], callback(err))
/**
* 第一个参数:文件路径
* 第二个参数:要写入的数据
* 第三个参数:文件编码 默认utf-8
* 第四个参数: 异步回调函数
* err: 如果成功err为null.否则读取失败
*/
```
1. 默认写入会覆盖
2. 如果文件名不存在,新创建再写入
3. 如果文件夹不存在,报错
示例:
```js
fs.writeFile('./data/bbb.txt','测试','utf-8',(err)=>{
if(err){
throw err;
}else{
console.log('写入成功');
};
});
```
#### 异步追加
异步地追加数据到文件,如果文件尚不存在则创建文件
```js
fs.appendFile(path, data[, options], callback(err))
```
#### Promise版本的fs方法
```js
const fs = require("node:fs/promises")
fs.readFile(path.resolve(__dirname, './hello.js'))
.then(buffer=>{
console.log(buffer.toString()) // 也可以直接用toSting()方法转成字符串
})
.catch(err=>{
console.log(err)
})
// 或者
;(async()=>{
try{
const buffer = await fs.readFile(path.resolve(__dirname, './hello.js')
console.log(buffer.toString())
}catch((err)=>{
console.log(err)
})
})()
```
```js
// 常见方法
fs.mkdir() // 创建目录
fs.rmkdir() // 删除目录
fs.rm() // 删除文件
fs.rename() // 重命名
fs.copyFile() // 复制
```
### path路径模块
==在服务端开发中,一般不要使用相对路径,而使用绝对路径==
```js
const path = require('path')
```
#### nodejs中的绝对路径和相对路径
* node中的相对路径 `./` 不是相对于当前文件所在路径而是相对于执行node命令的文件夹路径(当前被执行的文件所在的文件夹路径).
* 解决方案在nodejs中每一个js文件都有两个全局属性它可以帮助我们获取到文件的绝对路径
* \_\_filename:当前js文件绝对路径
* ==\_\_dirmame:当前js文件所在目录的绝对路径==
* windown中路径 用双反斜杠 `\\` 而不是 `\`
示例:
```js
const fs = require('fs')
let path = __dirname + '/aaa.txt'
console.log(path)
fs.readFile(path,'utf-8',(err,data)=>{
if(err){
console.log(err);
throw err;
}else{
console.log(data);
};
});
```
#### join()方法 路径拼接
```js
path.join([...paths])
/*使用path模块拼接文件路径与使用'+'连接符拼接的好处
1.会自动帮我们正确添加路径分隔符 '/',我们无需手动添加
2.当我们路径格式拼接错误的时候,能自动帮我们转换正确的格式
*/
```
示例:
```js
let filePath = path.join(__dirname, './page/login.html')
```
#### resolve()方法 路径处理
```js
// 把一个路径或路径片段的序列解析为一个绝对路径
// 传入路径从右至左解析,遇到第一个绝对路径解析停止
// 如果没有传入参数,将只返回当前根目录
path.resolve([...paths])
```
示例:
```js
// "/b" 就是遇到的第一个绝对路径
path.resolve('/a', '/b', 'c') // /b/c
path.resolve('/a', './b', 'c') // /a/b/c
//因为没有遇到第一个绝对路径,所以会一直向上解析(根目录路径/a/b/c)
path.resolve('a', 'b', 'c') // /Users/siyuan/Desktop/example/node测试/a/b/c
```
### process
> 获取进程信息, 或者对进程进行操作
使用: 全局变量, 直接使用
### 属性和方法
* process.exit(code): 结束当前进程, code默认0
* process.nextTick(()=>{}): 将函数插入tick队列, 调用栈=>tick=>微任务队列=>宏任务队列
## 服务器基础
### 基本的访问流程
1. 输入主机地址
2. 指定端口(如果没有指定, 默认是80)
3. 指定需要访问的资源路径
4. 发起请求
5. 获取服务器返回的结果并处理
### http协议
超文本传输协议(**H**yper**T**ext **T**ransfer **P**rotocol), 是基于TCP/IP协议之上的应用层协议
> HTTP是一个客户端终端用户和服务器端网站请求和应答的标准
> 客户端和服务器的通信必须遵守某种协议http协议就是最常见的一种
### 端口
端口是通过端口号来标记的端口号只有整数范围是从0 到655352^16-1
常见的端口号:
* 80web服务器端口
* 3306mysql数据服务器端口
查询端口状态`netstat`
以数字格式显示地址和端口信息`netstat -n`
### 常见的状态码
* 200: 请求已成功,请求所希望的响应头或数据体将随此响应返回
* 301: 请求资源永久重定向
* 302: 请求资源临时重定向
* 403: 无授权
* 404: 请求失败,请求所希望得到的资源未被在服务器上发现
* 500: 服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。一般来说,这个问题都会在服务器端的源代码出现错误时出现
### 返回数据的格式
* text/html格式html代码浏览器会以html语法解析
* text/css:样式浏览器会以css语法解析
* application/javascript:js代码浏览器会以js语法解析
* application/json:json格式字符串描述从服务器返回的数据
## hellow app.js
流程:
1. 导入模块 `const * = require(*)`
2. 创建服务器`const server = http.creatServer()`
3. 监听端口`server.listen(端口, ()=>{})`
4. 响应请求, 进行事件处理`server.on('request', (req,res)=>{})`
注意点:
* `req.url`可以获取当前用户请求的url
* 中文乱码
```js
res.setHeader('Content-type','text/html;charset=UTF-8')
// html页面不需要, 头部已有
```
* 客户端没有指定url,默认为`/`
```js
//1.导入http模块
//2.创建服务器
//3.监听端口
/*
第一个参数:端口号
第二个参数ip地址 默认不写就是本机ip127.0.0.1
第三个参数:一个回调函数,启动时会调用
*/
//4.处理请求
```
```js
const http = require('http')
const server = http.createServer()
server.listen(3000,'127.0.0.1',(err)=>{
console.log('服务器开启成功: http://127.0.0.1:3000');
})
server.on('request',(req,res)=>{
// 所有请求都响应 'hello word'
res.end('hello word')
})
```
## 响应页面
```js
const http = require('http')
const fs = require('fs')
const path = require('path')
const server = http.createServer()
server.listen(3000, () => {
console.log('已开始监听 http://127.0.0.1:3000')
})
server.on('request', (req, res) => {
// 每个请求,都执行这里的代码
const url = req.url
console.log(url)
switch (url) {
case '/':
case '/index':
res.end('hello word')
break
case '/login':
// 页面head已经有编码格式
fs.readFile(path.join(__dirname, './page/login.html'), (err, data) => {
if (err) {
res.end('404 not found')
} else {
res.end(data)
}
})
break
default:
res.end('404 not found')
break
}
})
```
## 允许跨域
```js
res.setHeader('Access-Control-Allow-Origin', '*')
```
## 响应不同的请求
`req.method`获取请求的类型
### get请求
```js
const http = require('http')
const fs = require('fs')
const path = require('path')
const server = http.createServer()
server.listen(3000, () => {
console.log('127.0.0.1:3000')
})
server.on('request', (req, res) => {
const url = req.url
console.log('url:', url)
const method = req.method
console.log('method', method)
if (url === '/getUserList' && method === 'GET') {
fs.readFile(path.join(__dirname, './4-user.json'), 'utf-8', (err, data) => {
if (err) {
console.log(err)
res.end('404')
} else {
res.setHeader('Content-type', 'text/html;charset=UTF-8')
res.end(data)
}
})
return
}
res.end('hello word')
})
```
### post请求
```js
/**
* node支持大容量的参数传递, 它会分批接收参数, 接收参数会触发两个事件
* 1.给req注册一个data事件
* req.on('data', (chunk)=>{})
* 每接收一次参数就触发一次, 接收到的chunk是字符串格式
* 如果参数较多,它支持分批进行参数的接收,当客户端每发送一次数据流,都会触发里面的回调函数,我们需要主动将这些数据拼接起来
*
* 2.给req注册一个end事件
* req.on('end', ()=>{})
* 当客户端post数据全部发送完毕之后就会触发这个事件
*
* 3.使用querystring模块解析接收完成的post参数数据
*/
```
```js
// 服务器
const http = require('http')
const fs = require('fs')
const path = require('path')
// 解析参数的querystring模块
const querystring = require('querystring')
const server = http.createServer()
server.listen(3000, () => {
console.log('127.0.0.1:3000')
})
server.on('request', (req, res) => {
// 允许跨域
res.setHeader('Access-Control-Allow-Origin', '*')
const url = req.url
console.log('url:', url)
const method = req.method
console.log('method', method)
if (url === '/login' && method === 'POST') {
let postData = ''
// 1.注册一个data事件
req.on('data', chunk => {
//具体多少次,取决于客户端带宽
postData += chunk
})
req.on('end', () => {
// 2.给req注册一个end事件
// 3.使用querystring模块解析接收完成的post参数数据
let postObj = querystring.parse(postData)
console.log('postObj',postObj)
if (postObj.username == 'admin' && postObj.password == '123456') {
res.end('yes')
} else {
res.end('no')
}
})
} else {
res.end('hello word')
}
})
```
登录页
```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>
<form method="post" action='http://127.0.0.1:3000/login'>
用户名:<input type="text" name='username' placeholder="请输入用户名"><br>
密码: <input type="password" name='passwrod' placeholder="请输入密码"> <br>
<input type="submit" value="提交">
</form>
</body>
</html>
```
## 模块化
可以先把路由和响应拆分出来
以一个登录页面为例:
### 程序入口
```js
// app.js
// 路由被拆分到路由模块
const http = require('http')
const router = require('./js/router')
const server = http.createServer()
server.listen('3000', function () {
console.log('http://127.0.0.1:3000')
})
server.on('request', function (req, res) {
router(req, res)
})
```
### 路由模块
```js
// js/router.js
// 响应被拆分到响应模块
const handler = require('./handler')
module.exports = function (req, res) {
//获取请求方式
let method = req.method.toLowerCase()
//获取请求url
let url = req.url
//判断请求方式和url
//读取注册页面并返回
if (method == 'get' && url == '/register') {
handler.getRegisterPage(req, res, url)
}
//静态资源处理
else if (method == 'get' && url.indexOf('/css/') != -1 || url.indexOf('/js/') != -1 || url.indexOf('/images/') != -1) {
handler.getStaticSource(req, res, url)
}
//实现用户注册
else if (method == 'post' && url == '/register') {
handler.userRegister(req, res, url)
}
}
```
### 响应处理
```js
const fs = require('fs')
const path = require('path')
const mime = require('mime')
module.exports = {
//响应页面注册
getRegisterPage: function (req, res) {
fs.readFile(path.join(__dirname, '../views/register.html'), function (err, data) {
if (err) {
res.end('404')
throw err
} else {
res.end(data)
}
})
},
//响应静态资源
getStaticSource: function (req, res, url) {
fs.readFile(path.join(__dirname, '../' + url), function (err, data) {
if (err) {
res.end('404')
throw err
} else {
//根据文件类型不同设置响应头
res.setHeader('Content-Type', mime.getType(url))
res.end(data)
}
})
},
//实现用户注册
userRegister: function (req, res) {
//分批接收数据
let str = ''
req.on('data', (chunk) => {
str += chunk
})
req.on('end', () => {
console.log('str',str)
//调用自定义模块把接收的数据转成对象
let obj = JSON.parse(str)
console.log(obj)
//读取旧的数据
fs.readFile(path.join(__dirname, '../data/users.json'), 'utf-8', function (err, data) {
//设置响应头
res.setHeader('Content-type', 'text/html;charset=UTF-8')
if (err) {
let ret = {
code: 404,
msg: '注册失败'
}
res.end(JSON.stringify(ret))
throw err
} else {
let arr = JSON.parse(data)
console.log('arr',arr)
console.log('obj',obj)
//把数据加到旧数据
arr.push(obj)
fs.writeFile(path.join(__dirname, '../data/users.json'), JSON.stringify(arr, null, ' '), function (err, data) {
if (err) {
let ret = {
code: 404,
msg: '注册失败'
}
res.end(JSON.stringify(ret))
} else {
let ret = {
code: 200,
msg: '注册成功'
}
res.end(JSON.stringify(ret))
}
})
}
})
})
}
}
```
### 页面
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>用户注册</title>
<link rel="stylesheet" href="../css/index.css" />
</head>
<body>
<div class="register">
<form id="myForm">
<ul>
<li>
<label for="">用户名</label>
<input type="text" name="username" class="name" />
</li>
<li>
<label for="">密码</label>
<input type="password" name="password" class="pass" />
</li>
<li>
<label for="">手机号</label>
<input type="text" name="phone" class="mobile" />
</li>
<li>
<label for=""></label>
<button type="submit" class="submit">立即注册</button>
</li>
</ul>
</form>
</div>
<script>
const form = document.querySelector('#myForm')
form.addEventListener('submit', e=>{
event.preventDefault()
const formData = new FormData(event.target)
const data = Object.fromEntries(formData.entries())
fetch('http://127.0.0.1:3000/register', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}).then(res=>{
console.log('res', res)
})
})
</script>
</body>
</html>
```
## 查看应用内存占用
Node.js提供了内置的 `v8` 模块,可以用来查看程序的内存占用情况。具体步骤如下:
```js
// 1. 在程序中引入v8模块
// 2. 手动触发垃圾回收
// 3. 获取内存占用信息
const v8 = require('v8')
v8.setFlagsFromString('--expose-gc')
global.gc()
const heap = v8.getHeapStatistics()
console.log(heap)
```
`getHeapStatistics()`方法返回一个包含有关V8堆内存使用情况的对象包括总内存使用量已分配内存量垃圾回收次数等信息。通过这些信息可以分析出程序的内存占用情况并进行优化。
注意:使用`v8`模块需要在启动Node.js时使用`--expose-gc`选项启用垃圾回收器的暴露功能。`node --expose-gc app.js`