nodejs+express+mysql实现接口服务
跟着课程和文档写的接口服务器,稍微对重点进行了一个总结,完整的项目源码可以戳这里获取
项目结构
- router:存储各个模块的路由
- router_handler:存储各个模块的处理函数
- schema:存储各个模块的验证规则
- utils:封装一些方法
- uploads:上传地点
- config.js:token配置
- app.js:入口
一开始写的时候有些晕,于是自己总结了构建路由的一些思路,按照这个思路可以将可能出现的错误锁定在一定范围内排查,多走几遍就信手拈来了
(通过postman测试,注意点有:1.可能要携带authorization头部,2.发送的请求体的类型要与服务端接受的相吻合):
- 在router挂载路由,处理函数先简单的返回ok;回到app.js中注册路由,测试一下是否成功响应。
- 在router_handler定义处理函数,简单地返回ok;将其导入到router中并替换掉之前写的处理函数,测试一下是否能成功响应。
- 定义验证规则,在router中导入验证规则和验证中间件,在需要验证的路由中注册该中间件;模拟数据,测试能否成功验证。
- 对router_handler的处理函数进行详细编写,导入mysql并执行sql语句,进行最后的测试。
中间件
本项目所涉及到的第三方中间件有:
- 处理token:
jsonwebtoken
,express-jwt
- 密码加密:
bcrypt
- 表单验证:
joi
,@escook/express-joi
- 图片上传:
multer
、string-random
01 封装响应msg
由于响应在很多地方都会用到,于是我分别对错误响应和正确响应都做了封装。
utils/msg.js
/* 全局注册后可使用:res.err(err,status)
@param {String|Error} err 错误信息 (*必填)
@param {Number} status 错误状态码,默认为500
*/
const errmw=(req,res,next)=>{
res.err=function(err,status=500){
res.send({
status,
msg:err instanceof Error?err.message:err
})
}
next()
}
/* 全局注册后可使用:res.ok(msg,data,status)
@param {String|Error} msg 提示信息
@param {Object} data 返回的数据
@param {Number} status 状态码,默认为200
*/
const okmw=(req,res,next)=>{
res.ok=function(msg='ok',data,status=200){
res.send({
status,
msg,
...data
})
}
next()
}
app.js
const {errmw,okmw}=require('./utils/msg')
app.use(errmw)
app.use(okmw)
使用
res.err('错误了!')
res.ok()
res.ok('成功获取',{
data
})
02 token
-
定义token的特征
config.js
module.exports = { jwtSecretKey: 'mimimama. ^_^',//密钥 expiresIn:'48h'//有效期 }
-
根据用户信息生成token
router_handler/user.js
//导入中间件和token配置 const jwt=require('jsonwebtoken') const config = require('../config')
在处理函数内部,对需要用到的用户信息进行签名,传入密钥和有效期:
exports.login = (req, res) => { //...注意,user这个用户信息对象不应该保存敏感信息如密码 const tokenStr=jwt.sign(user,config.jwtSecretKey,{ expiresIn: config.expiresIn }) res.ok('登陆成功',{ token:"Bearer "+ tokenStr }) }
-
注册验证token的中间件(全局)
app.js
const config = require('../config') const {expressjwt: jwt}=require('express-jwt') app.use( jwt({ secret:config.jwtSecretKey, algorithms: ["HS256"], }).unless({path:[/^\/api\//]}) //设置以/api/开头的不需要访问权限 )
我为了让app.js更简洁,把这个中间件提了出来:
// utils/expressJwt.js const config = require('../config') const {expressjwt: jwt}=require('express-jwt') const dejwt=()=>{ return jwt({ secret:config.jwtSecretKey, algorithms: ["HS256"], }).unless({path:[/^\/api\//]}) //设置以/api/开头的不需要访问权限 } module.exports=dejwt // app.js const dejwt=require('./utils/expressJwt') app.use(dejwt())
-
捕获token的错误类型(全局)
app.js(一定要写在后面)
app.use((err,req,res,next)=>{ if(err.name === 'UnauthorizedError') return res.err('身份认证失败!') res.err(err) })
此后,凡是
/api
之外的路由,在路由开始执行处理函数之前,都会去检查用户是否携带token并验证其有效性。另外,还可以通过req.auth
获取到token解析后的信息。
03 密码加密
在未处理之前,密码是以明文保存在数据库的,为了提高安全性,可以将其加密后进行保存。
-
密码加密
router_handler/user.js
const bcrypt = require('bcryptjs') let password = bcrypt.hashSync(password, 10)
-
密码比对
在登陆和重置密码的时候,需要对用户传过来的明文密码与保存在数据库中的密文密码进行比对
const bcrypt = require('bcryptjs') //在对应处理函数内部: const compareResult=bcrypt.compareSync(传过来的密码,数据库的密码) if(!compareResult) return res.err('密码错误!')
04 表单验证
-
定义验证规则
schema/user.js
//字符串类型 接受数字与字母 1~10个字符 必填 const username=joi.string().alphanum().min(1).max(10).required() //字符串类型 接受数字与字母 3~30个字符 必填 const password=joi.string().pattern(/^[a-zA-Z0-9]{3,30}$/).required()
-
导出验证规则
schema/user.js
//之所以加上body,是因为后面用到的验证中间件要求的这样写的 //也可以不用这个body,但需要对验证中间件进行修改 exports.user_schema={ body:{ username, //字段username的验证规则是上面写的username,所以省略了。或者写成username:username password } }
-
定义验证中间件
为了方便可以直接用别人写好的
@escook/express-joi
,这个包是基于joi
进行的封装。我为了了解原因,去把源码copy了下来,放在了utils/expressJoi中。更多api可以去joi官网进行了解。utils/expressJoi
const Joi = require('joi') // schemas:验证规则 // options:配置 const expressJoi = function (schemas, options = { strict: false }) { // 01 非严格模式 if (!options.strict) { // allowUnknown 允许提交未定义的参数项,默认值false // stripUnknown 过滤掉那些未定义的参数项,默认值false // 下面将改为允许提交未定义参数项,不过滤未定义参数项,并且把多余的配置项解构出来 options = { allowUnknown: true, stripUnknown: true, ...options } } // strict是自己定义的,由于现在没什么作用了,且joi选项本身不包含该项,故删除掉 delete options.strict // 02 定义中间件 // 分别对body、query、params的内容进行验证 return function (req, res, next) { ['body', 'query', 'params'].forEach(key => { // 如果用户没有传入该数组中的项,跳过 if (!schemas[key]) return // 用户传入了该数组中的项,那么对每一项里面的内容进行验证 // 比如说上面写了body,那么这里的schemas[key]就是body里面的内容 // 03 通过Joi.object()转为官网要求的类型,可以把schema理解为规则 const schema = Joi.object(schemas[key]) // 04 validate()用于验证内容是否符合规则,并返回一个对象, // 这个对象中error表示验证失败后返回的信息,value表示传进来的需要验证的内容 // 该方法允许传入需要验证的内容和配置 const { error, value } = schema.validate(req[key], options) // 如果上面返回了error,说明验证失败,抛出错误 // 否则就是验证成功,接着把传进来的信息挂载到req属性中,比如req.body if (error) { throw error } else { req[key] = value } }) next() } } module.exports = expressJoi
-
规则验证中间件(局部)
在需要验证的地方注册该中间件
router/user.js
const {user_schema} =require('../schema/user') //验证规则 const expressJoi=require('../utils/expressJoi') //验证中间件 // 注册 router.post('/reguser',expressJoi(user_schema),userHandler.regUser)
-
捕获验证错误(全局)
app.js
const Joi = require("joi") app.use((err,req,res,next)=>{ //...其他的错误捕获 if(err instanceof Joi.ValidationError) return res.err(err) res.err(err) })
05 图片上传
multer是专门处理formdata的包
-
确定存储路径
app.js
app.use('/uploads', express.static('./uploads'))
-
定义上传规则中间件
官方有更简便的写法。我这里为了过滤文件格式、定义文件名称,自己写了个规则。
utils/upload.js
const multer = require('multer') const path = require('path') const stringRandom =require('string-random') //生成随机字符串,防止图片被随意获取 // 过滤文件:仅支持jpge、png、jpg格式 function fileFilter (req, file, cb) { let extname = path.extname(file.originalname) let allow='.jpge|.png|.jpg' if(allow.includes(extname)){ cb(null, true) //表示通过 }else{ cb(new Error('仅支持'+allow+'文件格式')) } } // 存储引擎 const storage = multer.diskStorage({ // 存储路径 destination: function (req, file, cb) { cb(null, path.join(__dirname, '../uploads')) }, // 文件名称 filename: function (req, file, cb) { let extname = path.extname(file.originalname) cb(null, stringRandom(24, { numbers: true })+ extname) } }) const upload = multer({ storage, fileFilter }) module.exports=upload
-
注册中间件(局部)
upload.single()接受字段名,把该字段名的信息保存在
req.file
中,其余的数据会被挂载到req.body
中router/article.js
const upload=require('../utils/upload') router.post('/addarticle',upload.single('cover_img'),expressJoi(add_article_schema),addArticle)
-
使用
router_handler/article.js
exports.addArticle=(req,res)=>{ const file=req.file const articleInfo = { ...req.body,// 标题、内容、状态、所属的分类Id // cover_img中,filename是中间件处理后的文件名称, // 这里的属性值是图片的存储路径 // 为了能直接通过 http://localhost:3000/uploads/文件名称 来获取图片,这里需要转义一下 cover_img:'\/uploads'+'\/'+file.filename, publish_date: new Date(),//文章发布时间 author_id: req.auth.id,//文章作者的Id } //... }
MySQL模块
-
创建mysql对象并连接
db/index.js
const mysql = require('mysql') const db = mysql.createPool({ host: '127.0.0.1', user: 'root', password: '12345', database: 'event', }) module.exports = db
-
操作数据库
在处理函数内执行sql
const db=require('../db/index') let sql='insert into e_articles set ?' //?占位符,参数会传进来 //这里的参数即可以是单个对象,单个值,也可以是数组 db.query(sql语句,参数,(err,result)=>{ if(err) return res.err(err) //数据库内部的错误 if(result.affectedRows !== 1) return res.err('xxx') res.ok(\) })
错误记录
基本都是写sql语句的时候出的错误。
Error: Illegal arguments: string, undefined
不合法的参数:字符串 未定义
-------------
场景:
在重置密码的处理函数中
原本查询用户是否存在的语句是:select * from e_users where id=?',目的是想获得用户的旧密码
但我突然想起老师说写*效率低,于是改成了username,
核查过,表的字段确实有username
后面我在终端执行了 select username from e_users where id=7,是有结果的。
然后往下一看,下一条更新密码的语句中用到了我上一条查询语句结果的密码,而我上一条语句没有去获取密码字段,于是报错了!
---------------
结论:
如果看到类报错,建议去核实一下传给sql语句的参数是否可以正常获取到
Cannot set headers after they are sent to the client
不能在响应客户端之后设置头部
---------------------------
场景:
为了方便,在写新的处理函数时,我直接copy别的处理函数,打算参考着写
结果!我写好了现在处理函数,之前copy过来的忘记删掉了!
于是呢,我的处理函数就存在多个res.send。
这个翻译怪怪的,于是我将这个错误理解为:
如果已经响应给了客户端,就不能重复响应(因为每次响应都会带着头部,即set headers?)
------------------
结论:
检查自己是否调用了多次res.send(),
检查是否有些res.send()忘记return了
最近遇到了一个有些玄学的错误,错误内容大概是`res.err() is not a function`,
我就纳闷了,首先这个全局响应的中间件我是在开头就注册的,其次错误拦截中间件我也是在最后面写的。
这个问题我碰到过两次,第一次就换了下`const Joi=require('joi')`的位置,
把它从上面拉到了错误拦截中间件的上面,结果成功了;
第二次我是换了下`const {expressjwt: jwt}=require('express-jwt')`的位置,
把它拉到jwt中间件上面,也成功了。
这两者我一开始都是放在了全局响应中间件的后面,也就是偏前面的位置。
因为当时搞得我有些烦躁,莫名其妙的成功我也是非常谢天谢地,于是没太关注
晚上睡觉的时候突然想起这个问题,立了flag想对文章进行一下补充,
但今天还原了位置,想蹲到这个错误的时候,发现它根本不出来!
我估摸着可能跟commonjs的模块加载特性有关。先留个坑,以后碰到了再来填:)