Cal.com 开源仓库 embed-core 深入指南:用原生 JavaScript 在任意网页嵌入 Cal Link
【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy
Cal.com 的embed-core是仓库中负责「把预约链接(Cal Link)嵌入到任意第三方网页」的纯 JavaScript 核心模块,它以父页面脚本(embed.js)+ iframe 内脚本(embed-iframe.js)的双脚本架构工作,不依赖 React 等任何框架。读完本文,你将掌握 embed-core 的安装 snippet、指令队列与命名空间机制、inline / modal / floating button 三种嵌入形态、本地开发与 Playwright 测试流程、生产构建产物,以及让页面与 iframe 高度自适应兼容的具体做法。
embed-core 是什么:一个包,两个脚本,一条消息通道
embed-core 位于 packages/embeds/embed-core,包名为@calcom/embed-core(当前版本 1.5.3,见 package.json),自述定位为「embed Cal Link 的 vanilla JS 核心脚本」。
从源码结构看,它实际上分作两个运行在不同上下文里的脚本:
- 父页面侧入口src/embed.ts:负责在嵌入方网页中创建 iframe、解析指令队列、维护命名空间,并通过
postMessage与 iframe 通信; - iframe 内入口src/embed-iframe.ts:运行在 Cal.com 预约页 iframe 内部,负责把预约页「嵌入化」——告诉父页面 iframe 的渲染进度、上报高度变化、响应父页面的 UI 配置指令。
两者之间通过SdkActionManager(见 src/sdk-action-manager.ts)收发命名事件,例如__iframeReady(iframe 加载就绪)、__dimensionChanged(iframe 内容高度变化,用于撑高父页面)、__routeChanged(预约流程路由切换)、linkReady/linkFailed(预约链接加载成功 / 失败)、__closeIframe(请求关闭模态框)等。事件名以__开头的是 embed 内部事件,不带前缀的则对开发者开放,可通过cal("on", {...})订阅。
embed-core对外暴露的类型与入口统一收口在 index.ts:
export * from "./src/sdk-event"; export * from "./src/embed";其中sdk-event负责以正确的命名空间实例化 SDK 事件管理器,embed则导出全局Cal函数及其全部指令类型。
在任何网页上使用:snippet 与全局 Cal 对象
embed-core 的设计目标是「无论你用什么框架,都能嵌入」。官方使用说明见仓库文档体系(packages/embeds/embed-core/index.html 中内嵌的完整 snippet 即为可直接复制的标准安装代码,原文 README 同时指向开发者文档的 JavaScript 安装指南)。
其核心是一个带指令队列的全局函数Cal,工作机制如下(节选自 index.html 的 snippet):
((C, A, L) => { const p = (a, ar) => { a.q.push(ar); // 指令入队 }; const d = C.document; C.Cal = C.Cal || function () { const cal = C.Cal; const ar = arguments; if (!cal.loaded) { // 第一次调用时才开始加载 embed.js cal.ns = {}; cal.q = cal.q || []; d.head.appendChild(d.createElement("script")).src = A; cal.loaded = true; } if (ar[0] === L) { // "init" 指令:注册(或复用)一个命名空间 const api = function () { p(api, arguments); }; const namespace = ar[1]; api.q = api.q || []; if (typeof namespace === "string") { cal.ns[namespace] = cal.ns[namespace] || api; p(cal.ns[namespace], ar); p(cal, ["initNamespace", namespace]); } else p(cal, ar); return; } p(cal, ar); }; })(window, window.calOrigin + "/embed/embed.js", "init");这套机制的要点:
- 延迟加载:
Cal第一次被调用时才往<head>注入<script src=".../embed/embed.js">,并用cal.loaded防止重复加载; - 队列缓冲:embed.js 尚未就绪时,所有调用先进入
cal.q队列;embed.js 加载完成后,通过 src/embed.ts 中new Cal(DEFAULT_NAMESPACE, globalCal.q)及processQueue逐个消费队列,并重写队列的 push 方法,使后续调用立即执行而非再次入队; - 指令即调用:
Cal("inline", {...})、Cal("modal", {...})这类调用被视作一条条「指令」,由CalApi.processInstruction分发到对应方法执行(不认识的指令只打日志、不中断队列)。
指令总览与关键参数
从CalApi类的实现(src/embed.ts)可以整理出完整指令集:
| 指令 | 作用 | 关键参数 |
|---|---|---|
init | 初始化(可按命名空间) | calOrigin、debug、uiDebug、origin |
initNamespace | 显式初始化非默认命名空间 | namespace |
inline | 内联嵌入,追加为容器元素最后一个子节点 | calLink、elementOrSelector、config |
floatingButton | 悬浮按钮,点击打开模态框 | calLink、buttonText(默认Book my Cal)、buttonPosition(bottom-left/bottom-right)、buttonColor、buttonTextColor、attributes、calOrigin、config |
modal | 模态框嵌入 | calLink、config、calOrigin、__prerender、prerenderOptions |
preload | 预加载资源或预渲染 iframe | calLink、type、options、pageType、calOrigin |
prerender | 预渲染 headless router 路径 | 同preload,另支持options.slotsStaleTimeMs、options.iframeForceReloadThresholdMs |
ui | 运行时下发 UI 配置 | theme、styles、layout、cssVarsPerTheme、colorScheme等 |
on/off | 订阅 / 退订事件 | action、callback |
closeModal | 编程式关闭模态框 | 无(仅模态类嵌入可用,inline 下会抛错) |
其中modal与preload的实现细节值得一提:modal会为模态框分配唯一uid,并根据上一次渲染状态通过getNextActionForModal决定fullReload/connect/connect-no-slots-fetch/noAction四种动作(src/embed.ts);preload的prerenderIframe未显式设置时,只要传入type就默认预渲染 iframe,且不传 type 却要求预渲染 iframe 会直接抛错(src/embed.ts)。
免写代码:data-* 属性
embed-core 在 src/embed.ts 全局监听了document的 click 事件:任何带有data-cal-link属性的元素被点击(或其子元素被点击)时,都会自动触发modal指令。常用属性:
data-cal-link:要打开的 Cal Link 路径(必填);data-cal-namespace:使用的命名空间,缺省为默认命名空间;data-cal-config:JSON 字符串形式的配置对象(如{"theme":"dark", "flag.coep":"true"});data-cal-origin:可覆盖的 Cal 源地址。
data-cal-config中可用的预填与 UI 键定义在 src/types.ts 的KnownConfig中,包括theme(dark/light/auto)、layout(month_view/week_view/column_view)、"ui.color-scheme"、"ui.autoscroll"、"flag.coep"(跨源嵌入策略开关)、"cal.embed.pageType"(直接定位到预约页的指定步骤,如team.event.booking.slots)、"cal.embed.noSlotsFetchOnConnect"、useSlotsViewOnSmallScreen等。
命名空间(Namespace):一页多嵌入的关键
embed-core 支持在同一页面运行多个互不干扰的嵌入实例。Cal("init", "namespaceName", {...})注册命名空间,随后Cal.ns.namespaceName("inline"|"modal"|...)在该命名空间下执行指令;initNamespace指令则保证即使在 embed.js 尚未加载时就注册了新命名空间,其队列也会在加载后被正确实例化(src/embed.ts)。每个命名空间拥有独立的SdkActionManager,iframe 上也会通过iframe.name = "cal-embed=<namespace>"与 URL 上的embed查询参数标记归属。
开发与调试:一行命令起服务
README 的开发流程非常简洁:在 embed-core 目录执行
yarn dev它会先编译 Tailwind,再并行启动「vite 开发构建(watch 模式)」与「3100 端口静态服务器」,并自动打开浏览器进入http://localhost:3100/embed/。这个页面就是官方的Embed Playground(源码即 index.html),页面内集中了 inline 嵌入、模态框、悬浮按钮、主题切换、团队链接、路由表单、预渲染/预加载、页面参数转发、骨架屏等大量可交互测试用例。
针对headless router 预渲染场景,还单独提供了一张测试页http://localhost:3100/embed/routing-playground.html,用于验证prerender指令对router?form=...这类路径的预渲染行为。
开发相关的补充事实:
- 开发构建由 vite.config.js 驱动,
base设为/embed/,产物输出到仓库 apps/web/public/embed; - 环境变量在构建时通过
vite-plugin-environment注入,包括EMBED_PUBLIC_EMBED_FINGER_PRINT(commit 短哈希)、EMBED_PUBLIC_EMBED_VERSION(包版本)、EMBED_PUBLIC_WEBAPP_URL、EMBED_PUBLIC_VERCEL_URL、EMBED_PUBLIC_EMBED_LIB_URL、NEXT_PUBLIC_IS_E2E(见 package.json 的 build 脚本); - 若你的网站启用了 HTTPS,可用
yarn dev-https(会通过@vitejs/plugin-basic-ssl生成本地自签名证书);不想自动打开浏览器可用yarn dev-no-open; - 已记录的 DX 限制:Hot reload 对 CSS 文件不生效(因为 CSS 以字符串形式注入 Shadow DOM,见 vite.config.js 的注释与 README 的 DX 一节)。
运行测试:双服务 + Playwright
embed-core 的端到端测试基于 Playwright,目录位于 packages/embeds/embed-core/playwright。README 要求两个前置条件:
- 主应用(Cal.com web App)运行在 3000 端口(例如通过
yarn dx启动),因为测试要在 iframe 中真实完成预约流程; - embed-core 开发服务器运行在 3100 端口:
yarn dev然后在另一个终端执行:
yarn embed-tests-quickembed-tests-quick会以QUICK=true运行playwright test(见 package.json)。测试用例覆盖了 inline 嵌入(含暗色主题、COEP/CORP 请求头校验、iframe 不劫持滚动等,见 playwright/tests/inline.e2e.ts)、模态框、命名空间、预览页、action 事件、两步选时槽等场景(playwright/tests 下共 6 个 e2e 文件)。
README 还特别提示了一个行为边界:getEmbedIframe与addEmbedListeners目前只支持「全新加载」场景下的嵌入——打开一个嵌入、关闭后再打开另一个嵌入暂不支持。这一点在写自定义测试或复杂交互页面时需要留意。
发布到生产:构建 embed.js 产物
生产构建命令为:
yarn build其完整流程(见 package.json 的build与__build脚本)是:清空dist→ 以 git commit 短哈希与包版本号注入环境变量 → Tailwind 编译 →vite build→tsc --emitDeclarationOnly输出类型声明 → 把apps/web/public/embed下的资源复制进dist。
构建产物dist/embed/embed.js(README 中写作dist/embed.umd.js,指同一产物,package.json 的main字段为./dist/embed/embed.js)需要部署为一个可直接访问的静态 URL,README 给出的官方托管地址是http://cal.com/embed.js。snippet 中的window.calOrigin + "/embed/embed.js"即指向该地址,自托管时替换为自己的域名即可。
值得注意的打包细节:由于 Vite 单配置同时打包「库(embed.ts)」和「应用(preview.html)」,无法直接使用 UMD/IIFE 格式,因此构建脚本在生成 bundle 后手动用!function(){...}()包裹代码形成 IIFE 风格,避免变量泄漏到全局(vite.config.js)。
让任意页面兼容 Embed:mainclass 的约定
README 专门给出了「把页面改造成兼容 Embed」的最小要求——在承载页面全部内容的元素上定义mainclass,且该元素不要有自动外边距(auto margins):
- 加了
mainclass 后,iframe 高度会依据它自动调整:只要设备尺寸允许,main内的内容无需滚动即可完整可见。这对应源码中的__dimensionChanged事件——iframe 内页面渲染后,embed-iframe侧通过keepParentInformedAboutDimensionChanges(src/embed-iframe/lib/utils.ts)上报内容高度,父页面 src/embed.ts 据此设置iframe.style.height(模态框场景还会附加maxHeight); main区域之外也是用户点击时模态框关闭的边界。
此外,src/types.ts 中声明的UiConfig展示了 iframe 页面可接受的运行时 UI 配置面:hideEventTypeDetails、theme、styles(仅允许少量受控样式键,如eventTypeListItem、enabledDateButton、disabledDateButton的背景/文字色)、cssVarsPerTheme(按明暗主题分别覆盖 CSS 变量)、layout、colorScheme、disableAutoScroll、useSlotsViewOnSmallScreen。这套「白名单式」样式控制正是 README「Pending Documentation」里反复强调的设计取向:刻意不提供完全自由的 CSS 注入,以便后续修改 HTML 时能评估对嵌入样式的影响。
已知问题与演进方向(来自 README 的工程实录)
README 末尾坦诚列出了未完成事项,对二次开发者和集成方都有参考价值:
- 兼容性:部分浏览器未支持与优雅降级待补;iframe 刷新后无法重放已下发的指令;
- 可访问性与 UI/UX:模态框 loader 应允许用户自选;直链到某个事件时是否允许返回事件列表页待定;目前主题色仅针对浅色主题设计;团队链接的透明背景支持不完整;inline 模式默认设置了
border-radius,未来可能需要可配置; - 品牌:
Powered by Cal.diy与Try it for free是否只对免费账号展示、品牌区放哪里,均未定; - API:loader 颜色目前只能通过 CSS 定制,计划支持 UI 指令;
- 自动化测试:跑在 CI 中;Booking 页快照含当前月份,需要每月重新生成;
- 打包:CSS 中的注释未被剔除;
- 可调试性:希望 iframe 日志回传父页面形成单一时间线,并支持通过
on指令观察系统状态;embed.js 需要错误追踪; - 发布兼容性:embed-iframe.js 与 embed.js 版本不同步可能导致线上短暂不兼容,README 讨论了「版本化 + iframe 按版本加载」的理想方案与「embed.js 与应用同源部署」的快速缓解方案;
- Shadow DOM:当前处于 open 状态,意味着网站样式可能影响 loader;
- React 组件:计划支持带自动预加载的
onClick。
结语
embed-core 用一套精巧的「全局函数 + 指令队列 + 命名空间 + postMessage 事件总线」实现了与框架无关的预约嵌入能力。无论是想在自己的站点快速接入 Cal Link,还是希望深入理解 iframe 双脚本通信、预渲染、mainclass 高度自适应这类工程细节,packages/embeds/embed-core 的源码、index.html Playground 和 playwright/tests 测试都是最直接的一手资料——先用yarn dev打开 Playground 动手体验,再按本文梳理的指令、事件与构建流程逐项验证,即可快速上手甚至参与改进。
【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考