我认为做一个项目,首先我们要明确需求,然后就是分析结构、规划项目,把功能的关系想清楚才可以开始。
所以首先我们理一下项目思路:
我们制作的coderhub是一个类似于个人博客的网站。这就需要几个基本功能:
- 登录(注册)
- 发表动态/查看动态
- 评论动态/评论
- 给动态添加标签
- 上传头像/文件
通过上面可以看出,登录功能一定是最先需要的那个,所以首先我们需要实现登录功能。
还要介绍一点点东西,方便我们把代码写的优雅:
-
在
Node.js
中,中间件主要是指封装所有Http
请求细节处理的方法。一次Http
请求通常包含很多工作,如记录日志、ip
过滤、查询字符串、请求体解析、Cookie
处理、权限验证、参数验证、异常处理等,但对于Web
应用而言,并不希望接触到这么多细节性的处理,因此引入中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。也就是说,所有关系到http请求处理的,我们都把他们封装成中间件;而这种查询、验证等与数据库相关的,我们把他们抽出来放到数据库处理的文件里面去;关于错误有关的,比如HTTP状态码、错误提示信息等我们也把它抽到一个文件里面去。
-
有关控制函数和数据库的操作都各自封装成了一个类。使用的时候直接引入,调用用 类.方法就可以实现
-
createPool 创建连接池
在开发web应用程序时,连接池是一个很重要的概念。建立一个数据库连接所消耗的性能成本是很高的。在服务器应用程序中,如果为每一个接收到的客户端请求都建立一个或多个数据库连接,将严重降低应用程序性能。
因此在服务器应用程序中通常需要为多个数据库连接创建并维护一个连接池,当连接不再需要时,这些连接可以缓存在连接池中,当接收到下一个客户端请求时,从连接池中取出连接并重新利用,而不需要再重新建立连接。
数据库相关的,host、port、databse、user、password都封装为一个环境变量,然后在database文件里创建连接池,后面就可以引入调用
登录功能
-
我们需要一个登陆页面,那么首先应该想到的就是需要一个路由,这次我们使用koa来完成。所以,首先安装koa并导入koa-router,创建一个路由实例。prefix是统一资源定位符Url的前缀部分
const Router = require('koa-router') const userRouter = new Router({prefix:'/users'})
-
对用户请求的用户名和密码进行验证:包含是否为空、是否已经存在数据库中。写一个逻辑:
userRouter.post('/',verifyUser,handlePassword,create)
用户到底请求的是哪个路径,我们接收到请求时候要做什么处理:验证是否为空且是否在数据库中,如果非空且不存在,即前者验证通过,那么我们就将密码进行加密然后传入数据库储存起来。userRouter.post('/',verifyUser,handlePassword,create)
这个逻辑比较复杂,所以我们从简单开始实现,然后一步步添砖加瓦:
-
用户传过来的用户名和密码是否为空,不为空就创建数据插入到数据库(封装成中间件)
const verifyUser=async(*ctx*,*next*)*=>*{ //拿到用户名和密码 *const* {name,password} = *ctx*.request.body //判断用户名和密码不为空 if(!name || !password){ *const* error = new Error(errorType.NAME_OR_PASSWORD_IS_REQUIRED) return *ctx*.app.emit('error',error, *ctx*) } await next() }
async create(ctx,next){ const {name} = ctx.request.body const result = await service.create(name) ctx.body = result }
-
如果验证成功了,那么我们创建用户:向数据库里插入这条信息。所以现在逻辑通了,但是我们需要数据库的函数,所以去写这个方法。
async create(user){ const {name,password} = user; const statement = `INSERT INTO user (name,password) VALUES (?,?);` const result = await connection.execute(statement,[name,password]) return result[0] }//execute(sql [,parameters]):执行SQL语句。参数用作SQL语句中的参数指定值。
-
我们验证后发现可以创建成功,然后继续补充加密处理的逻辑,这里只用一个简单的
const handlePassword = async(ctx,next)=>{ const {password} = ctx.request.body ctx.request.body.password = md5password(password) await next() }
再试试插入一条数据,密码已经被加密存储了。接下来完成验证用户是否重复部分的操作
//判断注册的用户名是否被注册过 const result = await service.getUserByName(name) if(result.length){ const error = new Error(errorType.USER_ALREADY_EXIST) return ctx.app.emit('error',error,ctx) }
-
前面是没有实现功能之前,有一些错误,所以导致zs被插入了很多条,改正之后开始正常
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WQEQKDzW-1650814714287)(E:\markdown\前端\csdn\20220424-1.png)]
-
注册完用户之后应该开启登录功能
-
设置登录的路由和逻辑
authRouter.post('/login', verifyLogin,login)
所以说我们首先进行登陆的验证,比如姓名密码的填写、用户是否存在、密码是否正确
const verifyLogin = async(ctx,next)=>{ //获取用户名和密码 const {name,password} = ctx.request.body //判断用户名或密码是否为空 if(!name || !password){ const error = new Error(errorType.NAME_OR_PASSWORD_IS_REQUIRED) return ctx.app.emit('error',error, ctx) } //判断用户是否存在 const result = await userService.getUserByName(name) const user = result[0] if(!user){ const error = new Error(errorType.USER_DOSE_NOT_EXIST) return ctx.app.emit('error',error,ctx) } //判断密码是否和数据库中的一致(加密后) if(md5password(password) !==user.password ){ const error = new Error(errorType.PASSWORD_ERROR) return ctx.app.emit('error',error,ctx) } ctx.user = user await next() }
-
验证通过后该进行下一个登陆函数了,登陆成功后颁发token令牌
async login(ctx,next){ const {id,name} = ctx.user ctx.body= '登陆成功' const token = jwt.sign({id,name},config.jwtSecretKey,{expiresIn:config.expiresIn}) ctx.body={id,name,token} }
-
利用token的验证来识别是否认证成功
authRouter.get('/test', verifyAuth,success)
进行token的认证,没有token就是没有认证,如果token无效就是认证失败
//认证函数 const verifyAuth = async(ctx,next)=>{ const authorization = ctx.headers.authorization const token = authorization.replace('Bearer ','') if(!authorization){ const error = new Error(errorType.UNAUTHORIZATION) return ctx.app.emit('error',error,ctx) } try{ const result = jwt.verify(token,config.jwtSecretKey) ctx.user = result await next() }catch(err){ const error = new Error(errorType.UNAUTHORIZATION) ctx.app.emit('error',error,ctx) } }
-
如果成功认证,那么提示成功
async success(ctx,next){ ctx.body = "授权成功" }
-
登录功能基本就先这样了,如果后续需要添加会在后面补充,保证逻辑性。
发布动态/获取动态
在前面认证成功之后,接下来我们需要实现发布以及获取动态的功能。
发布动态:发布动态一定需要我们先认证,数据库里面有这个user并且登录之后,才可以发布动态
获取动态:未登录状态一样可以获取到动态信息,得到动态列表,只是不能进行评论等
-
首先还是要导入koa-router并创建路由实例,想想我们后面还有很多功能,一个一个导入、创建实例、挂载,非常的麻烦,所以我们在这就开始封装一个函数,动态挂载。这里需要借助fs模块
const fs = require('fs') const useRoutes = (app)=>{ fs.readdirSync(__dirname).forEach(file=>{ if(file === 'index.js') return ; const router = require(`./${file}`) app.use(router.routes()) app.use(router.allowedMethods()) }) } module.exports = useRoutes
传入app这个参数,就可以实现动态挂载了
const Koa = require('koa') const bodyParser = require('koa-bodyparser')//对于POST请求的处理,koa-bodyparser中间件可以把koa2上下文的formData数据解析到ctx.request.body中 const errorHandler = require('./error_handler') const useRoutes = require('../router') const app = new Koa() app.use(bodyParser()) useRoutes(app)
-
路由创建完成后,开始创建实例并实现功能:发表动态、查看动态列表、查看动态详情、修改动态、删除动态
const Router = require('koa-router') const dynamicRouter = new Router({prefix:'/dynamic'})
-
首先,先实现发表动态:我们需要登录,登录认证成功后发表动态,先让逻辑可行,直接发表
dynamicRouter.post('/',verifyAuth,createDyna)
实现发表的函数:
async createDyna(ctx,next){ //获取发表动态的用户和内容 const userId = ctx.user.id const content = ctx.request.body //将数据插入到数据库 const result = await dynaService.createDyna(userId,content) ctx.body = result }
还是一样,逻辑通了,但是我们缺少把动态插入数据库的函数,写到service.js中
async create(userId,content){ const statement = `INSERT INTO moment (content,user_id) VALUES (?,?)` const [result] = await connection.execute(statement,[content,userId]) return result }
验证后成功发布
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8IIb6uRR-1650814714289)(E:\markdown\前端\csdn\20220424-2.png)]
-
查看动态的详情:即返回一条消息,定义规则
dynamicRouter.get('/:momentId',detail)
去完成detail详情函数:
async detail(ctx,next){ const momentId = ctx.params.momentId const result = await dynaService.getDynaById(momentId) ctx.body = result }
要获取详情即从数据库查询到想要查的数据
async getDynaById(id){ const statement = `select * from moment where id=?` const [result] = await connection.execute(statement,[id]) return result[0] }
但是我们还需要获取到动态的作者、作者的头像等,这些在后面一起改
-
进行动态列表的获取,先写好路由,需要一个获取列表的list函数
dynamicRouter.get('/',list)
所以现在我们只需要去完成list函数
async list(ctx,next){ const {offset, size} = ctx.query const result = await dynaService.getDynaList(offset,size) ctx.body = result }
需要真正得到数据,还需要去数据库里进行查询:完成getDynaList函数
async getDynaList(offset,size){ const statement=` SELECT m.id id, m.content content, m.createAt createTime,m.updateAt updateTime, JSON_OBJECT('id',u.id,'name',u.name) user FROM moment m LEFT JOIN user u ON m.user_id = u.id LIMIT ?,?` const [result] = await connection.execute(statement,[offset,size]) return result
这样就可以获得去除前offset条之后的size条数据了
-
下面该修改和删除动态了,这就需要权限:先登录,确保发布者和登陆者相同,才可以进行操作
写路由规则:
dynamicRouter.patch('/:momentId',verifyAuth,verifyPermission,update) dynamicRouter.delete('/:momentId',verifyAuth,verifyPermission,remove
显示验证登陆状态,然后验证是否有权限修改,验证成功更新:
const verifyPermission = asyne (ctx,next)=>{ const { momentId } = ctx.params const { id ]= ctx.user try { const isPermission = await authService.checkMoment(momentId,id) if (!isPermission) throw new Error() await next() }catch (err){ const error = new Error(errorTypes. UNPERMISSION) return ctx.app.emit( 'error', error,ctx) }
更新的函数:有权限状态下提供动态的id和修改后的内容
async update(ctx,next) { const { momentId } = ctx.params const { content } = ctx.request.body const result = await dynaService.update(content,momentId); ctx.body = result; }
在数据库操作文件中写出对应的更新函数:
async update(content,momentId){ const statement = `UPDATE moment SET content=? WHERE id=?` const [result] = await connection.excute(statement,[content,momentId]) return result }
删除的函数:删除函数只需要在有权限的状态下提供动态id即可,调用数据库的函数
async remove(ctx,next){ const {momentId} = ctx.params const result = await dynaService.remove(momentId) ctx.body = result }
在数据库操作文件中写出对应的删除函数:
async remove(momentId){ const statement = `DELETE FROM moment WHERE id=?` const [result] = await connection.execute(statement,[momentId]) return result }
评论动态
我们可以对动态进行评论,以及对评论进行评论,那么相应的,如果这条动态/评论删除了,那么它下面对应得评论也应该消失。
-
导入路由模块
const Router = require('koa-router') const commentRouter = new Router ({prefix:'./comment'})
-
制定评论的路由规则,要求先登录,才可以对别人的动态进行评论
commentRouter.post('/',verifyAuth,create)
所以通过登录验证后,我们将进行create创建评论的操作
async create(ctx,next){ const {momentId,content} = ctx.request.body const {id} = ctx.user const result = await commService.create(momentId,content,id) ctx.body = result }
这里面就是数据库多对多的关系:一个人可以给多个动态评论,一个动态下的所有评论也可以对应多个人,所以应该是一个关系表,存储id对应的commenid的关系,下面去数据库中建立user_id、comment_id和content的关系:
async create (momentId, content, userId){ const statement = `INSERT INTO comment (content,moment_id, user_id) VALUES (?,?,?)` const [result] = await connection.execute(statement,[content,momentId,userId]) return result }
-
同样的,也可以对评论进行回复,路由关系如下:
commentRouter.post('/:commentId/reply',verifyAuth,reply)
登陆成功后可以回复别人的评论,函数如下:
async reply(ctx,next){ const {momentId,content,commentId} = ctx.request.body const {id} = ctx.user const result = await commService.reply(momentId,content,id,commentId) return result }
也还是在数据库里建立关系表:user_id、comment_id、content和moment_id的关系
async reply (momentId, content, userId,commentId){ const statement = `INSERT INTO comment (content,moment_id, user_id,comment_id) VALUES (?,?,?,?)` const [result] = await connection.execute(statement,[content,momentId,userId,commentId]) return result }
与动态类似,评论也可以修改和删除,要修改评论和删除评论,首先要确保登录,并且确定操作的是自己的评论,所以需要验证权限,而且删除时要注意的就是把对应的子评论一起删除
路由规则:
//修改评论 commentRouter.post('/:commentId/reply',verifyAuth,verifyPermission,update) //删除评论 commentRouter.delete('/:commentId',verifyAuth,verifyPermission,remove)
修改评论是在验证通过的情况下去更新
async update(ctx,next){ const {commentId} = ctx.params const {content} = ctx.request.body const result = await commService.update(commentId,content) ctx.body = result }
而更新则是将commentId对应的content进行修改
async update(commentId,content){ const statement = `UPDATE comment SET content=? WHERE id=?` const [result] = await connection.execute(statement,[content,commentId]) return result }
删除评论也是一样的,验证通过后删除关系表中对应的数据:
async remove(ctx,next){ const {commentId} = ctx.params const result = await service.remove(commentId) ctx.body = result }
利用数据库的操作完成删除:
async remove (commentId){ const statement = `DELETE DROM comment WHERE id=?` const [result] = await connection.execute(statement,[commentId]) return result }
文章分类(贴标签)
可以实现对文章的分类功能,同样是多对多的关系,一篇文章可以对应多个分类,多个文章也可以同属一个分类。
-
引入路由
const Router = require('koa-router') const labelRouter = new Router({prefix:'/label'})
-
制定路由规则,创建分类或者查看分类列表
labelRouter.post('/',verifyAuth,create) labelRouter.get('/',list)
-
创建一个分类,还是要求要登陆,必须在登录状态下才可以创建分类,通过验证后,就可以创建了。但是这里有个小漏洞,就是没有判断标签是否存在,后续会补上
async create(name){ const statement = `INSERT INTO label (name) VALUES (?)` const [result] = await connection.execute(statement,[name]) return result } ```
4. 给文章添加标签:逻辑:
//给动态添加标签
dynamicRouter.post('/:momentId/labels',verifyAuth,verifyPermission("moment"),verifyLabel,addLabel)
先进行登录验证,确认登陆状态后,进行表的修改,传递参数。然后进行标签的验证(判断这个标签是否存在,如果存在这个标签则不会重复贴上,如果不存在就创建标签)
const verifyLabel = async(ctx,next)=>{
//取出要添加的所有标签
const {labels} = ctx.request.body
//判断表中是否存在该标签
const newLables = []
for (let name of labels){
const isExist = await service.hasLabel(name)
const label = {name}
if(!isExist){
const result = await service.create(name)
label.id = result.insertId
}else{
label.id = isExist.id
}
newLables.push(label)
}
-
验证是否存在该标签的数据库查询语句:
async hasLabel(name){ const statement = `SELECT * FROM label WHERE name = ?` const [result] = await connection.execute(statement,[name]) return result[0] }
-
如果判断得到标签不存在,那么,就创建该标签:
async create(name){ const statement = `INSERT INTO label (name) VALUES (?)` const [result] = await connection.execute(statement,[name]) return result }
-
最后,把标签的id和动态的id对应起来,对应为一条关系数据,存储到关系表之中:
const addLabel = async(momentId,labelId)=>{ const statement = `INSERT INTO moment_label (moment_id, label_id) VALUES (?,?)` const [result] = await connection.execute(statement, [momentId,labelId]) return result }
-
获取分类列表,只查看的话不需要任何权限,可以直接进行:
async list(ctx,next){ const {limit,offset}=ctx.query const result = await labelService.getLabels(limit,offset) ctx.body = result }
-
借助数据库,查看limit条分类标签
async getLabels(limit, offset){ const statement = `SELECT * FROM label LIMIT ?,?` const [result] = await connection.execute(statement,[offset,limit]) return result[0] }
文件/图片管理(上传头像,动态配图)
可以让user上传图片作为头像而是页面更美观,同时也允许在发动态的时候配图使动态更丰富。
-
配置路由
const Router = require('koa-router') const fileRouter = new Router({prefix:'/upload'})
-
制定上传头像的路由规则
fileRouter.post('/avatar',verifyAuth,avatarHandler,saveAvatorInfo)
-
验证是否登录,验证成功后,生成一个文件目录来储存头像
const Multer = require('koa-multer') const avatarUpload = Multer({ dest:'./uploads/avator' }) const avatarHandler = avatarUpload.single('avator')
-
获取头像相关信息:
async saveAvatorInfo(ctx,next){ //获取图像相关信息 const {mimetype,filename,size} = ctx.req.file const {id} = ctx.user //将图像信息数据保存到数据库 const result = await FileService.createAvatar(filename,mimetype,size,id) const avatarUrl = `${APP_HOST}:${APP_PORT}/users/${id}/avatar` await userService.updateAvatarUrlById(avatarUrl,id) ctx.body='上传头像成功' }
需要进行数据库的处理函数:把图片文件名,文件类型,大小,对应的用户id传入avatar记录头像的数据库中
async createAvatar(filename,mimetype,size,userId){ const statement = `INSERT INTO avatar (filename,mimetype,size,user_id) VALUES (?,?,?,?)` const [result] = await connection.execute(statement,[filename,mimetype,size,userId]) return result }
把图像的图片地址传入数据库,这样点击头像的时候可以去新窗口查看
async updateAvatarUrlById(userId){ const statement = `UPDATE user SET avatar_url = WHERE id = ?` const [result] = await connection.execute(statement,[avatarUrl,userId]) return result }
-
上传动态中的图片,先配置路由规则,写好逻辑
fileRouter.post('/picture',verifyAuth,pictureHandler,pictureResize,savePictureInfo)
当我们登陆了之后,与上面相同,获取文件信息
const pictureUpload = Multer({
dest:'./uploads/picture'
})
const pictureHandler = pictureUpload.array('picture',9)
把图片设置三种大小,作为动态列表展示、详情展示、图片查看三种尺寸保存到数据库中:
const Jimp = require('jimp')
const pictureResize =async (ctx,next)=>{
//获取所有图像
const files = ctx.req.files
//图像处理
const destPath = path.join(file.destination,file.filename)
for (let file of files){
Jimp.read(file.path).then(image=>{
image.resize(1280,Jimp.AUTO).write(`${destPath}-large`)
image.resize(640,Jimp.AUTO).write(`${destPath}-middle`)
image.resize(320,Jimp.AUTO).write(`${destPath}-small`)
})
}
await next()
}
把图片信息都保存到数据库中:
async savePictureInfo(ctx,next){
const files = ctx.req.files
const {id} = ctx.user
const {momentId} = ctx.query
for (let file of files){
const {mimetype,filename,size} = ctx.req.file
await FileService.createFile(filename,mimetype,size,id,momentId)
}
ctx.body = '上传完成'
}
数据库中的处理逻辑:
async createFile(filename,mimetype,size,userId,momentId){
const statement = `INSERT INTO picture (filename,mimetype,size,user_id,momentId) VALUES (?,?,?,?,?)`
const [result] = await connection.execute(statement,[filename,mimetype,size,userId,momentId])
return result
}
数据都保存到数据库中,这就完成了基本的要求
一些补充
当我们越写越多的时候,我们需要考虑,在获取用户信息的时候,不光需要id、name等,还需要头像;这时会补充一些函数,比如
async avatarInfo(ctx,next){
const {userId} = ctx.params
const avatarInfo = await Fileservice.getAvatarById(userId)
ctx.response.set('content-type',avatarInfo.mimetype)
ctx.body = fs.createReadStream(`../../uploads/avator/${avatarInfo.filename}`)
}
数据库中对应的处理:
async getAvatarById(userId){
const statement = `SELECT * FROM avatar WHERE user_id = ?`
const [result] = await connection.execute(statement,[userId])
return result[0]
}
当我们查看动态详情的时候,还可以查询到动态里的图片信息:
async fileInfo(ctx,next){
const {filename} = ctx.params
const fileInfo = await fileService.getFileByFilename(filename)
const {type} = ctx.query
const types = ["small","middle","large"]
if(types.some(item => item === type)){
filename = filename + '-' +type
}
ctx.response.set('content-type',fileInfo.mimetype)
ctx.body = fs.createReadStream(`../../uploads/picture/${filename}-${type}`)
}
数据库中的处理:
async getFileByFilename(filename){
const statement = `SELECT * FROM file WHERE filename = ?`
const [result] = await connection.execute(statement,[filename])
return result[0]
}
后面还会说一些我在项目中遇到的bug以及改正方法,今天没有整理完。原计划今天发的,只能推迟到明天啦~