生成深度嵌套 HTML 结构的更智能方法

Avatar of Ana Tudor
Ana Tudor

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

假设我们想要以下 HTML 结构

<div class='boo'>
  <div class='boo'>
    <div class='boo'>
      <div class='boo'>
        <div class='boo'></div>
      </div>
    </div>
  </div>
</div>

手动编写这真的太痛苦了。而这篇文章诞生的原因是,我看到它用 Haml 这样生成后感到恐惧

.boo
  .boo
    .boo
      .boo
        .boo

我看到的代码实际上有大约二十层嵌套,但也许有些人用手机阅读东西,所以我们不要用 boos 填充整个视窗,即使万圣节快到了。

正如您可能已经知道的那样,手动写出每一层远非理想,尤其是在 HTML 由预处理器(或来自 JavaScript,甚至来自 PHP 等后端语言)生成时。我个人不喜欢深度嵌套,我自己也使用得不多,但如果您无论如何都要这样做,那么我认为值得以一种扩展良好且易于维护的方式进行。

所以,让我们首先看看这个基本情况及其变体的更好解决方案,然后看看用这种深度嵌套做的一些有趣的事情!

基本解决方案

我们需要的是递归方法。例如,使用 Haml,以下代码段可以解决问题

- def nest(cls, n);
-  return '' unless n > 0;
-  "<div class='#{cls}'>#{nest(cls, n - 1)}</div>"; end

= nest('👻', 5)

那里有一个表情符号类,因为我们可以这样做,因为这只是一个有趣的小例子。我绝对不会在实际网站上使用表情符号类,但在其他情况下,我喜欢在我的代码中添加一些乐趣。

我们还可以生成 使用 Pug 的 HTML

mixin nest(cls, n)
  div(class=cls)
    if --n
      +nest(cls, n)

+nest('👻', 5)

然后还有 JavaScript 选项

function nest(_parent, cls, n) {
  let _el = document.createElement('div');
	
  if(--n) nest(_el, cls, n);

  _el.classList.add(cls);
  _parent.appendChild(_el)
};

nest(document.body, '👻', 5)

使用 PHP,我们可以使用类似的东西

<?php
function nest($cls, $n) {
  echo "<div class='$cls'>";
  if(--$n > 0) nest($cls, $n);
  echo "</div>";
}

nest('👻', 5);
?>

请注意,这些代码产生的主要区别与格式和空格有关。这意味着,使用 .👻:empty 针对最里面的“boo”将在 Haml、JavaScript 和 PHP 生成的 HTML 中起作用,但在 Pug 生成的 HTML 中将失败。

添加层级指示器

假设我们希望每个 boos 都有一个层级指示器,作为自定义属性 --i,然后可以用来为每个 boos 提供不同的 background,例如。

您可能会认为,如果我们只想改变色调,那么我们可以使用 filter: hue-rotate() 而不使用层级指示器。但是,hue-rotate() *不仅会影响色调,还会影响饱和度和亮度*。它也不能像使用我们自己的依赖于层级指示器 --i 的自定义函数那样提供相同级别的控制。

例如,这是我在最近的一个项目中使用的东西,以便让 background 组件从一个级别平滑地过渡到另一个级别($c 值是多项式系数)

--sq: calc(var(--i)*var(--i)); /* square */
--cb: calc(var(--sq)*var(--i)); /* cube */
--hue: calc(#{$ch0} + #{$ch1}*var(--i) + #{$ch2}*var(--sq) + #{$ch3}*var(--cb));
--sat: calc((#{$cs0} + #{$cs1}*var(--i) + #{$cs2}*var(--sq) + #{$cs3}*var(--cb))*1%);
--lum: calc((#{$cl0} + #{$cl1}*var(--i) + #{$cl2}*var(--sq) + #{$cl3}*var(--cb))*1%);

background: hsl(var(--hue), var(--sat), var(--lum));

调整 Pug 以添加层级指示器如下所示

mixin nest(cls, n, i = 0)
  div(class=cls style=`--i: ${i}`)
    if ++i < n
      +nest(cls, n, i)

+nest('👻', 5)

Haml 版本 也不太一样

- def nest(cls, n, i = 0);
-   return '' unless i < n;
-   "<div class='#{cls}' style='--i: #{i}'>#{nest(cls, n, i + 1)}</div>"; end

= nest('👻', 5)

使用 JavaScript,我们有

function nest(_parent, cls, n, i = 0) {
  let _el = document.createElement('div');

  _el.style.setProperty('--i', i);
	
  if(++i < n) nest(_el, cls, n, i);

  _el.classList.add(cls);
  _parent.appendChild(_el)
};

nest(document.body, '👻', 5)

使用 PHP,代码如下所示

<?php
function nest($cls, $n, $i = 0) {
  echo "<div class='$cls' style='--i: $i'>";
  if(++$i < $n) nest($cls, $n, $i);
  echo "</div>";
}

nest('👻', 5);
?>

更像树的结构

假设我们希望每个 boos 都有两个 boos 子节点,形成如下结构

.boo
  .boo
    .boo
      .boo
      .boo
    .boo
      .boo
      .boo
  .boo
    .boo
      .boo
      .boo
    .boo
      .boo
      .boo

幸运的是,我们不需要对基本 Pug 混合函数进行太多更改来实现这一点(演示

mixin nest(cls, n)
  div(class=cls)
    if --n
      +nest(cls, n)
      +nest(cls, n)

+nest('👻', 5)

Haml 版本 也是如此

- def nest(cls, n);
-   return '' unless n > 0;
-   "<div class='#{cls}'>#{nest(cls, n - 1)}#{nest(cls, n - 1)}</div>"; end

= nest('👻', 5)

JavaScript 版本 需要更多努力,但不多

function nest(_parent, cls, n) {
  let _el = document.createElement('div');
  
  if(n > 1) {
    nest(_el, cls, n - 1);
    nest(_el, cls, n - 1)
  }

  _el.classList.add(cls);
  _parent.appendChild(_el)
};

nest(document.body, '👻', 5)

使用 PHP,我们只需要在 if 块中再次调用 nest() 函数一次

<?php
function nest($cls, $n) {
  echo "<div class='$cls'>";
  if(--$n > 0) {
    nest($cls, $n);
    nest($cls, $n);
  }
  echo "</div>";
}

nest('👻', 5);
?>

以不同的方式为顶级元素设置样式

当然,我们可以为顶级添加一个特殊的 .top(或 .root 或类似的)类,但我更喜欢将其留给 CSS

:not(.👻) > .👻 {
  /* Top-level styles*/
}

小心!

某些属性,例如 transformfilterclip-pathmaskopacity,不仅会影响元素本身,还会影响其所有后代。有时这是预期的效果,也是嵌套这些元素优于它们作为兄弟元素的原因。

但是,其他时候它可能不是我们想要的,虽然可以反转 transform 的效果,有时甚至可以反转 filter 的效果,但我们对其他效果无能为力。例如,我们无法在元素上设置 opacity: 1.25 来弥补其父元素的 opacity: .8

例子!

首先,我们有这个我最近为 CodePen 挑战制作的纯 CSS 点加载器

在这里,缩放变换和动画旋转的效果在内部元素上累加,不透明度也是如此。

接下来是这个阴阳舞,它使用树状结构

对于每个项目,除了最外面的项目(:not(.☯️) > .☯️)外,直径等于其父元素直径的一半。对于最里面的项目(.☯️:empty,我想我们可以称之为树叶),background 还有两个额外的 radial-gradient() 层。就像第一个演示一样,动画旋转的效果在内部元素上累加。

另一个例子是这些旋转的糖果触手

每个同心圆都代表一个嵌套级别,并将所有祖先的动画旋转效果与其自身的效果相结合。

最后,我们有这个三角形开口演示(请注意,它使用的是单独的变换属性,例如 rotatescale,因此需要在 chrome://flags 中启用 实验性 Web 平台功能 标志才能在 Chromium 浏览器中看到它工作)

三角形开口(实时演示)。

它使用的是基本嵌套混合函数的略微修改版本,以便在每个级别上也设置 color

- let c = ['#b05574', '#f87e7b', '#fab87f', '#dcd1b4', '#5e9fa3'];
- let n = c.length;

mixin nest(cls, n)
  div(class=cls style=`color: ${c[--n]}`)
    if n
      +nest(cls, n)

body(style=`background: ${c[0]}`)
  +nest('🔺', n)

这里动画的是单独的变换属性 scalerotate。这样做是为了我们可以为它们设置不同的时间函数。