Drupal 11:集成Flems代码沙箱以实现交互式代码示例

Drupal 11:寻找更好的代码示例展示方式

我运营这个网站大概有 18 年了,从网站创立以来,我发布的代码基本都采用相同的格式。代码通过 <code> 元素渲染到页面上,并且(大部分情况下)用 JavaScript 插件进行语法高亮显示。

我经常光顾像 Codepen 和 JSFiddle 这样的网站,还时常链接到这些网站以展示代码的使用示例。这些网站让我意识到本网站代码示例过于静态。最近我编写了更多前端代码,可静态代码示例并非展示这些功能实际运行情况的最佳选择。我能(而且已经)上传功能运行的图片和动图,不过这些图片的大小往往是相关代码示例的好几倍,只会让页面变得臃肿。

我真正想做的是能将活跃的代码示例或代码沙箱注入页面。这样用户就能和代码示例进行交互,而不只是静态地查看。显然,这对任何网站而言都是极具价值的学习工具。

我知道可以把 Codepen 示例嵌入页面,但这不仅需要高级订阅,还会导致代码与网站内容脱节。我想要一个能让我在 Drupal 网站后端编写文章和代码示例的解决方案。

将代码示例托管在第三方网站存在风险,要是该网站离线,我网站上的所有代码示例都将无法运行。自己托管的话,既能改善编辑体验,又能确保一切正常。

现在我要为网站找一个代码沙箱,用于演示简单的 JavaScript 和 CSS 代码,且不依赖第三方供应商。于是,我展开了一番搜索,以找到适合代码的容器。

在本文中,我会研究几个代码沙箱,以及它们如何集成到 Drupal 中。然后,我会着重关注一个代码沙箱,并将其嵌入文章内容。

一、代码沙箱快速分析

我对允许(或至少声称允许)某种形式自托管解决方案的代码沙箱进行了(绝非全面的)分析。这花了我好几个小时挖掘搜索结果、寻找示例。

1. Playground Elements

链接:https://github.com/google/playground-elements

鉴于它是谷歌创建的,我对这个沙箱寄予厚望,而且它似乎具备我所寻觅的诸多功能。该软件包宣称能直接在浏览器中运行多种不同语言编写的代码,无需后端服务器。

尽管 Playground Elements 声称易于安装,但我在让它发挥作用方面却状况百出。而且,我找到的所有该系统看似在运行的示例,好像都无法正常工作。我花了大概一个小时尝试让它在 Drupal 中运行,最终还是放弃了,我只能推测它根本无法运行,因为我没见过它正常工作的实例。

我可能不想再多一个谷歌集成了,而且这软件包似乎也无法使用,所以我决定弃用它。我猜谷歌或许已经停止对它的支持,任其自生自灭了。

2. Sandpack Client

链接:https://sandpack.codesandbox.io/docs/advanced-usage/client 

这是 CodeSandbox 网站使用的沙箱,虽然看似前景不错,但整个系统好像是围绕 React 组件构建的。经过一番实验,我意识到 sandpack-client 库只是系统的前端,得托管一个后端打包器才能让一切正常运转,而且这个打包器还得进行托管。

我不想找一个除 Drupal 之外还需要后端系统才能工作的系统,所以这对我来说似乎太复杂了。

3. Codeapi

链接:https://codapi.org

这是个很有趣的项目,包含许多不同的 playground 来运行各种不同的语言,JavaScript 已集成到系统中。其他语言需要后端系统才能正确处理。

虽然该系统运行得还不错,但我想要的是能展示 CSS 规则实际运行情况的工具,而这个编辑器目前还做不到。当你想在浏览器中运行任何类型的代码时,它是个不错的小工具,我可能会为网站上的其他语言研究它。

4. LiveCodes

链接:https://livecodes.io

这个项目一开始看起来像是托管产品,但实际上是个不错的系统,它包含在一小套 JavaScript 文件中。它无需复杂的构建步骤或后端服务器就能运行,因为它可以在浏览器内全部运行。唯一的缺点是嵌入时会连接到 livecodes.io 域名,其中包含几个无法禁用的讨厌的分析 cookie。由于应用程序的创建方式,也不容易关闭这些功能。

该系统围绕 JavaScript、CSS 和 HTML 文件构建,并扩展到 React 和 Vue 等系统。LiveCodes 有一个独立的桌面应用程序,可用于快速进行应用程序原型设计。该系统的文档相当不错(偶尔有一些小缺口),并详细描述了所有选项,还配有示例。

经过一番折腾,我让 LiveCodes 系统在 Drupal 中运行起来了,代码被注入到 LiveCodes 包含文件中并展示出来。这个软件包几乎非常适合这个目的,并且可以使用一个相当简单的 JSON 数组进行大量配置。此外,它有暗模式,这意味着它在这个网站上也不会显得格格不入。

事实上,它基本上会“回传数据”以收集大部分资源(并且不遵守我网站的 cookie 政策),这让我对这个工具相当反感。

5. Flems

链接:https://github.com/porsager/flems

尽管名字“很有趣”,但这个项目感觉挺符合我的需求。将该工具包含到页面中需要把 flems.html 文件作为 JavaScript 包含文件添加,这似乎有点奇怪,但一切都能正常工作。更重要的是,它不会调用第三方网站来下载随机文件。

下载的 flems.html 文件大约有 140KB,有点大,但这是该插件的 JavaScript、HTML 和 CSS 文件。Flems 工作不需要下载其他文件,只需要页面内的脚本进行初始化。

该软件包还具备运行 TypeScript 和 LiveScript 文件的能力(这些文件在运行时编译),这也是一个优点。编辑器使用 CodeMirror,这意味着它具有良好的浏览器支持,因为 CodeMirror 是一个相当不错的 JavaScript 代码编辑器。

事实上,Flems 唯一真正的问题是它已经有几年没有更新了。近年来,为了解决安全问题,软件包版本有所更新,但上一次真正的更新是在 2022 年。这不是一个大问题,但需要注意。

Flems 有一个完整的示例网站 https://flems.io,如果你有兴趣了解该软件包的全部潜力,值得一看。

Drupal 很乐意将 flems.html 文件作为第三方库包含进来,但我确实需要解决 Flems 引起的几个小问题。很快我就让一切在 Drupal 控制器上正常工作了,甚至配置了沙箱,并将几个测试文件注入到沙箱中作为演示。

6. 自定义构建?

另一个解决方案是构建一个涉及 iframe 的自定义系统,这是上述大多数系统使用的技术。HTML5 引入了 sandbox 属性,它提供了额外的限制,以进一步增强 iframe 沙箱的安全性,非常适合这个目的。

虽然这是可行的,但我想尽量避免这种方法,因为这意味着创建一个可能导致安全问题的系统,并将其嵌入到网站中。我更愿意选择一个现有的软件包,展示它有多好,甚至可能为它做出贡献。

二、将 Flems 集成到 Drupal 中

最后,我选择了 Flems 作为在 Drupal 中嵌入代码沙箱的解决方案。它有点过时,但该系统完全符合我的需求,我可以根据需要随时分叉并修改它(我已经这样做了)。我还创建了拉取请求,将这些更改贡献回原始软件包。

Flems 附带的说明很简略,但能达成目的。我们只需将一个 HTML 文件作为 JavaScript 包含文件添加,然后就可以在页面内运行 Flems 系统。该工具存在几个配置选项,可以以不同的方式进行自定义,包括文件、链接、颜色、标签和只读模式。

为了将 Flems 集成到 Drupal 中,我从 git 仓库下载了该软件包的最新版本,然后构建了核心 flems.html 文件。为此,我只需在 Flems 目录中使用 npm install 命令,然后使用 npm run build 命令。这将生成一个 dist 目录,重要的 flems.html 文件就位于该目录中。

在深入配置实体以允许通过 Drupal CMS 添加用户内容之前,我想先进行一个快速的概念验证。为此,我创建了一个带有单个控制器的小模块,以便我可以添加 Flems 正常工作所需的 HTML。

第一步是创建一个 *.libraries.yml 文件,以便我们可以注入 flems.html 文件。

  
    flems:
    header: true
    js:
    js/flems/dist/flems.html: { attributes: { type : 'text/javascript', charset : 'utf-8' } }
  

这个文件需要放在页面的头部(因此 header 属性设置为 true),这样我们就可以在页面上放置多个 Flems 实例。

有了这个设置,我们现在可以添加一小段 HTML 和 Flems 运行所需的脚本,我使用 nowdoc PHP 创建了这些内容。这种语法基本上允许我将一个大的多行字符串存储在一个变量中,而无需进行转义,这为我们提供了一种方便的机制,可以将示例脚本注入到页面中,而无需进行任何复杂的转义。

通过脚本创建的变量使用 markup Drupal 格式化器添加到控制器的渲染数组中。我们还传入一些允许的 HTML 标签,这样我们在 nowdoc 中包含的标签在渲染时就不会被剥离。

  
    $flemsScript = <<<'EOD'
    <div id="container" class="flems-container"></div>
    <script>
    const container = document.getElementById("container");
    const flems = Flems(container, {
    files: [{
    name: 'app.js',
    type: 'script',
    content: `console.log(\`Hello from JS!\`);`,
    },
    {
    name: 'index.html',
    type: 'document',
    content: '<p>Test</p>',
    },
    {
    name: 'styles.css',
    type: 'style',
    content: 'p {color: blue; font-size: 30px}',
    },
    ],
    middle          : 50,
    selected        : 'app.js',
    color           : 'rgb(60, 60, 60)',
    theme           : 'material', // Can be material, none or default.
    resizeable      : true,
    editable        : true,
    toolbar         : true,
    fileTabs        : true,
    linkTabs        : true,
    shareButton     : false,
    reloadButton    : true,
    console         : true,
    autoReload      : true,
    autoReloadDelay : 1000,
    autoHeight      : false,
    });
    </script>
    EOD;
    
    $build['flems'] = [
    '#markup' => $flemsScript,
    '#allowed_tags' => ['script', 'div', 'p'],
    ];
  

上述示例中使用的选项是 Flems 文档中的一些基本选项,有一些小的更改以自定义输出。有趣的是,代码输出将每秒刷新一次,这意味着当用户更改输入元素时,输出(几乎)会实时更改。

当我们加载控制器时,现在可以看到以下内容。

你可以在页面 https://www.hashbangcode.com/code-sandbox-test-page 上看到控制器的实际运行情况,在编写上述代码后,我对其进行了更改,以展示该工具更多的功能。上述脚本并没有完全保留原始代码的空白格式,但传达了核心思想。随意尝试一下!

这运行得非常好,事实上,我能够在沙箱中编辑代码,添加一个在绿色背景上显示随机花朵的画布元素。

我将保留这个测试页面,作为一种快速检查插件在测试环境中是否正常工作的方法。

现在我已经确认一切正常工作,我需要更新 Drupal 以接受用户输入并为页面生成所需的输出。

三、配置 Drupal 实体

我思考了如何将所需的数据添加到 Drupal 中,让代码示例能正确嵌入内容。我需要一种机制,能让我像添加代码块或图像一样,把代码示例添加到 CKEditor 字段中。

嵌入的代码示例要和当前的 <code> 标签并存,这些标签本身对于展示代码和分解工作原理很有用,而不是总是使用完整的代码沙箱。

在研究了几个实体嵌入模块后,我决定使用 Paragraphs Entity Embed 模块。这是 Entity Embed 模块的一个实现,它允许将段落注入到 CKEditor 字段中。使用这个系统解决了我如果从头开发这个系统可能会遇到的许多问题,包括管理实体的 CKEditor 对话框。

Paragraphs 模块的添加让我可以轻松创建所需的实体,并将代码注入其中,而不必创建任何自定义字段或复杂的格式化器。我需要为所需的 JavaScript、HTML 和 CSS “文件”设置一组字段,然后可以将这些字段渲染到编辑器中。我还需要一个选择字段,将一个值注入到 selected 配置项中。

以下是创建的段落字段。

段落的渲染模板对要在 Flems 界面中显示的文件做了一些假设。我想遵循与 CodePen 相同的布局,即只存在 JavaScript、HTML 和 CSS 文件。Flems 可以进行相当多的扩展,以允许创建自定义文件并包含外部脚本。出于我的目的,我只希望能够使用一个最小的代码沙箱来展示代码的实际运行情况,而不是一个完整的开发环境。

段落模板文件首先设置代码沙箱中标签的内容,将它们作为文件数组注入到模板中,并设置选定的标签。由于文件总是被命名为 app.jsindex.htmlstyles.css,所以我们可以将这些文件设置为默认值。

以下是段落文件中的相关部分。

  
    {% set jsValue = paragraph.field_javascript.0.value|replace({'\`': '\\`'}) %}
    {% set htmlValue = paragraph.field_html.0.value|replace({'\`': '\\`'}) %}
    {% set cssValue = paragraph.field_css.0.value|replace({'\`': '\\`'}) %}
    
    <div id="container-{{ paragraph.id() }}" class="flems-container"></div>
    
    {% set selected = (paragraph.field_selected_file.0.value|raw == 'html') ? 'index.html' : (paragraph.field_selected_file.0.value|raw == 'css') ? 'styles.css' : '.js' %}
    <script>
    const container{{ paragraph.id() }} = document.getElementById("container-{{ paragraph.id() }}");
    const flems{{ paragraph.id() }} = Flems(container{{ paragraph.id() }}, {
    files: [
    {% if jsValue %}
    
    name: 'app.js',
    type: 'script',
    content: `{{ jsValue|raw }}`,
    },
    {% endif %}
    {% if htmlValue %}
    {
    name: 'index.html',
    type: 'document',
    content: `{{ htmlValue|raw }}`,
    },
    {% endif %}
    {% if cssValue %}
    {
    name: 'styles.css',
    type: 'style',
    content: `{{ cssValue|raw }}`,
    },
    {% endif %}
    ],
    middle: 50,
    selected: '{{ selected }}',
    color: 'rgb(60, 60, 60)',
    theme: 'material',
    shareButton: false,
    autoReloadDelay: 1000,
    });
    </script>
  

我在这里需要使用 raw 过滤器来防止代码被转义。毕竟,目的是允许代码在这里运行,所以转义它只会破坏代码。如果你发现这里有任何明显的安全问题,请告诉我。

这里我没有包含一些细节,特别是关于权限的细节,但这展示了模板的基本用法。关于权限,目前我只允许网站管理员访问这个嵌入功能。我当然不希望匿名用户嵌入沙箱,因为它缺乏安全性和无限循环检测。

使用这个系统,可以通过 CKEditor 字段将多个代码沙箱注入到页面中。由于我使用了 Paragraphs Entity Embed 模块,一旦代码沙箱创建完成,我还可以在页面之间共享它们。

四、结论

这个系统运行得相当不错。以下是一个支持 Flems 的嵌入式段落示例。代码示例展示了画布 API 中贝塞尔曲线的不同点是如何放置的。你可以在 JavaScript 文件中调整一些参数,查看输出中的更新,输出每秒更新一次。

我一直在幕后用各种不同的示例测试这个系统,到目前为止,我对结果很满意。

这个项目花了不少时间才完成。找到一个合适且能正常工作的 JavaScript 库真是个挑战,我花了好几个小时才找到符合我需求的库。Flems 并不完美,但我已经为这个项目做出了一些贡献,让它能与 Drupal 配合使用。目前我对它的主要顾虑是它的可访问性不太好,不过我也在研究如何解决这个问题。我相信如果从最终构建中移除一些选项,文件大小也可以减小。

在本文中,我只是浅尝辄止地介绍了 Flems,因为这个软件包有很大的扩展空间。它已经支持 TypeScript,还可以将第三方软件包注入到输出中以扩展其功能。有将 Mithril.js 注入脚本以创建单页应用程序的示例,所以我可以想象添加 Three.js 或 GSAP 等内容来创建功能齐全的演示。

Drupal 集成花了一些时间,我还遇到了一些有趣的技术挑战需要克服。我创建的模块尽可能通用,这样它就不与这个网站绑定,并且有可能成为一个贡献模块。我没有将模块与 Flems 绑定,以便在以后可以插入不同的代码沙箱机制。如果有足够多的人对创建一个贡献模块感兴趣,我很乐意花时间去做这件事。

该模块和相关配置与 Flems 没有紧密耦合,所以应该可以用一个功能类似的软件包替换它。只要存在三种不同文件的概念(即 js、html、css),JavaScript 组件就可以转换为一个插件。

如果你知道任何其他符合我所寻找模式的自托管代码沙箱,请告诉我。

我没有计划对我所有的旧文章进行修改并添加这个系统。我的主要目标是为未来一些关于 JavaScript 和 HTML/CSS 的文章创建这个系统,以便能够在页面中展示这些示例的实际使用情况。

从现在开始,我将创建一些包含 Flems 代码沙箱的前端示例。我坚信通过在内容中嵌入交互式示例,能让这个网站在其他教程网站和博客中脱颖而出。