作为前端攻城狮,经常要面临的问题就是跨域,不论是调自己mock服务的数据,还是真实前后台联调。因为接口服务与前端工程通常都是独立的工程,而前端有很多协议需要遵循
同源策略
(后端则不需要)。解决跨域的方法有很多,可以在服务端配置,也可以在前端解决。作为前端开发者,我更偏向在前端把跨域解决掉,别问我为什么,我就是喜欢这种不依赖后端的感觉[傲娇脸]。
PS:我使用webpack构建工程,我的方法都是基于webpack的大前提下。如果你是用的其他构建工具,或者是没有使用构建工具的工程环境,我的方法可能不能适用。
不能操控服务端代码的情况
当你不能操控服务端代码时(比如前后端联调),你可以通过webpack配置代理。
1. 通过 http-proxy 代理
在webpack配置文件 webpack.config.js
中添加 devServer
配置。然后配置它的 proxy
属性,webpack-dev-server 在起服务后,会把匹配的本地请求转到 proxy
里配置的服务器上去请求,通过服务端的转发,实现跨域。
有点抽象,举个栗子:
我的前端工程服务端口8000。
我要联调的服务端口3000,域名‘champyin.com’。我在前端这么发请求:xhr.open(‘GET, ‘/api/students’, true);
那么我其实发的请求完整url为http://localhost:8000/api/students
,
直接发肯定是报错的,因为我的本地是没有这个接口的。现在我在webpack这么配置代理,给
/api
配置一个映射1
2
3
4
5devServer: {
proxy: {
'/api': 'http://champyin.com:3000'
}
}npm run dev
重启下前端工程,webpack-dev-server 在遇到’/api/students’这种以/api
开头的请求的时候,它不再往本地发了,而是向对应的http://champyin.com:3000
发请求。这是一种后端的请求转发,而后端没有跨域问题。这个时候,虽然在浏览器查看网络请求的时候,会看到前端发的请求地址是http://localhost:8000/api/students
,但其实它的背后真正获取响应的请求是http://champyin.com:3000/api/students
。灵活的
proxy
配置
很多时候,后端的接口不一定都有一个统一的前缀,这个时候,如果还按照上面的方法,那就要对每个不同的接口名配置一个映射,而且后台一旦增加接口,这里也要跟着加,每个映射的值还都是一样的,又麻烦又冗余。其实,webpack已经考虑了这一点,每一个映射规则的value可以是一个对象,并提供pathRewrite
参数来重置请求的上下文。复用一下上面的例子,只不过服务端稍有不同,服务端提供的接口没有统一的
/api
前缀,而是直接的接口名:/students
、/classes
、/grades
这个时候,proxy 可以这么配置:
1
2
3
4
5
6
7
8
9
10devServer: {
proxy: {
'/api': {
target: 'http://champyin.com:3000',
pathRewrite: {
'/api': ''
}
}
}
}这样我们的前端请求依然保留
/api
前缀不用变,而在转发之前,这个前缀会自动被重置去掉。现在我在前端依然这么发请求:xhr.open(‘GET, ‘/api/students’, true);
即我发的请求完整url为http://localhost:8000/api/students
,
而经过代理后的最终url为http://champyin.com:3000/students
这样,就不用因为后台接口不规范,而影响我们的前端代码的质量了。
当然,这个只是在联调模式可以这么做,真正生产环境的时候,还是要后台统一规范,或者部署一个中间层做这种代理转发。
可以操控服务端代码的情况
当你可以控制服务端代码时(比如自己mock数据),你还可以通过以下的方法避免跨域(变相地解决跨域)
2. 在dev-server内mock数据
webpack-dev-server内部其实是自己起了一个express来做服务。
webpack的devServer配置提供了一个before
方法,在启动服务之前,这个方法会被执行,我们可以把我们mock数据的逻辑写在这里面。1
2
3
4
5
6
7devServer: {
before(app) {
app.get('/api/sdutents', (req, res) => {
res.json({name: 'champyin', score: 100})
})
}
}
这个before方法会传一个参数供我们使用,这个参数就是webpack-dev-server内部起的express对象。
重启前端工程时,我们的mock服务也就启动了。这时,我们的mock接口跟前端其实在同一服务也就是webpack-dev-server的express服务下,也就不存在跨域了。
2. 在服务端启动webpack
假设后端我们的express mock接口代码长这样:1
2
3
4
5
6
7
8const express = require('express');
let app = express();
app.get('/api/students', (req, res) => {
res.json({name: 'champyin', score: 100})
})
app.listen(3000, () => {
console.log('server is on 3000');
});
现在我们要把webpack构建放到后端来起。在后端起webpack需要用到叫做webpack-dev-middleware
的中间件,整体逻辑大致是:
获取webpack模块 -> 获取webpack配置文件 -> 将配置文件传给webpack执行,获得compiler实例 -> 把compiler实例传给webpack-dev-middleware中间件,然后整个交给express作为express中间件执行 -> done!1
2
3
4
5
6
7
8
9
10
11
12
13
14// bin/www.js file
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
let webpackConf = require('../webpack.config.js');
let compiler = webpack(webpackConf);
let app = express();
app.use(webpackDevMiddleWare(compiler));
app.listen(3000, () => {
console.log('server is on 3000');
});
现在启动前端工程就不是npm run dev
了,而是node bin/www.js
。
这时,我们的前端就跟后端复用了同一个服务,自然也就不存在跨域了。
其实这相当于在改后端了,不过这个后端是前端可以控制的,所以也算是前端的扩展了。另外,如果可以改后端的话,还可以直接在express里面通过设置请求头来实现跨域,不过这个就跟webpack没有关系了,就不在这里细讲了。
总结
第一种方法是我们提倡的跨域配置,配置代理。首选推荐。
第二和第三种方法其实是在你能控制后端工程的情况下,把前后端工程合并成一个工程了,区别是第二种方法相当于把后端接口移到前端工程来起,第三种方法是把前端工程构建移到后端工程来起。避免了跨域,也算是变相地解决跨域的方案吧,在某些轻量的工程里可以快速搭建调试环境。