从0学习node,并完成接口的创建使用
目录
一、使用express框架搭建框架
-
了解express
Express它是一个NodeJS平台下面的框架,主要用于构于Web服务器项目,它是一个第三方的模块
-
安装特定版本express
npm i express@4.17.1 or yarn add express@4.17.1 or npm i express or yarn add express
-
初始化项目
npm init --yes or yarn init --yes //加了--yes以后,它会直接帮你生成,不再询问你的配置信息
二、了解可能用到的依赖
名称 | 版本 | 作用 |
---|---|---|
nodemon | - | npm i -g nodemon or npm install -global nodemon 用来监视node.js应用程序中的任何更改并自动重启服务 |
express | 4.17.1 | npm i express@4.17.1 Express它是一个NodeJS平台下面的框架,主要用于构于Web服务器项目,它是一个第三方的模块 |
cors | 2.8.5 | npm i cors@2.8.5 处理跨域问题 |
MySQL | 2.18.1 | npm i mysql 通过这个模块可以与MySQL数据库建立连接、执行查询等操作 |
sm-crypto | - | npm i sm-crypto 加密解密 |
bcryptjs | 2.4.3 | npm i bcryptjs 加密解密 |
express-jwt | 5.3.3 | npm install express-jwt 验证指定http请求的JsonWebTokens的有效性,如果有效就将JsonWebTokens的值设置到req.user里面,然后路由到相应的router |
multer | 1.4.2 | npm i multer@1.4.2 解析表单数据 |
三、开始nodejs服务器的创建
-
1.创建app.js作为启动节点
touch app.js
-
2.对app.js进行添加内容
-
导入express并初始化app.js
// 导入 express const express = require('express') // 创建服务器的实例对象 const app = express() /* * 在创建和启动中间添加中间件、路由、错误中间件等 * 错误中间件:在启动服务器的上面,其他中间件、路由的下面 */ /*****存放中间件*****/ /*****存放路由*****/ /*****存放错误中间件*****/ // 启动服务器 app.listen(9000, () => { console.log('api server running at http://127.0.0.1:9000') }) // 通过nodemon启动服务器 nodemon ./app.js
-
添加一个get路由
app.get('/', (req, res) => { // 通过res.send方法返回给前端数据 // res对象 https://www.jc2182.com/nodejs/nodejs-res.html res.send(res.ResultConfig("获取127.0.0.1:9000/数据成功").success()) })
-
导入cors中间件
// 安装依赖:npm i cors@2.8.5 ---------------------------- // 导入 cors 中间件 const cors = require('cors') // 将 cors 注册为全局中间件 app.use(cors())
-
添加封装统一返回结果中间件【在路由之前】
-
1.新建utils/public.js存放统一封装的方法
module.exports = class ReturnConfig { code; data; msg; constructor([...args]) { if (args.length == 1) { this.data = ""; this.msg = args[0]; } if (args.length == 2) { this.data = args[0]; this.msg = args[1]; } if (args.length == 3) { this.code = args[0]; this.data = args[1]; this.msg = args[2]; } } success() { return { code: this.code || 0, data: this.data, msg: this.msg }; } error() { return { code: this.code || -1, data: this.data, msg: this.msg }; } publicError() { return { code: this.code || -1, data: "未知错误", msg: this.msg }; } publicUserError() { return { code: this.code || -1, data: "请确认参数传递方式", msg: this.msg }; } }
-
2.导入返回结果中间件
// 一定要在路由之前,封装 统一返回结果 函数 const ReturnConfig = require("./utils/public") app.use((_, res, next) => { res.ResultConfig = function () { return new ReturnConfig(arguments); } next() })
-
-
修改"/"路由使用统一的封装方法
app.get('/', (req, res) => { res.send(res.ResultConfig("成功通过get方式访问路由为'/'的接口").success()) })
-
错误级别中间件
-
定义错误级别中间件
// 不定义错误级别中间件报错会暂停服务 app.use((err, req, res, next) => { // 身份认证失败后的错误 if (err.name === 'UnauthorizedError') return res.send(res.ResultConfig("401", err.message, "身份认证失败").error()) // 未知的错误 if (err) return res.send(res.ResultConfig(err, "未知错误").error()); })
-
测试可能出现的错误
-
1.将错误中间件中用不到的参数req、next换成 _ 【下划线】
English:SyntaxError: Duplicate parameter name not allowed in this context Chinese:语法错误:在此上下文中不允许重复的参数名称
-
2.在接口中的异步操作数据库时,在外层使用send发送会数据【程序崩溃】
报错:[nodemon] app crashed - waiting for file changes before starting...[应用程序崩溃-正在等待文件更改,然后再启动] 解决方法: // 在错误级别中间件上方添加 process.on('uncaughtException', function (err, res) { console.log('Caught exception: ', err); });
-
-
-
导入路由模块
-
创建router/index.js router/user/index.js两个文件
-
router/user/index.js
// 获取所有人员列表 router.get("/selectAll", (req, res) => { return res.send(res.ResultConfig(result, "获取成功").success()) }) // 将路由对象共享出去 module.exports = router
-
router/index.js
const userRouter = require('./user/index') module.exports = { userRouter };
-
-
将user路由添加到中间件
// 导入用户路由模块 const Router = require('./router/index') app.use('/user', Router.userRouter)
-
-
-
3.添加MySQL相关内容
-
安装依赖
npm i mysql
-
新建db/index.js
const mysql = require('mysql') const db = mysql.createPool({ host: '127.0.0.1', user: 'root', password: '2306545528', database: 'ningblog', }) module.exports = db
-
使用数据库可能出现的错误
错误: English:ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client Chinese:English:ER_NOT_SUPPORTED_AUTH_MODE: 客户端不支持服务器请求的身份验证协议;考虑升级MySQL客户端 原因:mysql8.0以上加密方式,Node还不支持。 解决: alter user 'root'@'localhost' identified with mysql_native_password by '1234';
-
优化user路由中的获取所有人员列表
-
添加查询数据库操作
// 获取所有人员列表 router.get("/selectAll", (req, res) => { const getUserDetailSQL = `select id, name, head_image ,iphone, creat_time from user` db.query(getUserDetailSQL, "", (err, result) => { if (err) { return res.send(res.ResultConfig(err.message).publicError()) } return res.send(res.ResultConfig(result, "获取成功").success()) }) // 在条用数据库时,不可以使用send发送数据,会导致程序崩溃 // return res.send(res.ResultConfig(result, "获取成功").success()) })
-
可能出现的问题
-
-
-
4.添加jwt鉴权
-
创建config/jwtConfig.js
module.exports = { jwtSecretKey: 'NingBest.', }
-
配置解析token中间件【!不同版本的jwt使用方法不同!】
// 配置解析 Token 的中间件 npm install express-jwt@5.3.3[不同的jwt会导致用法不同] // 配置后默认需要在请求头中添加Authorization:Bearer token const expressJWT = require('express-jwt') const jwtConfig = require("./config/jwtConfig.js") // 可以使用正则表达式进行匹配 /^\/api/ --> [/^\/api/] app.use(expressJWT({ secret: jwtConfig.jwtSecretKey }) .unless({ path: ["/", "/user/loginUser", "/user/register", /^\/profile\/upload\/[\s\S]{1,}/, "/file\/download" ] }))
-
再次调用获取所有人员列表接口
-
配置中间件:
{ "code": "401", "data": "No authorization token was found", "msg": "身份认证失败" }
-
不配置中间件:
-
-
注意点
使用: 请求头中添加的token会自动解析,并且存放在req.user中 req.user = { xxxxx, iat:1652159597,exp: 1653023597} iat: jwt的签发时间 exp: jwt的过期时间 前端使用: Authorization:Bearer token 注: 不同版本的jwt使用方法不同
-
-
5.完善用户表其他接口
-
注册新用户
-
user/index.js注册新用户
// 注册新用户 router.post('/register', (req, res) => { const userinfo = req.body if (!userinfo.name) return res.send(res.ResultConfig().publicUserError()) const getUserDetailSQL = `select * from user where name=?` const setUserDetailSQL = `insert into user set ?`; // 查询是否存在相同用户名 db.query(getUserDetailSQL, userinfo.name, function (err, result) { if (err) { res.send(res.ResultConfig(err.message).publicError()) } if (result.length > 0) { return res.send(res.ResultConfig("该用户名已存在").error()) } db.query(setUserDetailSQL, { name: userinfo.name, password: userinfo.password }, function (err, result) { if (err && results.affectedRows !== 1) { return res.send(res.ResultConfig(err.message, "新增失败").error()) } return res.send(res.ResultConfig("新增成功").success()) }) }) })
-
注意点
-
app.js
// 配置解析表单数据的中间件,注意:这个中间件,只能解析 application/x-www-form-urlencoded 格式的表单数据 // app.use(express.urlencoded({ extended: false })) // 通过 express.json() 这个中间件,解析表单中的 JSON 格式的数据 app.use(express.json())
-
index.js
//前端发送通过json发送的数据存放在req.body中 db.query方法 第一个参数:MySQL语句[string] 第二个参数:any[eg:number|string|[object,number]] 第三个参数:回调函数[参数1,参数2] 参数1:报错信息err 参数2:返回结果
-
-
-
登录
-
user/index.js登录
// 登录 router.post('/loginUser', (req, res) => { const userinfo = req.body console.log('userinfo: ', userinfo); // 导入jwt const config = require("../../config/jwtConfig") const getUserDetailSQL = `select * from user where name=?` db.query(getUserDetailSQL, userinfo.name, function (err, result) { if (err) { res.send(res.ResultConfig(err.message, "登陆失败").error()) } const user = result[0]; // 生成 Token 字符串 const tokenStr = jwt.sign({ id: user.id, name: user.name, head_image: user.head_image, iphone: user.iphone, creat_time: user.creat_time }, config.jwtSecretKey, { expiresIn: '240h', // token 有效期 }) // 解密 if (userinfo.password == user.password) { res.send(res.ResultConfig(tokenStr, "登陆成功").success()) } else { res.send(res.ResultConfig("登陆失败").error()) } }) })
-
注意点
-
token
// 生成 Token 字符串 const tokenStr = jwt.sign(user, config.jwtSecretKey, { expiresIn: timeout, // token 有效期 }) eg: user:{ id: user.id, name: user.name } timeout:'240h' eg: const tokenStr = jwt.sign({ id: user.id, name: user.name }, config.jwtSecretKey, { expiresIn: '240h', // token 有效期 })
-
密码
//前端加密,后端解密。 //md5加密:完全相同的一段数据,不论时间地点 //正规做法:前端加密,后端解密,后端通过自己的加密方式再次进行加密,存储到数据库 eg: 一:npm i sm-crypto 加密:sm4.encrypt(privateMsg, "2306545528NingBest19981222230654") 解密:sm4.decrypt(privateMsg, "2306545528NingBest19981222230654") 二:npm i bcryptjs@2.4.3 加密:userinfo.password = bcrypt.hashSync(userinfo.password, 10) 判断:bcrypt.compareSync(用户提交的密码, 数据库中的密码)
-
-
-
获取指定人的个人信息
-
user/index.js获取指定人的个人信息
// 获取指定人的个人信息 router.get("/getUserDetail/:id", (req, res) => { const id = req.params.id; const getUserDetailSQL = `select id, name, head_image ,iphone, creat_time from user where id=?` db.query(getUserDetailSQL, id, (err, result) => { if (err) { return res.send(res.ResultConfig(err.message).publicError()) } return res.send(res.ResultConfig(result[0], "获取成功").success()) }) })
-
注意点
-
获取对应参数
通过/getUserDetail/2方式传参[/getUserDetail/:id] 获取:res.params.id
-
-
-
添加公共方法
-
创建utils/index.js
const dayjs = require('dayjs') function getNowTime() { return dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss"); } module.exports = { getNowTime }
-
-
-
6.添加上传下载文件
-
创建router/upload/index.js
-
router/upload/index.js
const express = require('express') // 创建路由对象 const router = express.Router() const db = require("../../db/index") // 导入解析 formdata 格式表单数据的包 const multer = require('multer') // 导入处理路径的核心模块 const path = require('path') // 创建 multer 的实例对象,通过 dest 属性指定文件的存放路径 const upload = multer({ dest: path.join(__dirname, '../../upload') }) const fs = require("fs") //注:导入的db模块未使用,未添加数据库表进行上传记录 // 将路由对象共享出去 module.exports = router
-
app.js
//配置静态资源目录中间件[通过域名可以访问其中的文件] app.use("/profile/upload", express.static("upload"))
-
-
添加上传文件
-
上传文件
// 上传头像/文件 router.post('/upload', upload.single('file'), (req, res) => { //读取路径(req.file.path) const size = req.file.size / 1024 / 1024; if (size > 200) { return res.send(res.ResultConfig("", "文件过大,上传失败").error()) } // if(req.file.size > ) fs.readFile(req.file.path, (err, data) => { //读取失败,说明没有上传成功 if (err) { return res.send('上传失败') } const suffix = "." + req.file.originalname.split(".")[1]; const newName = "Ning" + Date.parse(new Date()) + suffix; fs.writeFile(path.join(__dirname, '../../upload/' + newName), data, (err) => { if (err) { return res.send('写入失败') } fs.unlink(path.join(__dirname, '../../upload/' + req.file.filename), (err) => { }) res.send(res.ResultConfig("/profile/upload/" + newName, "上传成功").success()) }) }) })
-
注意点
1.使用fs模块进行对文件的操作 2.限制文件大小 3.上传时进行对文件的重命名 4.重命名后重新写入一个相同的文件 5.写入成功后,在其中删除最初的文件
-
-
添加下载文件
-
下载文件
// 下载头像/文件 router.get("/download", (req, res) => { const filePath = path.join(__dirname, req.query.name) const name = req.query.name.replace(/\/profile\/upload\//, "") const realPath = filePath.replace(/\\router\\upload\\profile/, "") fs.readFile(realPath, (fileErr, fileData) => { if (fileErr) return res.send(res.ResultConfig("", "文件读取失败").error()) res.writeHead(200, { 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件 'Content-Disposition': 'attachment; filename=' + name, //告诉浏览器这是一个需要下载的文件 }) res.end(fileData); }) })
-
注意点
1.替换公共前缀 2.找到真正的路径地址 3.通过fs模块的readFile方法读取文件 4.添加head,并且动态写入name[浏览器下载的默认文件名] 5.通过res.end发送数据
-
-
注意点
使用 multer 解析表单数据:npm i multer@1.4.2
-
-
7.MySQL语句
-
添加测试MySQL接口
-
在user接口中添加测试MySQL接口【/router/user/index.js】
// 测试MySQL接口 router.get("/test", (req, res) => { const obj = { selectAll: { mysql: `select * from user`, params: 1 }, selectSome: { mysql: `select id,name from user`, params: 1 }, insert: { mysql: `insert into user set ?`, params: { name: "测试", password: "123456", head_image: "测试", } }, update: { mysql: `update user set ? where id = ?`, params: [ { name: "修改后的name", head_image: "修改后的头像", }, 10000014 ] }, delete: { mysql: `delete from user where id =?`, params: 10000014 }, } let name = "selectAll" // name = "selectSome" // name = "insert" // name = "update" // name = "delete" db.query(obj[name].mysql, obj[name].params, (err, result) => { if (err) { return res.send(res.ResultConfig(err.message).publicError()) } return res.send(res.ResultConfig(result, name + "成功").success()) }) })
-
-
基本语句
查询select 插入insert 更新update 删除delete
-
-
8.注意点
-
nodeJs中res.end和res.send 区别
官方说明: res.end() 终结响应处理流程。 res.send() 发送各种类型的响应。 res.end() 只接受服务器响应数据,如果是中文则会乱码 res.send() 发送给服务端时,会自动发送更多的响应报文头,其中包括 Content-Tpye: text/html; charset=uft-8,所以中文不会乱码
-
db.query是异步操作
-
在通过db.query请求时,中间传参不能直接使用res.params.id,需要使用中间变量
-