全栈项目create-react-app + node跨域的各种踩坑

768 阅读3分钟

前言

最近在用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实现跨域的误区

当项目都在云服务器上跑动了,却出现了意外的跨域问题 image.png

怀疑是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()
  }
})

image.png

第一个坑:全局添加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请求中的作用。

image.png

此处我使用的是axios,在post请求中,传输的对象默认是application/json的格式,而后端默认接受的是序列化后的格式,因此双方的格式不统一就会报错。

需要留意的是,如果参数是字符串,则不会出现格式不一致的问题

json格式:

image.png

序列化后的键值对格式

image.png

为解决格式统一的问题,我在拦截器中使用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)
  }
)