在本地开发前端项目时,经常会遇到需要本地请求测试环境或是生产环境地址,但是这样会导致跨域问题,如果我们使用了webpack,通常会通过代理解决跨域问题,那么接下来我们一起看看什么是跨域,使用webpack如何解决该问题,而webpack解决跨域的实现原理。
什么是跨域?
❝出于浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)
❞
那么同源策源具体又阻止了哪些交互呢,如下
- 1.无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB,既无法跨域去获取另一页面下的缓存数据;
- 2.无法接触非同源网页的 DOM;
- 3.无法向非同源地址发送 AJAX 请求;此点也是最关键最重要的一点,通常我们关注的也是如何通过请求跨域获取数据。
如何解决呢
本文着重讲述webpack解决跨域问题,当然解决问题的方式远远不止这一种,其他方式就不在此一一讲述。 话不多说,直接上代码:
- 在项目根目录config/index.js中
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/api': {
target: 'https://hkapp-sit.xxxxxxxx.com',
pathRewrite: {'^/api': '/api'},
changeOrigin: true, // target是域名的话,需要这个参数,
secure: false, // 设置支持https协议的代理
}
},
...
}
主要是配置dev对象下proxyTable对象,'/api'表示匹配所有以/api开始的路径,若匹配中了,执行该字段下的配置:
- target: 表示目标资源的地址;
- pathRewrite: 表示对该字段的重写;
- changeOrigin:该字段的主要作用是将请求头中host字段改写成target,以防服务器会对host字段做逻辑判断;
- secure: 设置HTTPS协议的代理,由于target默认不支持https协议,需要将该字段设置为false; 然后在打包配置文件夹下build下webpack.dev.conf.js引入之前请求配置:
const devWebpackConfig = merge(baseWebpackConfig, {
...
devServer: {
...
proxy: config.dev.proxyTable,
...
}
...
}
是不是到这个就大功告成了呢,原先我也天真的以为是的,但是在实际本地开发中,还是疯狂报跨域请求出错,后来通过某度我才知道,还缺了关键一步,等登等登: 在config文件夹下dev.env.js文件:
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
api: '"/api/"'
})
将需要匹配的接口字段设置成开发环境下的变量,在利用axios请求时设置url,在url前统一加上变量api,实现请求匹配代理,这样就真正大功告成了。。。 至于为啥'"/api"'是双层引号,大胆猜测,在webpack,合并配置merge时,内部是使用eval语句解析的;
最后我们来探索下webpack是如何实现跨域功能的呢
通俗的描述为:通过一些方法设置代理,在请求发送(接收)之前加入中间层,将不同的域名转换成相同的就解决了跨域的问题。
举个栗子:客户端发送请求时不直接到服务器而是先到代理的中间层在这里将 localhost:8080的这个域名转换成 www.xiaoxuosheng.com,再将请求发送到服务器这样在服务器端收到的请求就是使用的www.xiaoxuosheng.com域名同理,当服务器返回数据的时候,也是先到代理的中间层将www.xiaoxuosheng.com转换成www.xiaoxuosheng.com;这样在客户端也是在相同域名下访问的了。
这里就需要提到http-proxy-middlerware这个中间件了:
import { HttpProxyMiddleware } from './http-proxy-middleware';
import { Filter, Options } from './types';
export function createProxyMiddleware(context: Filter | Options, options?: Options) {
const { middleware } = new HttpProxyMiddleware(context, options);
return middleware;
}
export { Filter, Options, RequestHandler } from './types';
实际上通过HttpProxyMiddleware类创建新对象并返回新对象下的middleware对象,那么我们再看看HttpProxyMiddleware类的具体实现:
public middleware: RequestHandler = async (
req: Request,
res: Response,
next: express.NextFunction
) => {
if (this.shouldProxy(this.config.context, req)) {
try {
const activeProxyOptions = await this.prepareProxyRequest(req);
this.proxy.web(req, res, activeProxyOptions);
} catch (err) {
next(err);
}
} else {
next();
}
if (this.proxyOptions.ws === true) {
// use initial request to access the server object to subscribe to http upgrade event
this.catchUpgradeRequest((req.connection as any).server);
}
};
根据上面代码可以知道,核心部分就是 this.proxy.web(req, res, activeProxyOptions) 这行代码,而this.proxy又是通过this.proxy = httpProxy.createProxyServer({})创建,最终是利用了httpProxy库,那继续往下看httpProxy的核心内容:
function ProxyServer(options) {
...
...
this.web = this.proxyRequest = createRightProxy('web')(options);
this.webPasses = Object.keys(web).map(function(pass) {
return web[pass];
});
...
...
}
// 而createRightProxy的实现 :
// 参数 type 用于区分请求类型,'web' 为普通 http, https 请求,'ws' 为 websocket 请求
function createRightProxy(type) {
// 参数 options 为全局配置项
return function(options) {
return function(req, res /*, [head], [opts] */) {
// passes 任务队列
var passes = (type === 'ws') ? this.wsPasses : this.webPasses,
args = [].slice.call(arguments),
cntr = args.length - 1,
head, cbl;
// 解析回调函数
if(typeof args[cntr] === 'function') {
cbl = args[cntr];
cntr--;
}
// 混入该请求中特定的配置项 opts
var requestOptions = options;
if(
!(args[cntr] instanceof Buffer) &&
args[cntr] !== res
) {
requestOptions = extend({}, options);
extend(requestOptions, args[cntr]);
cntr--;
}
// head
if(args[cntr] instanceof Buffer) {
head = args[cntr];
}
// 请求的目标地址
['target', 'forward'].forEach(function(e) {
if (typeof requestOptions[e] === 'string')
requestOptions[e] = parse_url(requestOptions[e]);
});
if (!requestOptions.target && !requestOptions.forward) {
return this.emit('error', new Error('Must provide a proper URL as target'));
}
// 挨个执行任务队列,处理消息头,转发请求
for(var i=0; i < passes.length; i++) {
if(passes[i](req, res, requestOptions, head, this, cbl)) {
break;
}
}
};
};
}
后面的内容有点晦涩难懂,大家想了解更多更深层次的解析,如代理的建立,请求事件的监听、转发,请求队列的执行部分,可访问最下方node-http-proxy源码解析链接