前后端联调是前端和后端工程师在完成各自独立开发后,进行接口对接和功能验证的过程。在当前主流的前后端分离开发模式下,良好的联调实践对提升开发效率和保证项目质量至关重要。
高效的前后端联调需要规范的接口文档、合理的Mock策略、完善的调试机制和统一的错误处理流程。本指南将详细介绍这些关键环节的最佳实践。
接口命名示例:
GET /api/v1/users # 获取用户列表
GET /api/v1/users/{id} # 获取单个用户
POST /api/v1/users # 创建用户
PUT /api/v1/users/{id} # 更新用户
DELETE /api/v1/users/{id} # 删除用户
标准请求格式:
{
"data": {
"name": "张三",
"age": 18
},
"timestamp": 1645089832,
"sign": "abc123"
}
标准响应格式:
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"name": "张三"
}
}
// mock/user.js
import Mock from 'mockjs'
Mock.mock('/api/v1/users', 'get', {
code: 200,
message: 'success',
'data|10': [{
'id|+1': 1,
'name': '@cname',
'age|18-60': 1,
'email': '@email'
}]
})
// utils/request.js
import axios from 'axios'
const request = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
if (process.env.NODE_ENV === 'development') {
require('../mock')
}
request.interceptors.request.use(config => {
console.log('请求配置:', config)
config.headers['Authorization'] = getToken()
return config
})
request.interceptors.response.use(response => {
console.log('响应数据:', response.data)
return response.data
})
@Slf4j
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.example.controller.*.*(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
log.info("请求方法: {}", point.getSignature().getName());
log.info("请求参数: {}", Arrays.toString(point.getArgs()));
Object result = point.proceed();
log.info("响应结果: {}", result);
return result;
}
}
// 全局错误处理
const errorHandler = {
400: '请求参数错误',
401: '未登录或登录已过期',
403: '无权限访问',
404: '请求的资源不存在',
500: '服务器内部错误'
}
request.interceptors.response.use(
response => response.data,
error => {
const status = error.response?.status
ElMessage.error(errorHandler[status] || '未知错误')
return Promise.reject(error)
}
)
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
return Result.error(500, "系统错误");
}
}
// vue.config.js
module.exports = {
devServer: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许的来源
config.addAllowedOriginPattern("*");
// 允许的请求方法
config.addAllowedMethod("*");
// 允许的头信息
config.addAllowedHeader("*");
// 允许携带认证信息
config.setAllowCredentials(true);
// 预检请求的有效期,单位为秒
config.setMaxAge(3600L);
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
server {
listen 80;
server_name example.com;
location /api {
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header Access-Control-Expose-Headers 'Content-Length,Content-Range';
add_header Access-Control-Allow-Credentials 'true';
if ($request_method = 'OPTIONS') {
add_header Access-Control-Max-Age 1728000;
add_header Content-Type 'text/plain charset=UTF-8';
add_header Content-Length 0;
return 204;
}
proxy_pass http://backend-server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
// 前端配置
axios.defaults.withCredentials = true
// 后端配置
config.setAllowCredentials(true)
config.addAllowedOrigin("http://specific-domain.com") // 注意不能用*
2. 复杂请求处理
// 前端请求拦截
axios.interceptors.request.use(config => {
if (config.method === 'put' || config.method === 'delete') {
// 处理预检请求
config.headers['Content-Type'] = 'application/json';
}
return config;
})
3. 携带自定义头处理
// 后端配置
config.addAllowedHeader("Authorization");
config.addAllowedHeader("Custom-Header");
4.动态跨域配置
@Component
public class DynamicCorsProcessor implements CorsProcessor {
@Override
public boolean processRequest(CorsConfiguration config, HttpServletRequest request,
HttpServletResponse response) throws IOException {
String origin = request.getHeader("Origin");
// 根据业务需求动态判断是否允许该域名跨域访问
if (isAllowedOrigin(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
}
return true;
}
}
这些跨域解决方案覆盖了开发和生产环境的主要场景。实际使用时,应根据项目需求选择合适的方案,并注意安全性配置。同时,建议在开发环境使用代理方式解决跨域,在生产环境使用Nginx反向代理或后端CORS配置来处理跨域问题。
// 检测是否支持CORS
function checkCORSSupport() {
return 'withCredentials' in new XMLHttpRequest() ||
typeof XDomainRequest !== "undefined" ||
'fetch' in window;
}
// 根据浏览器支持情况选择请求方式
function createRequest() {
if ('withCredentials' in new XMLHttpRequest()) {
return new XMLHttpRequest();
} else if (typeof XDomainRequest !== "undefined") { // IE8, IE9
return new XDomainRequest();
}
throw new Error('CORS is not supported by this browser');
}
class CrossBrowserRequest {
constructor() {
this.xhr = null;
}
request(options) {
return new Promise((resolve, reject) => {
if ('withCredentials' in new XMLHttpRequest()) {
// 现代浏览器
this.standardRequest(options, resolve, reject);
} else if (typeof XDomainRequest !== "undefined") {
// IE8, IE9
this.ieRequest(options, resolve, reject);
} else {
// 不支持CORS的浏览器,使用JSONP替代
this.jsonpRequest(options, resolve, reject);
}
});
}
standardRequest(options, resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.open(options.method, options.url);
// 设置请求头
Object.keys(options.headers || {}).forEach(key => {
xhr.setRequestHeader(key, options.headers[key]);
});
xhr.withCredentials = options.withCredentials || false;
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(xhr.statusText);
}
};
xhr.onerror = () => reject(xhr.statusText);
xhr.send(options.data);
}
ieRequest(options, resolve, reject) {
const xdr = new XDomainRequest();
xdr.onload = () => {
resolve(xdr.responseText);
};
xdr.onerror = () => {
reject('XDomainRequest error');
};
xdr.ontimeout = () => {
reject('XDomainRequest timeout');
};
// IE的XDR对象不支持自定义请求头
xdr.open(options.method, options.url);
setTimeout(() => {
xdr.send(options.data);
}, 0);
}
jsonpRequest(options, resolve, reject) {
if (options.method !== 'GET') {
reject('JSONP only supports GET requests');
return;
}
const callbackName = 'jsonp_' + Date.now();
const script = document.createElement('script');
const url = new URL(options.url);
window[callbackName] = (data) => {
delete window[callbackName];
document.body.removeChild(script);
resolve(data);
};
url.searchParams.append('callback', callbackName);
script.src = url.toString();
script.onerror = () => {
delete window[callbackName];
document.body.removeChild(script);
reject('JSONP request failed');
};
document.body.appendChild(script);
}
}
const request = new CrossBrowserRequest();
// 使用示例
request.request({
method: 'GET',
url: 'https://api.example.com/data',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
},
withCredentials: true
})
.then(response => {
console.log('Response:', response);
})
.catch(error => {
console.error('Error:', error);
});
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 处理IE浏览器的特殊需求
config.addAllowedOriginPattern("*");
config.setAllowCredentials(true);
// IE10以下不支持复杂请求的预检
config.addAllowedMethod("GET");
config.addAllowedMethod("POST");
config.addAllowedMethod("PUT");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("OPTIONS");
// IE对某些响应头的限制
config.addAllowedHeader("Content-Type");
config.addAllowedHeader("Accept");
config.addAllowedHeader("Authorization");
// 添加JSONP支持
config.addExposedHeader("Access-Control-Allow-Origin");
config.addExposedHeader("Access-Control-Allow-Credentials");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
// 降级处理方案
const requestWithFallback = (url, options) => {
if (checkCORSSupport()) {
return fetch(url, options);
} else {
// 降级为JSONP或iframe方案
return jsonpFallback(url);
}
};
2. 文件上传处理:
function uploadFile(file) {
// 检查是否支持FormData
if (window.FormData) {
const formData = new FormData();
formData.append('file', file);
return fetch('/upload', {
method: 'POST',
body: formData
});
} else {
// 降级处理:使用iframe上传
return iframeUpload(file);
}
}
3. 安全性考虑:
// 添加额外的安全检查
function validateOrigin(origin) {
const allowedOrigins = [
'https://example.com',
'https://sub.example.com'
];
return allowedOrigins.includes(origin);
}
// 请求拦截器
function requestInterceptor(config) {
if (!validateOrigin(window.location.origin)) {
throw new Error('Unauthorized origin');
}
return config;
}
这些处理方案能够确保在不同浏览器环境下CORS请求的可靠性,同时提供了降级方案来处理不支持CORS的情况。在实际应用中,应该根据项目的具体需求和目标用户群的浏览器使用情况来选择合适的兼容性处理方案。
接口规范化
跨域解决
调试优化
安全性考虑
性能优化
运维支持
通过遵循本指南的规范和建议,团队可以建立起高效的前后端联调流程,妥善处理跨域问题,提升开发效率和项目质量。