cal.diy Embed Snippet 解析:用一段 Vanilla JS 拉起 Cal.com 嵌入预约全流程
【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy
导读
packages/embeds/embed-snippet是 cal.diy(Cal.com 开源调度基础设施)中体积最小、却承担"临门一脚"职责的模块:它是一段不依赖任何框架的纯 JavaScript 启动代码,负责在宿主页面中按需加载@calcom/embed-core,从而把 Cal Link(预约链接)以内嵌(inline)、弹窗(modal)或悬浮按钮(floating button)的形式渲染到任意网页。读完本文,你将掌握该 Snippet 的构建产物与两种注入方式、其"先入队后执行"的懒加载队列机制、命名空间(namespace)多实例原理,以及构建时通过环境变量改写embed.js地址的发布手法,并能在自己的站点上直接落地使用。
一、定位:整个 Embed 体系的第一块积木
在 cal.diy 的嵌入方案中,依赖关系是单向的:
@calcom/embed-snippet:纯 Vanilla JS,唯一职责是获取并执行@calcom/embed-core,从而在页面上展示 Cal Link 的嵌入效果;@calcom/embed-core:真正的"内核",负责创建 iframe、管理指令队列、处理命名空间与跨域消息(postMessage)、渲染 inline/modal/floating 三种形态;@calcom/embed-react:React 封装,底层同样依赖 embed-snippet(见 embed-snippet/package.json 中唯一的运行时依赖"@calcom/embed-core": "workspace:*")。
也就是说,无论你是用原生<script>还是 React 组件接入,最终都要通过这段 Snippet 完成"引导(bootstrap)"动作。README 中给出的模块职责定义("Vanilla JS embed snippet that is responsible to fetch @calcom/embed-core and thus show Cal Link as an embed on a page")精确对应了src/index.ts中的实现——它不做渲染,只负责"把内核拉进来并把指令送达"。
二、工作原理:为什么页面加载很快?——队列式懒加载
2.1 内核是异步注入的
Snippet 的核心逻辑全部集中在 src/index.ts 的EmbedSnippet()中。源码注释点明了设计初衷:
"As we want to keep control on the size of this snippet but we want some portion of it to be still readable."
即在控制体积与保持可读之间取平衡,所以作者刻意把这段代码写得短小而直白。其工作流程可以拆解为三步:
- 首次调用时注入脚本:检查
window.Cal.loaded标志,若内核尚未加载,则执行d.head.appendChild(d.createElement("script")).src = A; // A 即 embed.js 地址 cal.loaded = true;loaded标志保证同一页面不会重复下载 embed-core,这也正是embed-core/index.html中doubleInstallSnippet...测试场景反复执行 snippet 仍能保持幂等的依据。 - 把后续指令压入队列:内核脚本是异步加载的,在它执行完成前,所有用户调用(
Cal("init", ...)、Cal("inline", {...})等)都不会丢失,而是通过a.q.push(ar)追加到全局队列window.Cal.q中。 - 内核接管后消费队列:embed-core 加载完成后会读取并清空该队列,见 embed-core/src/embed.ts 的
processQueue()实现,随后将queue.push重定向为"立即执行",保证此后指令实时生效。
这就是"先入队、后执行"的经典懒加载引导模式:无论用户何时点击、调用多少次 API,指令都不会因为网络延迟而丢失。
2.2 全局 API 形态
加载后,页面全局会出现window.Cal函数(类型定义见 embed-core/src/embed.ts 中的GlobalCal/GlobalCalWithoutNs):
interface GlobalCalWithoutNs { (methodName: string, ...args: unknown[]): void; loaded?: boolean; // 内核是否已加载,避免重复下载 q: Queue; // 内核加载前的指令队列 ns: Record<string, GlobalCalWithoutNs>; // 已注册的命名空间 instance?: Cal; // 具体实例 }Snippet 源码中的let cal = C.Cal; if (!cal.loaded) {...}正是围绕这三个关键字段做初始化。
三、开发与构建:一条命令产出三种产物
3.1 构建命令与产物
README 的 Development 章节指出:yarn build会生成dist/snippet.es.js;如果要在 React 嵌入中测试,必须先构建本包,以便@calcom/embed-react拿到最新版 Snippet。
构建流程定义在 embed-snippet/package.json:
"build": "npx rimraf dist && vite build && tsc --emitDeclarationOnly --declarationDir dist"由 vite.config.js 的库模式配置可知,Vite 会以src/index.ts为入口,按 format 产出两类 JS 产物:
| 产物文件 | 格式 | 对应 package.json 字段 | 典型用途 |
|---|---|---|---|
dist/snippet.es.js | ESM | module | <script type="module" src=...>或 React 包引用 |
dist/snippet.umd.js | UMD | main | 传统<script>标签直接引用 |
dist/index.d.ts | 类型声明 | types | TypeScript 项目中的类型提示 |
这也是 README 中"which can be used as<script type="module" src=..."的落地出处。
3.2 发布前的环境注入
prepack脚本(package.json)揭示了一个关键发布细节:
"withEmbedPublishEnv": "NEXT_PUBLIC_EMBED_LIB_URL='https://app.cal.com/embed/embed.js' NEXT_PUBLIC_WEBAPP_URL='https://app.cal.com' yarn", "prepack": "yarn lint --filter='@calcom/embed-snippet' && yarn withEmbedPublishEnv build"打包发布时会强制注入线上环境变量,把 Snippet 默认加载的内核地址改写为https://app.cal.com/embed/embed.js。对应源码中的读取逻辑(src/index.ts):
const WEBAPP_URL = import.meta.env.EMBED_PUBLIC_WEBAPP_URL || `https://${import.meta.env.EMBED_PUBLIC_VERCEL_URL}`; const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_URL}/embed/embed.js`;可见环境变量的优先级为:显式指定EMBED_PUBLIC_EMBED_LIB_URL>WEBAPP_URL推导。源码第 72 行也留下了对使用者的直接建议:"Replace it withhttps://cal.com/embed.jsor the URL where you have embed.js installed"——即自托管部署时,应把内核地址指向自己的部署实例。
四、两种接入方式:模块引入 or 复制即用
4.1 方式一:ES Module 引入
构建完成后,以标准模块方式引入(README 中script type="module"的用法):
<script type="module"> import EmbedSnippet from "/dist/snippet.es.js"; EmbedSnippet(); window.Cal("init", { theme: "dark" }); window.Cal("inline", { elementOrSelector: "#my-cal", calLink: "pro/30min", }); </script>EmbedSnippet(url = EMBED_LIB_URL)接受可选参数url,允许你自定义 embed-core 的加载地址(src/index.ts);它同时导出了EmbedSnippetString(EmbedSnippet.toString()),即源码字符串本身,供需要"把代码内联进宿主页面"的场景使用。
4.2 方式二:复制代码直接内联
README 提到的第二种用法——"You can also copy the appropriate portion of the code and install it directly as<script>CODE_SUGGESTED_TO_BE_COPIED</script>"——适合不想引入任何构建工具的静态站点。核心步骤:
- 打开构建产物
dist/snippet.es.js,复制其中/*! Copying ends here. */标记内的那段引导代码(即 IIFE 部分); - 将其直接写入宿主页面:
<script> (function (C, A, L) { /* ... 复制的 snippet 代码 ... */ })(window, "https://你的域名/embed/embed.js", "init"); </script> - 之后即可通过
Cal("init", {...})、Cal("inline", {...})、Cal("modal", {...})等方式控制嵌入。
值得注意:由于 Snippet 本身刻意保持"短小可读",它才能被安全地复制粘贴进第三方页面、甚至 WordPress 插件中(见下文第五节的同步维护清单)。
五、命名空间(Namespace):同页多实例的关键机制
5.1 Snippet 侧的分流逻辑
同一个页面可能同时存在多个互不干扰的嵌入(例如一个团队页、一个个人页、一个暗色主题弹窗)。Snippet 用"命名空间"实现隔离,分流逻辑位于 src/index.ts:
- 当第一个参数为
"init"(源码常量L)且第二个参数是字符串(命名空间名)时,会为该命名空间创建一个独立的api函数并挂到cal.ns[namespace],同时向默认队列投递一条initNamespace指令; - 注释强调"even after re-execution of the snippet, the namespace is not overridden"(
cal.ns[namespace] = cal.ns[namespace] || api),确保重复执行 snippet 也不会覆盖已注册的命名空间; - 非 init 指令(如
inline、modal、ui)则直接入队。
5.2 embed-core 侧的实例化
对应地,embed-core 在 embed.ts 中实现了init与initNamespace:
// 默认命名空间为 ""(DEFAULT_NAMESPACE,见 embed.ts#L1552) init(namespaceOrConfig) { // 若 init 指令属于其他命名空间则忽略 if (initForNamespace !== this.cal.namespace) return; } initNamespace(namespace: string) { // 创建实例即自动开始消费该命名空间队列 globalCal.ns[namespace].instance = globalCal.ns[namespace].instance || new Cal(namespace, globalCal.ns[namespace].q); }initNamespace指令允许"默认队列"代为实例化非默认命名空间队列;而 embed.ts 的初始化循环则兼容旧版 Snippet——对不使用initNamespace指令的旧引导代码,也会在 embed-core 加载后统一补齐实例化(注释明确这是幂等操作)。一个命名空间对应一个 iframe 与一套独立配置,embed-core/index.html中"Two different namespace with two different init config"的测试场景正是对该能力的验证。
5.3 无 JavaScript 的降级路径:data-* 属性
若不想写任何 JS,embed-core 还提供声明式接入:在任意元素上设置data-cal-link、data-cal-namespace、data-cal-config属性,点击时由全局点击监听(embed.ts)自动触发modal。例如embed-core/index.html中的用法:
<button contenteditable="false">【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.
项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考