本章节带大家实现用户信息的相关接口,如获取用户信息、修改个性签名、重置用户密码、上传用户头像。

文件资源的上传和获取,是本章节的主要目的,无论是什么项目,服务端都需要处理文件资源,如 Excel、Word、音频、视频、图片、pdf 等。我们以最常见的图片资源为例,通过这个例子的分析和学习,同学们可以拓展思维,将其应用到其他的文件资源形式上。

知识点

  • 数据库的资源获取
  • 数据库的 update 更新
  • Egg 文件资源处理

获取用户信息

打开 /app/controller/user.js,添加 getUserInfo 方法,代码如下所示:

  1. // 获取用户信息
  2. async getUserInfo() {
  3. const { ctx, app } = this;
  4. const token = ctx.request.header.authorization;
  5. // 通过 app.jwt.verify 方法,解析出 token 内的用户信息
  6. const decode = await app.jwt.verify(token, app.config.jwt.secret);
  7. // 通过 getUserByName 方法,以用户名 decode.username 为参数,从数据库获取到该用户名下的相关信息
  8. const userInfo = await ctx.service.user.getUserByName(decode.username)
  9. // userInfo 中应该有密码信息,所以我们指定下面四项返回给客户端
  10. ctx.body = {
  11. code: 200,
  12. msg: '请求成功',
  13. data: {
  14. id: userInfo.id,
  15. username: userInfo.username,
  16. signature: userInfo.signature || '',
  17. avatar: userInfo.avatar || defaultAvatar
  18. }
  19. }
  20. }

接着我们将接口抛出,并且添加鉴权中间件,如下所示:

  1. 'use strict';
  2. /**
  3. * @param {Egg.Application} app - egg application
  4. */
  5. module.exports = app => {
  6. const { router, controller, middleware } = app;
  7. const _jwt = middleware.jwtErr(app.config.jwt.secret); // 传入加密字符串
  8. router.get('/api/user/get_userinfo', _jwt, controller.user.getUserInfo); // 获取用户信息
  9. };

修改个性签名

在 /controller/user.js 下,新建 editUserInfo 方法,添加如下代码:

  1. // 修改用户信息
  2. async editUserInfo () {
  3. const { ctx, app } = this;
  4. // 通过 post 请求,在请求体中获取签名字段 signature
  5. const { signature = '' } = ctx.request.body
  6. try {
  7. let user_id
  8. const token = ctx.request.header.authorization;
  9. // 解密 token 中的用户名称
  10. const decode = await app.jwt.verify(token, app.config.jwt.secret);
  11. if (!decode) return
  12. user_id = decode.id
  13. // 通过 username 查找 userInfo 完整信息
  14. const userInfo = await ctx.service.user.getUserByName(decode.username)
  15. // 通过 service 方法 editUserInfo 修改 signature 信息。
  16. const result = await ctx.service.user.editUserInfo({
  17. ...userInfo,
  18. signature
  19. });
  20. ctx.body = {
  21. code: 200,
  22. msg: '请求成功',
  23. data: {
  24. id: user_id,
  25. signature,
  26. username: userInfo.username
  27. }
  28. }
  29. } catch (error) {
  30. }
  31. }

此时我们还需要打开 /service/user.js,新建一个 editUserInfo 用于修改数据库中的用户信息,代码如下:

  1. // 修改用户信息
  2. async editUserInfo(params) {
  3. const { ctx, app } = this;
  4. try {
  5. // 通过 app.mysql.update 方法,指定 user 表,
  6. let result = await app.mysql.update('user', {
  7. ...params // 要修改的参数体,直接通过 ... 扩展操作符展开
  8. }, {
  9. id: params.id // 筛选出 id 等于 params.id 的用户
  10. });
  11. return result;
  12. } catch (error) {
  13. console.log(error);
  14. return null;
  15. }
  16. }

此时,我们在 router.js 脚本中,将修改接口抛出:

  1. router.post('/api/user/edit_userinfo', _jwt, controller.user.editUserInfo); // 修改用户个性签名

修改用户头像

在 controller 文件夹下新建一个脚本,名为 upload.js,如下:
接下来,先分析一波图片上传到服务器的逻辑。
1、首先我们需要在前端调用上传接口,并将图片参数带上,具体怎么带,后面代码部分会讲解。
2、在服务端接收前端传进来的图片信息,信息中含有图片路径信息,我们在服务端通过 fs.readFileSync 方法,来读取图片内容,并存放在变量中。
3、找个存放图片的公共位置,一般情况下,都会存放至 app/public/upload,上传的资源都存在此处。
4、通过 fs.writeFileSync 方法,将图片内容写入第 3 步新建的文件夹中。
5、最后返回图片地址,基本上图片地址的结构是 host + IP + 图片名称 + 后缀,后续代码中会为大家详细讲解返回的路径。
目前我们没有前端项目可以上传图片,所以这里我们先用HTML 简单写一个上传页面,如下所示:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <input id="upload" type="file"/>
  11. <script>
  12. var input = document.getElementById('upload')
  13. console.log(input);
  14. input.addEventListener('change', function(e) {
  15. var file = input.files[0]
  16. console.log(file);
  17. let formData = new FormData()
  18. formData.append('file', file)
  19. console.log('formData', formData)
  20. // 调用上传接口
  21. fetch('http://localhost:7001/api/upload', {
  22. method: 'post',
  23. body: formData
  24. }).then(res => {
  25. console.log(res);
  26. }).catch(err => {
  27. console.log(err);
  28. })
  29. })
  30. </script>
  31. </body>
  32. </html>

上述 HTML 的功能很简单,就是将上传的资源经过 FormData 实例封装之后,传给服务端。接下来,我们前往服务端接收数据,打开 upload.js,添加如下代码:

  1. 'use strict';
  2. const fs = require('fs');
  3. const moment = require('moment');
  4. const mkdirp = require('mkdirp');
  5. const path = require('path');
  6. const Controller = require('egg').Controller;
  7. class UploadController extends Controller {
  8. async upload() {
  9. const { ctx } = this;
  10. console.log('ctx.request.files', ctx.request.files);
  11. const file = ctx.request.files[0];
  12. let uploadDir = '';
  13. try {
  14. // files[0]表示获取第一个文件,若前端上传多个文件则可以遍历这个数组对象
  15. const f = fs.readFileSync(file.filepath);
  16. // 1.获取当前日期
  17. const day = moment(new Date()).format('YYYYMMDD');
  18. // 2.创建图片保存的路径
  19. const dir = path.join(this.config.uploadDir, day);
  20. const date = Date.now(); // 毫秒数
  21. await mkdirp(dir); // 不存在就创建目录
  22. // 返回图片保存的路径
  23. uploadDir = path.join(dir, date + path.extname(file.filename));
  24. // 写入文件夹
  25. fs.writeFileSync(uploadDir, f);
  26. } finally {
  27. // 清除临时文件
  28. ctx.cleanupRequestFiles();
  29. }
  30. console.log('uploadDir', uploadDir);
  31. ctx.body = {
  32. code: 200,
  33. msg: '上传成功',
  34. data: uploadDir.replace(/app/g, ''),
  35. };
  36. }
  37. }
  38. module.exports = UploadController;

我们从头到位分析资源上传接口逻辑。首先我们需要安装 moment 和 mkdirp,分别用于时间戳的转换和新建文件夹。

  1. npm i moment mkdirp -S

其次,egg 提供两种文件接收模式,1 是 file 直接读取,2 是 stream 流的方式。我们采用比较熟悉的 file 形式。所以需要前往 config/config.default.js 配置好接收形式:

  1. config.multipart = {
  2. mode: 'file'
  3. };

multipart 配置项有很多选项,比如 whitelist 上传格式的定制,fileSize 文件大小的限制,这些都可以在文档中查找到。
配置完成之后,我们才能通过 ctx.request.files 的形式,获取到前端上传的文件资源。
通过 fs.readFileSync(file.filepath) 读取文件,保存在 f 变量中,后续使用。
创建一个图片保存的文件路径:

  1. let dir = path.join(this.config.uploadDir, day);

this.config.uploadDir 需要全局声明,便于后续通用,在 config/config.default.js 中声明如下:

  1. // add your user config here
  2. const userConfig = {
  3. // myAppName: 'egg',
  4. uploadDir: 'app/public/upload'
  5. };

通过 await mkdirp(dir) 创建目录,如果已经存在,这里是不会再重新创建的,mkdirp 方法内部已经实现。
构建好文件的路径,如下:

  1. uploadDir = path.join(dir, date + path.extname(file.filename));

最后,我们将文件内容写入上述路径,如下:

  1. fs.writeFileSync(uploadDir, f)

成功之后返回路径:

  1. ctx.body = {
  2. code: 200,
  3. msg: '上传成功',
  4. data: uploadDir.replace(/app/g, ''),
  5. }

这里要注意的是,需要将 app 去除,因为我们在前端访问路径的时候,是不需要 app 这个路径的,比如我们项目启动的是 7001 端口,最后我们访问的文件路径是这样的 http://localhost:7001/public/upload/20210521/1621564997310.jpeg。
完成上述操作之后,我们还需要在做最后一步操作,解决跨域。首先安装 egg-cors 插件 npm i egg-cors,安装好之后,前往 config/plugins.js 下添加属性:

  1. cors: {
  2. enable: true,
  3. package: 'egg-cors',
  4. },

然后在 config.default.js 配置如下:

  1. config.cors = {
  2. origin: '*', // 允许所有跨域访问
  3. credentials: true, // 允许 Cookie 跨域跨域
  4. allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
  5. };

上述逻辑完成之后,我们打开之前写好的前端页面,点击上传图片,如下:
image.png
拿到这样一串路径,我们可以查看服务端项目 app/public 文件夹下,是否存入了图片资源:
image.png
在通过浏览器访问图片路径,如下代表图片成功上传至服务器:
image.png
此时我们拿到了服务器返回的图片地址,便可以将其提交至 editUserInfo 方法。我们为 editUserInfo 方法添加如下参数:

  1. // 修改用户信息
  2. async editUserInfo () {
  3. const { ctx, app } = this;
  4. // 通过 post 请求,在请求体中获取签名字段 signature
  5. const { signature = '', avatar = '' } = ctx.request.body
  6. try {
  7. let user_id
  8. const token = ctx.request.header.authorization;
  9. const decode = await app.jwt.verify(token, app.config.jwt.secret);
  10. if (!decode) return
  11. user_id = decode.id
  12. const userInfo = await ctx.service.user.getUserByName(decode.username)
  13. const result = await ctx.service.user.editUserInfo({
  14. ...userInfo,
  15. signature,
  16. avatar
  17. });
  18. ctx.body = {
  19. code: 200,
  20. msg: '修改成功',
  21. data: {
  22. id: user_id,
  23. signature,
  24. username: userInfo.username,
  25. avatar
  26. }
  27. }
  28. } catch (error) {
  29. }
  30. }

上述代码,在传参中添加了 avatar 参数,并且传入 ctx.service.user.editUserInfo 方法保存。

上述方法是我们没有 OSS 服务的情况下使用的,目前市面上更多的方式,是购买 OSS 服务,将图片等静态资源上传至 CDN,通过内容分发的形式,让使用的用户就近获取在线资源。这属于网站性能优化的一种方式,减少主域名下的资源请求数量,以此来降低网页加载的延迟。
七牛云免费提供了 10GB 的存储空间,如果有域名并且备案过的同学,可以利用它实现一个 CDN 的服务,将文件资源存到七牛云内,这样可以降低自己服务器的存储压力。