前言
最近在用create-react-app + node 做一个全栈项目,在跨域问题上,踩到了各种意料以外的坑。
使用代理proxy跨域
create-react-app项目里已配置根目录的setProxy.js文件去实现跨域, 一开始我在本地同时起前端+node后端项目,并引入插件'http-proxy-middleware'去实现跨域。 在axios请求的url加入前缀'/api',Proxy服务就会代理http请求到目标地址127.0.0.1:8081中,从而代替了浏览器的直接请求
setProxy.js:
const { createProxyMiddleware: proxy } = require('http-proxy-middleware') // 固定写法
// proxy 只能在开发环境使用,打包后无法使用。
module.exports = (app) => {
app.use(
proxy('/api', {
target: '127.0.0.1:8081', // 后端Node 服务地址
changeOrigin: true, // 跨域
pathRewrite: { '^/api': '' } // 把前缀清除掉再传给后端
})
)
}
注意axios已封装过
import { axios } from '@/utils/request'
export const getUserList = (param: searchParam) => {
return axios({
url: 'http://127.0.0.1:3001/api/user/list',
method: 'post',
data: param
})
}
这么一来,本地请求本地服务器的跨域问题就轻松解决,本人也感觉到十分有成就感。于是我便顺理成章把项目部署到阿里云服务器上。
在云服务器使用proxy实现跨域的误区
当项目都在云服务器上跑动了,却出现了意外的跨域问题
怀疑是ip地址+前缀或者后端服务的问题,于是我重新在本地页面上去请求服务端的接口,还是能正常越过跨域得到响应,也确认过url地址无误,便去查询proxy的原理。 原来http-proxy-middleware这个中间件只能在本地监听事件去给axios做正向代理,在build完部署到服务端后,这个proxy便不会再承担一个代理的功能。
因此 http-proxy-middleware 中间件只能在本地实现跨域,生产环境需另辟蹊径
生产环境中Node实现跨域的方案
常规的跨域方案除了proxy还有nginx,jsonp 但这次想更了解整个后端进程,便选择在node服务里通过cors去实现
1 .CORS插件
这是最简便的cors跨域方案,引入cors插件,两句代码搞定。CORS会自动添加合适的header跟处理数据格式,前端也不用再做额外的处理,堪称傻瓜式跨域方案。
const express = require('express')
const cors = require('cors')
const app = express()
app.use(cors())
2 .自行设置请求头header
全局注册给所有的请求添加headers:
app.all('*', function (req, res, next) {
// 设置cors跨域 这段话必须在use router语句的前面
res.set({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': '*'
})
if (req.method === 'OPTIONS') {
res.sendStatus(200) // 让options尝试请求快速结束
} else {
next()
}
})
第一个坑:全局添加header必须写在注册router的前面
我一开始猜测是app.all这句语法的问题,于是在这个接口的开端直接加Header,请求就可以成功了。
router.post('/user/list', (req, res) => {
res.header('Access-Control-Allow-Origin', '*')
const { firstName, lastName, age, address } = req.body
let ageSql
if (age === '') {
// 对age类型进行精确/模糊双重匹配
ageSql = ``
} else {
ageSql = `and age = '${age}'`
}
const sql = `select * from user where first_name like '%${firstName}%' and last_name like '%${lastName}%' and address like '%${address}%' ${ageSql};`
conn.query(sql, (err, result) => {
if (err) {
throw new Error('获取失败')
}
if (result) {
jsonWrite(res, result, { code: 200, message: '获取用户列表成功' })
}
})
})
但参考了大量的网文和官网,语法没问题,可是Access-Control-Allow-Origin一直没能正确添加
最后我发现一个问题,其他人使用这个方法都是跟接口放同一个JS里面,而我用router()做了模块化,于是我把app.use(module)的语句放在了上面代码的后面,果然成功了。由此可见app.all添加headers要在模块化之前方可生效
app.all('*', function (req, res, next) {
// 设置cors跨域 这段话必须在use router语句的前面
res.set({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': '*'
})
if (req.method === 'OPTIONS') {
res.sendStatus(200) // 让options尝试请求快速结束
} else {
next()
}
})
// 引入封装的路由
app.use(user)
app.use(goods)
第二个坑: post请求的格式处理
好了,正常的请求都成功了,但用post请求的时候还是会报一个跨域的错误,实际上并不是跨域,而是参数格式的问题。说到这个,需要先理解content-type在http请求中的作用。
此处我使用的是axios,在post请求中,传输的对象默认是application/json的格式,而后端默认接受的是序列化后的格式,因此双方的格式不统一就会报错。
需要留意的是,如果参数是字符串,则不会出现格式不一致的问题
json格式:
序列化后的键值对格式
为解决格式统一的问题,我在拦截器中使用Qs转换了参数,之后再传给后端,这样就不会再报上述的错误了。
// 请求拦截器
import qs from 'qs'
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
// 请求之前做些什么
const token = cookie.load('Bear_Token')
const { headers } = config
if (token && headers) headers.Authorization = `Bearer ${token}` // 增加header作为判断,因改版本的headers定义不包含undefined会报错
// 后台能够直接处理的数据格式,是一种经过序列化的键值对数据
// post 默认用的是application/json,因此需要转化格式
if (config.method === 'post') {
config.data = qs.stringify(config.data)
}
return config
},
(error) => {
return Promise.reject(error)
}
)