Node + React 实战:从0到1 实现记账本(六)

669 阅读8分钟

用户信息的相关接口

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

一.获取用户信息

话不多说,直接进入正题。首先打开 /app/controller/user.js,添加 getUserInfo 方法,代码如下所示:

// 获取用户信息
async getUserInfo() {
 const { ctx, app } = this;
 // 通过 app.jwt.verify 方法,解析出 token 内的用户信息
 const token = ctx.request.header.authorization;
 const decode = 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: '请求成功',
   username: userInfo.username,
   signtrue: userInfo.signtrue,
   avatar: userInfo.avatar || defaultAvatar,
 };
}

代码中已经添加详细的注释信息,就不再赘述了。

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

//router.js
router.get('/api/user/get_userinfo', _jwt, controller.user.getUserInfo); // 获取用户信息

直接通过 Postman 验证结构是否可行,如下所示:

image.png

注意,需要给 Headers 添加 authorization 属性,值为之前登录接口返回的 token 信息。

为了兼容后续的修改用户头像,将接口名称定义的宽一些,在 /controller/user.js 下,新建 editUserInfo 方法,添加如下代码:

// 修改个性签名
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 = app.jwt.verify(token, app.config.jwt.secret);
    if (!decode) return;
    // eslint-disable-next-line prefer-const
    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) {
    console.log(error);
  }
}

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

//  修改用户信息
async editUserInfo() {
  const { app } = this;
  try {
    // 通过 app.mysql.update 方法,指定 user 表
    return await app.mysql.update('user', {
      // 要修改的参数提,直接通过... 扩展操作符展开
      ...params,
    }, {
      // 筛选出id等于 params.id 的用户
      id: params.id,
    });
  } catch (error) {
    console.log(error);
    return null;
  }
}

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

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

打开 Postman 验证接口是否正确:

image.png

数据库也相应的修改成功:

image.png

二.修改用户头像

说到修改用户头像,正常情况下, 在前端是这样操作的。首先,点击用户头像;其次,弹出弹窗或进入手机相册,选择一张自己喜欢的头像,然后上传头像,最后将自己的头像替换成修改后的头像。

上述流程涉及到一个步骤,那就是「上传图片」。所以在编写修改头像信息接口之前,需要先实现一个「上传图片」的接口。上传图片的作用是比较宽泛的,不光是头像需要上传图片,其他很多操作也都需要用到,如朋友圈、商品图片等等。所以在 controller 文件夹下新建一个脚本,名为 upload.js,如下:

image.png 接下来,先分析一波图片上传到服务器的逻辑。

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

目前没有前端项目可以上传图片,所以这里先用HTML 简单写一个上传页面,如下所示:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<input type="file" id="upload"/>
<script>
    // 获取input标签的dom
    const input = document.getElementById('upload')
    // 监听他的变化
    input.addEventListener('change',(e)=>{
      // 获取到上传的file对象
      let file = input.files[0]
      // 声明 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('上传成功')
          return res.json()
        }  else {
          console.log('上传失败');
        }
      }).then(res=>{
        console.log('res is', res);
      }).catch(error => console.log(error))
    })
</script>
</body>
</html>

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

'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
    const file = ctx.request.files[0];

    // 声明存放资源的路径
    let uploadDir = '';

    try {
      // ctx.request.files[0] 表示获取第一个文件,若前端上传多个文件则可以遍历这个数组对象
      const f = fs.readFileSync(file.filepath);
      //  获取当前时间
      const day = moment(new Date()).format('YYYYMMDD');
      //  创建图片保存的路径
      const dir = path.join(this.config.uploadDir, day);
      // 毫秒数
      const date = Date.now();
      //  不存在就创建目录
      await mkdirp(dir);
      // 返回图片保存的路径
      uploadDir = path.join(dir, date + path.extname(file.filename));
      // 写入文件夹
      fs.writeFileSync(uploadDir, f);
    } finally {
      // 清除临时文件
      await ctx.cleanupRequestFiles();
    }
    ctx.body = {
      code: 200,
      msg: '上传成功',
      data: uploadDir.replace(/app/g, ''),
    };
  }
}

module.exports = UploadController;

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

npm i moment mkdirp -S

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

config.multipart = { 
    mode: 'file' 
};

multipart 配置项有很多选项,比如 whitelist 上传格式的定制,fileSize 文件大小的限制,这些都可以在文档中查找到。

配置完成之后,才能通过 ctx.request.files 的形式,获取到前端上传的文件资源。

通过 fs.readFileSync(file.filepath) 读取文件,保存在 f 变量中,后续使用。

创建一个图片保存的文件路径:

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

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

// add your user config here
const userConfig = {
  // myAppName: 'egg',
  uploadDir: 'app/public/upload'
};

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

构建好文件的路径,如下:

// 最后,将文件内容写入上述路径,如下:
fs.writeFileSync(uploadDir, f)

成功之后返回路径:

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

这里要注意的是,需要将 app 去除,因为在前端访问路径的时候,是不需要 app 这个路径的,比如项目启动的是 7001 端口,最后访问的文件路径是这样的 http://localhost:7001/public/upload/20220114/1642175834885.png

完成上述操作之后,还需要在做最后一步操作,解决跨域。首先安装 egg-cors 插件 npm i egg-cors,安装好之后,前往 config/plugins.js 下添加属性:

cors: {
  enable: true,
  package: 'egg-cors',
}

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

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

上述逻辑完成之后,打开之前写好的前端页面,点击上传图片,如下:

image.png

拿到这样一串路径,可以查看服务端项目 app/public 文件夹下,是否存入了图片资源:

image.png

在通过浏览器访问图片路径,如下代表图片成功上传至服务器:

image.png

此时拿到了服务器返回的图片地址,便可以将其提交至 editUserInfo 方法。为 editUserInfo 方法添加如下参数:

// 修改用户信息
async editUserInfo() {
  const { ctx, app } = this;
  //  通过post请求,在请求体中获取签名字段 signature
  const { signature = '', avatar = '' } = ctx.request.body;

  try {
    let user_id;
    const token = ctx.request.header.authorization;
    // 解密 token 中的用户名称
    const decode = app.jwt.verify(token, app.config.jwt.secret);
    if (!decode) return;
    // eslint-disable-next-line prefer-const
    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,
      avatar,
    });

    ctx.body = {
      code: 200,
      msg: '请求成功',
      data: {
        id: user_id,
        signature,
        username: userInfo.username,
        avatar,
      },
    };
  } catch (error) {
    console.log(error);
  }
}

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

三.上传资源知识拓展

上述方法是没有 OSS 服务的情况下使用的,目前市面上更多的方式,是购买 OSS 服务,将图片等静态资源上传至 CDN,通过内容分发的形式,让使用的用户就近获取在线资源。这属于网站性能优化的一种方式,减少主域名下的资源请求数量,以此来降低网页加载的延迟。

七牛云免费提供了 10GB 的存储空间,如果有域名并且备案过的,可以利用它实现一个 CDN 的服务,将文件资源存到七牛云内,这样可以降低自己服务器的存储压力。

四.总结

此时又完成了三个接口的编写,你会觉得,写服务端比写前端轻松多了。

其实不是这样的,每一个工种都有各自的难点。前端更多的是面向浏览器,而浏览器和用户是一对一的关系,前端更多的是注重视觉和交互方面的体验,让用户以最简单易用的方式去完成自己的诉求。

反观服务端,则是一份服务端代码,为多个终端服务,所以服务端更多是一对多的关系。这就很考验服务端的代码,以及数据库的工作效率。在流量峰值能否很好的响应每个用户发起的请求,极端情况就是天猫双十一这种请求量级,服务端的压力是难以想象的。

此项目只是为了学习和练手,没有考虑并发和安全性等问题。