上节回顾

  • 更新用户文本数据

工作内容

  • 更新用户数据
  • 图片上传 & 存储 & 静态化访问

准备工作

  • npm i -S koa-static // 先切换到/server目录下

业务逻辑

服务端文件路由配置

服务端拦截路由请求:

// 新建文件:server/router/assets.js
const Router =  require('@koa/router');

const controls =  require('../control/assets');

const routerUtils =  require('../utils/router');

const { upload } = controls;

const router =  new Router({

    prefix:  '/assets'

});

const routes = [

    {

    path:  '/:category/:id',

    method:  'POST',

    handle: upload

    }
] 

routerUtils.register.call(router, routes);

module.exports = router;
// 新建文件:server/control/assets.js
async  function  upload  (ctx, next) {
    console.log('--------upload=======')
}

module.exports = {
    upload,
}

koa-body支持

// 更新文件:server/app.js
...
const path =  require('path');
...
app.use(bodyParser({
    multipart: true, //支持文件数据
    formidable: {
        uploadDir: path.resolve(__dirname, './public/temp'), // 图片存储位置
        keepExtensions: true
    }
}));
...
  • multipart: true支持文件数据,这里以key:value的形式获取。
  • formidable.uploadDir文件上传存储位置,若不设置,默认存储到计算机用户目录的缓存位置,最好设置一个可控位置。
  • keepExtensions:true是否保有后缀名,默认不存。

Postman测试

  • 前期,登录时,已经设置全局变量token
  • Body --> form-data --> File --> Select Files

upload.gif

请求结果:没有任何返回。
vs code的调试控制台反馈:
error.png
设置存储文件的路径不存在。

保存文件

存储路径不存在,需要检测文件目录是否存在,若不存在,则新建。

// 新建文件:server/utils/dir.js
const path =  require('path');
const fs =  require('fs'); 

function  checkDirExist(dirname) {
    if (fs.existsSync(dirname)) {
        return true;
    } else {
        if (checkDirExist(path.dirname(dirname))) {
            fs.mkdirSync(dirname); //递归
            return true;
        }
    }
}

module.exports = {
    checkDirExist,
}

方式一、通过koa-body配置

// 更新文件:
...
const { checkDirExist } =  require('./utils/dir');
const fileTempDir = path.resolve(__dirname, './public/temp');
...
app.use(bodyParser({
    multipart: true,
    formidable: {
        uploadDir: fileTempDir,
        keepExtensions: true,
        onFileBegin(key, file) { // 利用钩子函数
            checkDirExist(fileTempDir);
            //file.path= path.resolve(fileTempDir, file.name) // 文件改名
        }
    }
}));
  • 利用koa-body配置onFileBegin,可以在处理文件之前,进行一些操作,如:测试目录是否存在、改名、改存储路径等。

测试结果:
filename

方式二、fs模块读写文件

那,为什么还要第二种方式呢?
利用koa-body配置onFileBegin改名,只能获取到file数据本身的数据信息,而无法获取ctx的上下文信息(如,这里准备根据请求参数创建目录存储文件)。

// 更新文件:server/control/assets.js
const fs =  require('fs');
const path =  require('path');
const { checkDirExist } =  require('../utils/dir');

async  function  upload  (ctx, next) {
    const file = Object.values(ctx.request.files)[0];
    const { category, id } = ctx.params;
    const filePath = file.path;
    // 最终要保存到的文件夹路径
    const dir = path.join(__dirname,`../public/${category}/${id}/`);
   try {
        // 检查文件夹是否存在——>如果不存在,则新建文件夹
        checkDirExist(dir);
        const reader = fs.createReadStream(filePath);
        const writer = fs.createWriteStream(path.resolve(dir, file.name));
        reader.pipe(writer);
        // 删除缓存文件
        fs.unlinkSync(filePath)
    } catch (err) {

    }
}

module.exports = {
    upload
}
  • 这里,根据上传路由的参数,将文件存到用户ID创建的avatar目录下。

Postman测试结果:
avatar

访问文件

这时候访问文件:
error
所以,需要将public目录添加到身份认证的unless白名单中:

// 更新文件:server/app.js
...
custom:  function(ctx) {
    const { method, path, query } = ctx;
    if(path ===  '/'){
        return true;
    }
    if(/^\\/public/.test(path)) { // public目录
        return true;
    }
    if(path ===  '/users' && query.action) {
        return true;
    }
    return false;
}
...

继续访问:
image.png

这是因为默认会请求动态资源,而图片数据静态资源

// 更新文件:
...
const koaStatic =  require('koa-static');
...
// 中间件:指定静态资源路径 vs. 使其与动态资源分离
app.use(koaStatic(path.join(__dirname, 'public/')))

继续访问,报错如上:
image.png

这是因为访问路径错了:访问路径不用带/public
dir.gif

若怀疑,不加koa-static,仅修改路径即可访问的可以自行试试。

服务端更新用户头像逻辑

上述内容中,修改了upload的业务逻辑,仅涉及到将文件保存到指定路径下,却没有去更新用户头像信息,并且,服务端没有返回数据。

// 更新文件:server/control/assets.js
const fs =  require('fs');
const path =  require('path');
const userModel =  require('../model/user');
const { checkDirExist } =  require('../utils/dir');

async  function  upload  (ctx, next) {
const file = Object.values(ctx.request.files)[0];
const { category, id } = ctx.params;
// 用户头像远程地址
const remotePath =  `${ctx.origin}/${category}/${id}/${file.name}`;
const filePath = file.path;
const dir = path.join(__dirname,`../public/${category}/${id}/`);
try {
    checkDirExist(dir);
    const reader = fs.createReadStream(filePath);
    const writer = fs.createWriteStream(path.resolve(dir, file.name));
    reader.pipe(writer);
    try {
    // 更新用户头像信息
        await userModel.updateOne(
        {
            _id: id
        },
        {
            avatar: remotePath
        }
    ).exec();
    } catch (err) {
        ctx.body = {
            code:  '404',
            data: null,
            msg:  '上传失败'
        };
        return;
    }
    fs.unlinkSync(filePath)
    ctx.body = {
        code:  '200',
        data: {
            filePath: remotePath
        },
        msg:  '上传成功'
    }
    } catch (err) {
        ctx.body = {
            code:  '404',
            data: null,
            msg:  '上传失败'
        }
    } 
} 

module.exports = {
    upload
}
  • 设置远程访问地址${ctx.origin}/${category}/${id}/${file.name},并将该地址更新到用户信息中。

这里将/server/public目录删除,重新测试:
sucess.gif

前端页面头像逻辑

//更新文件:
...
methods: {
...
handleAvatarSuccess  (res,  file)  {
    this.imageUrl  =  URL.createObjectURL(file.raw)
},

beforeAvatarUpload  (file)  {
    const  isJPG  = /^image\//.test(file.type)
    const  isLt2M  =  file.size  /  1024  /  1024  /  10  <  2
    if (!isJPG) {
    this.$message.error('上传头像图片只能是 JPG 格式!')
    }
    if (!isLt2M) {
    this.$message.error('上传头像图片大小不能超过 20MB!')
    }
    return  isJPG  &&  isLt2M
},
...
data () {
  return {
      uploadForm:  {
        action:  `//localhost:3000/assets/avatars/${this.$store.state.loginer.id}`,
        headers:  {
            'Authorization':  `Bearer ${localStorage.getItem('token')}`
        },
        multiple:  false,
        'show-file-list':  false,
        'on-success':  this.handleAvatarSuccess,
        'before-upload':  this.beforeAvatarUpload
      },
  ...
async  created  ()  {
    const  res  =  await  http.get(`/users/${this.userId}`)
    if (res.code  ===  '200') {
        this.loginer =  res.data
        this.dialogForm.form =  {...res.data}
        this.imageUrl =  res.data.avatar //初始化时,赋值
    }  else  {
        this.$message({
            type:  'error',
            message:  '获取用户信息失败'
        })
    }
}
  • 初始化时,给头像赋值;
  • 通过uploadForm设置上传配置
  • on-success上传成功时,将图片地址覆盖原有值;
  • before-upload上传之前,校验文件类型和大小;

效果展示:
fronEnd.gif

参考文档

koa-static
koa-body上传文件相关配置


米花儿团儿
1.3k 声望75 粉丝