前言
随着 AI 应用的爆火,从写文案到聊天,AI似乎无所不能。这种爆火背后,不仅仅是技术的进步,更是人们对即时、智能交互的需求爆发。而这种需求,早已不局限于某个App或平台------它正在悄然改变整个互联网的交互方式。如今,越来越多的网站开始意识到,嵌入智能功能不再是"锦上添花",而是"必不可少"。毕竟,在这个追求效率和体验的时代,一个"会说话"的网站,才能真正抓住用户的心,本篇文章将会带大家深入理解网站嵌入背后的技术原理。
什么是网站嵌入
网站嵌入即典型的第三方Javascript场景。第三方 Javascript 是指将 JavaScript 代码嵌入到非由Javascript 开发者掌控的用户环境中执行的技术。与运行在自主开发的Web页面中的JavaScript不同,"第三方Javascript"的执行环境往往不受开发者的完全控制。
网站嵌入是指在一个网页中快速集成另一个网页或外部内容的技术,使得用户无需跳转到其他网页即可查看嵌入的内容。
第三方 Javascript 的形态
我们已经确切知道了第三方JavaScript是在某个网站上被执行的代码,这就使得第三方代码能够访问到网站的HTML元素和JavaScript上下文。因此,我们可以通过多种方式操作目标页面,例如在文档对象模型(DOM)中创建新的元素、插入自定义的样式表、注册浏览器事件以捕获用户行为。在绝大多数情况下,如同使用JavaScript操作自己的网站或者应用一样,第三方脚本可以执行同样的操作,不同的是,第三方脚本操作的是他人的网站。
第三方 JavaScript 既然拥有了远程操作 Web 页面的能力,那它的使用场景就会非常多:
- 网站嵌入------内嵌在用户网页上的小型应用。
- 统计分析------收集用户网站的数据并进行统计分析,比如检测网站用户访问情况、页面浏览市场等。
常见的第三方 Javascript 例子
-
YouTube 播放器 API
YouTube 播放器 API 允许开发者在网站上嵌入 YouTube 视频播放器并使用 JavaScript 控制播放器。
-
百度统计
用户引入一段百度统计提供的Javascript代码, 这段代码负责统计浏览网站的用户的行为, 如访问量, 访问时常等。
-
百度地图SDK
用户在自有的站点内通过SDK形式插入、展示百度地图。
第三方 Javascript 引入
在网站上引入第三方 Javascript 的方式有两种,一种是通过标准的"阻塞式
为什么这里不考虑通过 npm 依赖的方式引入?因为这种方式后面升级更新太麻烦了,不能频繁的要求用户去升级依赖。
阻塞式脚本引入
阻塞式脚本引入很简单,只需要在用户的网站上嵌入一个标准的<script>标签,其 src 属性指向第三方 Javascript 的地址:
javascript
<script src="htto://xxx.com/test.js" />
虽然这种引入方式能达到预期,但是也有一个严重的缺点,一个像这样普通的
异步脚本引入
浏览器厂商很早就意识到同步加载脚本并不理想。为了解决这个问题,W3C针对
defer 和 async 都是异步加载第三方脚本,唯一的区别是 defer 需要等待页面加载完毕之后才会按顺序执行,而 async 不需要等待,一旦第三方脚本加载完毕就会立即执行。
虽然 async 和 defer 这两种属性都能防止脚本阻塞页面加载,但对于第三方脚本而言,async 是更好的选择。因为通过 async 加载的脚本可以在页面加载完毕之前执行,这样能够更早的初始化并运行第三方脚本而不必等待很长时间。
网站嵌入的挑战
与宿主环境相互影响
第三方 JavaScript 代码最终会在用户的网站这一未知的宿主环境中运行,而用户的网站是一个复杂的生态系统,包含了各种各样的 HTML、CSS、JavaScript 代码,以及第三方库和框架。这些元素均可能会影响第三方Javascript,可能会产生意想不到的结果,从而影响运行效果。尤其是运行于质量相对较为低劣的中小站点之中。
举例1 -- css 污染
宿主环境使用了css标签选择器
js
div {color: red;}
那么任何由第三方Javascript创建的视图, 都可能会收到这个CSS规则影响, 最终产出不符合预期的结果。
举例2 -- 基础库冲突
第三方JavaScript使用React 16, 宿主环境也使用了React 16或其他版本React,最终单运行上下文中,存在两个React实例,出现诸多问题, 如React合成事件(Synthetic Event)系统冲突、hooks不能运在多 React 实例的上下文。
React 17版本才升级支持了单运行环境多React实例。
请求跨域
第三方Javascript代码常需要与服务端进行通信, 用户宿主环境无法与第三方Javascript提供方保持相同域名, 因此会出现跨域问题。 常见的解决方案是 JSONP和CORS。
验证和会话
由于 HTTP 是一种无状态的协议,每次浏览器向服务器发送请求时服务器不会记住之前的请求信息,因此现在大多数的网站都采用 Cookie 来存储当前的用户会话信息。当用户登录到一个Web应用时,浏览器会返回标识用户当前会话的 Cookie。该Cookie被保存在浏览器中,后续同一网站的每个HTTP请求都会携带该 Cookie。服务端利用 Cookie 识别用户并在响应中返回特定用户的数据。
当 Cookie 被传输到第三方的域名或者从第三方域名传输时会被浏览器认为是第三方 Cookie。所以,倘若现在访问 a.com 网站,且页面上某些资源是从 b.com 请求的,用户浏览器同 b.com 之间传输的Cookie 会被认为是第三方的。
对于第三方 Cookie 不同浏览器的限制不同,在 Chrome 和 Firefox 浏览器中,对于第三方 Cookie 的限制时最严格的,默认情况下请求是不会携带第三方 Cookie,对于这种场景需要进行额外的配置,因此在开发第三方 Javascript 程序时, 需高度留意隐私限制。
如何和宿主环境通信
在前端嵌入场景下,宿主环境与嵌入应用之间的通信通常是必要的,以确保数据同步、交互协调等,常见的通信方式包括 postMessage
、window.parent
访问、事件监听,具体方案需要依赖嵌入的实现方式。
如何定制化UI
在嵌入的场景中定制化Ui是非常常见的,一些客户可能希望嵌入网页的视觉风格需要与宿主网站的品牌形象保持一致,包括颜色、字体、图标等元素,此时就需要嵌入方给用户暴露出更多的接口。
技术选型
轻量第三方Javascript + iframe
方案介绍
- 第三方 Javascript 嗅探宿主环境、跨域获取用户配置、 综合判断合理生成参数加载 iframe渲染嵌入页面。**保留轻量的第三方Javascript能力有利于长期的功能扩展,**未来如需要支持动态调整配置等需求时, 依赖 SDK 部分代码完成支持。
- iframe天生具备全栈 sandbox(HTML, CSS, Javascript, 存储等), 承载嵌入页面, 与宿主环境隔离。
优势
- 接入非常简单,接入方使用没有任何心智负担。
- iframe 天生具备隔离能力,无论是js、css、dom,都完全隔离开来。
缺点
-
dom 严重割裂,弹窗只能在 iframe 内部展示,无法覆盖全局。
-
通信困难
iframe是独立的运行上下文,并且通常是以跨域的形式出现,与宿主通信困难体现在3点:
-
方式困难
仅可通过 postmessage 等方式,难以同步执行、直接调用。
-
数据结构困难
仅可传输Transferable Object。
-
效率低,内存限制大
传输数据(除sharedArrayBuffer), 均需要做structuredClone。
-
-
隐私限制
对于跨域的场景, iframe 中的代码因跨域无法获取到用户隐私信息(cookie, localstory, indexDB)等。极大限制了功能实现。iframe 也难以感知到宿主环境状态。
轻量第三方Javascript + wujie
wujie 是腾讯推出的微前端框架,通过集成 iframe 和 webcomponent 的优势,实现了一套沙箱机制,解决了纯 iframe 方案的在 dom 割裂和通信上的缺点,下面是对其原理的介绍:
优势
-
利用 iframe 和 webcomponent 来搭建天然的 js 隔离沙箱和 css 隔离沙箱
-
通过 webcomponent 天然解决了弹窗适配问题。
-
完备的通信机制
承载嵌入网页的 iframe 和宿主应用是同域的,所以宿主、嵌入网页天然就可以通过window.parent、事件监听等方式进行通信,wujie 内部已经提供了相应的方案。
缺点
-
模拟沙箱存在副作用
wujie 内部为了将 iframe 和 webcomponent 进行连接做了大量的代理工作,而很多属性的代理是矛盾,进而会导致应用的一些功能失效,官方做了详细的介绍。
构成
整体嵌入模块由两部分组成 SDK 和 嵌入网页,二者配合实现嵌入功能。
SDK
通过 Script 标签插入到宿主环境并最终运行在宿主环境中的代码。标准意义上的第三方Javascript代码。先于嵌入网页的代码执行。
主要职责:
- 嗅探宿主环境
- 跨域请求服务端获取对话应用关键参数
- 插入沙盒(取决于沙盒方案), 如果是 iframe 方案,则插入 iframe 标签并设置 url。
- (未来) 与宿主环境通信、交互。
嵌入网页
运行于沙盒环境中,是一个完整的web页面,整体依托沙盒与宿主环境隔离。
案例
- 百度千帆AppBuilder
- coze
关键技术点考量
技术考量点
在网站嵌入中除了要保证嵌入的网页能够正常渲染交互之外,还必须具备一些技术 Feature。
无感知升级
用户将第三方提供的嵌入代码植入到自由网站之后,需要享受嵌入网页的的功能升级和bug修复,即嵌入网页进行功能优化和bug修复之后,用户不需要重复的去更新嵌入代码。
不污染宿主环境
除了要能够不受宿主环境的影响。嵌入模块也需要做到不影响宿主环境。避免出现加载使用嵌入模块后, 宿主环境样式、代码逻辑、性能出现异常。
防剽窃
HTML 天生开源, 需要避免恶意用户复制走嵌入至宿主环境的代码,注入到其他任意站点, 实时剽窃。为了避免剽窃,得要求嵌入模块只能运行于 URL 特定的宿主环境中, 嵌入至其他域名的系统中无法使用。
针对性技术实现
SDK代码使用HTTP协商缓存
SDK 代码需要禁用 HTTP 强缓存(HTTP1.1 Cache-Control, HTTP 1.0 Expires), 使用协商缓存(Etag + If-None-Match),SDK 更新发布后, 用户运行时即可立刻使用最新发布版本。
SDK Zero Denpendency
由于 SDK 代码是在宿主环境执行的,所以执行 SDK 逻辑高度重视是否会污染宿主环境问题, 尤其需要降低对基础库的依赖, 避免关键依赖库与宿主环境冲突, 影响宿主环境正常运行。例如视图层没不使用 react/vue 而使用原始 DOM 操作。
嵌入网页HTTP接口增加CSP(Cotent-Security-Policy)
CSP(Content Security Policy,内容安全策略)是一种 Web 安全机制,其核心思想是限制浏览器可加载的资源,如 JavaScript、CSS、图片等,只有符合白名单的资源才能被执行,用于防止跨站脚本攻击(XSS)、点击劫持、数据注入等常见的 Web 安全问题。
为了启用 CSP,服务端需要在 HTTP 响应中添加 CSP 头,浏览器解析 CSP 指令,在加载页面资源时,浏览器会遵循策略进行资源的加载。
在 CSP 中提供了一条 frame-ancestors
,用于控制网页是否可以嵌入在其他网站的 iframe
标签中,其语法如下:
js
Content-Security-Policy: frame-ancestors <source> <source>;
可以是:
- 'none':禁止所有嵌入
- 'self':只允许同源嵌入
- 特定域名:如 example.com
- 通配符:如 https://*.example.com
比如下面这个例子:
js
Content-Security-Policy: frame-ancestors https://example.com
此策略仅允许在 example.com 域名的站中使用 iframe 展示嵌入网页,除此之外,还要考虑该属性的兼容性,整体来看浏览器的兼容性比较高,而且支持的版本也比较低。
但是当用户使用非正规浏览器时还是会存在安全隐患。
demo
总结
通过对第三方 JavaScript 的深入分析,可以看出其在网站嵌入中的重要性和挑战。无论是提升用户体验还是实现复杂的交互功能,第三方 JavaScript 已成为现代 Web 开发中的核心组件。然而,在引入这些脚本的过程中,也必须平衡性能、安全性与易用性,确保不影响宿主环境的稳定性与安全性。
同时针对定制化UI的场景,作者暂时还没太好的方案,欢迎评论区讨论。