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(schemaVersion、appId、cells列表),浏览器端的 islands 运行时(见 frontend 源码目录 相关实现)负责用该 payload 水合(hydrate)页面,使已渲染的静态输出重新获得响应式能力。
需要注意的是,仓库中 webassembly_html.md 明确标注 islands 目前属于早期特性(Preview):API 大概率不会变动,但在被视为稳定之前仍有一些改进计划。
示例文档展示了什么
仓库中的 island_example.md 本身就是一篇由 islands 驱动的"活文档":它的正文里嵌入了三个真实的 island,分别演示了三类典型用法:
- 导入单元格:
import marimo as mo,为后续单元格准备运行时; - 交互 UI 单元格:一个
mo.ui.slider(0, 10, value=2)滑块,初始值为 2; - 响应式输出单元格:
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&family=Lora&family=PT+Sans:wght@400;700&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.js与style.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",官方推荐的通用流程是:
- 把所有代码片段加入生成器;
build()构建应用;- 用渲染出的 HTML 替换原代码片段;
- 把 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_url | 为True或字符串时改用本地 islands JS(默认http://localhost:5174) |
include_init_island | 是否在 body 最前面加入"初始化加载中"的旋转指示岛 |
include_payload | 是否在末尾追加 JSON payload 脚本标签(用于水合) |
max_width/margin/style | 控制包裹所有 island 的容器 div 的样式 |
其中max_width未指定时会根据 notebook 配置自动推断:width为compact或normal时为740px,medium时为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 包含cellId、code、outputHtml、outputMimetype、reactive、displayCode、displayOutput七个字段(见 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_code→build()→ 用渲染后的<marimo-island>HTML 替换原文 → 把render_head()注入页面<head>。仓库自身就是例证:本文主体所依据的 island_example.md 就是一篇嵌入了三个活岛(导入、滑块、响应式 Markdown)的 MkDocs 文档,页面加载后即由 marimo 运行时激活。
限制与注意事项
- 早期特性:官方在示例文档与 webassembly_html.md 中都标注了 Preview 状态——API 大概率稳定,但功能层面仍会持续改进;
- 浏览器端运行:响应式岛依赖 Pyodide 在浏览器中执行 Python,首次初始化有加载成本(可用 init island 缓解),且浏览器端需能访问 CDN 资源;
- 样式隔离:island 不访问父页面 HTML,其 UI 样式独立打包,避免与宿主站点样式互相污染;
- 保持 payload 完整:若自行后处理 HTML,切勿改动
application/vnd.marimo.islands+json脚本标签; build()仅一次:MarimoIslandGenerator实例的build()不可重复调用,多段内容请先全部add_code再统一构建;- 运行时版本对齐:
render_head(version_override=...)加载的 islands JS/CSS 版本应与生成时所用 marimo 版本匹配,避免 DOM 结构与运行时协议不一致。
结语
从本文可见,marimo islands 的价值在于把"笔记本单元格"降维成可嵌入的 HTML 原语:服务端负责执行与预渲染,浏览器端运行时负责水合与响应式联动,两者通过<marimo-island>自定义元素与 JSON payload 协议解耦。无论你是想给博客加一个可拖动的滑块演示、给文档站嵌入一段可运行的代码示例,还是为 SSG 工具链编写 marimo 集成,MarimoIslandGenerator与render_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),仅供参考