- 开发工具
- 数据科学
【免费下载链接】livebook
Automate code & data workflows with interactive Elixir notebooks
Livebook 允许用户在 notebook 中运行自定义 JavaScript(即 JS View 机制),用于构建绘图、地图等交互式输出类型。由于脚本内容由用户编写、来源不受信任,Livebook 将其严格限制在 iframe 沙箱中执行:运行在 http 协议时由本机独立端口提供页面,运行在 https 协议时则由托管在 livebookusercontent.com 目录对应的livebook_space项目)安全提供。阅读本文,你将完整掌握该 iframe 沙箱的架构设计、跨源消息协议、主应用与托管应用的配置要点,以及独立应用的构建与部署方式。
为什么 Livebook 要在 iframe 中执行自定义 JavaScript
Livebook 的 JavaScript View 是"用自定义能力扩展 Livebook"的抽象层,也是定义交互式输出类型(如绘图、地图)的主要构建模块。其前端 Hook js_view.js 的注释明确说明:
JavaScript is defined by the user, so we sandbox the script execution inside an iframe.
也就是说,用户定义的脚本是不可信代码,必须与 Livebook 主页面进行隔离。iframe 是浏览器提供的原生隔离边界,配合sandbox属性与跨源加载,可以限制脚本对父页面 DOM、Cookie、存储等的访问能力。
在渲染层面,js_view_component.ex 通过phx-hook="JSView"挂载该 Hook,并向浏览器端传入ref、assets-base-path、js-path、session-token、connect-token、iframe-port、iframe-url等关键属性,作为后续建立 iframe 通信与资源加载的依据。
双通道服务模型:http 本机端口与 https 托管服务
iframe 页面的加载来源由浏览器当前协议决定,核心逻辑位于 js_view/iframe.js:
function getIframeUrl(iframePort, iframeUrl) { const protocol = window.location.protocol; if (iframeUrl) { return iframeUrl.replace(/^https?:/, protocol); } return protocol === "https:" ? "https://livebookusercontent.com/iframe/v5.html" : `http://${window.location.hostname}:${iframePort}/iframe/v5.html`; }- http 模式:Livebook 主应用在本机启动一个独立端口的静态服务(
iframe_port),以http://<hostname>:<port>/iframe/v5.html提供页面; - https 模式:页面从
https://livebookusercontent.com/iframe/v5.html加载,该域名正是运行本仓库 iframe 目录中livebook_space应用的服务; - 自定义覆盖:若配置了
iframe-url(对应LIVEBOOK_IFRAME_URL),则以其为基础,仅将协议部分替换为当前页面协议,保持 http/https 一致性。
为什么 http 模式不能直接使用外部托管
iframe.js 的文件头注释给出了详细的设计考量:
srcdoc与data:URL 不可用:这两种方式会禁用 Cookie 及摄像头、麦克风等浏览器 API,无法满足交互式输出的需求;- 必须使用跨源加载:iframe 需要
allow-scripts,若同时来自同一源并使用allow-same-origin,脚本就能直接操纵父页面,存在严重安全风险; - 协议必须一致:https 页面中加载 http 源 iframe 会被浏览器拦截(混合内容),因此 https 模式必须使用另一个 https 源(livebookusercontent.com);
- 安全上下文限制:外部 http 内容不属于安全上下文,无法访问用户媒体设备。因此 http 模式不采用
http://livebookusercontent.com,而是使用与 Livebook 主应用不同端口的本地端点——不同的端口意味着不同的源,既满足跨源隔离,又保留本地可访问性。
内容完整性校验
由于 iframe 页面承载了与父页面通信的协议代码,Livebook 会在加载前手动校验其 SHA-256 摘要,防止中间人篡改。校验常量与逻辑见 iframe.js:
const IFRAME_SHA256 = "wcqj5QWCo66osdAWDnEgPRFyL7nfe8oNqNggnw4vvW8="; function verifyIframeSource(iframeUrl) { if (!iframeVerificationPromise) { iframeVerificationPromise = fetch(iframeUrl) .then((response) => response.text()) .then((html) => { if (sha256Base64(html) !== IFRAME_SHA256) { throw new Error( `The iframe loaded from ${iframeUrl} doesn't have the expected checksum ${IFRAME_SHA256}` ); } }); } return iframeVerificationPromise; }校验通过后,iframe 才被赋予沙箱属性与权限列表并设置src:
iframe.sandbox = "allow-scripts allow-same-origin allow-downloads allow-forms allow-modals allow-popups allow-top-navigation"; iframe.allow = "accelerometer; ambient-light-sensor; camera; display-capture; encrypted-media; fullscreen; geolocation; gyroscope; microphone; midi; usb; xr-spatial-tracking; clipboard-read; clipboard-write; bluetooth; serial; local-network-access";allow权限列表按需开放了摄像头、麦克风、全屏、地理位置等能力,这正是交互式输出(如音频输入、图像输出)所依赖的。
iframe 静态服务应用(livebook_space)剖析
iframe 目录是一个独立的 Mix 项目(应用名livebook_space),仅用于在 https 场景下托管 iframe 静态页面。其依赖极简,见 mix.exs:唯一的运行时依赖是bandit ~> 1.0。
应用启动:Bandit 监听 4000 端口
application.ex 中通过Bandit启动 HTTP 服务器:
children = [ {Bandit, scheme: :http, plug: LivebookSpaceWeb.Plug, port: 4000} ]路由与响应头:Plug 管道
plug.ex 使用Plug.Builder定义了完整处理链:
plug Plug.Static, from: {:livebook_space, "priv/static/iframe"}, at: "/iframe", cache_control_for_etags: "public, max-age=31536000", headers: [ {"access-control-allow-origin", "*"}, {"content-type", "text/html; charset=utf-8"} ] plug Plug.Static, from: :livebook_space, at: "/"关键点:
- 静态资源从
priv/static/iframe目录读取,挂载在/iframe路径下; - 缓存策略
max-age=31536000(一年):注释说明"iframes are versioned, so we cache them for long"——iframe 页面按版本号(v1~v5)组织,URL 变更即代表内容变更,因此可以放心长期缓存; - CORS 头
access-control-allow-origin: *:允许 Livebook 主页面跨源 fetch 该页面内容并校验其完整性; - 显式指定
content-type: text/html; charset=utf-8; - 根路径请求回退到
index.html,其余未匹配请求返回 404。
版本化的页面文件位于 iframe/priv/static/iframe 目录下(v1.html至v5.html),当前 Livebook 前端加载的是v5.html(见上述getIframeUrl)。
主应用侧的 iframe 端点与配置
当 Livebook 运行在 http 模式时,iframe 页面由主应用自身提供。iframe_endpoint.ex 与livebook_space的 Plug 配置几乎一致:同样挂载/iframe、启用gzip: true、一年缓存与 CORS 头,静态目录为priv_path()/static/iframe。
该端点的启动由 application.ex 的iframe_server_specs/0负责:在 Web 端点启用(server?)时,用 Bandit 在iframe_port端口启动一个独立的LivebookWeb.IframeEndpoint服务,并继承主 HTTP 端点的:ip配置。若端口被占用(如桌面应用场景),则自动回退为随机端口,见 iframe_endpoint_start。
各环境的默认端口
iframe_port在不同环境下的默认值(见 config/dev.exs、config/prod.exs、config/test.exs):
| 环境 | 默认 iframe 端口 |
|---|---|
| dev | 4001 |
| prod | 8081 |
| test | 4003 |
该值的读取逻辑位于 config.ex(iframe_port/0),并支持通过环境变量覆盖:LIVEBOOK_IFRAME_PORT与LIVEBOOK_IFRAME_URL的解析见 livebook.ex 与 config.ex。
相关环境变量一览
| 环境变量 | 作用 |
|---|---|
LIVEBOOK_IFRAME_PORT | 覆盖 iframe 服务端口(http 模式下使用) |
LIVEBOOK_IFRAME_URL | 覆盖 iframe 页面加载地址(https 模式下常用) |
LIVEBOOK_WITHIN_IFRAME | 标记 Livebook 自身被嵌入到外部 iframe 中(true时启用SameSite=None; Secure会话 Cookie,见 endpoint.ex) |
iframe 页面与父页面的消息协议(v5.html)
iframe/priv/static/iframe/v5.html 是 iframe 侧协议实现的完整载体,全部通过window.parent.postMessage与父页面通信,分为握手、事件、同步与工具四个部分。
握手与模块加载
- iframe 加载完成后立即发送
{ type: "ready" }(见 v5.html); - 父页面回复
readyReply,携带三项数据:token:本次通信的随机令牌;baseUrl:资源基地址,父页面随后动态创建<base>元素,使 iframe 内相对路径正确解析;jsPath:视图专属 JS 模块的相对路径,iframe 用绝对地址import(\${baseUrl}${jsPath}`)动态加载(因为` 的修改不影响已开始的 import 调用);
- 父页面随后发送
init消息,iframe 在 import 完成后调用模块的init(ctx, data)函数;若模块未导出init,则抛出明确错误,并在 iframe 内渲染错误信息(v5.html)。
暴露给用户脚本的 ctx API
v5.html 构建的ctx对象(v5.html)是用户 JS 模块唯一可用的接口:
| API | 说明 |
|---|---|
ctx.root | iframe 内的根 DOM 容器 |
ctx.handleEvent(event, callback) | 注册来自服务端的事件处理器,同一事件仅允许注册一次;待处理事件先入队,处理器就绪后按序消费 |
ctx.pushEvent(event, payload) | 向服务端推送事件(携带令牌,{ type: "event", event, payload }) |
ctx.importCSS(url)/ctx.importJS(url) | 动态加载外部 CSS 与 JS 资源 |
ctx.handleSync(callback) | 注册同步回调,用于服务端发起 ping 前让 iframe 先把延迟的 UI 变更推送到服务端 |
ctx.selectSecret(callback, preselectName, options) | 请求父页面让用户选择 Secret,选中后通过secretSelected消息回调 |
ctx.setSmartCellEditorIntellisenseNode(node, cookie) | 为智能单元格编辑器设置智能感知节点 |
高度自适应与 DOM 事件转发
- 高度同步:
onReady阶段用ResizeObserver监听document.body,任何高度变化都以{ type: "resize", height }上报父页面,父页面据此调整占位元素与 iframe 尺寸(v5.html); - 事件转发:iframe 将
mousedown、focus、keydown等 DOM 事件序列化后转发,并标注isTargetEditable(目标是否为input、textarea或contenteditable元素),父页面据此决定事件落点(v5.html)。
消息方向汇总
| 消息类型 | 方向 | 作用 |
|---|---|---|
ready | iframe → 父 | 告知页面就绪 |
readyReply | 父 → iframe | 下发 token、baseUrl、jsPath |
init | 父 → iframe | 传入初始化数据并触发模块init |
event | 双向 | 用户事件推送 / 服务端事件下发 |
resize | iframe → 父 | 高度变更上报 |
domEvent | iframe → 父 | DOM 事件转发 |
sync/syncReply | 父 ↔ iframe | 同步握手 |
selectSecret/secretSelected | 父 ↔ iframe | Secret 选择流程 |
父页面 JSView Hook:加载、校验与通信
主应用侧的核心实现在 js_view.js,其职责包括:
- 懒加载:通过
waitUntilInViewport观察占位元素,只有当输出进入视口时才真正加载 iframe(js_view.js); - iframe 位置同步:
createIframe把 iframe 提升到 notebook 根元素附近(避免单元格重排导致 iframe 反复重载),再用ResizeObserver与占位元素做绝对定位对齐(js_view.js); - 令牌鉴权:父页面生成
childToken,iframe 发来的每条消息都必须携带匹配令牌,否则抛出Token mismatch;同时代码注释也诚实地指出这是尽力而为的防护——脚本若在极端情况下拿到令牌仍可能发送消息,但其中最"危险"的动作仅限于快捷键转发(js_view.js); - 服务端通道:通过 Phoenix Channel
js_view与 Livebook 会话通信,令牌经 js_view_component.ex 以Phoenix.Token.sign/3签发;init:<ref>、event:<ref>、error:<ref>、pong:<ref>等消息的编解码见 channel.js,其中二进制负载使用带注解的 ArrayBuffer 编码以支持高效传输; - 资产 CDN 回退:
getAssetsBaseUrl先探测 Livebook 公共端点/public/health是否可无认证访问,若不可访问且存在assets-cdn-url,则回退到 CDN 地址,解决认证代理环境下 iframe 内跨源加载资源的问题(js_view.js)。
安全设计要点汇总
综合上述实现,Livebook 的 iframe 沙箱构成四层防护:
- 跨源隔离:http 模式使用不同端口、https 模式使用不同域名的独立源,规避
allow-same-origin+allow-scripts同源组合的风险; - 内容完整性:加载前对
v5.html做 SHA-256 校验,防止托管端内容被篡改; - 通信令牌:每次会话生成随机
childToken,父页面校验所有子消息来源; - 最小权限:
sandbox属性限定脚本行为,allow属性按需开放硬件与系统权限;配合一年期缓存与全开放 CORS 头,在性能(版本化 URL 长期缓存)与安全(校验+跨源)之间取得平衡。
构建与部署 livebook_space
独立 iframe 托管应用同样提供完整的生产化部署方案:
- Docker 镜像:Dockerfile 采用两阶段构建:第一阶段基于
hexpm/elixir:1.13.2-erlang-24.1.7-alpine-3.15.0编译并产出 release(要求 Elixir ~> 1.13,与 mix.exs 一致),第二阶段基于alpine:3.15.0仅保留运行产物与openssl、ncurses-libs、libstdc++运行时依赖,最终以/app/bin/livebook_space start启动; - Fly.io 部署:fly.toml 定义应用
livebook-space,内部端口 4000,对外暴露 80(http)与 443(tls+http),并配置 TCP 健康检查(grace_period = "30s"、interval = "15s"、restart_limit = 6、timeout = "2s")。
本地调试时,可进入iframe目录执行mix deps.get && mix run --no-halt启动该服务,再配合 Livebook 主应用的环境变量(如 https 模式下设置LIVEBOOK_IFRAME_URL)进行联调。
小结
Livebook 的 iframe 沙箱并非简单的<iframe>拼装,而是一套精心设计的跨源托管体系:独立端口/独立域名保证源隔离,版本化静态页面配合一年缓存与 SHA-256 校验保证内容安全与可验证,ready/readyReply握手配合随机令牌建立可信的双向 postMessage 通道,最终以init为界把用户自定义 JS 模块安全地接入选定输出区域。理解 iframe 目录所代表的livebook_space服务与主应用侧 js_view.js、iframe_endpoint.ex 的协作关系,是深入定制 Livebook 交互式输出与排查 iframe 加载问题的关键。
- 开发工具
- 数据科学
【免费下载链接】livebook
Automate code & data workflows with interactive Elixir notebooks
相关推荐
无界微前端JavaScript沙箱:iframe原生隔离的运行机制详解
无界微前端JavaScript沙箱:iframe原生隔离的运行机制详解 无界微前端框架作为极致的微前端解决方案,其核心优势在于基于WebComponent容器和
前端Qiankun沙箱机制:实现完美的JavaScript隔离
Qiankun沙箱机制:实现完美的JavaScript隔离 Qiankun的JavaScript沙箱机制是其微前端架构中最核心的技术,通过Proxy API实现
微前端前端框架Appsmith ChartWidget 消息契约解读:CUSTOM_ECHART 沙箱 iframe 与主 widget 的四类 postMessage 协议
Appsmith ChartWidget 消息契约解读:CUSTOM_ECHART 沙箱 iframe 与主 widget 的四类 postMessage 协议
低代码前端后端企业应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考