使用 Nunjucks 和 Grunt 实现基于组件的设计模式

Avatar of Morgan feeney
Morgan feeney 发表

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

最近,我参与了一个内部系统的创建,该系统用于构建 HTML 原型,旨在作为电子商务 CMS Hybris 的参考点。它最初很简单,使用 PHP。随着团队的壮大,代码库也随之增长,这最终导致代码变得杂乱无章,让我夜不能寐。

我们最初使用 PHP 是因为

  1. 包含。PHP 的 include() 函数允许创建可重用的代码块。例如:页眉、页脚和组件。
  2. 熟悉度。招聘的候选人需要具备 PHP 和内容管理系统(如 WordPress)的知识。
  3. 逻辑。例如 if () {} else {}。人们可以利用逻辑特性,或者选择编写纯 HTML,以适应不同技能的人员。
  4. 简单性。足够简单,可以让我们顺利开始。

从一开始,我们就努力构建一个有效的组件化系统。我们借鉴了诸如 原子设计 等系统的思路,以及 WordPress 和 Magento 等 CMS 如何实现模板化的方式。

PHP 中的设计模式

我们设计了一种方法来分割代码片段。我们称之为

  • 模板
  • 组件
  • 片段

模板是基于 PHP 文件的基本框架的网页。它们看起来像这样

<?php 
  require_once('../../_assets/classes/Prototype.class.php');
  $pageTitle = 'Boilerplate template | HTML';
  Prototype::getComponent('shared/head');
?>

<body>
  <?php Prototype::getComponent('shared/simple-header'); ?>
  <div id="main-content" class="container-outer">
    <div class="container-set-width">
      <div class="container-main">
          <!-- ADD YOUR STUFF HERE -->
      </div>
    </div>
  </div>
  <?php Prototype::getComponent('shared/footer'); ?>
  <?php Prototype::getComponent('shared/scripts'); ?>
</body>
</html>

组件指的是表单、轮播图以及更大块的可重用代码。理想情况下,组件应该是自包含的,并且可以放置在任何位置,而无需担心 CSS 继承会意外地影响其外观。

片段是任何小于组件的元素,例如产品图片或包含价格和产品标题的单个产品。

PHP 的不足之处

我们的 PHP 原型系统最主要缺乏的是模板继承。这导致代码库无序增长。有一些代码潜伏在周围,不再有任何用处,并且存在许多重复的文件。搜索和查找内容也是一种糟糕的体验。

PHP 最初满足了我们的需求,但最终我们用 Nunjucks 取代了它。Nunjucks 实现了不同类型的系统。

我们从 PHP 切换到 Nunjucks 的原因

  1. 模板继承。减少代码重复,并让我们能够维护更少的代码。
  2. 功能。由于 Nunjucks 具有内置功能(如 include、过滤器和宏),因此它优于我们尝试过的其他模板语言(如 Handlebars)。
  3. 文档。文档非常有用。它有助于让人们快速理解 Nunjucks,并提供可运行的示例。
  4. 交付成果。可以使用 Grunt 编译成 HTML。我们发现使用 PHP 来做这件事存在问题。
  5. 逻辑。我们仍然可以适应不同技能的人员,因为您仍然可以编写逻辑代码或纯 HTML。
  6. 简单性。

Nunjucks 中的设计模式

  1. 布局
  2. 模板
  3. 组件
  4. 部分

与之前使用 PHP 的列表相比,您会注意到这里新增了布局。部分是片段的另一种说法。

布局是 HTML 文件的基本框架,类似于模板,但这次只有一个文件用作基础,而不是每次都复制。如何实现?模板继承。引用 Nunjucks 文档

模板继承是一种简化模板重用的方法。编写模板时,您可以定义子模板可以覆盖的“块”。继承链可以任意长。

以下是一个Nunjucks 布局文件示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>{{ page_title }}</title>
    <meta name="description" content="{{ page_description }}">
    <link rel="stylesheet" href="{{ base_path }}css/style.css">
    <meta name="viewport" content="width=device-width, initial scale=1">
  </head>
  <body class="{{ body_classes }}">
  <article>
    <nav id="main-nav" class="container-fluid navbar navbar-dark bg-inverse {{ main_nav_classes }}">
      <div class="container">
        {% block nav %}
          {% include "components/nav.njk" %}
        {% endblock %}
      </div>
    </nav>
    <header id="main-header" class="container-fluid {{ main_header_classes }}">
      <div class="container">
        {% block header %}
          {% include "components/header.njk" %}
        {% endblock %}
      </div>
    </header>
    <section id="main-section" class="container-fluid {{ main_section_classes }}">
      <div class="container">
        {% block main %}
        <!-- This block (and any other block) can be replaced using template inheritance, or leave the default content in -->
          <h2>This `<h2>` tag appears by default, yet can be replaced</h2>
          <p class="lead">
            This `<p>` tag is also a default but can be replaced.
          </p>
        {% endblock %}
      </div>
    </section>
    <footer id="main-footer" class="container-fluid m-t-3 {{ main_footer_classes }}">
      <div class="container">
        {% block footer %}
          {% include "components/footer.njk" %}
        {% endblock %}
      </div>
    </footer>
  </article>
  {% block footer_scripts %}
    {% include "partials/footer-scripts.njk" %}
  {% endblock %}
  </body>
</html>

Nunjucks 和 Grunt 适用于该项目

我们的项目需求是特定的。它们需要某些东西。例如,业务需求之一是我们将 HTML 文件交付给离岸开发人员,以便将其实现到一些 Java 模板文件中。

当我们仍在使用 PHP 时,我们使用了几个 Grunt 插件:grunt-php-2-htmlgrunt-prettify来自动化此过程。但是,项目的结构经常会导致变量作用域出现问题。这会导致 PHP 错误嵌入到各个组件的 HTML 中。这可不是什么好事!然后,您需要打开文件并手动删除错误,这样就无法再自动化了,或者您最终会解决变量作用域出现问题的原因,而不是继续工作。这是一个在实施后不应该需要过多思考的过程

幸运的是,使用 grunt-nunjucks-2-html 意味着所有文件从一开始就编译成 HTML。为了查看前端,您需要查看一个 .html 文件。我们本质上将 Nunjucks 用作静态站点生成器。.njk 文件会被忽略,仅用于编译 HTML 文件,例如要交付的组件或用于查看的完整网页。这种语言和输出比旧的 PHP 系统更适合项目的需要。

使用 Nunjucks 保证代码一致性

我们的项目还有另一个方面与 Nunjucks 无关,但与设计模式息息相关。

我们使用 Bootstrap 框架(确切地说是 v3.3.4)。在团队中使用像 Bootstrap 这样的框架时,代码质量可能会出现问题。不应该出现这种情况,但有时约定并不总是得到遵守,对吧?Bootstrap 对于特定用例来说是一个很棒的资源,但并非所有用例都相同。文档非常棒,有很多示例,但有时当您与其他人一起工作并且框架被不同的人以不同的方式使用时,您可能会遇到麻烦。

如果想避免这种情况,您可以使用 Nunjucks 的另一个很棒的功能:。引用 Nunjucks

宏允许您定义可重用的内容块。它类似于编程语言中的函数。

以下是一个Nunjucks 宏示例

{% macro field(name, value='', type='text') %}
  <div class="field">
    <input type="{{ type }}" name="{{ name }}" value="{{ value | escape }}" />
  </div>
{% endmacro %}

现在 field 可以像普通函数一样调用了

{{ field('user') }}
{{ field('pass', type='password') }}

我们可以获取 Bootstrap 中的一些可重用部分,并将它们打包成可重用、可配置的代码块,为了本文的目的,我们称之为宏。

以下是一个Bootstrap 卡片宏示例

{% macro cardMacro (image, title, text, btnText="Button") %}
  <div class="card">
    <img class="card-img-top img-fluid" src="{{ image }}" alt="{{ title }}">
    <div class="card-block">
      <h4 class="card-title">{{ title }}</h4>
      <p class="card-text">{{ text }}</p>
      <a href="#" class="btn btn-primary">{{ btnText }}</a>
    </div>
  </div>
{% endmacro %}

每次使用时代码都完全相同。它相当于 Sass/Less 混合宏。

使用这种宏的一个好处是,无论谁使用它,代码都将始终保持一致。这是遵守标准的一种快速方法。另一个好处是,如果以后需要更新代码,您只需要在一个地方更新宏即可。

HTML 组件化及其在 CSS 框架中的应用。

想了解更多?如果您已经看到这里,那么也许您有类似的需求,并且希望了解更多关于如何使用 Nunjucks 和 Grunt 的信息。

我已经创建了一个名为:Bootstrap Patterns 的仓库,其中包含本文中使用的所有代码,以及更多帮助您入门的代码。我已经添加了您需要下载所需软件包的内容。由于我们使用的是 Grunt,因此您需要在您的机器上运行 Node。您应该具备使用任务运行器和 NPM 的基本知识。

仓库里有什么?

工作示例 展示了如何将 Nunjucks、Grunt 和 CSS 框架(本例中为 Bootstrap)组合在一起。它演示了如何将相同的可重用设计模式组合起来以实现多种变化。

让我们看看每个设计模式级别

1) 布局

文件 `layouts/layout.njk` 包含一个可重用的页面结构。我已经预料到使用 Bootstrap 创建网页所需的所有常见内容。例如,有一些用于元信息的变量:

<title>{{ page_title }}</title>

诸如以下的变量类:

<body class="{{ body_classes }}">

布局所需的通用 Bootstrap 类,例如 .container.container-fluid,添加到导航、标题、部分、页脚等元素中,以及 Nunjucks 变量、块和包含;以实现完全的灵活性。

<header id="main-header" class="container-fluid {{ main_header_classes }}">
  <div class="container">
    {% block header %}
      {% include "components/header.njk" %}
    {% endblock %}
  </div>
</header>

2) 组件

在文件:`components/nav.njk` 中,为了创建可重用模式,使用了以下 Nunjucks 功能:

  • 导入
  • 循环
  • 过滤器
{% import "macros/macro-search.njk" as macroSearch %}

<div class="nav navbar-nav">
  {% for item in navItems %}
    <a class="nav-item nav-link" href="{{ item.menu_item | lower | replace(" ", "") }}.html">{{ item.menu_item }}</a>
  {% endfor %}
  {% block navRight %}
    {{ macroSearch.search() }}
  {% endblock %}
</div>

此文件演示了如何组合 Nunjucks 的几个功能。我创建了一个宏:`macros/macro-search.njk`,我们将其导入 `components/nav.njk`,因为我们想在导航中添加一个搜索表单。

{% import "macros/macro-search.njk" as macroSearch %}

我们遍历一些名为 navItems 的 JSON 数据对象,这些对象用于构建导航,然后使用过滤器将 URL 转换为小写,并去除不需要的空格,以便链接有效。

{% for item in navItems %}
  <a class="nav-item nav-link" href="{{ item.menu_item | lower | replace(" ", "") }}.html">{{ item.menu_item }}</a>
{% endfor %}

3) 宏

我们在块标签中调用搜索表单宏,以防我们以后不想在该组件中显示搜索表单。

{% block navRight %}
  {{ macroSearch.search() }}
{% endblock %}

4) 模板

在模板中,我们这样 **扩展** 布局:

{% extends "layouts/layout.njk" %}

这使我们能够多次继承相同的布局,而无需在更多文件中重写代码。

…然后,**导航** 组件被调用到布局文件中的块标签中。

<nav id="main-nav" class="container-fluid navbar navbar-dark bg-inverse {{ main_nav_classes }}">
  <div class="container">
    {% block nav %}
      {% include "components/nav.njk" %}
    {% endblock %}
  </div>
</nav>

默认情况下,我将导航包装在块标签中。此默认值可以保留,也可以通过模板更改。

5) 部分

在本示例中,我唯一使用部分的地方是页脚中的一些脚本标签。但顾名思义,它是一段代码的一部分……它可以是任何小东西。

{% block footer_scripts %}
  {% include "partials/footer-scripts.njk" %}
{% endblock %}

最后

我在本文中提供的示例只是触及了可以实现的功能的表面。仓库中还有更多示例,所以请务必查看,并随时告诉我您的想法。

我要特别感谢 N.Brown 集团有限公司 UI 团队的成员,这些想法正是在那里产生的,这真是太棒了!