当遇到跨域开发时, 我们如何处理好前后端配置和请求库封装(koa/axios版)

徐小夕
• 阅读 1574

我们知道很多大型项目都或多或少的采用跨域的模式开发, 以达到服务和资源的解耦和高效利用. 在大前端盛行的今天更为如此, 前端工程师可以通过nodejs或者Nginx轻松搭建起web服务器.这个时候我们只需要请求后端服务器的接口即可实现系统的业务功能开发.这个过程中会涉及到web页面API服务器的跨域访问(由于受到浏览器的同源策略,但是业界已有很多解决方案,接下来会介绍).通过这种开发模式使得我们真正的实现了前后端完全分离. 当遇到跨域开发时, 我们如何处理好前后端配置和请求库封装(koa/axios版) 采用这种前后端单独开发部署的模式好处有如下几点:

  • 减少后端服务器的并发/负载压力
  • 前端项目和后端项目完全分离, 一定程度上提高了自动化部署的灵活性, 并且代码更易管理和维护
  • 提高前后端开发团队的工作效率, 各司其职, 出现bug更容易定位问题
  • 在大并发情况下可以同水平扩展前后端服务器,利用多台前端服务器做集群来抗住日均千万级的pv
  • 提高应用容错, 即使是API服务器挂了, 前端页面依然能正常访问
  • API服务器能同时为多个应用平台提供服务, 大量复用接口,提升效率。(比如说微服务)

虽然好处有很多, 但是为了实现以上的架构模式, 我们首先要解决的就是跨域问题.

浏览器的同源策略

同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

如果两个URL的protocol(协议,比如http协议,https协议)、port (端口号,如80)和 host(主机,如developer.mozilla.org) 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。也就是说如果不满足以上3个条件中的任意一个,则被视为跨域.

解决跨域问题的几种方式

业界解决浏跨域问题的方案很多, 笔者在这里粗略介绍一下:

  • JSONP实现跨域 通过script标签和url回调来实现跨域, 缺点是只支持get请求
  • CORS CORS需要浏览器和后端同时支持, 后端设置Access-Control-Allow-Origin 就可以开启 CORS
  • postMessage 可以实现跨文本档、多窗口、跨域消息传递(笔者之前写可插拔式聊天机器人就是采用该方案)
  • websocket websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,也是跨域的一种解决方案
  • nginx反向代理
  • document.domain + iframe 比较传统的跨域解决方案

目前作为大规模跨域开发使用最多的模式还是CORS方案,所以笔者接下来将具体介绍采用cors模式搭建前后端跨域访问通用解决方案, 为了方便,笔者后端将采用nodejs+koa, (java/php开发类似), 前端采用axios作为请求库来配合实现完整的cors模式.

跨域开发的后端配置(node/koa版)

要想彻底了解cors的跨域模式, 我们还是要深入实践中来, 笔者将采用nodejskoa中间件来实现cors模式的搭建.这里笔者先简单介绍一下cors:

跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头 来告诉浏览器 让运行在一个域上的Web应用被准许访问来自不同源服务器上指定的资源。

基本场景如下: 当遇到跨域开发时, 我们如何处理好前后端配置和请求库封装(koa/axios版) 对于简单的跨域场景,我们只需要设置请求头的Access-Control-Allow-Origin字段即可, 比如设置为号表示允许任何域名的访问. 当遇到跨域开发时, 我们如何处理好前后端配置和请求库封装(koa/axios版) 这里我们使用*koa2-cors**这个中间件来实现一下, 代码如下:

import koa from 'koa';
import cors from 'koa2-cors';
const app = new koa();
// 设置跨域
app.use(cors({
    origin: function (ctx) {
        return '*'
    }
}))

通过这样的配置, 我们就能轻松实现cors跨域, 不过现实开发中我们一般不会这么设置, 因为这样设置意味着任何人都能访问我们的服务,安全性无法保证. 作为小型的开放服务,可以采用这样的配置加上访问限流来实现免费图床类应用.(开放图床实现可以参考笔者之前写的文章使用nodeJs开发自己的图床应用)

在实际开发中, 我们会将origin的返回值设置为指定域名, 这样就只允许该域名下的请求访问, 所以正确的姿势如下:

import koa from 'koa';
import cors from 'koa2-cors';
const app = new koa();
const isDev = process.env.NODE_ENV === 'development';
// 设置跨域
app.use(cors({
    origin: function (ctx) {
        return isDev ? '*' : 'http://qutanqianduan.com'
    }
}))

通过这种方式, 我们在开发环境中, 可以让前端同事自由访问我们的API接口, 提高联调效率, 而在生产环境中只允许我们的WEB服务器所在域名访问.

更进一步

对于简单请求和简单的开发模式, 以上的设计就基本满足要求了, 但是对于复杂的业务场景, 我们的请求模式往往会涉及到更多的要求, 比如说需要携带cookie, 用户凭证或者自定义的请求头信息等(比如典型的JWT认证的token一般会存放到自定义的头信息中), 此时往往会发送预检请求(要求必须先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响). 当遇到跨域开发时, 我们如何处理好前后端配置和请求库封装(koa/axios版) 这里我们需要了解以下几个响应头部的字段:

  • Access-Control-Allow-Methods 表明服务器允许客户端使用的请求方法
  • Access-Control-Allow-Headers 表明服务器允许请求中携带的头部字段
  • Access-Control-Max-Age 表明响应的有效时间。在有效时间内,浏览器无须为同一请求再次发起预检请求
  • Access-Control-Expose-Headers 服务器允许浏览器访问的头信息白名单
  • Access-Control-Allow-Credentials 指定了当浏览器的credentials设置为true时是否允许浏览器读取response的内容

以上这5个响应头部字段非常重要,这也是我们解决复杂跨域场景的关键配置. 具体配置案例如下:

// 设置跨域
app.use(cors({
    origin: function (ctx) {
        if (ctx.url.indexOf(config.API_VERSION_PATH) > -1) {
          return isDev ? 'http://192.xxx.1.3:8000' : 'http://qutanqianduan.cn'; // 允许来自指定域名请求, 如果设置为*,前端将获取不到错误的响应头
        }
    },
    exposeHeaders: ['WWW-Authenticate', 'Server-Authorization', 'x-show-msg'],
    maxAge: 5,  //  该字段可选,用来指定本次预检请求的有效期,单位为秒
    credentials: true,  // 允许携带用户凭证
    allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的请求方法
    allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Requested-With'] // 允许接收的头部字段
}))

以上是采用koa2-cors实现的方案, 通过设置exposeHeaders, 我们就可以在浏览器端拿到服务器响应的头部字段'WWW-Authenticate', 'Server-Authorization', 'x-show-msg', 进而根据这些字段的值来实现定制化的消息机制.

需要注意的是, 我们服务器在设置credentials后,需要前端请求库配置设置,比如我们需要在axios中设置withCredentials为true, 代码如下:

import axios from 'axios'
const isDev = process.env.NODE_ENV === 'development'
const instance = axios.create({
    baseURL: isDev ? 'http://localhost:3000/api/xxx' : 'http://localhost/api/xxx',
    withCredentials: true
});

这样我们就能成功携带用户凭证并被跨域的后端服务器获取了.以上就实现了我们cors模式的后端配置, 对于nodeJS为主的后端选手, 基本任务已经完成, 对于java/PHP选手, 也可以参考类似的配置和库来实现. 接下来我们来实现前端请求库的封装.

跨域开发的前端请求库封装(axios版)

作为一名前端工程师, 没有一个上手的请求库是万万不行的, 目前业界比较好的轮子有axios, umi-request等, 但是后者在使用过程中有一些坑(毕竟基于fetch实现), 所以这里笔者将基于axios来简单实现一个跨域请求库的封装.方便大家集成在自己的vue或者react项目中. 接下来看看请求库封装的简单模型: 当遇到跨域开发时, 我们如何处理好前后端配置和请求库封装(koa/axios版) 笔者将基于http规范的错误类型进行基本的消息系统设计, 代码如下:

import axios from 'axios'
import { message } from 'antd'

const isDev = process.env.NODE_ENV === 'development'

const instance = axios.create({
    baseURL: isDev ? 'http://localhost:3000/api/xxx' : 'http://qutanqianduan/api/xxx',
    timeout: 10000,
    withCredentials: true
});

// 添加请求拦截器
instance.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    config.headers = {
        'x-requested-with': localStorage.getItem('user') || '',
        'authorization': localStorage.getItem('token') || ''
    }
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    if(response.headers['x-show-msg'] === 'zxzk_msg_200') {
        message.success(response.data.msg);
    }
    return response.data.result;
  }, function (error) {
    // 对响应错误做点什么
    const { response } = error;
    if(response.status === 404) {
        message.error('请求资源未发现');
    }else if(response.status === 403) {
        message.error(response.data.msg, () => {
            window.location.href = '/login'
        });
    }else {
        message.error(response.data.msg);
    }
    return Promise.reject(error);
  });

export default instance

以上笔者结合antd的message作为消息反馈UI,利用axios的请求和响应拦截来实现消息系统的设计, 以上只是基本的框架, 大家可以基于以上设计进行更加自定义的封装.

讲到这里, 大家是不是对跨域下的服务端和前端配置有了更进一步的了解了呢?

最后

如果想学习更多H5游戏, webpacknodegulpcss3javascriptnodeJScanvas数据可视化等前端知识和实战,欢迎在公号《趣谈前端》加入我们的技术群一起学习讨论,共同探索前端的边界。 当遇到跨域开发时, 我们如何处理好前后端配置和请求库封装(koa/axios版)

更多推荐

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这