《前端实战总结》之使用postMessage实现可插拔的跨域聊天机器人

徐小夕 等级 691 0 0

《前端实战总结》之使用postMessage实现可插拔的跨域聊天机器人 由于笔者之前的项目中接触过聊天机器人的项目,主要实现机器人客服模块,以及支持跨多平台使用的目的,所以特地总结一下,希望有所收获。

你将学到

  • 跨域技术常用方案介绍
  • postMessage实现跨域通信
  • 如何实现聊天机器人
  • node搭建本地服务器来实现渲染页面和跨域
  • 回答语料库设计思路

效果预览

《前端实战总结》之使用postMessage实现可插拔的跨域聊天机器人

正文

1. 跨域技术常用方案介绍

首先要强调的是跨域的安全限制都是对浏览器端来说的,服务器端是不存在跨域安全限制的。我们常用的跨域技术主要有如下几种:

  • JSONP跨域
  • iframe+domain跨域
  • nginx反向代理跨域
  • cors跨域
  • postMessage跨域

JSONP实现跨域请求的原理就是动态创建script标签,然后利用script的src 不受同源策略约束来跨域获取数据。JSONP 主要由回调函数和数据两部分组成。回调函数的名字一般是在请求中指定的。而数据就是传入回调函数中的 JSON 数据。我们一般可以在全局定义一个回调函数,然后在script标签里传入回调函数即可:

window.handleData = function(data){
    // ...
}
let script = document.createElement("script");
script.src = "https://xxxx.com/v0/search?q=xuxi&callback=handleData";
document.body.insertBefore(script, document.body.firstChild);

这样我们就能在回调函数handleData中拿到服务器接口返回的数据了。

虽然jsonp实现跨域方式很简单,但是只支持get请求,对传输的数据量有一定限制。cors跨域是目前我们用的比较多的本地调试方式,原理就是在服务端设置响应头header的Access-Control-Allow-Origin字段,这样浏览器检测到header中的Access-Control-Allow-Origin,这样就可以跨域了。

至于我们设置了cors之后在network中出现了两次请求的问题,其实涉及到cors跨域的请求预检,分为简单请求和非简单请求两种,这块知识可以单独抽离出一篇文章,感兴趣的可以自己学习了解一下。

2. postMessage实现跨域通信

window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议,端口号以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

本质上说postMessage()是基于消息事件机制来实现跨域通信,它隶属于消息窗体本身,比如window以及window内嵌的frame的window,基本使用形式如下:

someWindow.postMessage(message, targetOrigin, [transfer]);

参数介绍:

  • someWindow 窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames
  • message 将要发送到其他 window的数据。意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化
  • targetOrigin 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)。不提供确切的目标将导致数据泄露等安全问题
  • transfer 是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权

我们可以通过如下方式来监听message:

window.addEventListener("message", receiveMessage, false);

function receiveMessage(event){
  let origin = event.origin || event.originalEvent.origin; 
  if (origin !== "http://aaa:8080")
    return;

  // ...
  console.log(event.data)
}

// 派发消息的页面
winB.postMessage(_({text: '休息休息'}), origin)

我们的event里有如下几个核心的属性:

  • data 从其他 window 中传递过来的对象
  • origin 调用 postMessage 时消息发送方窗口的 origin . 这个字符串由 协议、“://“、域名、“ : 端口号”拼接而成
  • source 对发送消息的窗口对象的引用; 您可以使用此来在具有不同origin的两个窗口之间建立双向通信

3. 实现聊天机器人

在熟悉以上知识点之后,我们开始来写我们聊天机器人的demo。 首先我们写两个html,分别为a.html和b.html,然后用node分别代理两个不同页面,设置不同端口:

// a.js
//依赖一个http模块,相当于java中的import,与C#中的using
var http = require('http');
var fs = require('fs');
var { resolve } = require('path');

//创建一个服务器对象
server = http.createServer(function (req, res) {
//设置请求成功时响应头部的MIME为纯文本
res.writeHeader(200, {"Content-Type": "text/html"});
//向客户端输出字符
let data = fs.readFileSync(resolve(__dirname, './a.html'))
res.end(data);
});
//让服务器监听本地8000端口开始运行
server.listen(8000,'127.0.0.1');
console.log('http://127.0.0.1:8000')

// b.js
// ...
server.listen(8001,'127.0.0.1');

由上可知我们a.html代理在8000端口下,b.html代理在8001端口下,由浏览器的同源策略可知他们存在跨域问题。

跨域实现之后我们可以开始搭建页面层级了,我们这里将b页面以iframe的形式嵌入到a页面,具体结构如下:

《前端实战总结》之使用postMessage实现可插拔的跨域聊天机器人 这样我们就可以愉快的搭建postMessage体系了。

首先我们在a页面通过发送按钮和输入框将消息发送给b页面,大致结构如下:

<body>
    <div class="wrap">
        <iframe src="http://127.0.0.1:8001" frameborder="0" id="b"></iframe>
        <div class="control">
            <input type="text" placeholder="请输入内容" id="ipt">
            <span id="send">发送</span>
        </div>
    </div>
    <script>
        window.onload = function() {
            let origin = 'http://127.0.0.1:8001';
            let _ = (data) => JSON.stringify(data);
            let winB = document.querySelector('#b').contentWindow;
            let sendBtn = document.querySelector('#send');
            sendBtn.addEventListener('click', (e) => {
                let text = document.querySelector('#ipt');
                winB.postMessage(_({text: text.value}), origin)
                text.value = '';
            }, false)
            winB.postMessage(_({text: ''}), origin)
        }
    </script>
</body>

我们可以通过iframe的contentWindow来拿到b页面窗体的引用,然后在发送按钮的点击事件中触发postMessage将数据发送给B。B页面结构如下:

<body>
    <div class="content">
            <h4>Lab智能机器人</h4>
            <div class="content-inner"></div>
    </div>
    <script>
        // 语料库
        const pool = [];
        window.addEventListener("message", receiveMessage, false);
        let content = document.querySelector('.content-inner');
        let initContentH = content.scrollHeight;
        let _ = (data) => JSON.stringify(data);
        function createChat(type, mes) {
            let dialog = document.createElement('div');
            dialog.className = type === 0 ? 'dialog robot' : 'dialog user';
            let content =  type === 0 ? `
                <span class="tx">${type === 0 ? 'lab' : 'user'}</span>
                <span class="mes">${mes}</span>
            ` : `
                <span class="mes">${mes}</span>
                <span class="tx">${type === 0 ? 'lab' : 'user'}</span> 
            `;
            dialog.innerHTML = content;
            return dialog
        }

        function scrollTop(el, h) {
            if(el.scrollHeight !== h) {
                el.scrollTop = h + 100;
            }
        }

        function receiveMessage(event){
            // 兼容其他浏览器
            let origin = event.origin || event.originalEvent.origin; 
            if(origin === 'http://127.0.0.1:8000') {
                let data = JSON.parse(event.data);
                if(data && !data.text) {
                    mes = { text: '你好,我是机器人Lab,请问有什么可以帮到您的吗?' };
                    event.source.postMessage(_(mes), event.origin)
                    content.appendChild(createChat(0, mes.text))

                }else {
                    content.appendChild(createChat(1, data.text))
                    scrollTop(content, initContentH)

                    setTimeout(() => {

                        content.appendChild(createChat(0, '正在解决'))
                        scrollTop(content, initContentH)
                    }, 2000);
                } 
            }
        }
    </script>
</body>

我们在b页面中去解析a页面的数据并做出相应的回答。这样,我们的基本聊天机器人就实现了。

4. 回答语料库设计思路

至于当我们在a页面发送了一个消息,b页面如何解析并回答,可以有如下几种思路:

  • 通过后端接口实现,即我们可以将a的数据作为参数传递给某个后端接口,让后端来实现返回需要的数据,这种在AI机器人中应用的很广泛。
  • 纯前端实现。前端定义回答的语料库,通过关键词匹配来拿到实现应答,这种一般用于普通的预设问题的回答。

5.实现可插拔式

可插拔式就是一个页面可以放在不同平台使用。这种我们可以设置origin白名单,只需要将b页面封装,其他系统可以使用类于a页面的方式,只提供发送信息的接口,这样我们就可以在不同平使用了。

《前端实战总结》之使用postMessage实现可插拔的跨域聊天机器人 关于本聊天程序的所有代码我已经提交到GitHub,感兴趣的朋友可以下载体验一下,或者基于他实现更智能的聊机器人。

最后

如果想了解更多webpack,node,gulp,css3,javascript,nodeJS,canvas等前端知识和实战,欢迎在公众号《趣谈前端》加入我们一起学习讨论,共同探索前端的边界。

《前端实战总结》之使用postMessage实现可插拔的跨域聊天机器人

更多推荐

收藏
评论区

相关推荐

《前端实战总结》之使用postMessage实现可插拔的跨域聊天机器人
由于笔者之前的项目中接触过聊天机器人的项目,主要实现机器人客服模块,以及支持跨多平台使用的目的,所以特地总结一下,希望有所收获。 你将学到 跨域技术常用方案介绍 pos
JAVA拦截器,JAVA返回结果跨域问题解决
遇到的问题: ------ 通过拦截器做权限控制,没有权限时返回了json值,结果前端请求时提示跨域了 备注:我的前端站点和后端站点不是一个地址 **报错1:** Access to XMLHttpRequest at 'http://localhost:8089/appcicd/appinfo/getappinfos' from origi
JAVA服务端配置允许跨域请求
CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。 #### 1.加入CROS依赖的包 <dependency> <groupId>com.thetransactionco
JavaEE从服务器端解决Ajax跨域问题
1、Ajax跨域简介   1、指的是浏览器不能执行其他网站的脚本。是浏览器施加的安全限制。js本身不跨域,使用form表单和iframe直接请求,是不会跨域的;   2、只要两个url的协议、域名、端口其中有一个不同,从其中一个url中使用ajax请求另一个url,则属于Ajax跨域;   3、ajax请求接口,只是不能进入回调函数,接口还是可以正常请
PHP开启CORS
CORS 定义 ------- Cross-Origin Resource Sharing(CORS)跨来源资源共享是一份浏览器技术的规范,提供了 Web 服务从不同域传来[沙盒](https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%25E
Nginx解决跨域问题(CORS)
前言 == CORS(Cross-Origin Resource Sharing) 跨域资源共享,是一种允许当前域(domain)的资源(比如html/js/web service)被其他域(domain)的脚本请求访问的机制,通常由于同域安全策略(the same-origin security policy)浏览器会禁止这种跨域请求。  如:a.c
Angular 应用解决跨域访问的问题
在前后台分离的应用中,Angular 与 Java 是一对好搭档。但是如果是分开部署应用,则势必会遇到跨域访问的问题。 什么是跨域 ----- 启动应用之后,有些浏览器会提示如下告警信息: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Angular2+ iframe跨域调用父页面js
**业务场景:列表页面添加一个导入功能,该导入功能由第三方页面提供,导入完成后需要通知主列表刷新数据。** **先来看看iframe跨域调用父页面的实现逻辑(以postMessage方式为例)** **_(postMessage介绍:[https://developer.mozilla.org/zh-CN/docs/Web/API/Window/pos
IE iframe跨域访问cookie丢失解决方法
问题描述:IE浏览器加载Iframe跨域访问后Cookie丢失,用户登录状态失效 解决方法:在PHP中加入以下代码即可解决 header("Access-Control-Allow-Origin:*"); header("Access-Control-Allow-Credentials: true"); head
SpringBoot实现jsonp跨域通信
实现jsonp跨域通信 ----------- > 实现基于jsonp的跨域通信方案 ### 原理 > 浏览器对非同源ajax请求有限制,不允许发送跨域请求 > 目前跨域解决方案有两种 > > * cros配置 > * jsonp请求 > > cros为新规范,通过一个head请求询问服务器是否允许跨域,若不允许则被拦截 > jso
SpringBoot解决跨域问题
在开发前后端分离的项目时,常常会碰到跨域请求的问题。这是因为浏览器的安全性限制,不允许Ajax访问协议不同、域名不同、端口号不同的数据接口,否则会出报 No 'Access-Control-Allow-Origin' header is present on the requested resource 错误。 SpringBoot通过设置cors(跨源
SpringBoot实现jsonp跨域通信
实现jsonp跨域通信 ----------- > 实现基于jsonp的跨域通信方案 ### 原理 > 浏览器对非同源ajax请求有限制,不允许发送跨域请求 > 目前跨域解决方案有两种 > > * cros配置 > * jsonp请求 > > cros为新规范,通过一个head请求询问服务器是否允许跨域,若不允许则被拦截 > jso
SpringBoot解决跨域问题
在开发前后端分离的项目时,常常会碰到跨域请求的问题。这是因为浏览器的安全性限制,不允许Ajax访问协议不同、域名不同、端口号不同的数据接口,否则会出报 No 'Access-Control-Allow-Origin' header is present on the requested resource 错误。 SpringBoot通过设置cors(跨源
Vue 前端验证码
⭐前言 --- 在vue项目中,登录界面必不可少。简单项目里,验证码通常由数字字母构成。一般有两种产生方式:前端,后端。后端生成,前端直接调用接口,将返回的url放入a标签即可。而前端生成,则大多用canvas画布实现,如何让验证码随屏幕大小变化,还能保持原样不失真,这就是我们要实现的功能。当然,在创建vue项目时,我们必须得克服跨域问题。No '
vue 使用axios 出现跨域请求的两种解决方法
最近在使用vue axios发送请求,结果出现跨域问题,网上查了好多,发现有好几种结局方案。 1:服务器端设置跨域 header(“Access-Control-Allow-Origin:\*”); header(“Access-Control-Allow-Headers:content-type”); header(“Access-Cont