marimo Islands 实战指南:在任意 HTML 页面中嵌入响应式 Python 交互单元
2026/9/13 17:54:19 网站建设 项目流程

marimo Islands 实战指南:在任意 HTML 页面中嵌入响应式 Python 交互单元

【免费下载链接】marimoA reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All in a modern, AI-native editor.项目地址: https://gitcode.com/GitHub_Trending/ma/marimo

导读

marimo islands 是 marimo 提供的一种"HTML 岛屿"机制:它允许你把单个 notebook 单元格(cell)的代码与输出渲染成一段独立的 HTML 片段,嵌入到博客、文档站、教程等任意网页中,页面加载后由 marimo 的响应式运行时(基于 Pyodide 的浏览器端运行时)接管并使其可交互。本文以仓库中的示例文档docs/guides/island_example.md为主体,结合marimo/_islands/_island_generator.py等源码,完整讲解 islands 的 HTML 结构、head 资源引入、MarimoIslandGenerator生成 API、响应式/非响应式行为以及本地开发与构建流程,读完你就能在自己的静态站点中落地交互式 Python 内容。

什么是 marimo islands

marimo islands 是 "island architecture"(岛屿架构)思想在数据笔记本领域的实现:把页面视为一片"海洋",其中散布的每个"岛屿"都是一个独立渲染、可独立交互的 marimo 单元格。与 marimo 的"应用模式"(app mode)不同,island 模式没有顶层 app 外壳,也不访问父页面的 HTML——每个 island 携带自己的静态输出与代码,由运行时按需激活。

仓库 前端 islands 开发文档 对它的定义是:"marimo islands 是一种渲染 HTML '岛屿' 的方式,每个岛屿包含静态输出和代码"。它在 marimo 中主要服务于两类场景:

  • 创建交互式博客文章、教程与教学材料;
  • 作为构建静态站点生成器(SSG)或文档工具集成的底层构件。

从源码结构看,marimo 在后端把每个单元格编译为一段标准 HTML 片段,同时在marimo/_schemas/islands.py中定义了配套的 JSON payload(schemaVersionappIdcells列表),浏览器端的 islands 运行时(见 frontend 源码目录 相关实现)负责用该 payload 水合(hydrate)页面,使已渲染的静态输出重新获得响应式能力。

需要注意的是,仓库中 webassembly_html.md 明确标注 islands 目前属于早期特性(Preview):API 大概率不会变动,但在被视为稳定之前仍有一些改进计划。

示例文档展示了什么

仓库中的 island_example.md 本身就是一篇由 islands 驱动的"活文档":它的正文里嵌入了三个真实的 island,分别演示了三类典型用法:

  1. 导入单元格import marimo as mo,为后续单元格准备运行时;
  2. 交互 UI 单元格:一个mo.ui.slider(0, 10, value=2)滑块,初始值为 2;
  3. 响应式输出单元格mo.md(f'Hello, islands! {"🏝️" * slider.value}'),其输出随滑块值实时变化,并附带一个只读的代码编辑器展示源码。

这三个单元格构成了一个最小但完整的响应式依赖链:滑块 → 输出文本,充分体现了 islands 之间依然保持 marimo 的响应式数据流。

Island 的 HTML 结构逐层拆解

示例文档末尾的 "See the HTML" 折叠块给出了渲染后的完整 HTML。一个 island 在 DOM 中是一个自定义元素<marimo-island>,核心结构如下(以滑块单元格为例):

<marimo-island><!-- 1. marimo islands 运行时 JS 与样式 --> <script type="module" src="https://cdn.jsdelivr.net/npm/@marimo-team/islands@0.5.0/dist/main.js"></script> <link href="https://cdn.jsdelivr.net/npm/@marimo-team/islands@0.5.0/dist/style.css" rel="stylesheet" crossorigin="anonymous" /> <!-- 2. 字体(Fira Mono / Lora / PT Sans,带 preconnect 优化) --> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&amp;family=Lora&amp;family=PT+Sans:wght@400;700&amp;display=swap" rel="stylesheet" /> <!-- 3. KaTeX 样式(渲染 Markdown 中的 LaTeX 数学公式) --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww" crossorigin="anonymous" />

手动引入还是自动生成?

虽然可以像示例文档那样手写这些标签,但更推荐用生成器自动产出。后端 render_head() 方法正是用来生成这套 head 内容的,它会按当前 marimo 版本(version_override参数可覆盖)拼接:

  • https://cdn.jsdelivr.net/npm/@marimo-team/islands@{version}/dist/main.jsstyle.css
  • Google Fonts 的 preconnect 与字体表(源码注释解释了为何字体不打包进 CSS,以及嵌入式页面使用display=swap以保证字体加载兼容性);
  • KaTeX 样式表;
  • 一个隐藏的<marimo-filename hidden>标记元素(marimo_tags)。

如果传入_development_url=True,则改用本地开发服务器(默认http://localhost:5174)的/dist/main.js/dist/style.css,便于在开发模式下热更新调试 islands 前端。

用 MarimoIslandGenerator 生成岛屿页面

仓库 _island_generator.py 是 islands 后端的核心,MarimoIslandGenerator的设计目标是"让其他 SSG 框架用 marimo-islands 把 Python 代码转成 HTML",官方推荐的通用流程是:

  1. 把所有代码片段加入生成器;
  2. build()构建应用;
  3. 用渲染出的 HTML 替换原代码片段;
  4. 把 head 内容放入<head>标签。

方式一:从代码片段生成(add_code + build)

import asyncio import sys from marimo import MarimoIslandGenerator # Windows 下需要设置事件循环策略 if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) async def main(): generator = MarimoIslandGenerator() block1 = generator.add_code("import marimo as mo") block2 = generator.add_code("mo.md('Hello, islands!')") # 构建应用(内部会实际执行一次 notebook,捕获每个单元格的输出) app = await generator.build() output = f""" <html> <head> {generator.render_head()} </head> <body> {block1.render(display_output=False)} {block2.render()} </body> </html> """ print(output) output_file = "output.html" with open(output_file, "w", encoding="utf-8") as f: f.write(output) if __name__ == "__main__": asyncio.run(main())

这里两个关键调用值得展开:

  • add_code():接收一段 Python 源码,内部先做dedent(除非is_raw=True),然后分配单元格 ID、用compile_cell编译、注册到内部 App,最后返回一个MarimoIslandStub。它的四个参数正是上一节提到的三个开关加上is_raw
    • display_code(默认False):是否在 HTML 中展示代码;
    • display_output(默认True):是否在 HTML 中包含输出;
    • is_reactive(默认True):该代码块是否随 Pyodide 在浏览器中运行;
    • is_raw(默认False):是否跳过 dedent 原样处理代码。
  • build():是异步方法且只能调用一次(再次调用会抛出ValueError("You can only call build() once"))。它内部通过run_notebook真正执行一次 notebook,拿到SessionView与每个单元格的CellOutput,随后每个 stub 才能通过stub.output拿到执行结果。这解释了为什么必须在add_code全部完成后、render()之前调用build()

MarimoIslandStub.render()是产出 HTML 的最终入口,它支持在调用时以参数覆盖构造时的默认值(例如block1.render(display_output=False)),并且有一个as_raw=True模式:此时会去掉marimo-island包装、直接输出解码后的原始值,主要用于无 JS 场景(如 PDF 导出)。

方式二:从 notebook 文件生成(from_file)

如果你的内容已经是一个 marimo notebook(.py文件),可以直接导入:

from marimo import MarimoIslandGenerator # 从 notebook 文件创建生成器,每个单元格对应一个 stub generator = MarimoIslandGenerator.from_file("./<notebook-name>.py", display_code=False) # 不 build 也可以渲染 HTML(基础渲染可用,但单元格未真正执行) html = generator.render_html(include_init_island=False) print(html) output_file = "output.html" with open(output_file, "w", encoding="utf-8") as f: f.write(html)

from_file() 内部通过load_notebook读取 notebook,遍历cell_manager.cell_data()把每个单元格的代码逐个add_code进来,并保留 notebook 的 App 配置(如width等布局设置)。一个值得注意的细节是它会记录源文件绝对路径(_source_filename),这样单元格内引用__file__mo.notebook_dir()时解析到的是 notebook 文件本身而非宿主进程,避免后续chdir改变路径语义。

一键产出完整 HTML:render_html

如果不想自己拼装页面骨架,render_html() 可以一步生成完整的独立 HTML 文档(含<!doctype html><head><title><body>)。它的可选参数包括:

参数作用
version_override指定 CDN 加载的 marimo islands JS/CSS 版本
_development_urlTrue或字符串时改用本地 islands JS(默认http://localhost:5174
include_init_island是否在 body 最前面加入"初始化加载中"的旋转指示岛
include_payload是否在末尾追加 JSON payload 脚本标签(用于水合)
max_width/margin/style控制包裹所有 island 的容器 div 的样式

其中max_width未指定时会根据 notebook 配置自动推断:widthcompactnormal时为740pxmedium时为1110px,否则为none。页面标题则取app_title配置,未设置时回退为app_id

响应式与非响应式:data-reactive 的语义

data-reactive="true"是 island 交互性的总开关。在 _island_generator.py 中:

  • 通过add_code(..., is_reactive=False)添加的单元格,渲染时会得到data-reactive="false"
  • 通过from_file导入时,_disabled_cell_ids()会结合单元格的 disabled 状态与数据流图(DirectedGraph.is_disabled)自动把被禁用的单元格标记为非响应式;
  • 非响应式 island 的<marimo-cell-code>内容为空字符串(uri_encode_component(self.code) if resolved_is_reactive else ""),即完全不向浏览器暴露代码。

使用建议:非响应式岛适合昂贵的计算或纯静态内容——它在服务端执行一次、输出固化,不随其他岛变化而重新运行,也不消耗浏览器端运行时资源。响应式岛则构成依赖网络:如示例文档中滑块slider与文本mo.md(f'Hello, islands! {"🏝️" * slider.value}')之间的联动,调整滑块时下游岛在浏览器内即时重算。

初始化加载岛(init island)

由于岛屿运行时依赖 Pyodide 在浏览器中加载 Python 解释器,首次加载需要时间。为此 render_init_island() 会生成一个特殊的静态岛:一个旋转加载动画 + "Initializing..." 文字,data-reactive="false",作为 body 的第一个元素插入。它的职责是:

  • 在 Pyodide 就绪前展示加载状态,避免页面出现空白;
  • 运行时初始化完成后自动消失,把舞台交给真正的岛屿。

如果不想显示加载动画(例如页面以静态内容为主、不依赖水合),可以在render_html(include_init_island=False)中关闭它。

Payload 水合机制

include_payload=True时,页面末尾会追加一个 JSON 脚本标签,类型为application/vnd.marimo.islands+json(常量定义见 marimo/_schemas/islands.py):

<script type="application/vnd.marimo.islands+json">{"schemaVersion":1,"appId":"main","cells":[{"cellId":"cell-1","code":"mo.md('Hello, islands!')","outputHtml":"\u003cspan\u003eHello, islands!\u003c/span\u003e","outputMimetype":"text/markdown","reactive":true,"displayCode":false,"displayOutput":true}]}</script>

每个单元格的 payload 包含cellIdcodeoutputHtmloutputMimetypereactivedisplayCodedisplayOutput七个字段(见 MarimoIslandCellPayload)。工作方式为:DOM 提供可见的 island 槽位,payload 提供运行时的单元格代码与输出元数据,运行时据此水合页面。仓库 webassembly_html.md 特别提醒:如果你对生成的 HTML 做后处理,务必原样保留这个 JSON 脚本标签及其内容,否则水合会失败。JSON 字符串中的 HTML 敏感字符在写入脚本标签前会被转义。

演示站点的源码级实现:islands 是如何自举的

仓库中的 frontend/islands/generate.py 是"用 islands 生成 islands 演示页面"的自举示例:它用MarimoIslandGenerator.add_code()声明了 30 多个示例岛(Getting Started、Basic UI Components、Advanced Components、Data Display、Layout & Composition、Island Features、Error Handling 七大类),覆盖滑块、按钮、文本输入、下拉框、复选框、单选、数字输入、多选、区间滑块、Tabs、代码编辑器、表格、Markdown/LaTeX、表单、display_code/is_reactive组合以及错误显示等全部常见能力。运行方式:

# 生成带 CDN 链接的正式版 HTML(默认,用于部署) uv run ./islands/generate.py > islands/__demo__/index.html # 本地生产构建测试 MODE=local uv run ./islands/generate.py > islands/__demo__/index.html # Vite 开发模式(pnpm dev:islands 会自动完成) MODE=dev uv run ./islands/generate.py > islands/__demo__/index.html

生成器依据MODE环境变量选择三种脚本注入方式(get_script_tags):

  • cdn:使用generator.render_head()输出正式 CDN 资源;
  • local:指向本地生产构建http://127.0.0.1:8001/main.js
  • dev:注入 Vite 客户端与 React Refresh 全局钩子,指向/src/core/islands/main.ts源码入口。

生成的演示产物见 frontend/islands/demo/index.html,其中甚至包含一个故意触发ModuleNotFoundError的岛,用来展示错误如何在岛内以 MIME 渲染器呈现。此外,generate.py 底部还有一个 Tailwind CSS 隔离测试区:位于.marimo容器之外的样式不受全局 Tailwind 影响,而容器内的样式(含.dark模式)会被应用——这印证了 island 架构"不访问父页面 HTML、样式局部化"的设计。

本地开发与生产构建

仓库 frontend/islands/development.md 给出了完整的开发闭环:

# 快速启动演示(自动生成 demo HTML 并启动带 HMR 的 Vite dev server, # 同时监听 generate.py 变更自动重载) pnpm dev:islands # 使用预构建的 marimo wheel(免去本地编译) VITE_WASM_MARIMO_PREBUILT_WHEEL=true pnpm dev:islands

修改islands/generate.py保存后,浏览器会自动刷新展示新岛——因为 Vite 把该脚本的变更作为热更新触发源。生产构建则使用:

pnpm build:islands # 产物: # - frontend/islands/dist/main.js # - frontend/islands/dist/style.css

集成到 SSG 与注意事项

在文档站中嵌入

islands 最常见的落地场景是静态站点与文档工具。可以将MarimoIslandGenerator写成一个构建期脚本:遍历 Markdown/源码中的代码块 →add_codebuild()→ 用渲染后的<marimo-island>HTML 替换原文 → 把render_head()注入页面<head>。仓库自身就是例证:本文主体所依据的 island_example.md 就是一篇嵌入了三个活岛(导入、滑块、响应式 Markdown)的 MkDocs 文档,页面加载后即由 marimo 运行时激活。

限制与注意事项

  1. 早期特性:官方在示例文档与 webassembly_html.md 中都标注了 Preview 状态——API 大概率稳定,但功能层面仍会持续改进;
  2. 浏览器端运行:响应式岛依赖 Pyodide 在浏览器中执行 Python,首次初始化有加载成本(可用 init island 缓解),且浏览器端需能访问 CDN 资源;
  3. 样式隔离:island 不访问父页面 HTML,其 UI 样式独立打包,避免与宿主站点样式互相污染;
  4. 保持 payload 完整:若自行后处理 HTML,切勿改动application/vnd.marimo.islands+json脚本标签;
  5. build()仅一次MarimoIslandGenerator实例的build()不可重复调用,多段内容请先全部add_code再统一构建;
  6. 运行时版本对齐render_head(version_override=...)加载的 islands JS/CSS 版本应与生成时所用 marimo 版本匹配,避免 DOM 结构与运行时协议不一致。

结语

从本文可见,marimo islands 的价值在于把"笔记本单元格"降维成可嵌入的 HTML 原语:服务端负责执行与预渲染,浏览器端运行时负责水合与响应式联动,两者通过<marimo-island>自定义元素与 JSON payload 协议解耦。无论你是想给博客加一个可拖动的滑块演示、给文档站嵌入一段可运行的代码示例,还是为 SSG 工具链编写 marimo 集成,MarimoIslandGeneratorrender_head()/render_body()这套 API 都提供了开箱即用的通路,而 generate.py 这份自举的示例则是你最好的参考模板。

【免费下载链接】marimoA reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All in a modern, AI-native editor.项目地址: https://gitcode.com/GitHub_Trending/ma/marimo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询