Skip to content

feat: create/remove iframe command#895

Draft
xiaoxustudio wants to merge 24 commits intoOpenWebGAL:devfrom
xiaoxustudio:feat/createFrame-command
Draft

feat: create/remove iframe command#895
xiaoxustudio wants to merge 24 commits intoOpenWebGAL:devfrom
xiaoxustudio:feat/createFrame-command

Conversation

@xiaoxustudio
Copy link
Member

@xiaoxustudio xiaoxustudio commented Mar 12, 2026

相关issue: #520

此PR增加了指令createIframeremoveIframe指令

createIframe

将创建一个iframe元素挂载到界面上,其content内容是iframe路径

createIframe:<path> [...];
; 例如
createIframe:1.html -id=test -allowSameOrigin -allowScripts -allowModals -wait -returnValue=a;

我们可以设置sandbox属性:

iframe权限 对应指令选项 描述
allow-forms allowForms 允许 iframe 内提交表单
allow-scripts allowScripts 允许 iframe 内执行 JavaScript 脚本(包括定时器、事件等)
allow-same-origin allowSameOrigin 允许 iframe 内容拥有同源身份,可访问自身 Cookie/LocalStorage 等
allow-top-navigation allowTopNavigation 允许 iframe 内的链接跳转到父页面(主页面)的上下文
allow-popups allowPopups 允许 iframe 通过 window.open() 等方式弹出新窗口
allow-modals allowModals 允许 iframe 弹出模态窗口(如 alert()、confirm()、prompt())
allow-pointer-lock allowPointerLock 允许 iframe 使用 Pointer Lock API(如游戏鼠标锁定)
allow-popups-to-escape-sandbox allowPopupsToEscapeSandbox 允许 iframe 弹出的新窗口不受当前沙箱限制
allow-downloads allowDownloads 允许 iframe 内触发文件下载操作
allow-presentation allowPresentation 允许 iframe 使用 Presentation API(投屏/演示功能)
allow-top-navigation-by-user-activation allowTopNavigationByUserActivation 仅允许用户主动触发(如点击)的顶级导航操作
allow-storage-access-by-user-activation allowStorageAccessByUserActivation 允许用户主动触发后访问父页面的存储权限
allow-orientation-lock allowOrientationLock 允许 iframe 使用 Screen Orientation API 锁定屏幕方向

其余的选项:

选项值 描述
id 标识iframe唯一id
wait 等待iframe调用完成回调
returnValue iframe 返回的值将被存储到的变量名称
width iframe元素宽度
height iframe元素高度
hidden iframe元素是否隐藏

PS:参数前面如果使用@标识开始,则将注入到iframe中,如-@test-@arg=123,从window.webgal.params中获取

  • 创建的iframe元素将会在全局window注入webgal变量,包含的方法如下:
export type WebGalAPIEventsKeyNames =
  | 'sentence' // 语句执行
  | 'save' // 保存存档
  | 'load'; // 加载存档

export interface WebGalAPI {
  // 获取响应式状态的方法
  getReactiveStore: (
    source: string | string[] | ((store: RootState) => any),
    callback: (newValue: any, oldValue: any) => void,
    options?: { immediate?: boolean },
  ) => () => void;
  // 获取特定状态的方法
  getStageState: () => RootState['stage'];
  getGUIState: () => RootState['GUI'];
  getUserData: () => RootState['userData'];
  getSaveData: () => RootState['saveData'];
  // 操作
  getGameVar: (key: string) => any;
  getGlobalGameVar: (key: string) => any;
  setGameVar: (key: string, value: any) => void;
  setGlobalGameVar: (key: string, value: any) => void;
  openIframe: (key?: string) => void; // 打开已加载的iframe
  closeIframe: (key?: string) => void; // 关闭已加载/本iframe
  nextSentence: () => void; // 执行下一条语句
  isBlockSentence: () => boolean; // 是否阻塞语句执行
  complete: (returnValue?: any) => void; // 语句执行完成回调
  // 事件
  on: (event: WebGalAPIEventsKeyNames, callback: (data?: any) => void) => void;
  off: (event: WebGalAPIEventsKeyNames, callback: (data?: any) => void) => void;
  postIframeMessage: (key: string, data?: any) => void; // 向指定iframe发送消息
  // 持久化数据
  getPersistentData: (key?: string) => any;
  setPersistentData: (key: string, value: any) => void;
  clearPersistentData: (key?: string) => void;
}

removeIframe

移除一个指定id的iframe,其content内容是创建时设置的id

removeIframe:<id> [...];
; 例如
removeIframe:test;
选项值 描述
save 移除时是否保留数据(保留后可在iframe中使用openIframe重新打开)

示例

实现了一个简单的好感度显示和各个API的示例代码

start.txt:

setVar:haogandu=0;
changeBg:bg.webp -next;
createIframe:2.html -id=tt -allowSameOrigin -allowScripts -hidden;
createIframe:1.html -id=test -allowSameOrigin -allowScripts -allowModals -wait -returnValue=a -width=100% -@injectArgs={haogandu};
label:start;
webgal:当前好感值:{haogandu};
choose:好感+1:addhaogan|结束:end;

label:addhaogan;
setVar:haogandu=haogandu+1;
jumpLabel:start;

label:end;
removeIframe:test;
removeIframe:tt;
webgal:移除Iframe;
webgal:拿到的值为{a};

1.html:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    * {
      margin: 0;
      padding: 0;
      font-size: 130%;
    }

    .container {
      width: 100%;
      box-shadow: 0 0 10px #000;
      background: rgba(0, 0, 0, .4);
      color: white
    }

    button {
      background: none;
      border: none;
      color: white;
      cursor: pointer;
      font-size: 1.2em;
      padding: 5px 10px;
      margin: 5px;
    }

    button:hover {
      background: rgba(255, 255, 255, .1)
    }

    button:active {
      background: rgba(255, 255, 255, .2)
    }
  </style>
</head>

<body>
  <div class="container">
    <span>访问次数:<span id="fangwen"></span></span>
    <span>好感度:<span id="haogandu"></span></span>
    <button type="button" onclick="complete(1)">完成</button>
    <button type="button" onclick="closeFrame()">关闭</button>
    <button type="button" onclick="nextSentence()">下一句</button>
  </div>
  <script>
    const { on, getReactiveStore, getGameVar, complete, closeFrame, nextSentence, isBlockSentence,
      getPersistentData, setPersistentData } = window.webgal
    /* 初始化 */
    const v = getPersistentData("visitor") ?? 0
    document.getElementById("fangwen").textContent = v + 1
    document.getElementById("haogandu").textContent = getGameVar("haogandu") ?? 0
    // 获取传入参数
    console.log(window.webgal.params)
    // 向tt的iframe发送消息
    window.webgal.postIframeMessage("tt", "hello")
    /* 监听 */
    getReactiveStore("stage.GameVar.haogandu", (state) => {
      document.getElementById("haogandu").textContent = state
    })
    on("sentence", () => {
      if (isBlockSentence()) console.log("当前语句被阻塞")
      const v = getPersistentData("visitor") ?? 0
      document.getElementById("fangwen").textContent = v + 1
      setPersistentData("visitor", v + 1)
    })
    on("save", (index) => {
      alert("保存存档:" + index)
    })
    on("load", (index) => {
      alert("加载存档:" + index)
    })
  </script>
</body>

</html>

2.html:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

<body>
  <script>
    window.addEventListener('message', (event) => {
      if (event.data.type === 'webgal-iframe-message') {
        console.log('[webgal-iframe-message] 收到来自', event.data.sourceId, '的消息:', event.data.data);
      }
    });
  </script>
</body>

</html>
image

有些特性可能未实现,还需参考和讨论。

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求的核心目标是增强应用程序的扩展性,通过引入 createFrameremoveFrame 命令,允许开发者在运行时动态地嵌入和管理 iframe。这为集成外部网页内容、实现复杂的用户界面或加载独立的交互式模块提供了强大的能力。通过在 iframe 中注入 webgal API,实现了主应用与嵌入内容之间的无缝通信和状态同步,极大地提升了应用的灵活性和功能边界。

Highlights

  • 新增 createFrame 命令: 引入了 createFrame 命令,允许在应用中动态创建和管理 iframe 元素。此命令支持配置 iframesandbox 属性、尺寸、唯一 id,并可选择等待 iframe 完成加载及处理返回值。
  • 新增 removeFrame 命令: 提供了 removeFrame 命令,通过指定的 id 移除已创建的 iframe
  • iframe 内部 API 注入: 在创建的 iframe 内部注入了一个 webgal 全局变量,提供了一系列 API,包括获取响应式状态、特定状态、游戏变量以及通知主进程 iframe 完成的方法,实现了主应用与 iframe 之间的双向通信。
  • Redux 状态管理集成: iframe 的生命周期和状态(如添加、移除、重置)已集成到 Redux stage 状态管理中,确保了 iframe 能够被统一管理和持久化。
  • iframe 渲染与管理组件: 引入了 IframeIframeContainer React 组件,负责 iframe 的实际渲染、状态监听、API 注入以及错误处理。
Changelog
  • packages/parser/src/config/scriptConfig.ts
    • 新增了 createFrameremoveFrame 命令到脚本配置中。
  • packages/parser/src/interface/sceneInterface.ts
    • commandType 枚举中新增了 createFrameremoveFrame 类型。
  • packages/webgal/src/Core/controller/frame/index.ts
    • 新增了 frameApiManager,用于管理 iframe 相关的 API 注册和调用。
  • packages/webgal/src/Core/controller/scene/sceneInterface.ts
    • commandType 枚举中新增了 createFrameremoveFrame 类型。
  • packages/webgal/src/Core/controller/stage/resetStage.ts
    • 更新了 resetStage 函数,在重置阶段时清空所有 iframe
  • packages/webgal/src/Core/gameScripts/createFrame.ts
    • 新增了 createFrame 脚本,实现了创建 iframe 的逻辑,包括参数解析、sandbox 属性处理、URL 规范化以及与 Redux 状态的交互。
  • packages/webgal/src/Core/gameScripts/removeFrame.ts
    • 新增了 removeFrame 脚本,实现了根据 id 移除 iframe 的逻辑。
  • packages/webgal/src/Core/parser/sceneParser.ts
    • 导入并注册了 createFrameremoveFrame 脚本到 SCRIPT_TAG_MAP 中。
  • packages/webgal/src/Stage/Iframe/Iframe.tsx
    • 新增了 Iframe React 组件,负责渲染单个 iframe,注入 webgal API,并处理状态监听和消息通信。
  • packages/webgal/src/Stage/Iframe/IframeContainer.module.scss
    • 新增了 IframeContainer 的样式文件。
  • packages/webgal/src/Stage/Iframe/IframeContainer.tsx
    • 新增了 IframeContainer React 组件,用于管理和渲染 stage 状态中所有活动的 iframe
  • packages/webgal/src/Stage/Iframe/interface.ts
    • 新增了 WebGalAPIReactiveWatcher 接口定义,用于规范 iframe 内部可用的 API 和响应式状态监听器。
  • packages/webgal/src/Stage/Stage.tsx
    • 在主 Stage 组件中集成了 IframeContainer,使其能够显示和管理 iframe
  • packages/webgal/src/store/stageInterface.ts
    • 定义了 IIFrame 接口,并向 IStageState 中添加了 frames 数组以存储 iframe 数据。
  • packages/webgal/src/store/stageReducer.ts
    • 导入了 IIFrame 接口,更新了 initState 以包含 frames 数组,并添加了 addFrameremoveFrameresetFrame reducer 来管理 iframe 状态。
Activity
  • 目前没有与此拉取请求相关的活动。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@xiaoxustudio xiaoxustudio marked this pull request as draft March 12, 2026 13:57
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

此 PR 增加了 createFrameremoveFrame 指令,用于在游戏中创建和移除 iframe 元素,并提供了一套 API 供 iframe 与主应用通信。整体实现思路清晰,功能完整。

我发现了一些可以改进的地方:

  • createFrame 中处理 URL 的逻辑存在一个 bug,可能导致合法的 http 链接被错误处理。
  • Iframe.tsx 组件在向 iframe 注入 API 时存在竞态条件,可能导致 API 未能及时注入。
  • postMessage 使用了不安全的 targetOrigin,存在安全风险。
  • 部分 TypeScript 类型可以更精确,以增强类型安全。

具体的修改建议请见代码注释。

@xiaoxustudio xiaoxustudio changed the title feat: create/remove frame command feat: create/remove iframe command Mar 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant