面向意大利面条式遗留代码库的响应式 jQuery(或者当你无法拥有美好事物时)

Avatar of Meredith Matthews
Meredith Matthews

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

现在我能听到你喊叫:“为什么当有更好的工具可用时,你还想用 jQuery?简直是疯狂!你到底是什么样的疯子?” 这些都是合理的问题,我将通过一些背景信息来回答它们。

在我目前的工作中,我负责维护和更新一个遗留网站。 它很古老。 前端依赖于 jQuery,就像大多数旧的遗留系统一样,它并不处于最佳状态。 这本身并不是最糟糕的,但我正在与其他约束条件作斗争。 例如,我们正在进行系统的全面重写,因此大量重构工作没有得到批准,而且我未经全面安全审查也不得向现有系统添加新依赖项,这在历史上可能需要长达一年的时间。 有效地,jQuery 是我唯一可以使用的 JavaScript 库,因为它已经存在。

我的公司直到最近才意识到前端开发人员可能拥有重要的技能可以贡献,因此应用程序的整个前端都是由不了解最佳实践的开发人员编写的,而且他们往往蔑视自己的任务。 因此,代码质量参差不齐,总体上相当糟糕且不符合规范。

是的,我在 _这种_ 遗留代码库中工作:典型的 jQuery 意大利面条。

总会有人去做,而且世界上总是会有比全新项目更多的遗留代码,所以我们总是会很多。 我也不需要你的同情。 处理这些东西,学习如何应对如此大规模的前端意大利面条,让我成为一个更优秀,但也更暴躁的开发人员。

那么你如何知道你是否拥有意大利面条式 jQuery 呢? 我发现的一个可靠的代码味道是缺乏久经考验的 .toggle()。 如果你成功地一段时间没有考虑 jQuery,它是一个可以消除跨浏览器兼容性问题的库,同时也使 DOM 查询和变异变得非常容易。 这本身并没有错,但是如果你不小心,直接的 DOM 操作可能非常难以扩展。 你编写的 DOM 操作越多,你对 DOM 变异的防御性就越强。 最终,你可能会发现自己用这种方式编写了整个代码库,并且与不太理想的范围管理相结合,你实际上是在一个所有状态都在 DOM 中的应用程序中工作,而且你永远不能相信你需要进行更改时 DOM 的状态; 更改可能会从你的应用程序中的任何地方突然出现,无论你喜欢与否。 你的代码变得更程序化,使用更多明确的指令来膨胀代码,试图从 DOM 本身获取所有需要的数据,并将其强制到需要它的状态。

这就是为什么 .toggle() 通常是第一个被放弃的:如果你不能确定某个元素是否可见,你必须使用 .show().hide()。 我不是说 .show().hide() 应该被认为是危险的™,但我发现它们是可能存在更大问题的一个良好指标。

你可以采取哪些措施来解决这个问题? 我的同事和我发现的一种解决方案直接借鉴了我们更愿意使用的响应式框架:可观察对象和状态管理。 我们都发现,在**将 DOM 视为单向数据流**模板的同时,手动滚动状态对象和事件驱动的更新函数会导致更**可预测的结果**,这些结果更容易随着时间的推移而改变。

我们每个人解决问题的方式略有不同。 我对响应式 jQuery 的看法明显带有 Vue 注入的风格,并利用了一些“高级”CSS。

如果你查看脚本,你会发现发生了两件不同的事情。 首先,我们有一个 State 对象,它保存了页面所有值,而且我们有一堆事件。

var State = {
  num: 0,
  firstName: "",
  lastName: "",
  titleColor: "black",
  updateState: function(key, value){
    this[key] = value;
        
    $("[data-text]").each(function(index, elem){
      var tag = $(elem).attr("data-tag");
      $(elem).text(State[tag]);
    });
    
    $("[data-color]").each(function(index, elem){
      var tag = $(elem).attr("data-tag");
      $(elem).attr("data-color", State[tag]);
    });
  }
};

我承认,我喜欢自定义 HTML 属性,并且在整个解决方案中都大量使用它们。 我一直不喜欢 HTML 类如何经常充当 CSS 挂钩和 JavaScript 挂钩,以及如果你同时将一个类用于这两个目的,你就会在脚本中引入脆弱性。 这个问题使用 HTML 属性可以完全消除。 类再次成为类,而属性则变成我需要的任何元数据或样式挂钩。

如果你查看 HTML,你会发现 DOM 中每个需要显示数据的元素都有一个 data-tag 属性,其值对应于 State 对象中包含要显示数据的属性,以及一个没有值的属性,该属性描述需要对应用它的元素进行的转换类型。 此示例有两种不同的转换类型:文本和颜色。

<h1 data-tag="titleColor" data-color>jDux is super cool!</h1>

关于事件。 我们想对数据进行的每次更改都由一个事件触发。 在脚本中,你会发现我们关心的每个事件都列出了其自己的 .on() 方法。 每个事件都会触发一个更新方法,并发送两条信息:需要更新的 State 对象中的哪个属性以及它应该设置的新值。

$("#inc").on("click", function(){
  State.updateState("num", State.num + 1)
});

$("#dec").on("click", function(){
  State.updateState("num", State.num - 1)
});

$("#firstNameInput").on("input", function(){
  State.updateState("firstName", $(this).val() )
});

$("#lastNameInput").on("input", function(){
  State.updateState("lastName", $(this).val() )
});

$('[class^=button]').on("click", function(e) {
  State.updateState('titleColor', e.target.innerText);
});

这将我们带到了 State.updateState(),即保持页面与状态对象同步的更新函数。 每次运行时,它都会更新页面上所有标记的值。 这并不是每次都重新执行页面上所有操作的最有效方法,但它要简单得多,而且正如我希望我已经清楚地说明的那样,这是一个针对不完美代码库的不完美解决方案。

$(document).ready(function(){
  State.updateState();
});

更新函数首先做的就是根据它接收的属性更新值。 然后它执行我提到的两种转换。 对于文本元素,它会列出所有 data-text 节点,获取它们的 data-tag 值,并将文本设置为标记属性中的任何内容。 颜色的工作方式略有不同,它将 data-color 属性设置为标记属性的值,然后依赖于 CSS,CSS 将样式化 data-color 属性以显示正确的样式。

我还添加了一个 document.ready,以便我们可以在加载时运行更新函数并显示默认值。 你可以从 DOM 获取默认值,也可以进行 AJAX 调用,或者像这里一样直接使用它们加载 State 对象。

就是这样! 我们所做的就是将状态保存在 JavaScript 中,观察我们的事件,并对发生的更改做出反应。 很简单,对吧?

这里有什么好处? 使用这种模式可以将单一事实来源保留在你可以控制、可以信任和可以强制执行的状态对象中。 如果你曾经失去对 DOM 正确性的信任,你只需使用不带任何参数重新运行更新函数,你的值就会再次与状态对象保持一致。

这是一种笨拙且原始的方法吗? 绝对是。 你想用它来构建整个系统吗? 当然不。 如果你有更好的工具可用,你应该使用它们。 但是如果你像我一样身处高度限制的遗留代码库中,请尝试使用响应式 jQuery 编写你的下一个功能,看看它是否会让你的代码和你的人生变得更简单。