为什么JavaScript会吞噬HTML:代码示例

Web开发在不断发展。最近,一种趋势变得流行了,这与如何开发Web应用程序的普遍接受的思想基本矛盾。有些人对他寄予厚望,而另一些人则感到失望。每个人都有其自己的原因,简而言之很难解释。



网页的代码通常由三个部分组成,每个部分都履行其职责:HTML代码定义结构和语义,CSS代码确定外观,而JavaScript代码确定其行为。在涉及设计人员,HTML / CSS开发人员和JavaScript开发人员的团队中,这种分离是很自然的:设计人员定义视觉元素和用户界面,HTML和CSS开发人员将这些视觉元素放置在浏览器中的页面上,JavaScript开发人员将用户交互添加到将所有内容捆绑在一起并“使其发挥作用”。每个人都可以完成自己的任务,而不会干扰其他两类开发人员的代码。但是所有这些对于所谓的“旧样式”都是正确的。

近年来,JavaScript开发人员开始用JavaScript而不是HTML来确定页面的结构(例如,使用React js框架),这有助于简化用户交互代码的创建和维护。没有js框架,开发现代网站要困难得多。当然,当您告诉某人需要将他编写的HTML代码分解成几部分并与他非常熟悉的JavaScript混合时,这(出于明显的原因)会被怀有敌意。对话者至少会问我们为什么需要此功能,以及如何从中受益。

作为跨职能团队中的JavaScript开发人员,有时会问我这个问题,而且我通常很难回答。我在该主题上找到的所有材料都是为已经熟悉JavaScript的读者编写的。对于专门研究HTML和CSS的人来说,这不是很好。但是,HTML-in-JS模式(或其他提供相同好处的模式)可能需要一段时间,因此,我认为这是Web开发中涉及的每个人都应该理解的重要事项。

在本文中,我将为感兴趣的人提供代码示例,但我的目标是解释这种方法,以便在没有这些方法的情况下也可以理解。

Likbez:HTML,CSS和JavaScript


为了使本文的读者最大化,我想简要谈一谈这些语言在创建网页中所起的作用,以及它们之间最初存在何种分隔。如果大家都知道这一点,则可以跳过本节。

HTML:用于结构和语义


HTML代码(超文本标记语言)定义了将放置在页面上的内容的结构和语义。例如,本文的HTML代码包含您现在正在阅读的文本,并且根据指定的结构,此文本放在标题之后和粘贴CodePen之前的单独的段落中

例如,创建一个包含购物清单的简单网页:



我们可以将此代码保存在文件中,在Web浏览器中将其打开,然后浏览器将显示结果。如您所见,此示例中的HTML代码是一个页面,其中包含标题“购物清单”(2个项目),一个输入文本框,一个“添加项目”按钮和一个两个项目列表(“鸡蛋”和“油”)。用户将在其Web浏览器中输入地址,然后浏览器将向服务器请求此HTML代码,然后下载并显示它。如果列表中已有元素,则服务器可以发送带有现成元素的HTML,如本例所示。

尝试在输入字段中输入内容,然后单击“添加项目”按钮。您将看到什么都没有发生。该按钮未与任何可能更改HTML的代码相关联,HTML本身无法更改任何内容。我们将在一分钟内返回到此。

CSS:改变外观


CSS代码(层叠样式表)定义页面的外观。例如,本文的CSS定义了您正在阅读的文本的字体,间距和颜色。

您可能已经注意到,我们的购物清单示例看起来非常简单。HTML不允许指定间距,字体大小和颜色之类的内容。这就是为什么我们使用CSS。我们可以为该页面添加CSS代码来装饰它:



如您所见,此CSS代码更改了文本字符的大小和粗细以及背景颜色。通过类推,开发人员可以为自己的样式编写规则,并且这些规则将依次应用于任何HTML结构:如果我们在此页面中添加section,button或ul元素,则将应用相同的字体更改。
但是“添加项目”按钮仍然无能为力:这里我们只需要JavaScript。

JavaScript:实现行为


JavaScript代码定义页面上交互式或动态元素的行为。例如,CodePen是使用JavaScript编写的。

为了使示例中的“添加项目”按钮在不使用JavaScript的情况下工作,我们需要使用特殊的HTML代码强制其将数据发送回服务器(如果您突然感兴趣,请使用<form action \ u003d'...'>)。此外,浏览器将重新加载整个HTML文件的更新版本,而不保存页面的当前状态。如果此购物清单是大型网页的一部分,那么用户所做的一切都会丢失。读取文本时向下移动多少像素无关紧要-重新启动时,服务器会将您返回到开头;观看视频的分钟数也无关紧要-重新启动时,它将重新开始。

这就是所有Web应用程序过去工作的方式:每次用户与网页交互时,他似乎都关闭了Web浏览器并再次打开它。对于一个简单的案例研究而言,这并不重要,但是对于一个复杂的大型页面(可能需要花费一些时间才能加载),这种方法对浏览器或服务器均无效。

如果要更改页面上的某些内容而不完全重新加载该页面,则需要使用JavaScript:



现在,当我们在输入字段中键入文本并单击“添加项目”按钮时,我们的新元素将添加到列表中,并且顶部的元素数将更新!在实际的应用程序中,我们还将添加一些代码以在后台将新项目发送到服务器,以便在下一次页面加载时出现。

即使在这个简单的示例中,将JavaScript与HTML和CSS分开也是合理的。更复杂的交互如下所示:加载并显示HTML,随后启动JavaScript以向其中添加和更改某些内容。但是,随着Web应用程序复杂性的增长,我们需要仔细监视JavaScript代码的更改内容,更改时间和更改时间。

如果我们继续用购物清单开发此应用程序,则可以添加按钮以编辑或从清单中删除商品。假设我们为删除元素的按钮编写JavaScript代码,但是忘记在页面顶部添加添加更新有关元素总数信息的代码。突然,我们得到一个错误:用户删除项目后,页面上指示的总数将与列表不匹配!

注意到错误后,我们通过在添加项目代码和删除项目代码中添加了相同的totalText.innerHTML行来修复该错误。现在,我们有了相同的代码,该代码在多个地方重复。稍后,我们要更改此代码,以便在页面顶部显示“ Items:2”,而不是“(2 items)”。我们必须确保不要忘记在所有三个位置上更新此代码:在HTML中,在“添加项目”按钮的JavaScript代码中以及在“删除项目”按钮的JavaScript代码中。如果不这样做,则将出现另一个错误,因此与用户进行交互后,此文本将发生巨大变化。

在这个简单的示例中,我们已经看到在代码中混淆起来是多么容易。当然。有一些方法和实践可以简化JavaScript代码,从而更轻松地解决此问题。但是,随着事情变得更加复杂,我们将不得不继续进行结构调整并不断重写项目,以便可以轻松地开发和维护该项目。尽管HTML和JavaScript分别存储,但是要确保两者之间的同步可能会花费很多精力。这是诸如React之类的新JavaScript框架变得流行的原因之一:它们旨在提供HTML和JavaScript之间更正式和有效的关系。要了解其工作原理,我们首先需要更深入地研究计算机科学。

命令式与声明式编程


要理解的关键是思想上的差异。大多数编程语言只允许您遵循一种范例,尽管其中某些范例一次支持两种范例。重要的是要理解这两种范例,以便从JavaScript开发人员的角度理解HTML-in-JS的主要优点。

  • . «» , . ( ) : , , . , , . - «». Vanilla JavaScript , jQuery. JavaScript .
    • « X, Y, Z».
    • : , .selected; , .selected.

  • . , . , , «» (), , - . , . (, React), , , . , , , , . , :
    • « XYZ. , , .
    • 示例:如果用户选择了此元素,则该元素具有.selected类。


HTML是一种声明性语言


暂时忘记JavaScript。这是一个重要的事实:HTML是一种声明性语言。在HTML文件中,您可以声明以下内容:

<section>
  <h1>Hello</h1>
  <p>My name is Mike.</p>
</section>

当网络浏览器读取此HTML代码时,它将独立确定必要的步骤并执行它们:

  1. 创建一个section元素
  2. 创建一个1级标题元素(h1)。
  3. 将标题元素文本设置为“ Hello”。
  4. 将title元素放置在section元素中。
  5. 创建一个段落元素(p)。
  6. 将段落元素的文本设置为“我的名字是麦克”。
  7. 将段落元素放置在section元素中
  8. section元素放置在HTML文档中。
  9. 在屏幕上显示文档。

对于Web开发人员来说,浏览器如何执行这些操作并不重要:重要的是它执行这些操作。这是一个很好的例子,说明了两种编程范例之间的区别。简而言之,HTML是一种声明性抽象,它包装在强制性实现的Web浏览器呈现引擎周围。他在乎如何,所以您只需要担心什么。在编写声明性HTML时,您可以享受生活,因为Mozilla,Google和Apple的好人在创建Web浏览器时为您编写了命令性代码。

JavaScript是命令式语言


我们已经在上面的购物清单示例中研究了命令式JavaScript的简单示例,并且我描述了应用程序功能的复杂性如何影响实现它们所需的工作量以及此实现中出现错误的可能性。

现在让我们看一个稍微复杂的任务,看看如何使用声明性方法简化它。想象一个包含以下内容的网页:

  • 标记标志的列表,选中后每行颜色会改变;
  • 下面的文本,例如“ 1 of 4 selected”(当选择标志时应更新);
  • 全选按钮,如果已经选中所有复选框,则应禁用该按钮;
  • 选择无按钮,如果未选中复选框,则应禁用该按钮。

这是纯HTML,CSS和命令式JavaScript的实现:



const captionText = document.getElementById('caption-text');
const checkAllButton = document.getElementById('check-all-button');
const uncheckAllButton = document.getElementById('uncheck-all-button');
const checkboxes = document.querySelectorAll('input[type="checkbox"]');

function updateSummary() {
  let numChecked = 0;
  checkboxes.forEach(function(checkbox) {
    if (checkbox.checked) {
      numChecked++;
    }
  });
  captionText.innerHTML = `${numChecked} of ${checkboxes.length} checked`;
  if (numChecked === 0) {
    checkAllButton.disabled = false;
    uncheckAllButton.disabled = true;
  } else {
    uncheckAllButton.disabled = false;
  }
  if (numChecked === checkboxes.length) {
    checkAllButton.disabled = true;
  }
}

checkAllButton.addEventListener('click', function() {
  checkboxes.forEach(function(checkbox) {
    checkbox.checked = true;
    checkbox.closest('tr').className = 'checked';
  });
  updateSummary();
});

uncheckAllButton.addEventListener('click', function() {
  checkboxes.forEach(function(checkbox) {
    checkbox.checked = false;
    checkbox.closest('tr').className = '';
  });
  updateSummary();
});

checkboxes.forEach(function(checkbox) {
  checkbox.addEventListener('change', function(event) {
    checkbox.closest('tr').className = checkbox.checked ? 'checked' : '';
    updateSummary();
  });
});

头痛


要使用命令式JavaScript来实现此功能,我们需要给浏览器一些详细的说明。这是上面示例中对代码的口头描述。
在我们的HTML中,我们声明原始页面结构:
  • 有四个行元素(一个表的一行,它是一个列表),每个元素都包含一个标志。第三个标志被检查。
  • 文本为“已选择4个中的1个”。
  • 有一个全选按钮已打开。
  • 有一个全选按钮,已禁用。

在我们的JavaScript中,我们写了关于以下每个事件发生时需要更改的内容的说明:
当标志从未标记变为已标记时:
  • 找到包含复选框的行,然后将.selected CSS类添加到其中。
  • 在列表中找到所有标志,并计算其中有多少标志,没有多少标志。
  • 查找文本并进行更新,以指示所选购买的正确数量及其总数。
  • 找到“全选”按钮,然后将其打开(如果已禁用)。
  • 如果现在选中所有复选框,请找到“全选”按钮并将其关闭。

当标志从已标记变为未标记时:
  • 找到包含标志的行,然后从中删除.selected类。
  • 在列表中找到所有复选框,并计算选中了多少复选框,未选中的复选框。
  • 查找简历文本项,并使用已验证的数字和总数进行更新。
  • 找到“全选”按钮,然后将其打开(如果已禁用)。
  • 如果未选中所有复选框,请找到“全选”按钮并将其关闭。

当按下全选按钮时:
  • 在列表中找到所有复选框并标记它们。
  • 在列表中找到所有行,然后向其添加.selected类。
  • 查找文本并进行更新。
  • 找到全选按钮并将其关闭。
  • 找到“全选”按钮并将其打开。

当按下“全选”按钮时:
  • 在列表中找到所有复选框,然后清除所有复选框。
  • 在列表中找到所有行,然后从中删除.selected类。
  • 找到简历文本元素并进行更新。
  • 找到全选按钮并将其打开。
  • 找到“全选”按钮并将其关闭。

哇...好多吧?好吧,我们最好不要忘记为所有可能的情况编写代码。如果我们忘记或弄乱了这些说明中的任何一条,我们将得到一个错误:总计将与复选框不匹配,或者当您单击该按钮时,该按钮将打开而没有任何作用,或者所选行将显示错误的颜色或其他内容。到目前为止,我们还没有考虑过,直到用户抱怨后才知道。

我们这里确实有一个大问题:没有实体会包含有关状态的完整信息应用程序(在这种情况下,这是对“标记了哪些标志?”这个问题的答案),并将负责更新此信息。当然,标志知道它们是否被选中,但是表,文本和每个按钮的行的CSS代码也应意识到这一点。此信息的五个副本分别在整个HTML中存储,并且当这些信息在任何这些位置发生更改时,JavaScript开发人员都必须抓住这一点并编写命令性代码以与其他位置的更改同步。

这仍然是一个小页面组件的简单示例。即使这组指令看起来让人头疼,也可以想象一下,当您需要在命令式JavaScript中实现所有这些功能时,大型Web应用程序将变得多么复杂和脆弱。对于许多复杂的现代Web应用程序,此解决方案无法从“完全”一词扩展。

我们正在寻找真理的源头


像React这样的工具允许声明性地使用JavaScript。正如HTML是对在Web浏览器中显示的指令的声明性抽象一样,React是JavaScript的声明性抽象。

还记得HTML是如何使我们专注于页面的结构而不是实现细节的,浏览器如何显示这种结构?同样,当我们使用React时,我们可以专注于结构,并根据存储在一个地方的数据对其进行定义。此过程称为单向绑定,应用程序状态数据的位置称为“单个事实来源”。当真相的来源发生变化时,React会自动为我们更新页面结构。他将在后台处理所需的步骤,就像HTML的网络浏览器一样。尽管此处以React为例,但这种方法也适用于其他框架,例如Vue。

让我们回到上面示例的复选框列表。在这种情况下,我们想知道的“真相”非常简洁:检查哪些标志?其他细节(例如,文本,线条的颜色,打开了哪些按钮)已经是根据事实来源获得的信息。他们为什么还要拥有自己的此信息副本?他们应该只使用一个真实的只读来源,并且所有页面元素都应该“仅知道”哪些复选框已选中并具有相应的行为。您可以说表格的行,文本和按钮应该能够根据是否选中复选框自动响应该复选框(“看看那里发生了什么?”)

告诉我你想要什么(你真正想要的)


为了用React实现这个页面,我们可以用一些简单的事实描述来代替列表:

  • 有一个称为checkboxValues的true / false值列表,显示了被标记的字段。
    • 示例:checkboxValues \ u003d [false,false,true,false]
    • 此列表显示我们有四个标志,只有第三个被设置。

  • 对于表格中checkboxValues中的每个值,都有一行:
    • 如果值为true,则具有一个名为.selected的CSS类,并且
    • 包含一个标志,检查该标志是否为true。

  • 有一个文本元素,其中包含文本``选择的{y}的文本{x},其中{x}是checkboxValues中真实值的数量,{y}是checkboxValues中值的总数。
  • 如果checkboxValues的值为假,则启用全选按钮。
  • Select None, , checkboxValues ​​ true.
  • , checkboxValues.
  • Select All , checkboxValues ​​ true.
  • Select None checkboxValues ​​ false.

您会注意到,最后三段仍然是命令式指令(“发生这种情况时,请执行此操作”),但这是我们需要编写的唯一命令式代码。这是三行代码,它们都更新了一个真实的来源。

其余的是声明性语句(“ ...”),现在直接内置到页面结构的定义中。为此,我们使用特殊的JavaScript语法扩展-JavaScript XML(JSX)为元素编写代码。它类似于HTML:JSX允许您使用类似于HTML的语法来描述接口的结构以及常规JavaScript。这使我们可以将JS逻辑与HTML结构混合使用,因此结构可以随时不同。这完全取决于checkboxValues的内容。

我们在React上重写示例:



function ChecklistTable({ columns, rows, initialValues, ...otherProps }) {
  const [checkboxValues, setCheckboxValues] = React.useState(initialValues);
  
  function checkAllBoxes() {
    setCheckboxValues(new Array(rows.length).fill(true));
  }
  
  function uncheckAllBoxes() {
    setCheckboxValues(new Array(rows.length).fill(false));
  }
  
  function setCheckboxValue(rowIndex, checked) {
    const newValues = checkboxValues.slice();
    newValues[rowIndex] = checked;
    setCheckboxValues(newValues);
  }
  
  const numItems = checkboxValues.length;
  const numChecked = checkboxValues.filter(Boolean).length;
  
  return (
    <table className="pf-c-table pf-m-grid-lg" role="grid" {...otherProps}>
      <caption>
        <span>{numChecked} of {numItems} items checked</span>
        <button
          onClick={checkAllBoxes}
          disabled={numChecked === numItems}
          className="pf-c-button pf-m-primary"
          type="button"
        >
          Check all
        </button>
        <button
          onClick={uncheckAllBoxes}
          disabled={numChecked === 0}
          className="pf-c-button pf-m-secondary"
          type="button"
        >
          Uncheck all
        </button>
      </caption>
      <thead>
        <tr>
          <td />
          {columns.map(function(column) {
            return <th scope="col" key={column}>{column}</th>;
          })}
        </tr>
      </thead>
      <tbody>
        {rows.map(function(row, rowIndex) {
          const [firstCell, ...otherCells] = row;
          const labelId = `item-${rowIndex}-${firstCell}`;
          const isChecked = checkboxValues[rowIndex];
          return (
            <tr key={firstCell} className={isChecked ? 'checked' : ''}>
              <td className="pf-c-table__check">
                <input
                  type="checkbox"
                  name={firstCell}
                  aria-labelledby={labelId}
                  checked={isChecked}
                  onChange={function(event) {
                    setCheckboxValue(rowIndex, event.target.checked);
                  }}
                />
              </td>
              <th data-label={columns[0]}>
                <div id={labelId}>{firstCell}</div>
              </th>
              {otherCells.map(function(cell, cellIndex) {
                return (
                  <td key={cell} data-label={columns[1 + cellIndex]}>
                    {cell}
                  </td>
                );
              })}
            </tr>
          );
        })}
      </tbody>
    </table>
  );
};

function ShoppingList() {
  return (
    <ChecklistTable
      aria-label="Shopping list"
      columns={['Item', 'Quantity']}
      rows={[
        ['Sugar', '1 cup'],
        ['Butter', '½ cup'],
        ['Eggs', '2'],
        ['Milk', '½ cup'],
      ]}
      initialValues={[false, false, true, false]}
    />
  );
}

ReactDOM.render(
  <ShoppingList />,
  document.getElementById('shopping-list')
);


JSX看起来很奇怪。当我第一次遇到这个问题时,在我看来这根本是不可能的。我最初的反应是:“什么?HTML不能在JavaScript代码中!” 我不是唯一的一个。但是,这不是HTML,而是JavaScript装扮成HTML。实际上,这是一个强大的解决方案。

还记得上面的20条命令吗?现在我们有三个。其余(内部)命令式指令React本身在幕后为我们执行-每次checkboxValues更改时。

使用此代码,即使行的文本或颜色与复选框不匹配,或者打开了按钮(尽管应将其禁用),也不会再出现这种情况。现在,一整类错误根本无法在我们的Web应用程序中发生。所有工作都是在单一事实来源的基础上完成的,我们的开发人员可以编写更少的代码,晚上睡得更好。好吧,JavaScript开发人员,至少...

JavaScript击败HTML:饿死了


随着Web应用程序变得越来越复杂,在HTML和JavaScript之间保持经典的任务分离变得越来越痛苦。HTML最初是为静态网页设计的。为了在那里添加更多复杂的交互功能,必须在命令式JavaScript中实现适当的逻辑,随着每一行代码变得越来越混乱和脆弱。

现代方法的优点:可预测性,可重用性和组合性


使用单一事实来源的能力是此模型的最重要优势,但它还有其他优势。在JavaScript代码中定义页面元素意味着我们可以重用组件(网页的各个块),从而防止我们在多个位置复制和粘贴相同的HTML代码。如果需要更改组件,只需在一个地方更改其代码即可。在这种情况下,更改将在组件的所有副本中发生(如果在一个Web应用程序中甚至在许多Web应用程序中使用可重用的组件,则这些更改)。

我们可以将简单的组件像LEGO多维数据集一样组合在一起,创建更复杂和有用的组件,而不会使其逻辑过于混乱。而且,如果我们使用其他开发人员创建的组件,则可以轻松汇总更新或错误修复,而无需重写代码。

相同的JavaScript,仅在配置文件中


这些好处有一个缺点。人们充分理解HTML和JavaScript的分离是有充分的理由的。如前所述,放弃常规HTML文件会使以前从未使用过JavaScript的人的工作流程复杂化。那些以前可以独立更改Web应用程序的人,现在必须掌握其他复杂技能,以保持自己的自治权,甚至可以加入团队。

也存在技术缺陷。例如,某些工具(例如linters和parsers)仅接受普通HTML作为输入,而使用第三方JavaScript插件可能会更困难。另外,JavaScript不是最好的语言,但是它是Web浏览器接受的统一标准。新的工具和功能使它变得更好,但是在使用它之前,您仍然需要了解一些陷阱。

另一个潜在的问题:当页面的语义结构被分解为抽象的组件时,开发人员可能会停止考虑将由此生成哪些HTML元素。特定的HTML标签,例如section和aside,具有自己的语义,即使使用div和span等通用标记,即使它们在页面上看起来相同,这些语义也会丢失这对于确保不同类别用户的Web应用程序的可用性特别重要。

例如,这将影响视障用户的屏幕阅读器的行为。对于开发人员来说,这些也许不是最有趣的任务,但是JavaScript开发人员应始终记住,在这种情况下,保留HTML的语义是最重要的任务

有意识的需求与无意识的趋势


最近,在每个项目中使用框架已成为一种趋势。有人认为HTML和JavaScript的分离已经过时了,但事实并非如此。对于不需要复杂的用户交互的简单静态网站,这是正确的。在这里,更热情的React粉丝可能会不同意我的观点,但是如果您的JavaScript只是创建一个非交互式网页,则不要使用它。 JavaScript的加载速度不如常规HTML。因此,如果您不设置任务以获得新的开发经验或提高代码的可靠性,那么此处的JavaScript弊大于利。

此外,无需在React中编写整个网站。还是Vue!还是那里还有什么……很多人不知道这一点,因为所有教程基本上都展示了如何使用React从头开发站点。如果在一个简单的网站上只有一个小的复杂小部件,则可以将React用于一个组件。您不必总是担心webpack,Redux,Gatsby或其他人推荐的“最佳做法”。

但是,如果应用程序非常复杂,那么使用现代的声明性方法绝对值得。React是最好的解决方案吗?没有。他已经有了强大的竞争对手。然后,将会出现越来越多的问题……但是声明性编程将无处可去,在某些新框架中,这种方法可能会被重新考虑并更好地实现。


All Articles