Appearance
后端实战-后端实战-用户信息相关接口实现(修改个签、修改密码、上传头像)
前言
上一章节带大家实现了登录注册接口,鉴权的相关工作基本完成。本章节带大家实现用户信息的相关接口,如获取用户信息、修改个性签名、重置用户密码、上传用户头像。
文件资源的上传和获取,是本章节的主要目的,无论是什么项目,服务端都需要处理文件资源,如 Excel
、Word
、音频、视频、图片、pdf
等。我们以最常见的图片资源为例,通过这个例子的分析和学习,同学们可以拓展思维,将其应用到其他的文件资源形式上。
知识点
数据库的资源获取
数据库的
update
更新Egg
文件资源处理
获取用户信息
话不多说,我们直接进入正题。首先我们打开 /app/controller/user.js
,添加 getUserInfo
方法,代码如下所示:
javascript
// 获取用户信息
async getUserInfo() {
const { ctx, app } = this;
const token = ctx.request.header.authorization;
// 通过 app.jwt.verify 方法,解析出 token 内的用户信息
const decode = await app.jwt.verify(token, app.config.jwt.secret);
// 通过 getUserByName 方法,以用户名 decode.username 为参数,从数据库获取到该用户名下的相关信息
const userInfo = await ctx.service.user.getUserByName(decode.username)
// userInfo 中应该有密码信息,所以我们指定下面四项返回给客户端
ctx.body = {
code: 200,
msg: '请求成功',
data: {
id: userInfo.id,
username: userInfo.username,
signature: userInfo.signature || '',
avatar: userInfo.avatar || defaultAvatar
}
}
}
代码中已经添加详细的注释信息,我就不再赘述了。
接着我们将接口抛出,并且添加鉴权中间件,如下所示:
javascript
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller, middleware } = app;
const _jwt = middleware.jwtErr(app.config.jwt.secret); // 传入加密字符串
router.post('/api/user/register', controller.user.register);
router.post('/api/user/login', controller.user.login);
router.get('/api/user/get_userinfo', _jwt, controller.user.getUserInfo); // 获取用户信息
router.get('/api/user/test', _jwt, controller.user.test);
};
我们直接通过 Postman
验证结构是否可行,如下所示:
注意,需要给
Headers
添加authorization
属性,值为之前登录接口返回的token
信息。
修改个性签名
为了兼容后续的修改用户头像,我们将接口名称定义的宽一些,我们在 /controller/user.js
下,新建 editUserInfo
方法,添加如下代码:
javascript
// 修改用户信息
async editUserInfo () {
const { ctx, app } = this;
// 通过 post 请求,在请求体中获取签名字段 signature
const { signature = '' } = ctx.request.body
try {
let user_id
const token = ctx.request.header.authorization;
// 解密 token 中的用户名称
const decode = await app.jwt.verify(token, app.config.jwt.secret);
if (!decode) return
user_id = decode.id
// 通过 username 查找 userInfo 完整信息
const userInfo = await ctx.service.user.getUserByName(decode.username)
// 通过 service 方法 editUserInfo 修改 signature 信息。
const result = await ctx.service.user.editUserInfo({
...userInfo,
signature
});
ctx.body = {
code: 200,
msg: '请求成功',
data: {
id: user_id,
signature,
username: userInfo.username
}
}
} catch (error) {
}
}
此时我们还需要打开 /service/user.js
,新建一个 editUserInfo
用于修改数据库中的用户信息,代码如下:
javascript
// 修改用户信息
async editUserInfo(params) {
const { ctx, app } = this;
try {
// 通过 app.mysql.update 方法,指定 user 表,
let result = await app.mysql.update('user', {
...params // 要修改的参数体,直接通过 ... 扩展操作符展开
}, {
id: params.id // 筛选出 id 等于 params.id 的用户
});
return result;
} catch (error) {
console.log(error);
return null;
}
}
此时,我们在 router.js
脚本中,将修改接口抛出:
javascript
router.post('/api/user/edit_userinfo', _jwt, controller.user.editUserInfo); // 修改用户个性签名
打开 Postman
验证接口是否正确:
数据库也相应的修改成功:
修改用户头像
说到修改用户头像,正常情况下, 我们在前端是这样操作的。首先,点击用户头像;其次,弹出弹窗或进入手机相册,选择一张自己喜欢的头像,然后上传头像,最后将自己的头像替换成修改后的头像。
上述流程涉及到一个步骤,那就是「上传图片」。所以在编写修改头像信息接口之前,我们需要先实现一个「上传图片」的接口。上传图片的作用是比较宽泛的,不光是头像需要上传图片,其他很多操作也都需要用到,如朋友圈、商品图片等等。所以我们在 controller
文件夹下新建一个脚本,名为 upload.js
,如下:
接下来,先分析一波图片上传到服务器的逻辑。
1、首先我们需要在前端调用上传接口,并将图片参数带上,具体怎么带,后面代码部分会讲解。
2、在服务端接收前端传进来的图片信息,信息中含有图片路径信息,我们在服务端通过 fs.readFileSync
方法,来读取图片内容,并存放在变量中。
3、找个存放图片的公共位置,一般情况下,都会存放至 app/public/upload
,上传的资源都存在此处。
4、通过 fs.writeFileSync
方法,将图片内容写入第 3 步新建的文件夹中。
5、最后返回图片地址,基本上图片地址的结构是 host + IP + 图片名称 + 后缀
,后续代码中会为大家详细讲解返回的路径。
目前我们没有前端项目可以上传图片,所以这里我们先用HTML
简单写一个上传页面,如下所示:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片上传</title>
</head>
<body>
<input type="file" id='upload' />
<script>
// 获取 input 标签的 dom
var input = document.getElementById('upload')
// 监听它的变化
input.addEventListener('change', function(e) {
// 获取到上传的 file 对象
var file = input.files[0]
// 声明 FormData 实例 formData
let formData = new FormData()
// 添加实例属性 file
formData.append('file', file)
console.log('formData', formData)
// 调用服务端上传接口。
fetch('http://localhost:7001/api/upload', {
method: 'POST',
body: formData
}).then(res => {
if(res.ok) {
console.log('success')
return res.json();
} else {
console.log('error')
}
}).then(res => {
console.log('res is', res);
})
})
</script>
</body>
</html>
上述 HTML
的功能很简单,就是将上传的资源经过 FormData
实例封装之后,传给服务端。接下来,我们前往服务端接收数据,打开 upload.js
,添加如下代码:
javascript
'use strict';
const fs = require('fs')
const moment = require('moment')
const mkdirp = require('mkdirp')
const path = require('path')
const Controller = require('egg').Controller;
class UploadController extends Controller {
async upload() {
const { ctx } = this
// 需要前往 config/config.default.js 设置 config.multipart 的 mode 属性为 file
let file = ctx.request.files[0]
// 声明存放资源的路径
let uploadDir = ''
try {
// ctx.request.files[0] 表示获取第一个文件,若前端上传多个文件则可以遍历这个数组对象
let f = fs.readFileSync(file.filepath)
// 1.获取当前日期
let day = moment(new Date()).format('YYYYMMDD')
// 2.创建图片保存的路径
let dir = path.join(this.config.uploadDir, day);
let date = Date.now(); // 毫秒数
await mkdirp(dir); // 不存在就创建目录
// 返回图片保存的路径
uploadDir = path.join(dir, date + path.extname(file.filename));
// 写入文件夹
fs.writeFileSync(uploadDir, f)
} finally {
// 清除临时文件
ctx.cleanupRequestFiles();
}
ctx.body = {
code: 200,
msg: '上传成功',
data: uploadDir.replace(/app/g, ''),
}
}
}
module.exports = UploadController;
我们从头到位分析资源上传接口逻辑。首先我们需要安装 moment
和 mkdirp
,分别用于时间戳的转换和新建文件夹。
bash
npm i moment mkdirp -S
其次,egg
提供两种文件接收模式,1 是 file
直接读取,2 是 stream
流的方式。我们采用比较熟悉的 file
形式。所以需要前往 config/config.default.js
配置好接收形式:
javascript
config.multipart = {
mode: 'file'
};
multipart
配置项有很多选项,比如 whitelist
上传格式的定制,fileSize
文件大小的限制,这些都可以在文档中查找到。
配置完成之后,我们才能通过 ctx.request.files
的形式,获取到前端上传的文件资源。
通过 fs.readFileSync(file.filepath)
读取文件,保存在 f
变量中,后续使用。
创建一个图片保存的文件路径:
javascript
let dir = path.join(this.config.uploadDir, day);
this.config.uploadDir
需要全局声明,便于后续通用,在 config/config.default.js
中声明如下:
javascript
// add your user config here
const userConfig = {
// myAppName: 'egg',
uploadDir: 'app/public/upload'
};
通过 await mkdirp(dir)
创建目录,如果已经存在,这里是不会再重新创建的,mkdirp
方法内部已经实现。
构建好文件的路径,如下:
javascript
uploadDir = path.join(dir, date + path.extname(file.filename));
最后,我们将文件内容写入上述路径,如下:
javascript
fs.writeFileSync(uploadDir, f)
成功之后返回路径:
javascript
ctx.body = {
code: 200,
msg: '上传成功',
data: uploadDir.replace(/app/g, ''),
}
这里要注意的是,需要将 app
去除,因为我们在前端访问路径的时候,是不需要 app
这个路径的,比如我们项目启动的是 7001 端口,最后我们访问的文件路径是这样的 http://localhost:7001/public/upload/20210521/1621564997310.jpeg
。
完成上述操作之后,我们还需要在做最后一步操作,解决跨域。首先安装 egg-cors
插件 npm i egg-cors
,安装好之后,前往 config/plugins.js
下添加属性:
javascript
cors: {
enable: true,
package: 'egg-cors',
},
然后在 config.default.js
配置如下:
javascript
config.cors = {
origin: '*', // 允许所有跨域访问
credentials: true, // 允许 Cookie 跨域跨域
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
};
上述逻辑完成之后,我们打开之前写好的前端页面,点击上传图片,如下:
拿到这样一串路径,我们可以查看服务端项目 app/public
文件夹下,是否存入了图片资源:
在通过浏览器访问图片路径,如下代表图片成功上传至服务器:
此时我们拿到了服务器返回的图片地址,便可以将其提交至 editUserInfo
方法。我们为 editUserInfo
方法添加如下参数:
javascript
// 修改用户信息
async editUserInfo () {
const { ctx, app } = this;
// 通过 post 请求,在请求体中获取签名字段 signature
const { signature = '', avatar = '' } = ctx.request.body
try {
let user_id
const token = ctx.request.header.authorization;
const decode = await app.jwt.verify(token, app.config.jwt.secret);
if (!decode) return
user_id = decode.id
const userInfo = await ctx.service.user.getUserByName(decode.username)
const result = await ctx.service.user.editUserInfo({
...userInfo,
signature,
avatar
});
ctx.body = {
code: 200,
msg: '修改成功',
data: {
id: user_id,
signature,
username: userInfo.username,
avatar
}
}
} catch (error) {
}
}
上述代码,在传参中添加了 avatar
参数,并且传入 ctx.service.user.editUserInfo
方法保存。
上传资源知识拓展
上述方法是我们没有 OSS
服务的情况下使用的,目前市面上更多的方式,是购买 OSS
服务,将图片等静态资源上传至 CDN
,通过内容分发的形式,让使用的用户就近获取在线资源。这属于网站性能优化的一种方式,减少主域名下的资源请求数量,以此来降低网页加载的延迟。
七牛云免费提供了 10GB 的存储空间,如果有域名并且备案过的同学,可以利用它实现一个 CDN
的服务,将文件资源存到七牛云内,这样可以降低自己服务器的存储压力。
总结
此时我们又完成了三个接口的编写,你会觉得,写服务端比写前端轻松多了。
其实不是这样的,每一个工种都有各自的难点。前端更多的是面向浏览器,而浏览器和用户是一对一的关系,前端更多的是注重视觉和交互方面的体验,让用户以最简单易用的方式去完成自己的诉求。
反观服务端,则是一份服务端代码,为多个终端服务,所以服务端更多是一对多的关系。这就很考验服务端的代码,以及数据库的工作效率。在流量峰值能否很好的响应每个用户发起的请求,极端情况就是天猫双十一这种请求量级,服务端的压力是难以想象的。
所以每个工种,只要做得精,都能发光发热。