嵌入其他网站

用一个 script 标签将应用添加到其他网站:text build --embed、<text-console>、服务端渲染的路由和托管。

text build --embed 构建应用后,其他网站就能用一个 script 标签添加应用,例如营销页面、文档站点、WordPress 或 Webflow 页面,或 React 18 应用。打开 DevTools 的访客会在控制台中看到你的应用。其他访客只下载 embed.js(gzip 压缩后约 3 kB),除非页面中有 <text-console>。

嵌入功能处于实验阶段。text build --embed、embed 选项、<text-console> 和 @textjs/app/experimental 的导出可能在次要版本中更改。参见破坏性变更。

将应用添加到网站

构建应用:

text build --embed

将 dist/ 上传到任意静态托管服务或 CDN,或复制到网站的 public/ 文件夹,然后在网站页面中添加一个标签:

<script type="module" src="https://console.example.com/embed.js"></script>

embed.js 相对于自身 URL 加载其他文件,因此 dist/ 可用于任意路径或源。其中只有 embed.js 和 assets/,没有可能覆盖网站自身文件的 index.html 或 404.html。包含服务端渲染的路由、路由处理程序或中间件的应用需要服务器,不能只上传静态文件。vercel 和 cloudflare 适配器可运行路由处理程序和中间件,而服务端渲染的路由需要 text start 或 bun 适配器。

要向访客展示应用的位置,在页面任意位置添加 <text-console>。按下任何带有 data-textjs-engage 的元素也会启动应用:

<button type="button" data-textjs-engage>Open in the console</button>

要在本地试用,运行 text dev --embed。它提供 /embed.js,每次请求都会生成新的首帧,并在 /(使用 standalone 时为 /__textjs/embed)提供预览页面,以其他网站的方式加载它。其他本地端口上的网站也可以加载 http://localhost:3000/embed.js。

配置

text build --embed 等同于在 text.config.ts 中设置 embed: true。传入对象可更改默认值:

text.config.tsts
import { defineConfig } from "@textjs/app";export default defineConfig({  embed: {    initialPath: "/status",  },});
选项默认值说明
initialPath"/"控制台打开时的路由。必须以 / 开头
globalName"text"应用启动后用于挂载控制台 API 的全局变量。设为 false 则不挂载
initialClearfalse在首帧之前清空页面的控制台。默认情况下,页面之前的日志保留在应用上方
standalonefalse同时构建应用自身的页面(index.html),与不带 embed 的 text build 一致
allowOrigins[]允许其页面运行此服务器上服务端渲染的路由的网站。参见服务端渲染的路由
credentialsfalse在来自 allowOrigins 的实时会话请求中发送此服务器的浏览器凭据,而不丢弃 cookie 和 Authorization。需要精确指定源

viewport.width × viewport.height(默认为 120 × 36)是首帧的尺寸。title、hints 和 action 是 <text-console> 的默认值。设置 ppr: false 或 middleware.ts 可能匹配 initialPath 时,embed.js 不包含预渲染的首帧,因为任何人都能下载 embed.js。镜像会保持空白,直到应用绘制。

运行行为

  • 在出现控制台使用意图之前,不加载其他内容。 embed.js 输出预渲染的首帧,然后等待。当 DevTools 停靠时(在加载时或之后)、访客在页面上按下 DevTools 快捷键时(F12、Ctrl+Shift+J、I、C 或 K,Mac 上为 ⌘⌥J、I、C 或 K),或按下 <text-console> 或 [data-textjs-engage] 元素时,应用才会加载。此后,在同一标签页中重新加载会立即启动应用。独立窗口中的 DevTools 不会改变页面尺寸,因此从菜单打开 DevTools 的访客会一直看到首帧,直到按下上述元素之一。
  • 在触摸屏上,应用不会启动。 在匹配 (hover: none) and (pointer: coarse) 的设备上,embed.js 不输出日志,也不添加监听器。它只会为镜像加载 <text-console> 的文件。
  • 只有访客激活应用后,按键才会传给应用。 要激活应用,访客可按下 <text-console> 或 [data-textjs-engage] 元素,或点击页面(链接、按钮和字段除外),将焦点从 DevTools 移回页面。在页面自身的字段或 [data-textjs-bypass] 元素中输入的内容不会传给应用,滚轮始终滚动页面。
  • 页面的 URL、历史记录和标题始终不变。 路由、<Link> 和 useRouter() 在控制台内部导航。在客户端渲染的应用中,绝对 URL 会在新标签页中打开。
  • 页面上的 Text.js 应用优先。 它启动时,正在运行的嵌入应用会卸载、停止读取按键并移除其全局变量。如果嵌入应用加载时另一个应用正占用控制台,它会输出一条警告且不启动。
  • 嵌入应用打包了自己的 React 19。 它没有 react-dom 或裸导入,因此页面的 React 版本、react 的导入映射和 React DevTools 都不会影响它。

文件加载时机

文件加载时机gzip
embed.js随页面加载2.9 kB
assets/text-console-[hash].js页面包含 <text-console> 时4.0 kB
assets/embed-client-[hash].js 和 assets/yoga-[hash].wasm出现控制台使用意图时112.8 kB + 29.0 kB
assets/embed-player-[hash].js出现控制台使用意图时,为服务端渲染的路由加载,替代客户端22.7 kB
assets/bidi-[hash].js应用首次显示从右到左的文本时(参见加载 bidi 引擎)10.1 kB

这些大小对应 120 × 36 的带边框状态框。text build --embed 会输出你的应用的大小。assets/embed-[hash].js 是 embed.js 的副本,使用一个永不改变的名称(参见部署)。

<text-console>

embed.js 定义 <text-console>,且仅在页面包含该元素时加载其文件。它使用 Shadow DOM,因此页面的 CSS 不会影响其内部,也无需更改 style-src 'self' 策略。

<text-console></text-console><text-console mirror></text-console><text-console chrome></text-console>

不带属性时,它是一个单行栏。它先显示浏览器打开控制台的快捷键,然后提示点击,再列出应用的按键;按键传给应用时会亮起。在 Safari 中,第一步会提示在 Safari 设置中打开“显示网页开发者功能”。

带有 mirror 时,它会在页面上显示应用的帧:在其他内容加载之前显示预渲染的首帧,之后显示运行中的应用。按下它会启动应用并将访客的按键传给应用,因此关闭 DevTools 时应用也能运行。chrome 会在镜像周围绘制 DevTools 窗口,并隐含启用 mirror。

属性说明
mirror显示应用的帧
chrome在 DevTools 窗口中显示帧
title栏的标题(默认值:text.config.ts 中的 title)
hints要列出的按键,格式为 "← →:switch region, Enter:open"(默认值:text.config.ts 中的 hints)
action首先要执行的操作,显示为“点击这里并…”(默认值:text.config.ts 中的 action,或按下第一条提示中的按键)

在触摸屏上,镜像显示首帧但不启动应用,单行栏则隐藏。该元素的文本为英文。

预留空间

text build --embed 会为应用的网格输出一行 CSS。将其粘贴到页面的样式表中。它会在元素文件加载之前设置 <text-console> 的尺寸,因此文件加载完成时页面内容不会移动。对于默认的 120 × 36 网格,这行 CSS 为:

text-console{display:block;min-height:58px}text-console:is([mirror],[chrome]){position:relative;box-sizing:content-box;min-height:0;max-width:885px;aspect-ratio:885/522}text-console[chrome]{padding-top:43px}@media (hover:none) and (pointer:coarse){text-console:not([mirror],[chrome]){display:none}}

单行栏高 58 px。镜像保持网格的宽高比,最大为其自然宽度(120 列时为 885 px),chrome 则增加 43 px 高度。尺寸不依赖字体。@textjs/app/experimental 中的 reserveCss(cols, rows) 返回同一行 CSS。

首屏中的镜像通常是页面最大的元素,因此决定页面的最大内容绘制(LCP)。首帧在 embed.js 和元素文件依次加载后绘制。页面其余部分的绘制与没有嵌入应用时相同。页面下方的镜像不影响 LCP。对于有自身延迟脚本的页面,例如 React 应用,<script type="module" async src="…"> 会运行 embed.js 而不等待这些脚本,因此首屏中的镜像会更早绘制。

样式

要将镜像居中,将其行内方向的外边距设为自动:

text-console {  margin-inline: auto;}

默认使用深色。设置 color-scheme: light 可使用浅色,或设置用于混合所有其他色阶的两种颜色。::part(frame) 用于设置帧本身的样式。chrome 保留 DevTools 自身的背景。

text-console {  --text-console-bg: #fafafa;  --text-console-fg: #171717;}text-console::part(frame) {  border-radius: 0;}

服务端渲染的路由

当 initialPath 指向带有 export const runtime = "server" 的路由时,embed.js 会加载帧播放器,而非客户端运行时。它从提供 embed.js 的服务器流式接收帧,因此需运行 text start 或 bun 适配器。使用其他适配器会导致构建失败。参见服务端渲染。

其他网站的页面只有在其源位于 allowOrigins 中时,才能打开实时会话:

text.config.tsts
import { defineConfig } from "@textjs/app";export default defineConfig({  embed: {    initialPath: "/status",    allowOrigins: ["https://www.example.com", "https://*.example.com"],  },});
  • 会话只响应打开它的源。
  • 默认情况下,Text.js 会丢弃来自其他网站的会话的 cookie 和 Authorization 请求头,因此中间件看到的是未登录的访客。credentials: true 会保留这些凭据,并要求精确指定源。当页面和服务器共享可注册域名(www.example.com 和 console.example.com),且 cookie 覆盖该服务器(Domain=example.com,或在 console.example.com 上设置)时,浏览器会发送这些凭据。对于其他网站,浏览器只发送设置了 SameSite=None; Secure 的 cookie,而阻止第三方 cookie 的浏览器不会发送任何 cookie。
  • 实时会话在连接和重新连接时,会立即向 /__textjs/live 发送 OPTIONS 预检请求。预检请求不携带 cookie 或 Authorization,因此匹配该路径的中间件必须允许 OPTIONS 请求在未登录时通过。
  • 在反向代理或 CDN 后运行时,设置 server.trustProxy(参见在代理后运行)。不设置时,每位访客的地址都是代理的地址,因此所有访客共享同一个客户端会话数量限制。

除非设置了 credentials,否则 text dev 无需 allowOrigins 就接受 localhost、127.0.0.1 和 [::1] 任意端口上的页面。

托管

text start 以及 bun、vercel 和 cloudflare 适配器会使用以下响应头提供嵌入构建。static 适配器会将这些响应头写入 _headers,供 Cloudflare Pages 和 Netlify 读取。在其他托管环境中,例如存储桶或网站自身的 public/ 文件夹,需自行设置:

路径响应头
/embed.jsCache-Control: public, max-age=0, must-revalidate
/assets/*Cache-Control: public, max-age=31536000, immutable
网站位于其他源时,以上两者Access-Control-Allow-Origin: * 和 Cross-Origin-Resource-Policy: cross-origin

浏览器使用 CORS 获取模块脚本和 WebAssembly,因此其他源上的文件需要 Access-Control-Allow-Origin。将 yoga-[hash].wasm 以 application/wasm 类型压缩后提供(参见 WebAssembly)。

部署

每次页面加载时都会重新验证 embed.js,因此新访客会立即获取新部署的版本。已加载旧版 embed.js 的标签页,在访客打开控制台时仍会导入上一次构建的文件,这可能发生在数小时后。保留上一次部署的 assets/ 在线:将新文件复制到旧文件所在位置,不要使用会删除旧文件的同步方式,一两天后再删除旧文件。要固定使用某次构建,将其 assets/embed-[hash].js 放入标签中。该次构建的文件也必须保持在线。

文件不存在时,embed.js 会输出一条警告,提示访客重新加载,但不会启动应用,也不会自行重新加载页面。

内容安全策略

embed.js 不需要内联脚本或样式。将以下内容添加到宿主页面的策略中;当 dist/ 与页面同源时,用 'self' 替换源地址:

指令值用途
script-srchttps://console.example.comembed.js 及其分块
script-src'wasm-unsafe-eval'客户端渲染应用中的 Yoga
connect-srchttps://console.example.comyoga-[hash].wasm,以及服务端渲染的路由所用的 /__textjs/live
img-srcdata:仅用于应用绘制 <Image> 或 <Pixels> 的 <text-console mirror>

自行加载应用

@textjs/app/experimental 导出了构成 embed.js 的各个部分,供需要自行加载应用的页面使用:

导出说明
startEmbed(paint, boot, src, element?)执行 embed.js 的流程:输出第一帧,等待控制台使用意图,然后调用 boot
bootEmbedClient(options)启动客户端渲染的应用,使用内存路由,并按页面交互状态控制键盘输入
bootEmbedPlayer(options)启动服务端渲染的路由的帧播放器
createHostInput(options)遵循上述规则的键盘输入
createTextConsoleMount(config)返回用于 <text-console> 的 mount(element, shell?) 函数
reserveCss(cols, rows)为 <text-console> 预留空间的 CSS

ENGAGE_SELECTOR 和 BYPASS_SELECTOR 分别用于选择启动应用的元素和自行处理按键的元素。

在 GitHub 上编辑此页