NodeJS 回炉重造笔记
文件操作与模块化的概念
NodeJS中有一个库叫做fs,可以对文件进行操作。比如:
fs.readFile('文件','编码',(err,data) = > {......})就可以对文件进行读操作,err代表错误信息,data代表文件内容。
var fs = require('fs')
fs.writeFile('./a.txt','Content',function(err) {
if (err) {
console.error('Error writing file:', err);
} else {
console.log('File written successfully');
}
});fs.writeFile() 可以对文件进行覆写操作,并且只接受错误信息一个参数。
追加内容:
const fs = require('fs');
fs.appendFile('./a.txt', '追加的内容\n', (err) => {
if (!err) console.log('追加成功');
});
在export导出的时候,可以使用as进行别名,别的文件引入的时候也必须使用别名
export { add as plus, sub as minus }在NodeJS的CommonJS中,每一个JS文件都会获得一个module,可以将它打印出来。
在CommonJS中导出内容:
module.exports = val导出多项内容:
exports.a = '1'
exports.b = '2'
module.exports = {a:1,b:2}可以选择一个一个地挂载属性,也可以直接导出对象。
导入内容:
var m1 = require('./m1.js')文件的后缀名是可选的。
自定义脚手架
脚手架工具可以帮你自动生成一个模板化的目录结构与文件,省去了重复与复杂的操作
npm init 脚手架工具名称我们还可以自己去创建一个自定义的脚手架工具。我们首先要做的就是创建自定义全局命令。
我们可以在bin目录下新建JS文件(比如cli.js),再用脚手架工具创建后,它就会包含在package.json中,然后我们使用sudo npm link把这个命令挂载到全局(要在bin目录以外的地方执行。)npm unlink --global name 可以从全局中卸载
#! /usr/bin/env node/usr/bin/env node 会自动在 环境变量 PATH 中查找 node 的位置,这样脚本在各种系统上都能正常执行。这样直接用./cli.js就能运行
#! /usr/bin/env node
console.log('mycli');如果我们在cli.js写入一个输出,这时候我们在系统任意位置执行package.js里面bin下的名字就可以执行这个文件的内容了
{
"name": "nodejslearn",
"version": "1.0.0",
"description": "",
"main": "index.js",
"bin": {
"nodejslearn": "bin/cli.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs"
}这里直接在命令行输入nodejslearn就可以执行文件的内容
process.argv[2]这个可以获取到命令后面跟着的参数
#! /usr/bin/env node
if(process.argv[2] === "--help") {
console.log("This is a help message for the CLI tool.");
}Commander
不过这样还是太麻烦,我们可以使用第三方库commander来帮我们解决这个问题。
官方文档:https://github.com/tj/commander.js/blob/HEAD/Readme_zh-CN.md
安装库:
npm install commander#! /usr/bin/env node
const { program } = require('commander');
program.option('-f, --framwork <framwork>','设置框架')
program.parse(process.argv); //解析命令行参数
可以在引入后通过option 配置一些些命令参数的选项。-代表命令的缩写,--表示命令的全称。<framwork>就是表示如果使用了这个参数,后面必须要再跟一个子参数。
program
.command('create <project> [other...]') //命令
.alias('crt') //别名
.description('创建一个项目') //描述
.action((project, args) => { //行为
console.log(project);
console.log(args)
})command可以去定义一个命令,也可以在这个对象上写专属于这个命令的参数,如果像刚才那么写的就是全局的参数。
代码模块化拆分
我们可以创建一个lib文件夹存放主要代码,lib/core存放核心业务代码
我们可以把一个功能拆分成多个文件,然后再去分别调用使用,有助于项目结构,代码清晰,方便日后的增量
命令行问答交互工具
要实现命令行交互,我们要先npm i inquirer ,这个工具是专门用来给命令行做问答功能的
var inquirer = require('inquirer');
inquirer.prompt([
{
type: 'input', //设置问答类型
name:'username', //设置变量名
message: '请输入用户名' //提示信息
}
]).then(answers => { //回调接收用户的回答
console.log(answers);
})var inquirer = require('inquirer');
var config = require('../../config');
const myAction = (project, args) => {
inquirer.prompt([
{
type: 'list',
name: 'framwork',
choices: config.framwork,
message: '请选择一个框架'
}
]).then(answers => {
});
}
module.exports = myAction;有输入框类型,也有选择类型。我们要在对象里传入这一个问答的配置选项,然后执行命令的时候用户所输入的内容就会传给name里面写的变量存储。
下载远程仓库模板代码
我们可以使用npm i download-git-repo 来去用node下载Github,Gitlab...等远程仓库内的代码。
官方文档:
https://www.npmjs.com/package/download-git-repo
const download = require('download-git-repo')
download('direct:https://github.com/thirstywaterx/waterpage.git','./xxx',{clone:true},(err) => {
console.log(err)
})可以写上要下载库的地址,下载到的路径,以及下载方式,这里的方式是克隆。
下载等待交互提示
为了优化用户的体验,我们可以在下载的时候增加一个等待的小动画。
我们可以使用npm i ora 这个工具来去实现这个需求。
官方文档:
https://www.npmjs.com/package/ora
const ora = require('ora')
const spinner = ora().start()
spinner.text = 'loading......'
setTimeout(() => {
spinner.stop(); //结束
spinner.succeed('loaded') //成功并结束
spinner.fail('failed') //失败并结束
spinner.info('info message') //信息提示
}, 3000)
调用这些API在分别在终端上呈现不同的显示效果
命令行样式输出
我们可以用命令行渲染工具npm i chalk 来实现。它可以使输出的文本具有颜色或底色
官方文档:https://www.npmjs.com/package/chalk
const chalk = require('chalk')
console.log(chalk.blue('hello'))在命令行输出蓝色的一条文本
console.log(chalk.rgb(255,60,90)('hello'))除了使用预设的颜色,还可以通过RGB去设置颜色。.bold()可以对字体进行加粗。
console.log(chalk.blue.bold('Done!'), chalk.bold('you run'));在一行文本内连续使用不同的样式
HTTP服务器
使用Node创建HTTP服务器
//1.导入http模块
var http = require('http')
//2.创建服务器
//获取到服务器的实例对象
var server = http.createServer()
server.listen(8080,function() {
console.log("http://127.0.0.1:8080");
})
//监听服务器的事件
//监听请求时间
server.on('request',(req,res) => {
console.log('666');
res.write('666')
res.end()
})http.createServer() 实际上并没有真正创建了一个服务器,而是返回了服务器的对象。server.on() 可以监听时间,这个代码里面就监听了request 事件,当服务端接收到客户端的请求时,就会触发里面的事件。res.write()可以将内容输出出来,但是如果只有这个代码的话,客户端那里会卡死访问不了接口,因为客户端一直在等待服务端发送数据,它不知道服务端的处理有没有结束,我们就需要用res.end()来发一个结束的信号。
服务器响应不同的数据类型
server.on('request',(req,res) => {
res.setHeader('Content-Type','text/plain;charset=utf-8')
res.write('你好')
res.end()
})如果我们不设置请求头直接输出中文的话会出现乱码,这是因为客户端不知道数据是什么数据类型的原因,这时候我们就需要用请求头去规定数据的类型。比如果这里,就规定了数据类型为文本,且编码为utf-8
server.on('request',(req,res) => {
res.setHeader('Content-Type','text/html;charset=utf-8')
res.write('<h1>你好</h1>')
res.end()
})把类型改成text/html可以让浏览器解析里面的html标签。但是这样直接在里面写html未免太难受了,所以我们可以借用fs直接调用文件。
//1.导入http模块
var http = require('http')
var fs = require('fs')
//2.创建服务器
//获取到服务器的实例对象
var server = http.createServer()
server.listen(8080, function () {
console.log("http://127.0.0.1:8080");
})
//监听服务器的事件
//监听请求时间
server.on('request', (req, res) => {
// res.setHeader('Content-Type', 'text/html;8charset=utf-8')
// res.write('<h1>你好</h1>')
// res.end()
if (req.url == '/') {
fs.readFile('./index.html', 'utf-8', function (err, data) {
res.write(data)
res.end()
})
}else {
fs.readFile('./image.png',function(err,data) {
res.end(data)
})
}
})但是如果html里面有图片的话,反而会无法显示了(没有图片处理逻辑的情况下)。这是因为客户端向服务端发送请求时,服务端只返回了html,当解析html向服务端请求图片的时候,因为没有相关逻辑而导致无法返回而无法显示。所以这里使用fs读取了图片并用res.end()进行返回。
HTTP不同请求方法处理

req.method可以获取到客户端的请求类型,req.url可以获得客户端的请求连接。我们可以通过require('url')引入一个库的方式来对链接进行 处理,比如获取查询字符串。
//1.导入http模块
var http = require('http')
var fs = require('fs')
var url = require('url')
//2.创建服务器
//获取到服务器的实例对象
var server = http.createServer()
server.listen(8080, function () {
console.log("http://127.0.0.1:8080");
})
//监听服务器的事件
//监听请求时间
server.on('request', (req, res) => {
if (req.method == "GET") {
console.log(url.parse(req.url, true).query.id)
if (req.url == '/') {
fs.readFile('./index.html', 'utf-8', function (err, data) {
res.write(data)
res.end()
})
} else {
fs.readFile('./image.png', function (err, data) {
res.end(data)
})
}
} else if (req.method == "POST") {
//......
}
})这里就通过url.parse解析到了查询字符串中的?id(这里面的true表示以对象形式返回),并且用if判断了请求方法。
但是这种解析url的方式已经被官方启用,现在一般采用面向对象式的写法
const myUrl = new URL('https://example.com/path?a=1');
console.log(myUrl.hostname); // example.com
console.log(myUrl.pathname); // /path
console.log(myUrl.searchParams.get('a')); // 1通过url.searchParams()来获取。
接受并处理POST数据消息
POST数据是包含在请求体中发送过来的,我们需要用req.on()去监听data这个事件(当有数据传过来的时候就触发),req.on()还可以监听end事件(当数据传输完毕后触发)。但是传过来的数据是由16进制呈现的,我们还需要对其进行转义。
if (req.method == "POST") {
var data = ''
req.on('data',function(d) {
console.log(d);
data += d
})
req.on('end',function() {
console.log(require('querystring').parse(data))
})
}这里我们就使用了querystring这个工具把数据转成了一个对象。因为客户端的数据可能不是连续发过来的,我们需要等数据传输完毕再进行操作,所以可以声明一个data变量,逐步把数据赋值给它。
Express
express官网:https://expressjs.com/
我们可以用npx来执行一个包的命令,然后自动销毁它,这样与把整个包下载下来执行命令相比更加高效,适合只需要执行某个特定的命令
我们可以用npx express-generator来使用脚手架工具构建express框架的一个基本的目录结构,然后再使用npm install来安装脚手架工具在package.json里面提供的我们可能会用到的工具
npm install express后,我们就可以对它进行使用了。
const express = require("express")
const app = express()
//接受get请求
app.get('/',function(req,res) {
})
//接受post数据
app.post()
//监听端口
app.listen(3000,()=> {
console.log('Run http://127.0.0.1:3000');
})可以看出,使用express后代码明显简洁了很多,不需要再用res.method去判断请求类型了,只需要用app.get()或app.post()方法就行。我们要给方法内传入客户端访问的路径,然后再给一个回调函数来处理请求与响应。
const express = require("express")
const fs = require("fs")
const {promisify} = require("util")
const app = express()
const readFile = promisify(fs.readFile)
//接受get请求
app.get('/',async function (req, res) {
try {
let back = await readFile('./db.json', 'utf-8')
const jsonObj = JSON.parse(back)
res.send(jsonObj.users)
} catch {
res.status(500).json({error})
}
})
// //接受post数据
// app.post()
//监听端口
app.listen(3000, () => {
console.log('Run http://127.0.0.1:3000');
})这里处理了请求,并将内容以json的形式返回到了客户端。如果出现了错误,就会以app.status()返回状态码以及json格式的错误信息。
我们可以注意到,这个片段代码并没有使用常规的回调函数,而是使用了异步+promisify。如果都使用回调的话,在日后代码量增长的趋势下会形成"回调地狱",所以要使用promisify这个util里面的方法来把一个函数变成promise,用await执行的时候会把返回的数据返回回来,我们可以将它赋值给一个变量。因为原版的fs.readFile()传入的回调函数会接受到两个参数,分别为err和data,在promisify的情况下,如果抛出错误会被try...catch捕捉到。
promisify的作用不言而喻,就是快速把一个方法转换成promise,把回调函数内接受到的数据返回。但是我们会发现一个问题,如果回调函数接受的参数不是按照err,data排列的呢?那就用不了promisify了,因为promisify就是为node的回调格式(error-first)而设计的,就是第一个参数为错误信息,第二个为数据。
处理客户端POST数据
app.use(express.urlencoded())
app.use(express.json())
app.post('/',async (req,res) => {
console.log(req.headers);
console.log(req.body)
})客户端发来的数据是存在req.body内的,但是如果我们直接打印它的话会输出undefind,因为服务端不知道客户端传来的数据是什么类型的以及如何处理,所以我们需要根据请求头来灵活变化。要通过app.use() 来加载中间,比如express.urlencoded()或express.json()来分别处理表单与JSON数据
修改用户信息

1xx系列码一般用于websocket中。
app.put('/:id', async (req, res) => {
// console.log(req.params.id)
// console.log(req.body)
try {
let userInfo = await db.getDb()
//将字符串转换为整数
let userId = Number.parseInt(req.params.id)
//在数组查找对应id的用户,找到后赋值给user
let user = userInfo.users.find(item => item.id === userId)
if (!user) {
res.status(403).json({
error: "用户不存在"
})
}
const body = req.body
user.username = body.username ? body.username : user.username
user.age = body.age ? body.age : user.age
userInfo.users[userId - 1] = user
if(!await db.serveDb(userInfo)) {
res.status(201).json({
msg: "修改成功"
})
}
} catch (error) {
res.status(500).json({ error })
}
})如果我们想要修改数据库里面的一个数据,就需要让客户端发送PUT请求。比如我想修改id为1用户的名字与年龄,请求链接就要是这样的:http://127.0.0.1:3000/1 ,后面的"1"代表了指向用户的id,服务端就需要去接受这个数字来知道自己具体要修改哪个用户。这个代码片段封装了一个db.getDb()和一个db.serveDb()来分别获取用户列表和写入用户列表的功能。这里的app.put()的路径形参使用了/:id,表示这里可以接受一个叫id的参数,用req.params.id即可获取它的值,所以我们在请求路径后面填的1就可以被这个方法得到。然后再用find在用户列表中寻找指定用户,并返回那个用户的对象,这时我们就可以通过比较值与原值是否相同来判断数据是否发生了变化来决定是否替换值,最后全部写入数据库。
MongoDB
再使用之前,我们要先去官网安装MongoDB: https://www.mongodb.com/
安装文档:
1
启动 MongoDB。
您可以通过发出以下命令来启动
mongod进程:sudo systemctl start mongod如果收到类似下方的错误(在启动
mongod时):
Failed to start mongod.service: Unit mongod.service not found.首先运行以下命令:
sudo systemctl daemon-reload然后再次运行上面的启动命令。
2
验证 MongoDB 是否已成功启动。
sudo systemctl status mongod您可以通过选择执行以下命令,来确保 MongoDB 将在系统 重新启动后启动:
sudo systemctl enable mongod3
停止 MongoDB。
您可以根据需要,通过发出以下命令来停止
mongod进程:sudo systemctl stop mongod4
重启 MongoDB。
您可以通过发出以下命令来重启
mongod进程:sudo systemctl restart mongod通过观察
/var/log/mongodb/mongod.log文件中的输出,可以跟踪错误或重要消息的进程状态。5
开始使用 MongoDB。
在与
mongod相同的主机上启动mongosh会话。您可以在不使用任何命令行选项的情况下运行mongosh,从而连接在本地主机上运行且默认端口号为 27017 的mongod。mongosh如需进一步了解使用
mongosh进行连接(例如连接在其他主机和/或端口上运行的mongod实例),请参阅 mongosh 文档。为了帮助您开始使用 MongoDB,MongoDB 提供了各种驱动程序版本的入门指南。关于驱动程序文档,请参阅 开始使用 MongoDB 开发。
使用mongosh 即可进入MongoDB的shell界面
基础命令
查看数据库:
show dbs;test> show dbs;
admin 40.00 KiB
config 12.00 KiB
local 40.00 KiB切换到数据库:
use 数据库名查看当前在哪个数据库:
db退出数据库:
exit;或者是:
quit();创建数据库
使用use 不存在的数据库 也可以创建一个新的数据库,但是这个数据库并没有被真正地创建,它是仅存在于内存当中的,我们需要给他添加数据才能真正落地
db.cc.insert({x:1,y:2})可以这样向cc这个集合插入这样的一个文档。集合可以理解为表,文档可以理解为有多个键值对的一个组。
删除数据库
db.dropDatabase()这是一种"自杀"行为。我们必须要先切换到要删除的库才能执行删除操作,在别的地方是不行的。
查看集合
show collections删除集合
db.集合名.drop()基本增删查改
增
db.cc.insertOne()可以插入一条数据
db.cc.insertMany([
{username:"lisi1",age:16},
{username:"lisi2",age:18}
])db.cc.insertMany() 可以以数组的形式插入多条数据
查
db.cc.find()这个可以查找集合内的数据,什么都不传就是默认返回所有。
db.cc.find({username:"lisi"})比如这条就是查找cc集合内用户名为lisi的数据
db.cc.find({age:{$gt:15}})$gt 表示大于,$lt表示小于。这里就查找了年龄大于15的所有结果,通过用对象传入操作符来规定条件。
db.cc.findOne()可以只返回一个符合条件的结果
改
db.cc.updateOne({username:"lisi"},{$set:{age:30}})这条就是查询用户名为lisi的数据,并把age改为30。这里的$set操作符是必须的,如果没有就会用更新数据覆盖整个文档。
还有一些常见的更新操作符:
$set // 修改字段
$unset // 删除字段
$inc // 数字自增
$push // 数组追加
$pull // 数组删除
$rename // 字段改名批量修改:
db.cc.updateMany({age:{$gt:15}},{$set:{username:"Monica"}})这里就是查找所有年龄大于15的文档并把他们的username全部修改为Monica
删
db.cc.deleteOne({age:18})这个会删除一条年龄为18的文档。如果有多个文档符合结果,只会删除查询结果的第一个。
删除多条:
db.cc.deleteMany({age:{$gt:15}})我们可以看到,所有的键值后面都可以用操作符号对数据进行筛选。
NodeJS连接MongoDB
官方文档:
https://www.mongodb.com/zh-cn/docs/drivers/
https://www.npmjs.com/package/mongodb
在使用特定编程语言连接数据库的时候,我们需要安装对应的驱动。
npm isntall mongodbconst { MongoClient } = require("mongodb")
const client = new MongoClient("mongodb://127.0.0.1:27017")
const main = async () => {
await client.connect()
const db = client.db("mytest") //连接指定数据库
const cc = db.collection("cc") //指定集合
var d = await cc.find() //接受查询结果
console.log(await d.toArray()); //查询默认返回游标数据,需要转换一下
}
main().finally(() => client.close())
//fianlly 在promise中无论成功还是失败都要执行使用Node进行增删改查操作
const { MongoClient } = require("mongodb")
const client = new MongoClient("mongodb://127.0.0.1:27017")
const clientFun = async function (c) {
await client.connect()
const db = client.db("mytest") //连接指定数据库
return db.collection(c) //指定集合
}
const main = async () => {
var cc = await clientFun("cc")
// var d = await cc.find() //接受查询结果
const d = await cc.insertOne({username:"monica",age:60})
console.log(d);
}
main().finally(() => client.close())
//fianlly 在promise中无论成功还是失败都要执行大部分操作和在命令行一样,但是要加上await异步.查询多条数据的时候默认是返回游标,游标就是一个指向数据的一个指针,不直接返回所有数据,优化了性能,如果想要直接打印就要给数据加上.toArray()
Express中间件
概念及其运用
const express = require('express')
// 加一个注释,用以说明,本项目代码可以任意定制更改
const app = express()
const PORT = process.env.PORT || 3000
app.use((req, res, next) => {
console.log(`${req.method},${req.url},${Date.now()}`);
next()
})
app.get("/", (req, res) => {
res.send("/index")
})
app.get("/register", (req, res) => {
res.send("/register")
})
app.get("/login", (req, res) => {
res.send("/login")
})
app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`)
})
Express的中间件可以在所需逻辑代码执行前进行一些操作。比如这里的中间件,就是在每个请求发起后都打印一次请求的方式,连接等......通过app.use(函数) 来去使用。因为代码是按顺序执行的,所以,不要把中间间放到需要运用到的代码的后面,要不然根本没有效果。中间件内的next()是必须执行的,用于调用下一个中间件或路由的函数,否则就会卡死在中间件内无法进行下一步
不同中间件类型的使用方式


应用程序级别中间件
应用程序级别的中间件之一就是app.use(),可以注册 全局中间件 或 针对某个路径的中间件。
app.get("/user",(req,res,next)=> {
})其实这种处理请求的也可以看作是一种中间件。这里的next其实可以省略不写,只有当需要把操作传递给下一个中间件的时候才需要,而路由级中间件往往最后就已经通过res.send()或其他方法结束了请求。
app.get("/user",(req,res,next)=> {
console.log(req.method);
next();
},function(req,res) {
console.log("666");
res.send("/userd")
})
这里就使用了next()在一个路由中间件里面进行了嵌套,这样做有利于使代码层次更清晰,方便管理。
路由级别中间件
app.js:
const express = require('express')
const router = express.Router()
router.get("/",(req,res)=> {
console.log(req.method);
res.send("/index")
})
router.get("/users",(req,res)=> {
console.log(req.method);
res.send("/users")
})
module.exports = router./router/index.js:
const express = require('express')
const router = express.Router()
router.get("/",(req,res)=> {
console.log(req.method);
res.send("/index")
})
router.get("/users",(req,res)=> {
console.log(req.method);
res.send("/users")
})
module.exports = router注意到,这里的app.use()里面传入了路径。这是合法的,这么写表示,当路径为什么时,调用什么函数。
在/router/index.js我们可以看到使用了router.get()而非app.get()。app.get()是挂载到全局的,并且请求路径要写全,这会导致代码很冗余且可读性大大降低,而router.get()是局部的,它只会应用到所有挂载它的js文件上面,也就是说,即使两个js文件都使用router.get()处理了相同的路径,只要他们没有关联,就是允许存在的。
并且router.get()还可以利用公共路径。我们可以在app.js里面用app.use()挂上一个公共前缀/user,这样在./router/index.js里的router.get()就可以直接传入后面的路径进行处理,不用写全路径,还保证了可读性。如果我们访问http://localhost:3000/user/ 那么就会先被app.user("user",router)捕获到,然后交给引入的router到./router/index.js里路由到根路径的处理函数。
app.use((req,res,next) => {
res.status(404).send('404 Not Found.')
})如果前端输入了错误的路径,应该返回404。我们要把这个404的中间件放到app.js最下面,因为代码是按顺序执行的,当上面的路由都匹配不到的时候,再执行这个代码返回404。
错误处理中间件
当app.use()传入四个参数的时候,nodejs就会认为这是一个处理错误信息的中间件。
app.use((err,req,res,next)=> {
console.log(err);
res.status(500).send('service Error')
})当我们这么写并放到底部的时候,如果服务器发生了错误,它就会执行。
其他
还有更多的中间件,我们都可以去express的官方文档查找。
Exprss路由及响应方法
app.all("/xx",(req,res)=>{
res.send("xxx")
})app.all()可以匹配所有的请求方法。
app.get('/us?er',(req,res)=>{
res.send(`${req.method}---${req.url}`)
})我们甚至还可以在请求路径里面写上正则表达式。这里的问号表示前面的一个字符可以省略不写
app.get('/user/:id/video/:vid',(req,res)=>{
console.log(req.params.id);
res.send(`${req.method}---${req.url}`)
})获取参数,前后的id不能一样。
app.get('/',(req,res)=> {
console.log(req.method);
res.send('index')
})
.post("/users",(req,res)=> {
console.log(req.method);
res.send('users')
})这是路由的链式调用,可以少些一次app,更美观。
app.route('/users')
.get((req, res) => {})
.post((req, res) => {})
.put((req, res) => {})同一路径可以挂载上多个请求方法,这样些可以方便些。
app
.get('/user',(req,res)=> {
res.download('./app.js')
})
.post('/video',(req,res)=> {
res.send(`${req.method}---${req.url}`)
})res.download()可以给客户端返回一个下载的指令
res.redirect()这个可以重定向。
res.render()这个可以渲染模板
res.sendStatus()将状态码和数据一并返回。
很不幸运的是,因为事故,笔记丢失了一半,我也懒得重写了,接下来的内容可能和上面相比比较跳跃。
1. Mongoose 与 MongoDB 数据库建模
Mongoose 是一个 MongoDB 对象建模工具 (ODM),旨在处理异步环境。它为你的数据提供了一种基于模式 (Schema) 的解决方案,包含内置类型转换、验证、查询构建等功能。
1.1 连接数据库
通常在项目的入口或专门的 model/index.js 文件中进行链接。
// model/index.js
const mongoose = require('mongoose')
async function main() {
// 连接到本地 MongoDB 数据库 express-video
await mongoose.connect('mongodb://127.0.0.1:27017/express-video')
}
main()
.then(() => console.log('MongoDB 链接成功'))
.catch(err => console.error('MongoDB 链接失败', err))
// 导出所有模型
module.exports = {
User: mongoose.model('User', require('./userModel')),
}1.2 定义 Schema (数据模式)
Schema 定义了文档的结构、默认值、验证器等。
// model/userModel.js
const mongoose = require("mongoose")
const md5 = require("../util/md5") // 引入加密工具
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true // 必填校验
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true,
set: value => md5(value), // Setter: 存入数据库前自动加密
select: false // 查询时默认不返回密码字段,保护安全性
},
phone: {
type: String,
required: true
},
image: {
type: String,
default: null // 默认值
},
createAt: {
type: Date,
default: Date.now()
}
})
// 导出 Schema (在 index.js 中被 model 包装)
module.exports = userSchema2. RESTful API 接口规范
REST (Representational State Transfer) 是一种软件架构风格。符合 REST 风格的 API 称为 RESTful API。
2.1 核心原则
1. 资源 (Resources): URL 代表资源,通常使用名词复数。
好:
/api/users(用户集合),/api/videos(视频集合)坏:
/api/getUsers,/api/deleteVideo(不要在 URL 中包含动词)
2. HTTP 动词: 使用标准的 HTTP 方法表达操作意图。
GET: 获取资源 (查)POST: 新建资源 (增)PUT/PATCH: 更新资源 (改)DELETE: 删除资源 (删)
3. 状态码 (Status Codes): 使用标准状态码响应结果。
200 OK: 请求成功201 Created: 资源创建成功 (常用于 POST)
400 Bad Request: 参数/请求错误401 Unauthorized: 未授权 (未登录或 Token 无效)403 Forbidden: 禁止访问 (无权限)404 Not Found: 资源不存在500 Internal Server Error: 服务器内部错误
2.2 路由设计示例 (Express)
// router/user.js
const express = require('express')
const router = express.Router()
const userController = require('../controller/userController')
// 定义路由
router
.post('/registers', userController.register) // 注册 (创建用户)
.post('/logins', userController.login) // 登录 (通常也用 POST,因为要发送敏感数据)
.get('/lists', userController.list) // 获取列表
.delete('/:id', userController.delete) // 删除指定 ID 用户
module.exports = router3. JWT (JSON Web Token) 认证机制
JWT 是一种开放标准 (RFC 7519),用于在各方之间以 JSON 对象安全地传输信息。它是无状态的认证机制,非常适合现代 Web 应用和微服务。
3.1 结构
JWT 由三部分组成,用 . 分隔Header.Payload.Signature
1. Header: 声明类型 (JWT) 和加密算法 (如 HS256)。
2. Payload: 存放有效载荷 (如用户 ID、Email、过期时间)。注意:不要存放密码等敏感信息。
3. Signature: 签名,防止篡改。使用 Header + Payload + 服务器密钥 (Secret) 生成。
3.2 核心流程
1. 签发 (Sign): 用户验证通过后,服务器生成 Token 返回给客户端。
2. 存储: 客户端收到 Token 后存储 (LocalStorage / Cookie)。
3. 携带: 后续请求在 HTTP Header Authorization: Bearer <token>) 中携带 Token。
4. 验证 (Verify): 服务器解析 Token,验证签名和过期时间,确认用户身份。
3.3 代码实现示例
// util/jwt.js
const jwt = require("jsonwebtoken")
const { promisify } = require("util")
const { uuid } = require("../config/config.default") // 密钥
// 将 jwt.sign 和 jwt.verify 转换为 Promise 形式方便 async/await 调用
const tojwt = promisify(jwt.sign)
const verify = promisify(jwt.verify)
// 生成 Token 工具
module.exports.createToken = async (userinfo) => {
return await tojwt(
{ userinfo }, // Payload
uuid, // Secret Key
{ expiresIn: 60 60 24 } // 过期时间: 24小时
)
}
// 验证 Token 中间件
module.exports.verifyToken = async (req, res, next) => {
// 获取 Authorization 头,格式通常为 "Bearer <token>"
let token = req.headers.authorization
token = token ? token.split(" ")[1] : null
if (!token) {
return res.status(401).json({ error: "请提供 token" })
}
try {
// 验证 token
let userInfo = await verify(token, uuid)
req.user = userInfo // 将解析出的用户信息挂载到 req 对象,供后续控制器使用
next() // 验证通过,放行
} catch (error) {
res.status(401).json({ error: "Token 无效或已过期" })
}
}4. 实战:用户注册与登录流程
结合 Mongoose、MVC 结构和 JWT 实现完整的认证流程。
4.1 用户注册 (Register)
流程: 接收数据 -> 数据校验 -> 检查是否存在 -> 生成用户(密码加密) -> 保存入库 -> 返回结果。
// controller/userController.js
exports.register = async (req, res) => {
// 1. 数据校验 (通常在路由层的 validator 中间件处理完)
// 2. 实例化 Model
const userModel = new User(req.body)
// 3. 保存 (触发 Model 中的 'set' 钩子自动加密密码)
const dbBack = await userModel.save()
// 4. 处理返回数据 (转为 JSON 并去除密码字段)
let user = dbBack.toJSON()
delete user.password
// 5. 响应客户端
res.status(201).json({ user })
}数据校验中间件 (express-validator):
// middleware/validator/userValidator.js
const { body } = require('express-validator')
const { User } = require('../../model/index.js')
const validate = require('./errorBack') // 统一错误处理封装
module.exports.register = validate([
body('username')
.notEmpty().withMessage('用户名不能为空')
.bail() // 如果前面失败,停止后续校验
.isLength({ min: 3 }).withMessage('用户名长度不能小于3'),
body("email")
.notEmpty().isEmail().withMessage('邮箱格式不正确')
.custom(async val => { // 自定义校验:查库看邮箱是否已注册
const emailValidate = await User.findOne({ email: val })
if (emailValidate) {
return Promise.reject('邮箱已存在')
}
}),
// ... 其他字段校验
])4.2 用户登录 (Login)
流程: 接收账号密码 -> 查询用户 -> 校验密码 -> 生成 JWT -> 返回 Token。
// controller/userController.js
exports.login = async (req, res) => {
// 1. 根据邮箱查询用户 (包括密码字段,如果 Schema 设置了 select: false 这里可能需要 .select('+password'))
// 注意:前面的 model 定义里 select:false,这里 findOne 生成的实例可能不含密码,
// 若 md5 在客户端做或通过钩子处理,比对逻辑需注意。
// 在本项目的逻辑中,假设 req.body 包含明文密码,且存入数据库是密文。
// 通常做法:需手动对 req.body.password 进行 md5 加密后,再去数据库比对,或者取出数据库密文和输入值加密后比对。
// 简化版逻辑演示:
var dbBack = await User.findOne(req.body) // 这里假设 req.body 已经构造成能匹配数据库的形式
if (!dbBack) {
res.status(402).json({ error: "邮箱或密码不正确" })
return
}
// 2. 生成 Token
dbBack = dbBack.toJSON()
const token = await createToken({
id: dbBack._id,
email: dbBack.email
})
// 3. 将 Token 添加到返回对象
dbBack.token = token
// 4. 响应
res.status(200).json(dbBack)
}4.3 路由整合
最后在 router/user.js 中将各部分串联起来:
router
.post('/registers', validator.register, userController.register) // 注册:先校验 -> 后 业务
.post('/logins', userController.login) // 登录
.get("/lists", verifyToken, userController.list) // 受保护路由:先鉴权 -> 后 业务用户数据修改
exports.update = async (req,res) => {
var id = req.user.userinfo._id
var dbBackDate = await User.findByIdAndUpdate(req.user.userinfo._id,req.body,{new:true})
res.status(202).json({user:dbBackDate})
}User.findByIdAndUpdate() 可以通过id查询到要更改的数据,然后再传入更改的数据。但是它默认会返回修改之前的数据,我们可以加上{new:true} 配置项来返回新的。
由于我们已经在jwt.js里把验证完token的用户数据放到req.user里面了,这里直接调用就行