JavaScript 类,如何在实践中应用 "Separation of Concerns" 和 "Don't repeat Yourself"(DRY)

JavaScript classes, how does one apply "Separation of Concerns" and "Don't repeat Yourself" (DRY) in practice

我正在学习 JavaScript classes 是如何工作的,我只是在寻找一些关于如何实现一些非常简单的东西的建议,我希望能为一些元素设置动画。

我创建了一个名为 myAnimation 的 class,构造函数接受 1 个参数,它是一个元素。它所做的只是淡出和淡入淡出,一切都非常简单。当页面上只有一个标题元素时它工作正常,我只是不确定如何让它与多个标题一起工作。

请原谅我的幼稚;这对我来说都是全新的,这只是我设法让自己尝试帮助自己理解其工作原理的一个基本示例。

class myAnimation {
  constructor(element) {
    this.element = document.querySelector(element);
  }
  fadeOut(time) {
    if (this.element.classList.contains('fadeout-active')) {
      this.element.style.opacity = 1;
      this.element.classList.remove('fadeout-active');
      button.textContent = 'Hide Heading';
    } else {
      this.element.style.opacity = 0;
      this.element.style.transition = `all ${time}s ease`;
      this.element.classList.add('fadeout-active');
      button.textContent = 'Show Heading';
    }
  }
}

const heading = new myAnimation('.heading');
const button = document.querySelector('.button');
button.addEventListener('click', () => {
  heading.fadeOut(1);

});
<div class="intro">
  <h1 class="heading">Intro Heading</h1>
  <p>This is the intro section</p>
  <button class="button">Hide Heading</button>
</div>

<div class="main">
  <h1 class="heading">Main Heading</h1>
  <p>This is the main section</p>
  <button class="button">Hide Heading</button>
</div>

这是因为 document.querySelector(".button") 只有 returns 第一个元素 class .button (reference).

您可能想尝试 document.querySelectorAll(".button") (reference) 添加您的事件侦听器。

(虽然这只会切换您的第一个标题 - 出于同样的原因。;))

在我发表评论后,我想以一种我认为可能是 OP 意图的方式制作脚本 运行。

尽管它演示了需要做什么才能正确 运行,但整个基础设计证明不适合 OP 真正可能需要实现的目标。

class 被称为 Animation,但从一开始它就混合了元素动画和改变单个以某种方式在全局范围内的状态 button

即使现在 运行ning,该设计并不能证明是真正合适的,因为现在将要设置动画的元素和它将与之交互的按钮一起传递到构造函数中。

功能分组正确,只是位置和命名不太合适。

OP 可能会考虑所提供代码的下一个迭代步骤...

class Animation {
  constructor(elementNode, buttonNode) {

    this.element = elementNode;
    this.button = buttonNode;

    // only in case both elements were passed ...
    if (elementNode && buttonNode) {
    
      // couple them by event listening/handling.
      buttonNode.addEventListener('click', () => {

        // - accessing the `Animation` instance's `this` context
        //   gets assured by making use of an arrow function.
        this.fadeOut(1);
      });
    }
  }
  fadeOut(time) {
    if (this.element.classList.contains('fadeout-active')) {

      this.element.style.opacity = 1;
      this.element.classList.remove('fadeout-active');

      this.button.textContent = 'Hide Heading';
    } else {
      this.element.style.opacity = 0;
      this.element.style.transition = `all ${time}s ease`;
      this.element.classList.add('fadeout-active');

      this.button.textContent = 'Show Heading';
    }
  }
}

function initializeAnimations() {
  // get list of all elements that have a `heading` class name.
  const headingList = document.querySelectorAll('.heading');

  // for each heading element do ...
  headingList.forEach(function (headingNode) {
    // ... access its parent element and query again for a single button.
    const buttonNode = headingNode.parentElement.querySelector('.button');
    
    // if the related button element exists ...
    if (buttonNode) {

      // ... create a new `Animation` instance.
      new Animation(headingNode, buttonNode);
    }
  });
}

initializeAnimations();
.as-console-wrapper { max-height: 100%!important; top: 0; }
<div class="intro">
  <h1 class="heading">Intro Heading</h1>
  <p>This is the intro section</p>
  <button class="button">Hide Heading</button>
</div>

<div class="main">
  <h1 class="heading">Main Heading</h1>
  <p>This is the main section</p>
  <button class="button">Hide Heading</button>
</div>

...新的一天,下一个可能的迭代步骤...

第二次迭代分离关注点。

它通过重命名 class 并仅实现 class 特定行为来实现。因此 FadeToggle class 仅提供 切换特定 功能。

然后代码被分成两个处理初始化的函数。为了更好地重用初始化代码和 html 结构需要重构为更通用的东西。每个容器的 data 属性具有用于淡化目标元素的触发元素,将用作配置存储,为初始化过程提供所有必要的信息。 (甚至可以提供单独的过渡持续时间值。)

最后有一个处理函数,它的实现方式可以被 bind 重复使用,以生成一个闭包,为每个 触发目标提供所有必要的数据一对.

class FadeToggle {
  // a clean fade-toggle implementation.
  constructor(elementNode, duration) {

    duration = parseFloat(duration, 10);
    duration = Number.isFinite(duration) ? duration : 1;

    elementNode.style.opacity = 1;
    elementNode.style.transition = `all ${ duration }s ease`;

    this.element = elementNode;
  }

  isFadeoutActive() {
    return this.element.classList.contains('fadeout-active');
  }

  toggleFade(duration) {
    duration = parseFloat(duration, 10);
    if (Number.isFinite(duration)) {

      this.element.style.transitionDuration = `${ duration }s`;
    }
    if (this.isFadeoutActive()) {

      this.element.style.opacity = 1;
      this.element.classList.remove('fadeout-active');
    } else {
      this.element.style.opacity = 0;
      this.element.classList.add('fadeout-active');
    }
  }
}

function handleFadeToggleWithBoundContext(/* evt */) {
  const { trigger, target } = this;

  if (target.isFadeoutActive()) {
    trigger.textContent = 'Hide Heading';
  } else {
    trigger.textContent = 'Show Heading';
  }
  target.toggleFade();
}

function initializeFadeToggle(elmNode) {
  // parse an element node's fade-toggle configuration.
  const config = JSON.parse(elmNode.dataset.fadeToggleConfig || null);

  const selectors = (config && config.selectors);
  if (selectors) {
    try {
      // query both the triggering and the target element
      const trigger = elmNode.querySelector(selectors.trigger || null);
      let target = elmNode.querySelector(selectors.target || null);

      if (trigger && target) {

        // create a `FadeToggle` target type.
        target = new FadeToggle(target, config.duration);

        // couple trigger and target by event listening/handling ...
        trigger.addEventListener(
          'click',
          handleFadeToggleWithBoundContext.bind({
            // ... and binding both as context properties to the handler.
            trigger,
            target
          })
        );
      }
    } catch (exception) {
      console.warn(exception.message, exception);
    }
  }
}

function initializeEveryFadeToggle() {
  // get list of all elements that contain a fade-toggle configuration
  const configContainerList = document.querySelectorAll('[data-fade-toggle-config]');

  // do initialization for each container separately.
  configContainerList.forEach(initializeFadeToggle);
}

initializeEveryFadeToggle();
.as-console-wrapper { max-height: 100%!important; top: 0; }
<div class="intro" data-fade-toggle-config='{"selectors":{"trigger":".button","target":".heading"},"duration":3}'>
  <h1 class="heading">Intro Heading</h1>
  <p>This is the intro section</p>
  <button class="button">Hide Heading</button>
</div>

<div class="main" data-fade-toggle-config='{"selectors":{"trigger":".button","target":".heading"}}'>
  <h1 class="heading">Main Heading</h1>
  <p>This is the main section</p>
  <button class="button">Hide Heading</button>
</div>

...下午,完善状态变化的处理...

还有硬连线数据,直接写入代码。为了摆脱每次发生切换更改时都会(重新)呈现的字符串值,可能会给基于 data 的配置方法再一次机会。

这一次,每个触发元素都可能具有提供状态相关值的配置。因此,初始化过程需要负责检索此数据,并根据淡入淡出切换目标的初始状态渲染它。

这个目标直接提出了触发器元素的渲染函数的必要性,因为不仅需要在初始时更改触发器的状态,而且在每次淡入淡出切换时也需要更改。

这将再次改变处理函数,此外它还具有绑定状态值,以便将此类数据委托给渲染进程...

class FadeToggle {
  // a clean fade-toggle implementation.
  constructor(elementNode, duration) {

    duration = parseFloat(duration, 10);
    duration = Number.isFinite(duration) ? duration : 1;

    elementNode.style.opacity = 1;
    elementNode.style.transition = `all ${ duration }s ease`;

    this.element = elementNode;
  }

  isFadeoutActive() {
    return this.element.classList.contains('fadeout-active');
  }

  toggleFade(duration) {
    duration = parseFloat(duration, 10);
    if (Number.isFinite(duration)) {

      this.element.style.transitionDuration = `${ duration }s`;
    }
    if (this.isFadeoutActive()) {

      this.element.style.opacity = 1;
      this.element.classList.remove('fadeout-active');
    } else {
      this.element.style.opacity = 0;
      this.element.classList.add('fadeout-active');
    }
  }
}

function renderTargetStateDependedTriggerText(target, trigger, fadeinText, fadeoutText) {
  if ((fadeinText !== null) && (fadeoutText !== null)) {
    if (target.isFadeoutActive()) {

      trigger.textContent = fadeinText;
    } else {
      trigger.textContent = fadeoutText;
    }
  }
}

function handleFadeToggleWithBoundContext(/* evt */) {
  // retrieve context data.
  const { target, trigger, fadeinText, fadeoutText } = this;

  target.toggleFade();

  renderTargetStateDependedTriggerText(
    target,
    trigger,
    fadeinText,
    fadeoutText
  );
}

function initializeFadeToggle(elmNode) {
  // parse an element node's fade-toggle configuration.
  let config = JSON.parse(elmNode.dataset.fadeToggleConfig || null);

  const selectors = (config && config.selectors);
  if (selectors) {
    try {
      // query both the triggering and the target element
      const trigger = elmNode.querySelector(selectors.trigger || null);
      let target = elmNode.querySelector(selectors.target || null);

      if (trigger && target) {

        // create a `FadeToggle` target type.
        target = new FadeToggle(target, config.duration);

        // parse a trigger node's fade-toggle configuration and state.
        const triggerStates = ((
          JSON.parse(trigger.dataset.fadeToggleTriggerConfig || null)
          || {}
        ).states || {});

        // get a trigger node's state change values.
        const fadeinStateValues = (triggerStates.fadein || {});
        const fadeoutStateValues = (triggerStates.fadeout || {});

        // get a trigger node's state change text contents.
        const fadeinText = fadeinStateValues.textContent || null;
        const fadeoutText = fadeoutStateValues.textContent || null;

        // rerender trigger node's initial text value.
        renderTargetStateDependedTriggerText(
          target,
          trigger,
          fadeinText,
          fadeoutText
        );

        // couple trigger and target by event listening/handling ...
        trigger.addEventListener(
          'click',
          handleFadeToggleWithBoundContext.bind({
            // ... and by binding both and some text values
            // that are sensitive to state changes
            // as context properties to the handler.
            target,
            trigger,
            fadeinText,
            fadeoutText
          })
        );
      }
    } catch (exception) {
      console.warn(exception.message, exception);
    }
  }
}

function initializeEveryFadeToggle() {
  // get list of all elements that contain a fade-toggle configuration
  const configContainerList = document.querySelectorAll('[data-fade-toggle-config]');

  // do initialization for each container separately.
  configContainerList.forEach(initializeFadeToggle);
}

initializeEveryFadeToggle();
.as-console-wrapper { max-height: 100%!important; top: 0; }
<div class="intro" data-fade-toggle-config='{"selectors":{"trigger":".button","target":".heading"},"duration":3}'>
  <h1 class="heading">Intro Heading</h1>
  <p>This is the intro section</p>
  <button class="button" data-fade-toggle-trigger-config='{"states":{"fadeout":{"textContent":"Hide Heading"},"fadein":{"textContent":"Show Heading"}}}'>Toggle Heading</button>
</div>

<div class="main" data-fade-toggle-config='{"selectors":{"trigger":".button","target":".heading"}}'>
  <h1 class="heading">Main Heading</h1>
  <p>This is the main section</p>
  <button class="button">Toggle Heading</button>
</div>