上节回顾
- 更新用户文本数据
工作内容
- 更新用户数据
- 图片上传 & 存储 & 静态化访问
准备工作
-
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
请求结果:没有任何返回。vs code
的调试控制台反馈:
设置存储文件的路径不存在。
保存文件
存储路径不存在,需要检测文件目录是否存在,若不存在,则新建。
// 新建文件: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
,可以在处理文件之前,进行一些操作,如:测试目录是否存在、改名、改存储路径等。
测试结果:
方式二、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
测试结果:
访问文件
这时候访问文件:
所以,需要将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;
}
...
继续访问:
这是因为默认会请求动态资源,而图片数据静态资源
// 更新文件:
...
const koaStatic = require('koa-static');
...
// 中间件:指定静态资源路径 vs. 使其与动态资源分离
app.use(koaStatic(path.join(__dirname, 'public/')))
继续访问,报错如上:
这是因为访问路径错了:访问路径不用带/public
若怀疑,不加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
目录删除,重新测试:
前端页面头像逻辑
//更新文件:
...
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
上传之前,校验文件类型和大小;
效果展示:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。