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

徐小夕
• 阅读 1182

我们知道很多大型项目都或多或少的采用跨域的模式开发, 以达到服务和资源的解耦和高效利用. 在大前端盛行的今天更为如此, 前端工程师可以通过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版)

更多推荐

点赞
收藏
评论区
推荐文章
秃头王路飞 秃头王路飞
5个月前
webpack5手撸vue2脚手架
webpack5手撸vue相信工作个12年的小伙伴们在面试的时候多多少少怕被问到关于webpack方面的知识,本菜鸟最近闲来无事,就尝试了手撸了下vue2的脚手架,第一次发帖实在是没有经验,望海涵。languageJavaScript"name":"vuecliversion2","version":"1.0.0","desc
浅梦一笑 浅梦一笑
5个月前
初学 Python 需要安装哪些软件?超级实用,小白必看!
编程这个东西是真的奇妙。对于懂得的人来说,会觉得这个工具是多么的好用、有趣,而对于小白来说,就如同大山一样。其实这个都可以理解,大家都是这样过来的。那么接下来就说一下python相关的东西吧,并说一下我对编程的理解。本人也是小白一名,如有不对的地方,还请各位大神指出01名词解释:如果在编程方面接触的比较少,那么对于软件这一块,有几个名词一定要了解,比如开发环
blmius blmius
1年前
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
技术小男生 技术小男生
5个月前
linux环境jdk环境变量配置
1:编辑系统配置文件vi/etc/profile2:按字母键i进入编辑模式,在最底部添加内容:JAVAHOME/opt/jdk1.8.0152CLASSPATH.:$JAVAHOME/lib/dt.jar:$JAVAHOME/lib/tools.jarPATH$JAVAHOME/bin:$PATH3:生效配置
光头强的博客 光头强的博客
5个月前
Java面向对象试题
1、请创建一个Animal动物类,要求有方法eat()方法,方法输出一条语句“吃东西”。创建一个接口A,接口里有一个抽象方法fly()。创建一个Bird类继承Animal类并实现接口A里的方法输出一条有语句“鸟儿飞翔”,重写eat()方法输出一条语句“鸟儿吃虫”。在Test类中向上转型创建b对象,调用eat方法。然后向下转型调用eat()方
刚刚好 刚刚好
5个月前
css问题
1、在IOS中图片不显示(给图片加了圆角或者img没有父级)<div<imgsrc""/</divdiv{width:20px;height:20px;borderradius:20px;overflow:h
小森森 小森森
5个月前
校园表白墙微信小程序V1.0 SayLove -基于微信云开发-一键快速搭建,开箱即用
后续会继续更新,敬请期待2.0全新版本欢迎添加左边的微信一起探讨!项目地址:(https://www.aliyun.com/activity/daily/bestoffer?userCodesskuuw5n)\2.Bug修复更新日历2.情侣脸功能大家不要使用了,现在阿里云的接口已经要收费了(土豪请随意),\\和注意
晴空闲云 晴空闲云
5个月前
css中box-sizing解放盒子实际宽高计算
我们知道传统的盒子模型,如果增加内边距padding和边框border,那么会撑大整个盒子,造成盒子的宽度不好计算,在实务中特别不方便。boxsizing可以设置盒模型的方式,可以很好的设置固定宽高的盒模型。盒子宽高计算假如我们设置如下盒子:宽度和高度均为200px,那么这会这个盒子实际的宽高就都是200px。但是当我们设置这个盒子的边框和内间距的时候,那
艾木酱 艾木酱
5个月前
快速入门|使用MemFire Cloud构建React Native应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专
helloworld_28799839 helloworld_28799839
5个月前
常用知识整理
Javascript判断对象是否为空jsObject.keys(myObject).length0经常使用的三元运算我们经常遇到处理表格列状态字段如status的时候可以用到vue