我们知道很多大型项目都或多或少的采用跨域的模式开发, 以达到服务和资源的解耦和高效利用. 在大前端盛行的今天更为如此, 前端工程师可以通过nodejs或者Nginx轻松搭建起web服务器.这个时候我们只需要请求后端服务器的接口即可实现系统的业务功能开发.这个过程中会涉及到web页面向API服务器的跨域访问(由于受到浏览器的同源策略,但是业界已有很多解决方案,接下来会介绍).通过这种开发模式使得我们真正的实现了前后端完全分离. 采用这种前后端单独开发部署的模式好处有如下几点:
- 减少后端服务器的并发/负载压力
- 前端项目和后端项目完全分离, 一定程度上提高了自动化部署的灵活性, 并且代码更易管理和维护
- 提高前后端开发团队的工作效率, 各司其职, 出现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的跨域模式, 我们还是要深入实践中来, 笔者将采用nodejs和koa中间件来实现cors模式的搭建.这里笔者先简单介绍一下cors:
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头 来告诉浏览器 让运行在一个域上的Web应用被准许访问来自不同源服务器上指定的资源。
基本场景如下: 对于简单的跨域场景,我们只需要设置请求头的Access-Control-Allow-Origin字段即可, 比如设置为号表示允许任何域名的访问. 这里我们使用*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 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响). 这里我们需要了解以下几个响应头部的字段:
- 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项目中. 接下来看看请求库封装的简单模型: 笔者将基于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游戏, webpack,node,gulp,css3,javascript,nodeJS,canvas数据可视化等前端知识和实战,欢迎在公号《趣谈前端》加入我们的技术群一起学习讨论,共同探索前端的边界。