我们常说的如何解决跨域问题, 一般都是指调用接口跨域。 但其实在老年代下, 分布式服务进行集成开发时, iframe嵌套情况下,如何解决跨窗口通信问题, 也是跨域问题的一种。 因此我尝试分成两个部分来说明。
网络接口跨域
1. nodejs
以 express框架为例, 其他框架原理一样, 都是在创建一个中间件的时候,对此进行监听, 并指向对应需要代理的地址
先安装相关框架和插件
npm install express http-proxy-middleware -D
然后编写一个简单的服务即可
const express = require('express');
const { createProxyMiddleware : proxy } = require('http-proxy-middleware'); // 获取对应函数 ,并通过一个新的名称接收, 不这样做, 直接使用createProxyMiddleware也可以
const app = express();
app.use('/proxy', proxy({
target: 'http://localhost:8888',
changeOrigin: true
}))
app.listen(3000)
此时当我们请求 localhost:3000/proxy
时, 就会自动代理到 localhost:8888
地址上去。
假设此时8888
端口上有一个接口为 localhost:8888/api/test
, 那么对应到3000
端口访问时, 就是localhost:3000/proxy/api/test
了。我们打包好的项目需要跑一个nodejs服务时, 只需要将dist文件夹放到某一公共资源目录下, 假设为 public
下。
我们做一个static指向就好了
app.use(express.static('public'));
...
app.listen(80)
此时我们完全可以在项目中通过/proxy/...
的形式调用localhost:8888
上面的接口了。
我们可能会想修改原有的接口, 比如原来的接口是 /api/test
。 我们在做代理的时候, 想换一个接口名称,此时只需要为 proxy
(createProxyMiddleware
)方法的配置对象, 添加一个pathRewrite
属性,并且将需要重新更换接口的通过键值对的形式编写即可。
比如, /api/test
接口, 想更换成/test
访问。 那么我们只需要:
app.use('/proxy', proxy({
target: 'http://localhost:8888',
changeOrigin: true,
pathRewrite: {
'/api/test' : '/test' , // key为 原来的接口 val为新的代理接口
}
}))
此时访问`http://localhost:8888/api/test` 只需要通过 `localhost/proxy/test` 即可。
::: tip express框架反向代理接口, 就是通过一个第三方中间件,监听一个路径后, 并指向第三方中间件创建的proxy对象。只是碰巧这里使用了http-proxy-middleware罢了。 如果想查看更多 关于http-proxy-middleware的配置项, 可以查阅 相关npm地址 :::
2. webpack
vue-cli脚手架是基于 webpack的, 并且它有一个替代webpack.config.js
的文件。 即vue.config.js
。 由于两者实质一样,因此可以理解为都是对webpack.config.js
文件配置对象的设置问题
module.exports = {
...,
devServer: {
...,
proxy: {
target: 'localhost:8888',
changeOrigin: true,
pathRewrite: {
'/api/test' : '/test'
}
}
}
}
webpack中其实已经考虑到了请求跨域问题, 因此它自动就安装了http-proxy-middleware
中间件, 且触发机制就是给 devServer
配置一个proxy
代理属性即可,且此属性对应一个配置对象,此对象的配置参数即为http-proxy-middleware
中间件的配置参数。
3. nginx
nginx配置反向代理,相对而言代码量可能最少。
首先打开 nginx.conf
配置文件, 并找到以下代码块。
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
我们的关注点只需要在 server即可,因为每一个server对象都是一个服务器地址。
listen 为监听的端口号,
server_name 为当前访问的地址。
location 为访问地址+端口后的路径设置, 比如上方的值为 /
即 localhost:80/
时将会访问到当前的服务器。
root 为nginx根目录文件下的哪一个静态文件夹, 效果同app.use(express.static(根目录下文件夹名称))
。 一般此文件夹下方会有一个index.html 作为访问此地址时的首页。
index 为将root指向的文件下的哪一个文件作为首页的入口。 一般值为index.html 如果有多个值, 用空格隔开,代表如果前一个文件没找到, 就依次找后面的文件,如果找到某一个, 就将其作为首页文件使用并返回。
而我们要实现一个反向代理, 只需要在location 中 添加一行代码即可
location / {
root html;
index index.html index.htm;
proxy_pass http://127.0.0.1:8080
}
回到根目录后双击nginx.exe
后,服务就启动了。 或者通过cmd
命令start nginx
也可以。
此时访问 localhost/
就指向了 http://127.0.0.1:8080/
4. jsonp
jsonp
的原理是script脚本标签可以无限制访问公共资源, 因此在通过script请求接口时,在接口的结尾给与一个函数名称作为参数, 同时注明为 .js文件。后端通过对当前接口的拦截,获取到结尾的函数名时,处理对应的数据 ,并将对应的数据作为当前函数的参数, 并编写一个简易的js代码, 内容为 调用当前这个函数。并将此js文件返回出去即可。
前端还需要做的是, 需要手动实现这个方法 ,并在方法体内, 处理传过来的数据, 当然, 这个方法的定义,需要在请求接口之前。
以上说的是实现的整个流程, 实际上,前端需要做的内容很少, 只需要在调用接口时, 传一个函数名作为参数, 然后在调用前实现这个函数即可。这个函数就是请求到数据后的回调函数了。
实操敲一遍:
<ul id="res">
</ul>
<script>
function callback(res){
var dom = document.getElementById('res');
dom.innerHTML = res.map((data, i)=>
`<li>${i+1} , ${data}</li>`
).join('\n')
}
</script>
<script src="http://localhost:3333/test/callback.js"></script>
nodejs服务器js代码:
const express = require('express');
const fs = require('fs');
const app = express();
app.get('/test/:cb', (req, res)=> {
let filePath = req.params.cb;
let callback = filePath.replace(/\.js$/, '');
// 模拟处理数据
let data = getData(20,10);
let content = `${callback}([${data}])`
fs.writeFileSync(filePath, content);
fs.readFile(filePath, (err, data) => {
if (err) {
throw err;
} else {
res.send(data);
}
});
})
function getData(max, min){
return Array.from({
length: Math.floor(Math.random()*(max-min) + min)
}, (item,i) => `"数据${min+i}"`);
}
app.listen(3333)
此处使用了 express框架, fs模块。 此处没有处理动态创建的js文件, 如果担心这样会创建很多js文件的话, 可以在获取到数据的时候, 将刚才创建的js文件删除即可:fs.unlink(filePath)
。
缺点: 此方法只能请求GET请求。
5. CORS(Cross-Origin Resource Sharing)跨域资源共享
异步请求时, 浏览器如果发现请求的接口和当前的地址不满足(同源策略)[], 就会自动给当前的请求头添加一个origin
,值为当前地址的源。Origin:http://wyy.56eye.cn
后端在接收到请求后,会在 Response Headers
中加入一个属性 Access-Control-Allow-Origin
。浏览器判断响应中的 Access-Control-Allow-Origin
值是否和当前的地址相同,匹配成功后才继续响应处理,否则报错。
一般此类请求, 又分为简单请求 和 复杂请求。
简单请求: GET, POST, HEAD
非简单请求 PUT, DELETE, ...
同时, 请求头中Content-type
的值需要是 application/x-www-form-urlencoded
、multipart/form-data
、text/plain
之一。 如果是application/json
就不是简单请求了,而属于非简单请求。
默认情况下, cors请求 是不会携带cookie的, 如果需要携带cookie,则分情况设置:
假设为 XMLHttpRequest
则在其实例化对象上, 设置 withCredentials
的值为true
。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
如果设置了以上属性, 还是没有将对应的cookie传输过去的话, 则需要设置Access-Control-Allow-Credentials
这个属性值为 true
了。
其他设置和正常请求无差异。 如果需要修改Content-type
时,通过setRequestHeader
方法即可。
原生XMLHttpRequest
代码如下:
var url = 'http://localhost:3333/post/test';
var params = {code: 1, msg: '我是测试'};
var xhr = XMLHttpRequest()
xhr.withCredentials = true; // 允许传输cookie
xhr.open(url, POST)
xhr.setRequestHeader('Content-type', 'application/x-www-from-urlencoded')
xhr.send(JSON.stringify(params)) // post才传这个
xhr.readystatechange = function(){
if(xhr.readyState == 4 && xhr.status == 200){
console.log(xhr.responseText)
}
}
对应nodejs后台:
const express = require('express');
const app = express();
app.all('*', (res , req, next) => {
req.header('Access-Control-Allow-Origin', '*'); // 手动修改 Origin值为 * , 此时将会通过浏览器的跨域检测
req.header('Access-Control-Allow-Method', 'PUT') // 默认支持 GET POST HEAD 的简单请求, 非简单请求可以通过这个属性添加。 对应一个 Access-Control-Allow-Methods , 接收一个字符串,各个方法用逗号隔开。
next()
})
app.listen(3333)
frame窗口跨域
1. postMessage 方法
目标Window 通过 window.postMessage(message)
的方式 ,可以给所有 iframe页面发送一个 MessageEvent
对象。
假设此时有一个 win2 , 在 win2 中编写如下代码:
window.addEventListener('message', (event)=>{
var origin = event.origin || event.originalEvent.origin;
if(origin === 'http://localhost:8080'){
// 说明目标window的同源地址与条件相符
let {data, source: targetWindow} = event;
console.log(data, targetWindow)
}
})
2. document.domain 跨子域
当在一个页面中嵌入了另一个页面时(iframe),父窗口无法获取子窗口中的window
对象, 有时候需要通过前者对后者进行控制。 此时只需要设置两个窗口中的document.domain
为同一个值(假设两个页面同地址,不同端口, 就可以设置为同地址作为共同的值)
A frame (www.example.com:8080)
document.domain = 'example.com';
B frame (www.example.com:3333)
document.domain = 'example.com';
此时B 页面 嵌套于 A 页面时, A页面就能获取B页面的window对象了。
3. window.name
此处主要利用 每一个窗口都会有一个window.name的特性实现的。
a.html 和 c.html 跨域, 此时a想获取 c发来的数据。
我们可以将 c页面以iframe的形式嵌套在a页面中, 且为了不影响界面显示, 可以给一个 display:none
。 再然后,c将需要传给 a页面的数据,赋值给当前窗口window.name
上。 再然后将一个跟a.html同源的页面修改到 iframe的src属性上。 此时 iframe对应的页面会进入到新的页面,但是由于iframe始终只有一个window对象, 因此新的页面是可以读写iframe 的window.name
属性的。 此时就能获取到对应的数据了。
这种方法有以下特点:
-
window.name
只能接收一个字符串, 如果非字符串类型, 会自动转换, 因此建议传对象或数组时,先通过JSON.stringify
方法转义一下。 -
name属性接收的最大值是2MB, 有大小限制。
-
当一个页面,进入到多个页面时, 多个页面共用一个
window.name
。 该页面及所有载入进来的其他页面都对window.name
属性具有读写权限。
4. hash hashchange popStatu pupStatu
此方法系列 主要监听 url地址的。 假设 父窗口需要和子窗口通信, 此时子窗口通过iframe的形式嵌入。
父窗口:
var child = document.getElementsByTagName('iframe')[0];
var alias = 'changestatu';
var src = child.src + '#' + alias;
child.src = src;
子窗口:
window.addEventListener('hashchange', (e)=>{
let hash = window.location.hash
if(hash === '#changestatu') {
// TODO 改变内部某个状态,以响应父页面传来的变量
}
})