跨域问题不完全解决方案

456 阅读9分钟

我们常说的如何解决跨域问题, 一般都是指调用接口跨域。 但其实在老年代下, 分布式服务进行集成开发时, 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-urlencodedmultipart/form-datatext/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属性的。 此时就能获取到对应的数据了。

这种方法有以下特点:

  1. window.name 只能接收一个字符串, 如果非字符串类型, 会自动转换, 因此建议传对象或数组时,先通过JSON.stringify方法转义一下。

  2. name属性接收的最大值是2MB, 有大小限制。

  3. 当一个页面,进入到多个页面时, 多个页面共用一个 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 改变内部某个状态,以响应父页面传来的变量
        
    }

})