Drupal 11:使用 Storybook 预览单目录组件
单目录组件(SDC)由一组文件组成,这些文件协同工作,能在 Drupal 网站上创建一个小型组件。该组件涵盖了以一致方式显示某些内容所需的所有模板、样式、脚本和图像。
不同的 SDC 可以相互嵌套,这意味着可以通过不同组件协同工作来生成内容,进而构建一个网站。
SDC 的强大之处在于其自包含性。若要构建一个复杂的组件,在小部件中显示数据,将其构建为 SDC 能确保每次包含该组件时,其外观和功能都保持一致。
Storybook 是一个 JavaScript 应用程序,为组件开发提供了一个前端工作区。这意味着我们能在组件用于网站之前对其进行开发和预览。通过使用一个模块,我们可以在 Drupal 主题中构建组件,然后在将其注入 Drupal 模板之前,在 Storybook 中对其进行预览。在 Drupal 开发中,这种方式能极大提高开发效率。
在本文中,我公司将介绍如何在 Drupal 中创建一个 SDC,然后使用 Storybook 来创建和预览该组件。这对于 Drupal 模块开发和 Drupal 升级到 Drupal11 都有一定的参考价值。
首先,让我们创建一个 SDC,用作此应用程序的示例。
一、创建单目录组件
要在 Storybook 中预览 SDC,我们首先需要创建一个 SDC,在本文的其余部分将以此为例。假设你有一个自定义主题,可以在其中构建 SDC。
这里我们不会深入探讨 SDC,因为这可能是一个很大的主题,所以我们只创建所需的元素。如果你需要更多信息,官方的 Drupal SDC 文档实际上非常不错。还有一本《Drupal 10 主题开发》书籍,其中有关于在 Drupal 中构建和使用 SDC 的全面指南。
在这个例子中,我公司将创建一个简单的作者组件,用于显示文章作者的姓名、简介和头像。以下是该组件正常运行所需的文件。
author.component.yml 文件将作者的 URL 定义为一个属性,并定义了几个插槽,用于传入姓名、简介和头像信息。
name: 作者
description: "显示作者信息"
props:
type: object
properties:
author_url:
type: string
title: 作者 URL
examples:
- /author/philipnorton42
slots:
name:
title: "姓名"
bio:
title: "简介"
avatar:
title: "头像"
关于 props 和 slots 的区别已经有很多论述,但就这个例子而言,props 只是作者的 URL,而 slots 是其他所有内容。我们需要将头像和简介作为插槽传递,因为这些内容可能包含 HTML,甚至可以作为可渲染项呈现。
在主 author.twig 文件中,我们创建一些简单的标记,并添加 author.component.yml 文件中定义的属性。这里的块很重要,但只有在我们为 Storybook 设置故事时才会发挥作用,这将在本文后面介绍。
<aside class="author">
<div>
{% block avatar %}
{{ avatar }}
{% endblock %}
</div>
<div class="author_bio">
<p>
<a href="{{ author_url }}" rel="bookmark">
<span>{{ name }}</span>
</a>
</p>
{% block bio %}
{{ bio }}
{% endblock %}
</div>
</aside>
作者组件的 CSS 文件存储在 author.css 中,相对简单。它只是创建了一个带有阴影的区域和一个包含左右两部分的弹性容器。
aside.author {
clear: both;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 10%), 0 4px 6px -2px rgba(0, 0, 0, 10%);
padding: 1rem;
margin-bottom: 2rem;
margin-top: 2rem;
display: flex;
aside.author:first-child {
width: 20%;
text-align: center;
img {
max-width: 100%;
height: auto;
}
}
aside.author:last-child {
width: 80%;
}
div.author_bio {
margin-left: 1rem;
p {
margin-top: 0;
}
}
}
这三个文件都存放在一个名为 author 的目录中,该目录位于一个名为 my_theme 的主题内。
二、Storybook 模块
Drupal 和 Storybook 之间的连接由 Drupal Storybook 模块提供。该模块提供了所需的 Drush 命令,可用于创建 Storybook 所需的文件,并且有多个端点允许 Storybook 获取并渲染组件。在 Drupal 升级到 Drupal11 的过程中,合理使用该模块能更好地进行 Drupal 开发和 Drupal 模块开发。
让它们协同工作的第一步是引入 Storybook 模块。
composer require drupal/storybook
然后像安装其他模块一样安装它。
drush pm:install storybook
现在,对于每个要在 Storybook 中显示的组件,我们需要创建一个 <组件名称>.stories.twig 文件。故事通过 {% stories %} Twig 标签定义,每个故事在 {% stories %} 内部的 {% story %} Twig 标签中定义。
一个典型的故事文件基本结构如下。
{% stories componentName with { title: 'Components/ComponentName' } %}
{% story default with {
name: '1. 默认',
args: {
some_property: '一个值'
}
} %}
{% embed 'my_theme:componentName' %}
{% endembed %}
{% endstory %}
{% endstories %}
故事部分的标题用于在 Storybook 左侧菜单栏中显示故事。每个故事的名称用于在同一菜单栏中命名故事,由于第一个故事总是最先显示,因此通常称为“默认”。
在故事的 args 部分设置的任何参数都会传递给 Storybook 应用程序,这允许用户在 Storybook 界面中调整这些参数。
你可以选择使用 with 属性将变量传递给 embed 标签。我们使用 args 属性创建的任何变量都可以自动在 embed 语句内的模板中使用,但这是一种为模板添加额外(静态)覆盖的有用方法。
{% embed 'my_theme:componentName' with {another_property: '另一个值'} %}
{% endembed %}
如果你的组件没有 HTML 元素,你可以使用 include Twig 标签稍微简化故事。这会将参数传递给模板,但 Twig 会对任何字符串进行转义,任何标记都会丢失。如果没有 HTML 传递给 Twig 模板,这是一种理想的情况。
{% story default with {
name: '1. 默认',
args: {
some_property: '一个值'
}
} %}
{{ include('my_theme:componentName', {
some_property
}) }}
{% endstory %}
这种结构的好处是,如果我们想展示组件以不同方式使用的情况,可以在 *.stories.twig 文件中添加额外的示例。这可以通过为每个要添加的示例添加一个额外的 {% story %} 标签来实现。
在下面的示例中,我们为故事添加第二个示例,展示组件带有更长标题的情况,以及作为列表元素的一部分(这只是一个愚蠢的示例,用于展示组件在不同标记中的情况)。
{% stories componentName with { title: 'Components/ComponentName' } %}
{% story default with {
name: '1. 默认',
args: {
some_property: '一个值'
}
} %}
{% embed 'my_theme:componentName' %}
{% endembed %}
{% endstory %}
{% story wrappedComponent with {
name: '2. 包装组件',
args: {
some_property: '一个稍微长一点的标题,如果我们的样式设置不正确,可能会导致设计出现问题。'
}
} %}
<ul>
<li>
{% embed 'my_theme:componentName' %}
{% endembed %}
</li>
</ul>
{% endstory %}
{% endstories %}
这使我们能够添加组件使用不同属性或不同周围(或注入)HTML 标记的不同示例。如果我们有一个接受另一个组件作为内容的组件,我们可以提供展示该组件如何与不同注入标记进行交互的故事。
对于我们的作者组件,我们创建了一个名为 author.stories.twig 的文件,因为我们要向模板传递 HTML,所以需要使用 embed Twig 标签。这就是 Twig block 标签发挥作用的地方。为了将故事 args 部分的值传递给模板,我们需要覆盖这些块,并将参数以原始形式注入模板。这使我们能够在 Storybook 参数中使用 HTML,而无需在模板本身中添加原始输出,这样做可能不安全。这也允许参数在 Storybook 界面中使用。
作者组件的完整故事文件如下所示。
{% stories author with { title: 'Components/作者' } %}
{% story default with {
name: '1. 默认',
args: {
author_url: '/author/philipnorton42',
name: '菲尔·诺顿',
bio: '<p>菲尔是 #! code 的创始人兼管理员,是一名在英国西北部工作的 IT 专业人士。</p>',
avatar: '<img loading="lazy" src="https://picsum.photos/id/237/480/480" width="480" height="480" alt="测试图片" class="image-style-medium">'
}
} %}
{% embed 'my_theme:author' %}
{% block avatar %}
{{ avatar|raw }}
{% endblock %}
{% block bio %}
{{ bio|raw }}
{% endblock %}
{% endembed %}
{% endstory %}
{% endstories %}
这里我公司使用 https://picsum.photos/ 生成测试图片,而不是使用任何真实内容,但图片大小将与 Drupal 中生成的图片相同。
一旦你设置好故事,就可以运行 Drush 命令来生成所有故事。Storybook 本身无法使用 <组件名称>.stories.twig 文件,需要一个 <组件名称>.stories.json 文件将其与 Drupal 链接起来。这个文件只包含有关现有参数的信息,但主要的 <组件名称>.stories.twig 文件仍然用于渲染模板。
我们可以使用 storybook:generate-stories 命令为单个组件生成故事,需要提供组件的路径(相对于 Drupal 网站根目录)。
drush storybook:generate-stories themes/custom/my_theme/components/author/author.stories.twig
或者,我们可以一次性生成所有故事。
drush storybook:generate-all-stories
这个命令会为主题中的每个 <组件名称>.stories.twig 文件创建一个 <组件名称>.stories.json 文件。我公司发现即使有 40 多个组件,这个命令也只需要几秒钟就能运行完,并且只有当 Twig 文件自上次生成后有更新时,才会更新故事文件。
注意:不要将这些 *.stories.json 文件提交到你的仓库,因为它们是使用当前网站的 URL 生成的。这意味着 Storybook 将通过你的 Drupal 安装绝对引用文件。实际上,一个好的做法是在你的主题中添加一个 .gitignore 文件,以防止这些文件被提交。
# 忽略所有故事文件。
*.stories.json
如果你想将 Storybook 作为一个托管应用程序使用,那么在部署时需要构建 Storybook JSON 文件。
你可以使用以下命令监视 Twig 文件的更改并自动运行此命令。
watch --color drush storybook:generate-all-stories
如果你正在积极使用 SDC 和 Storybook 进行开发,那么需要开启 Twig 开发模式,可以使用以下 Drush 命令来实现。
drush state:set twig_debug 1
drush state:set twig_cache_disable 1
drush state:set disable_rendered_output_cache_bins 1
这样做可以使主题开发更加容易,因为它可以防止 Twig 模板被缓存,还会在主题中添加 HTML 注释,以显示正在使用的模板。
最后,Storybook 模块有一个名为 render storybook stories 的权限,渲染故事需要此权限。以下命令将为匿名用户激活此权限。
drush role:perm:add anonymous 'render storybook stories'
确保在生产环境中不要为匿名用户开启此权限。如果你确实在生产环境中激活了该模块。
完成模块设置和组件配置后,现在我们来看看如何安装 Storybook。
三、安装 Storybook
Storybook 是一个独立的应用程序,它会向 Drupal 发送请求,以渲染你使用故事文件配置的组件。虽然可以在 Drupal 项目的根目录中安装 Storybook,但我公司 强烈 建议为 Storybook 单独创建一个目录。这样可以防止 Storybook 成为项目的硬依赖,并避免干扰其他可能出于编码标准目的而需要安装在项目根目录中的包,如 ESLint。
Drupal Storybook 模块的文档提到使用 Yarn 安装 Storybook,但在本文中,我公司将介绍使用 npm 安装和运行 Storybook。
创建一个名为 storybook 或 tests 的目录;只要是 Storybook 可以存放的地方即可。然后,使用以下命令安装 Storybook。
npm init -y
npx storybook init --type server
这将安装 Storybook 并在本地环境中启动它;它还会在你的 package.json 文件中添加一些配置项。
最终你会得到一个类似以下内容的 package.json 文件。
{
"name": "tests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@storybook/addon-docs": "^9.1.8",
"@storybook/addon-webpack5-compiler-swc": "^4.0.1",
"@storybook/server-webpack5": "^9.1.8",
"storybook": "^9.1.8"
}
}
Storybook 自带了一些默认的测试故事,如果你想将 Storybook 用作主题开发服务,这些故事可以帮助你入门。但我们对此不感兴趣,所以需要告诉 Storybook 我们的组件所在的位置。
打开新创建的 .storybook 目录下的 main.js 文件,确保 "stories" 配置项如下所示。
"stories": [
"../../web/themes/custom/my_theme/components/**/*.stories.@(json|yaml|yml)"
],
这告诉 Storybook 在我们使用 Storybook Drupal 模块创建的主题中查找故事。
你还可以删除 tests 目录下的 "stories" 目录,因为其中包含一些用于帮助你入门的 Storybook 示例组件。我们现在已经忽略了它们,但删除它们可以防止未来将它们与网站相关的设计内容混淆。
四、跨域资源共享(CORS)缓解
在使用 Storybook 之前,你需要允许 Storybook 与你的 Drupal 网站进行通信。如果不执行此步骤,你会在 Storybook 网站的控制台中看到 CORS 错误,这将导致其无法正常工作。你的 Storybook 网站可能只会显示组件的空白页面,或者甚至显示来自 Storybook 的错误消息。
要绕过 CORS 规则,你需要在 Drupal 网站的 development.services.yml 文件中添加以下参数。
parameters:
storybook.development: true
cors.config:
enabled: true
allowedHeaders: [ '*' ]
allowedMethods: [ '*' ]
allowedOrigins: [ '*' ]
exposedHeaders: false
maxAge: false
supportsCredentials: true
然后,必须在 settings.php 文件中引用该服务文件。
$settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
不建议在生产环境中运行 Storybook,因此此设置仅适用于本地和开发/暂存网站。
五、运行 Storybook
最后,我们可以运行 Storybook 并查看我们的工作成果。
有几种方法可以实现这一点,但我们首先关注在本地运行应用程序。假设你已经安装了 Node,你可以切换到 tests 目录(或你安装 Storybook 的任何目录)并运行以下命令。
npm run storybook
这将运行 Storybook 应用程序,并自动打开一个浏览器窗口。
你应该会看到我们之前配置的故事的预览。
我们在 <组件名称>.story.twig 文件中规定的参数会在预览窗口下方的 Storybook 界面中显示为选项。这使我们能够更改组件中的值,以查看不同的参数和选项如何影响组件的表现。通过这种方式,我们可以在不刷新页面的情况下,使用不同的内容或设置测试组件。
这是因为 Storybook 创建了一个指向 Drupal 中 Storybook 模块端点的 iframe。这样做允许 Drupal 返回组件所需的标记以及任何 CSS 和 JavaScript,以确保组件正常工作。
此外,由于组件是使用包含它的主题进行渲染的,因此你还可以获得与该组件关联的所有主题文件。这意味着主题设置的任何全局样式都将应用于组件,因此你可以假设组件在主题中完全渲染时的外观与此完全相同。
如果你在 Storybook 中查看故事时遇到空白页面,请检查你的 Drupal 日志以获取更多信息。有时,通过 Storybook 与组件的交互可能会导致错误,这些错误不会在 Storybook 中显示,但 Drupal 会记录这些错误。
六、在 DDEV 中运行 Storybook
一种流行的运行 Storybook 的方式是使用 DDEV 作为包装器。这是有道理的,因为如果你使用 DDEV,就不需要在本地环境中安装 Node。
然而,要让 Storybook 在 DDEV 中运行,你需要做一些更改。
首先,编辑你的 .ddev/config 文件,并添加以下配置行。这告诉 DDEV 打开端口 6006 和 6007 供 Storybook 使用,并且 node.js 守护进程应该监视 package.json 文件;这样可以确保 Docker 容器在我们需要时保持运行。
web_extra_exposed_ports:
- name: storybook
container_port: 6006
http_port: 6007
https_port: 6006
web_extra_daemons:
- name: node.js
command: "tail -F package.json > /dev/null"
directory: /var/www/html/tests
由于我们希望在不打开它的情况下将 Storybook 作为服务运行,因此还需要更改 .storybook 目录中的 package.json 文件,以更新 Storybook 的启动方式。打开文件并在 storybook 命令末尾添加指令 --no-open。
"scripts": {
"storybook": "storybook dev -p 6006 --no-open",
"build-storybook": "storybook build"
},
现在,你可以运行以下命令在 DDEV 中启动 Storybook 应用程序。
ddev exec "cd tests && npm run storybook"
然后你可以通过地址 https://<你的 DDEV 站点>.ddev.site:6006 查看。
如果以这种方式运行,Storybook 与 Drupal 之间的通信可能会出现问题。最大的问题可能是由于同时使用 http 和 https 导致的混合内容问题。如果你遇到问题,可以尝试使用 --uri 标志运行 storybook:generate-all-stories 命令,以强制使用正确的域名和协议。
ddev drush storybook:generate-all-stories --uri=https://drupaldev.ddev.site/
还可以修改 Storybook 中的 preview.js 文件,使其使用正确的域名和协议指向你的站点。
/** @type { import('@storybook/server-webpack5').Preview } */
const preview = {
parameters: {
server: {
url: `https://<你的 DDEV 站点>.ddev.site/storybook/stories/render`,
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
进行这些更改后,你应该能够使一切正常工作。
七、构建和托管 Storybook
我们可以构建 Storybook 所需的文件,并将其作为一个独立的网站提供服务,而不是将 Storybook 作为应用程序运行。
要构建 Storybook 文件,在 tests 目录中运行以下命令。
npm run build-storybook
这将在一个名为 storybook-static 的目录中创建 Storybook 所需的文件,但如果没有指向这些文件的 Web 服务器,你将无法对这些文件进行任何操作(因为它们位于 Web 根目录之外)。
location ^~ /storybook-static {
add_header "Access-Control-Allow-Origin" *;
add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS";
disable_symlinks off;
alias /your/project/directory/tests/storybook-static;
index index.html;
}
这允许你将 Storybook 作为一个独立的网站部署和提供服务,与你的开发环境并列。如果你要向客户展示你的组件,这是一种非常有用的技术,因为这意味着在组件获得批准之前,你不需要将它们集成到主题中。
在 DDEV 中托管 Storybook
如果你使用 DDEV 并想使用这种方法,可以按以下方式运行构建命令。
ddev exec "cd tests && npm run build-storybook"
接下来,你需要更新 .ddev/nginx_full/nginx-site.conf 中的 Nginx 配置。删除该文件顶部的 #ddev-generated 行,并在 Nginx 配置中添加以下指令。
location ^~ /storybook-static {
add_header "Access-Control-Allow-Origin" *;
add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS";
disable_symlinks off;
alias /var/www/html/tests/storybook-static;
index index.html;
}
运行 ddev restart 后,你应该能够在 https://<你的 DDEV 站点>.ddev.site/storybook-static 位置看到 Storybook。
八、总结
现在,我们已经在 Storybook 中预览了作者组件,并确认其正常工作,我们可以将其添加到我们的主题中。我公司在开头创建的作者组件现在在主题中使用如下所示(在作者最小视图模式模板中)。
{{ include('hashbangcode_theme:author', {
author_url: url,
name: label,
bio: content.field_author_short_bio,
avatar: content.field_author_avatar
}) }}
如果你要使用单目录组件进行构建,那么也应该使用 Storybook。在 Storybook 中预览和操作组件的方式意味着你可以在问题出现之前就发现它们。能够在故事中添加其他标记也意味着你可以预览组件在页面上的呈现方式,这对于包含其他组件的组件来说至关重要。
你应该独立构建 SDC,但如果你发现两个组件之间存在奇怪的交互,你可以创建一个故事来模拟这种交互并解决任何问题。这为视觉回归测试提供了一个很好的基础。
你的开发工作流程应该是先构建组件的骨架,并立即为该组件创建一个故事。然后你可以在 Storybook 中完善组件,准备好使用时再将其添加到主题中。
如果你有一个已经包含组件但没有使用 Storybook 的主题,那么你绝对应该考虑使用这个系统。为组件添加故事并不是一项艰巨的任务,每个组件可能只需要 10 - 15 分钟。
我公司在一个大型项目中使用了 Storybook,该项目有很多小部件需要以非常特定的方式进行样式设置。我公司使用 Storybook 构建了一组 SDC,以确保样式正确,当它们被添加到页面上时,它们完美地工作。经过数小时的努力让组件看起来美观后,只需将其添加到网站并看到它完美运行,这是非常有成就感的。
我公司还使用 Storybook 向客户展示组件。事实上,不止一次演示只是在 Storybook 中展示组件,并在将其添加到 Drupal 之前对组件进行更改。通过在开发网站旁边托管 Storybook,你也可以让客户与他们的内部股东一起进行同样的操作。
Storybook 最常见的问题是如何让它与 Drupal 良好协作。CORS 错误和混合内容模式可能是导致 Storybook 和 Drupal 出现问题的最常见原因。我公司还发现,Storybook 有时会大量缓存输出,特别是在右侧菜单栏中,因此更新组件有时会有点麻烦。
Storybook 是一个独立的应用程序,因此需要像其他依赖项一样进行更新和维护。然而,通过将其安装在单独的目录中,我们可以防止它对我们的 Drupal 网站产生影响。Storybook 应用程序由开发人员维护,所以它不是一个即将被淘汰的旧应用程序。在撰写本文时,Storybook 的最新版本是 9.1.8,于 5 天前发布。我公司认为维护这个应用程序的好处大于为组件创建故事所花费的时间。
这不是在 Drupal 中预览 SDC 的唯一方法,下次我们将探讨如何使用 SDC - 组件库模块来预览 SDC。


