前言

最近使用next.js开发过程中发现服务端set-cookie返回设置到浏览器不成功,于是研究了一下如何处理,分享给大家。

操作

前后台cookie如何相互传递?

1、前端如何传递cookie到后台?

前端通过axios(或者fetch也可以)调用后台接口的时候通过request请求头header的cookie属性(前端是你的浏览器中存在Cookie)带到后台,前提是要同源,如:前端地址是:www.baidu.com,后台是:www.baiud.com/api或者api.baidu.com,这样的才能访问浏览器中的cookie。

image.png

2、后台如何传递cookie到前端?

后台通过response请求头header的set-cookie属性带到前端浏览器,自动就能写到指定域名下。

image.png

image.png

了解next.js的执行过程

const pageA = (props) => {
    return <div> this is Page A</div>
}

export async function getServerSideProps(context) {
  
  const res = await axios({ url: "http://www.baidu.com/api/getUserList", data: xxx });
  const data = res?.data;

  return {
    props: {
      data
    }
  }
}

export default pageA;

上面是一段非常简单的next.js页面的代码,它分为两部分,页面pageA和服务端获取接口数据getServerSideProps,当刷新页面或者首次打开页面时首先执行的是getServerSideProps方法,执行完成 之后才到pageA方法体中,是这样一个执行过程。

这也是SSR渲染最核心的地方,先从后台返回数据再渲染出页面,减少SPA解释js的等待时间。

注意:getServerSideProps方法只有在页面第一次渲染的时候才执行(或者刷新页面、跳转页面),后面就不会再进来了。

next.js客户端如何传递cookie到后台?

首先我们看一个问题就是上面的http://www.baidu.com/api/getUserList接口是没办法传递cookie到后台的,为什么不能把cookie传递到后台呢?怎么才能传递cookie到后台呢?

首先回答第一个问题,因为getServerSideProps执行在服务端所以是拿不到浏览器里面的cookie的,这时候需要通过next.js的context属性拿到。

回答第二个问题,只有通过下面的代码手动指定,这也是SSR特殊的地方。

axios.defaults.headers.cookie = context.req.headers.cookie || null

当然如果页面已经渲染完,这时候你通过页面控制接口的访问的时候就不用这么麻烦,因为浏览器会自动帮你把cookie带到接口的请求头:request header cookie上。

next.js服务端如何传递cookie到客户端浏览器?

已经了解如何将cookie从客户端传递到服务端之后 ,我们再来解决如何将cookie从服务端传递到客户端浏览器中,上面已经讲过后台是通过接口中返回的response请求头中的set-cookie属性传递过来的,如果是SPA那么直接就可以设置到Cookie中,但是我们是SSR是next.js当然没那么简单了,那么我们如何设置呢?

要做两步操作:
1、对axios返回请求头做设置
2、getServerSideProps方法中再次设置set-cookie

const axiosInstance = axios.create({
  baseURL: `http://www.baidu.com/api`,
  withCredentials: true,
});


axiosInstance.interceptors.response.use(function (response) {
    axiosInstance.defaults.headers.setCookie = response.headers['set-cookie']
    return response;
}, function (error) {

  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error);
});

export default axiosInstance;

上面的axiosInstance.defaults.headers.setCookie = response.headers['set-cookie']代码就是把后台返回的set-cookie属性赋值给axiosInstance.defaults.headers.setCookie,然后,回到getServerSideProps方法中,再在最后返回给浏览器中,如下所示:

const pageA = (props) => {
    return <div> this is Page A</div>
}

export async function getServerSideProps(context) {

  // 1、获取cookie并保存到axios请求头cookie中
  axios.defaults.headers.cookie = ctx.req.headers.cookie || null
  
  const res = await axios({ url: "http://www.baidu.com/api/getUserList", data: xxx });
  const data = res?.data;

  // 2、判断请求头中是否有set-cookie,如果有,则保存并同步到浏览器中
  if(axios.defaults.headers.setCookie){
    ctx.res.setHeader('set-cookie', axios.defaults.headers.setCookie)
    delete axios.defaults.headers.setCookie
  }
  return {
    props: {
      data
    }
  }
}

export default pageA;

这样就完成了后台set-cookie同步cookie到客户端Cookie中,但是,这里还有个问题,就是getServerSideProps方法中如果你请求多于一个接口时,set-cookie只有最后一个起使用,什么意思呢?

const res1 = await axios({ url: "http://www.baidu.com/api/getUserList1", data: xxx });
const res2 = await axios({ url: "http://www.baidu.com/api/getUserList2", data: xxx });
const res3 = await axios({ url: "http://www.baidu.com/api/getUserList3", data: xxx });

上面三个方法执行之后,只有getUserList3这个接口的set-cookie保存到客户端Cookie中,这是为什么呢?我们再来看看这段代码:

axiosInstance.defaults.headers.setCookie = response.headers['set-cookie']

上面这段代码每次执行完之后axiosInstance.defaults.headers.setCookie都会被response.headers['set-cookie']直接覆盖了,所以当代码从getUserList1执行到getUserList3之后,set-cookie就是最后一个方法的set-cookie了。

看到上面的问题你是不是已经想到了,对,就是合并把三个方法里面的set-cookie合并到axiosInstance.defaults.headers.setCookie中,所以我们再来修改下代码:

// 添加响应拦截器
axiosInstance.interceptors.response.use(function (response) {

  // 目标:合并setCookie
  // A、将response.headers['set-cookie']合并到axios.defaults.headers.setCookie中
  // B、将axios.defaults.headers.setCookie合并到axios.defaults.headers.cookie中,目的是:每次请求axios请求头中的cookie都是最新的

  // 注意:set-cookie格式和cookie格式区别
  /** axios.defaults.headers.setCookie和response.headers['set-cookie']格式如下
   *
   *  axios.defaults.headers.setCookie = [
   *    'name=Justin; Path=/; Max-Age=365; Expires=Mon, 15 Aug 2022 13:35:08 GMT; Secure; HttpOnly; SameSite=None'
   *  ]
   *
   * **/

  /** axios.defaults.headers.cookie 格式如下
   *
   *  axios.defaults.headers.cookie = name=Justin;age=18;sex=男
   *
   * **/
  // A1、判断是否是服务端,并且返回请求头中有set-cookie
  if(typeof window === 'undefined' && response.headers['set-cookie']){
    // A2、判断axios.defaults.headers.setCookie是否是数组
    // A2.1、如果是,则将response.headers['set-cookie']合并到axios.defaults.headers.setCookie
    // 注意:axios.defaults.headers.setCookie默认是undefined,而response.headers['set-cookie']默认是数组
    if(Array.isArray(axiosInstance.defaults.headers.setCookie) ){

      // A2.1.1、将后台返回的set-cookie字符串和axios.defaults.headers.setCookie转化成对象数组
      // 注意:response.headers['set-cookie']可能有多个,它是一个数组

      /** setCookie.parse(response.headers['set-cookie'])和setCookie.parse(axios.defaults.headers.setCookie)格式如下
       *
       setCookie.parse(response.headers['set-cookie']) = [
          {
            name: 'userName',
            value: 'Justin',
            path: '/',
            maxAge: 365,
            expires: 2022-08-16T07:56:46.000Z,
            secure: true,
            httpOnly: true,
            sameSite: 'None'
          }
       ]
       * **/
      const _resSetCookie = setCookie.parse(response.headers['set-cookie'])
      const _axiosSetCookie = setCookie.parse(axiosInstance.defaults.headers.setCookie)
      // A2.1.2、利用reduce,合并_resSetCookie和_axiosSetCookie对象到result中(有则替换,无则新增)
      const result = _resSetCookie.reduce((arr1, arr2)=>{
        // arr1第一次进来是等于初始化化值:_axiosSetCookie
        // arr2依次是_resSetCookie中的对象
        let isFlag = false
        arr1.forEach(item => {
          if(item.name === arr2.name){
            isFlag = true
            item = Object.assign(item, arr2)
          }
        })
        if(!isFlag){
          arr1.push(arr2)
        }
        // 返回结果值arr1,作为reduce下一次的数据
        return arr1
      }, _axiosSetCookie)

      let newSetCookie = []
      result.forEach(item =>{
        // 将cookie对象转换成cookie字符串
        // newSetCookie = ['name=Justin; Path=/; Max-Age=365; Expires=Mon, 15 Aug 2022 13:35:08 GMT; Secure; HttpOnly; SameSite=None']
        newSetCookie.push(cookie.serialize(item.name, item.value, item))
      })
      // A2.1.3、合并完之后,赋值给axios.defaults.headers.setCookie
      axiosInstance.defaults.headers.setCookie = newSetCookie
    }else{
      // A2.2、如果否,则将response.headers['set-cookie']直接赋值
      axiosInstance.defaults.headers.setCookie = response.headers['set-cookie']
    }


    // B1、因为axios.defaults.headers.cookie不是最新的,所以要同步这样后续的请求的cookie都是最新的了
    // B1.1、将axios.defaults.headers.setCookie转化成key:value对象数组
    const _parseSetCookie = setCookie.parse(axiosInstance.defaults.headers.setCookie)
    // B1.2、将axios.defaults.headers.cookie字符串转化成key:value对象
    /** cookie.parse(axiosInstance.defaults.headers.cookie)格式如下
     *
     *  {
     *    userName: Justin,
     *    age: 18,
     *    sex: 男
     *  }
     *
     * **/
    const _parseCookie = cookie.parse(axiosInstance.defaults.headers.cookie)

    // B1.3、将axios.defaults.headers.setCookie赋值给axios.defaults.headers.cookie(有则替换,无则新增)
    _parseSetCookie.forEach(cookie => {
      _parseCookie[cookie.name] = cookie.value
    })
    // B1.4、将赋值后的key:value对象转换成key=value数组
    // 转换成格式为:_resultCookie = ["userName=Justin", "age=19", "sex=男"]
    let _resultCookie = []
    for (const key in _parseCookie) {
      _resultCookie.push(cookie.serialize(key, _parseCookie[key]))
    }
    // B1.5、将key=value的cookie数组转换成key=value;字符串赋值给axiosInstance.defaults.headers.cookie
    // 转换成格式为:axios.defaults.headers.cookie = "userName=Justin;age=19;sex=男"
    axiosInstance.defaults.headers.cookie = _resultCookie.join(';')
  }
  return response;
}, function (error) {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error);
});


export default axiosInstance;

有点多?不要怕,把注释去掉就一点点东西,大家可以通过注释看看上面代码如何实现的,当然你可以直接复制过去也行,上面代码完成两个目标:

A、将response.headers['set-cookie']合并到axios.defaults.headers.setCookie中
B、将axios.defaults.headers.setCookie合并到axios.defaults.headers.cookie中,目的是:每次请求axios请求头中的cookie都是最新的

最后,这样就完成了Cookie在客户端和服务端传递了。

最后我还想说一下,这种情况仅限出现在第一次渲染页面时getServerSideProps方法中遇到的问题,而当页面渲染完成之后,就不用这么麻烦了。

总结

1、如果想了解如何解决跨域问题的可以看这篇文章
2、next.js页面第一次渲染页面只有在getServerSideProps方法中通过context获取和设置cookie。


Awbeci
3.1k 声望213 粉丝

Awbeci