今天在看 upload 组件源码时,在组件源码当中调用的本地启动的 nodejs 服务写的上传接口,遇到跨域问题:
问题一、在 upload.md 中调用 nodejs 服务中的 上传接口,控制台报跨域报错。
解决方法1:在根目录增加 vue.config.js 文件,解决控制台报错。
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000', // 后端地址
changeOrigin: true,
pathRewrite: {
'^/api' : '/api'
},
}
}
}
};
问题二、基于上面的问题,控制台不再报错后,接口一直 404。
解决方法1:nodejs 增加 cor 组件,前端代码中使用域名+接口的形式调用,可以正常调用接口:
const Koa = require('koa');
const serve = require('koa-static');
const path = require('path');
const fs = require('fs').promises; // 使用 promises 版本的 fs
const { koaBody } = require('koa-body');
const jsonError = require('koa-json-error');
const parameter = require('koa-parameter');
const koajwt = require('koa-jwt');
const cors = require('@koa/cors'); // 新增:CORS 支持
const { connectDB } = require('./db/index.js');
const user = require('./routes/user.js');
const department = require('./routes/department.js');
const departmentApplication = require('./routes/departmentApplication.js');
const certificate = require('./routes/certificate.js');
const post = require('./routes/post.js');
const userApplication = require('./routes/userApplication.js');
const menu = require('./routes/menu.js');
const app = new Koa();
// ===== 添加 CORS 中间件 =====
app.use(
cors({
origin: 'http://localhost:8085', // 修改为你的前端地址
credentials: true, // 允许携带 cookies 或 token
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
})
);
// 静态资源中间件
app.use(serve(path.join(__dirname, 'uploads'))); // 上传文件目录
app.use(serve(path.join(__dirname, 'public'), { prefix: '/static' })); // 公共静态资源目录
// 处理根路径和 favicon.ico 请求
app.use(async (ctx, next) => {
if (ctx.request.url === '/') {
ctx.body = '<h1>Welcome to the Koa server</h1>';
return;
}
if (ctx.request.url === '/favicon.ico') {
ctx.type = 'image/x-icon';
try {
ctx.body = await fs.readFile(
path.join(__dirname, 'public', 'favicon.ico')
);
} catch (err) {
ctx.status = 404;
ctx.body = 'Favicon not found';
}
return;
}
await next();
});
// JWT 校验中间件
app.use(
koajwt({
secret: 'jqh-server-jwt',
}).unless({
path: [
/^\/api\/user\/login/, // 登录页面不做权限控制
/^\/static\//, // public下的 static 开头的资源不做权限控制
/^\/favicon\.ico$/, // favicon.ico 图标不做权限控制
],
})
);
// 错误处理、body 解析、参数校验中间件
app.use(jsonError());
app.use(
koaBody({
multipart: true, // 支持文件上传
formidable: {
maxFileSize: 2000 * 1024 * 1024, // 限制文件上传大小,例如2GB
},
})
);
app.use(parameter(app));
// 连接数据库
connectDB();
// 注册路由
app.use(user.routes()).use(user.allowedMethods());
app.use(department.routes()).use(department.allowedMethods());
app
.use(departmentApplication.routes())
.use(departmentApplication.allowedMethods());
app.use(certificate.routes()).use(certificate.allowedMethods());
app.use(post.routes()).use(post.allowedMethods());
app.use(userApplication.routes()).use(userApplication.allowedMethods());
app.use(menu.routes()).use(menu.allowedMethods());
// 监听端口
app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});
前端部分:
<template>
<div>
<el-upload
ref="uploadRef"
:auto-upload="false"
:on-change="handleAvatarChange"
:show-file-list="false"
class="upload-avatar-wrapper"
accept="image/*"
:action="uploadUrl"
>
<img
v-if="form.avatar"
:src="form.avatar"
class="avatar"
style="width:60px;height:60px;text-align:center;line-height:60px;border:1px #ddd solid;"
/>
<div
v-else
class="avatar-uploader-icon"
style="width:60px;height:60px;text-align:center;line-height:60px;border:1px #ddd solid;"
>
<i class="el-icon-plus"></i>
</div>
</el-upload>
<el-button @click="customUpload">上传</el-button>
</div>
</template>
<script>
export default {
data() {
return {
token:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcGVyQ29kZSI6InN1cGVyMDAxIiwiaWF0IjoxNzQ3MTE5NjEzLCJleHAiOjE3NDc3MjQ0MTN9.YQBUW1nt8MtVaTUE-sJ8R78h9ngbnzjmLZRk4kkUMHU',
form: {
operName: 'super001',
password: '123456',
avatar: '',
},
selectedFile: null,
uploadUrl: 'http://localhost:3000/api/user/uploadAvatar',
};
},
methods: {
// 文件选择变化时
handleAvatarChange(file) {
const reader = new FileReader();
reader.onload = (e) => {
this.form.avatar = e.target.result; // 正确赋值
};
if (file.raw) {
reader.readAsDataURL(file.raw);
this.selectedFile = file.raw;
}
},
// 自定义上传方法(由按钮触发)
customUpload() {
if (!this.selectedFile) {
this.$message.warning('请先选择一个文件');
return;
}
const formData = new FormData();
formData.append('operName', this.form.operName);
formData.append('password', this.form.password);
formData.append('avatar', this.selectedFile);
fetch(this.uploadUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${this.token}`,
},
body: formData,
credentials: 'include',
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log('Success:', data);
this.selectedFile = null;
this.$message.success('上传成功');
})
.catch((error) => {
console.error('Error:', error);
this.$message.error('上传失败');
});
},
},
};
</script>
<style>
/* 样式部分保持不变 */
.upload-avatar-wrapper {
width: 60px;
height: 60px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.upload-avatar-wrapper:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
}
.avatar {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
解决方法2:使用的是 npm run dev 启动的项目,通过 package.json 可以看到所使用的 webpack 配置文件。
找到 webpack.demo.js 配置文件:
后端 nodejs 代码不用使用 cor 组件,前端代码如下:
<template>
<div>
<el-upload
ref="uploadRef"
:auto-upload="false"
:on-change="handleAvatarChange"
:show-file-list="false"
class="upload-avatar-wrapper"
accept="image/*"
:action="uploadUrl"
>
<img
v-if="form.avatar"
:src="form.avatar"
class="avatar"
style="width:60px;height:60px;text-align:center;line-height:60px;border:1px #ddd solid;"
/>
<div
v-else
class="avatar-uploader-icon"
style="width:60px;height:60px;text-align:center;line-height:60px;border:1px #ddd solid;"
>
<i class="el-icon-plus"></i>
</div>
</el-upload>
<el-button @click="customUpload">上传</el-button>
</div>
</template>
<script>
export default {
data() {
return {
token:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcGVyQ29kZSI6InN1cGVyMDAxIiwiaWF0IjoxNzQ3MTE5NjEzLCJleHAiOjE3NDc3MjQ0MTN9.YQBUW1nt8MtVaTUE-sJ8R78h9ngbnzjmLZRk4kkUMHU',
form: {
operName: 'super001',
password: '123456',
avatar: '',
},
selectedFile: null,
uploadUrl: '/api/user/uploadAvatar',
};
},
methods: {
// 文件选择变化时
handleAvatarChange(file) {
const reader = new FileReader();
reader.onload = (e) => {
this.form.avatar = e.target.result; // 正确赋值
};
if (file.raw) {
reader.readAsDataURL(file.raw);
this.selectedFile = file.raw;
}
},
// 自定义上传方法(由按钮触发)
customUpload() {
if (!this.selectedFile) {
this.$message.warning('请先选择一个文件');
return;
}
const formData = new FormData();
formData.append('operName', this.form.operName);
formData.append('password', this.form.password);
formData.append('avatar', this.selectedFile);
fetch(this.uploadUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${this.token}`,
},
body: formData,
credentials: 'include',
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log('Success:', data);
this.selectedFile = null;
this.$message.success('上传成功');
})
.catch((error) => {
console.error('Error:', error);
this.$message.error('上传失败');
});
},
},
};
</script>
<style>
/* 样式部分保持不变 */
.upload-avatar-wrapper {
width: 60px;
height: 60px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.upload-avatar-wrapper:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
}
.avatar {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
总结:建议使用前端增加反向代理的方式。产生问题的原因,对源码当中的项目启动配置不熟悉,想当然的增加 vue.config.js 配置,以为可以解决问题,行不通时,考虑从后端代码解决问题。问题解决完成之后,已经有了退路,冷静下来寻找问题的原因,发现启动脚本 npm run dev 中使用的 build 文件夹下的 webpack.demo.js 配置,在这个配置中增加反向代理并重启后,跨域问题解决。