Github.com 前端弃用 jQuery 始末

1024 译站按:早在今年7月份,Github 一名员工发布了一条 Twitter, 大意是 Github 网站前端决定放弃使用 jQuery,改用原生 JavaScript。一时间在前端业界引起了不少讨论,那么究竟是什么原因让 Github 决定放弃了使用多年的 jQuery 呢? 这篇文章会给出答案。

最近我们完成了一个里程碑:我们把 jQuery 依赖从 Github.com 前端代码中移除了。实施多年的渐进式解耦 jQuery,直至我们能够完全移除它,现在画上了句号。在这篇文章中,我们会解释一下我们是如何开始依赖 jQuery 的,我们又是如何意识到不再需要它的,以及指出我们如何做到使用标准浏览器 API 完成所有事情,而不是换成另一个库或框架。

为什么早期需要 jQuery

GitHub.com 在2007年末将 jQuery 1.2.1 作为依赖项。稍微介绍下背景,那是在谷歌发布第一版 Chrome 浏览器的一年之前。当时没有使用 CSS 选择器查询 DOM 元素的标准方式,没有标准方式给元素添加视觉动画,IE 倡导的 XMLHttpRequest 接口 也跟其他许多 API 一样,在浏览器之间表现不一致。

jQuery 让 DOM 操作、动画和 AJAX 请求变得简单, 主要是让 web 开发人员能创建出更现代、更动态的体验从而在普通页面中脱颖而出。最重要的是,有了 jQuery,在一种浏览器中内置的 JavaScript 特性在其他浏览器上通常也能工作。在 Github 早期,当它的大部分功能还在不断开发的时候,这让小规模的开发团队能够快速搭建原型和推出新功能,而无需针对每种浏览器调整代码。

jQuery 简洁的接口也被当做蓝本来开发各种扩展库,这些扩展库后来成为 GitHub.com 前端的构建模块: pjaxfacebox

我们会一直感谢 John Resig 和 jQuery 的贡献者们创建和维护了这样一个有用、在当时看来必不可少的库。

之后几年的 Web 标准

在几年时间里,GitHub发展成为一家拥有数百名工程师和一支专注的团队的公司,开始关注向 web 浏览器提供的 JavaScript 代码的体积和质量。我们一直关注的一件事就是技术债务,有时候技术债务会随着依赖项而增长,这些依赖项曾经提供了价值,但是随着时间的推移这些价值会下降。

具体到 jQuery,我们把它跟现代浏览器对 web 标准的快速支持做了对比,发现:

  • $(selector) 很容易用 querySelectorAll() 替代;
  • CSS 类名切换现在也可以用 Element.classList 实现;
  • CSS 现在支持在样式中定义视觉动画 而无需 JavaScript;
  • $.ajax 请求可以用 Fetch 标准
  • addEventListener() 接口在跨浏览器使用上已经足够稳定;
  • 我们可以用一个轻量库轻松地封装 事件代理模式
  • 随着 JavaScript 语言的演进, jQuery 提供的一些语法糖已经变得多余。

此外,链式语法并不能满足我们编写代码的意图。例如:

$('.js-widget')
  .addClass('is-loading')
  .show()

这种语法写起来简单,但是根据我们的标准,它并没有很好地传达意图。代码作者是想要单个还是多个js-widget 元素?另外,如果我们更新了页面标签内容,不小心漏掉了 js-widget 类名,浏览器会抛出一个异常通知我们有地方出错了吗?默认情况下,当选择器没有匹配的时候,jQuery 会静默地跳过整个表达式;但对我们来说,这种行为是 bug ,而不是功能。

最后,我们想用Flow来标记数据类型以在构建时做静态检查,并得出结论:链式语法不适合做静态检查,因为几乎所有 jQuery 方法调用结果都是相同类型的。我们之所以选择 Flow 而不是其他同类工具,是因为当时 @flow weak 模式允许我们渐进、高效地对基本上无类型的代码库应用类型系统。

总而言之,摆脱 jQuery 意味着我们可以更多地依赖 web 标准,我们的前端开发人员可以把 MDN web 文档 作为实际上的默认文档, 未来可以维护更有弹性的代码,并且最终从我们的压缩包中剔除 30 KB 的依赖,从而缩短页面载入时间和 JavaScript 执行时间。

增量解耦

即使有这么一个最终目标,我们也知道分配所有的资源来用原生 JS 重写一切是不可能的。即便做到了,如此匆忙的工作很可能导致网站功能的倒退,而往后我们又不得不处理掉它们。相反,我们是这样做的:

  • 设置指标,跟踪每一行代码的 jQuery 调用比率,并随着时间的推移对该图表进行监控,以确保它要么保持不变,要么下降,而不是向上。

    Graph of jQuery usage going down over time.

  • 我们不鼓励在任何新代码中引入 jQuery。 为了方便自动化,我们创建了 eslint-plugin-jquery ,任何人只要尝试使用 jQuery 的功能,比如$.ajax,CI 检查就会失败。

  • 在旧代码中有大量违反 eslint 规则的行为,所有这些规则都是在代码注释中使用特定的eslint-disable 规则进行注释的。对于这段代码的读者来说,这些注释将作为一个明确的信号,表明这段代码没有遵循我们当前的编码实践。

  • 我们创建了一个 pull request 机器人,当有人试图添加一个新的eslint-disable规则时,它会添加一条针对本次 pull request 的评审备注提醒我们的团队。通过这种方式,我们可以提早进入代码复审并提出替代方案。

  • 大量旧代码都与 jQuery插件 pjax 和 facebox 的外部接口有显式的耦合,因此我们在保持接口相对不变的情况下,用原生 JS 替换内部实现方式。有了静态检查的帮助,我们对这样的重构更加有信心了。

  • 大量代码使用了 rails-behaviors 接口(这是我们针对“非侵入式” JS做的 Ruby on Rails 适配方法), 它们是通过给特定表单附加 AJAX 生命周期处理器的方式完成的:

      // 原来的方式
      $(document).on('ajaxSuccess', 'form.js-widget', function(event, xhr, settings, data) {
        // 在某个DOM里插入响应数据
      })
    
    

    我们没有一次性用新方式重写所有调用,而是触发模拟的 ajax* 生命周期事件,并让这些表单依然按照原来的方式异步提交数据;只是这次在内部使用了fetch()

  • 我们维护了一个定制化的 jQuery 构建包, 每当找出不再使用的 jQuery 的某个模块时,我们就从中移除,并构建一个更小的版本。例如,当我们移除了最后一个 jQuery 特有的 CSS 伪类选择器如 :visible:checkbox 的使用时,我们就可以移除 Sizzle 模块了;当最后一次出现的$.ajax 调用已经被 fetch() 替换时,我们就可以移除 AJAX 模块了。这达到了双重目的:加快 JavaScript 执行速度,同时保证了新代码不会去使用移除掉的功能。

  • 正如我们的站点统计数据所显示的,只要是可行的,我们一直在放弃对旧版 Internet Explorer 的支持。一旦某个版本的 IE 使用率低于特定的阈值,我们就停止为其提供 JavaScript,并专注于测试和支持更现代的浏览器。尽早放弃支持 IE 8-9 让我们能够用上许多原生的浏览器特性,否则这些特性很难 polyfill。

  • 作为我们在GitHub.com上构建前端功能的改进方法的一部分,我们专注于尽可能多地使用常规的HTML基础,并且只在渐进增强的原则下才添加 JavaScript 行为。因此,尽管那些网页表单和其他 UI 元素是用 JS 强化实现的,但是在禁用 JavaScript 的浏览器下同样能工作。在某些情况下,我们可以完全删除某些遗留行为,而不必用原生 JS 重写。

这些年来,经过这样的努力,我们得以逐渐减少对 jQuery 的依赖,直到不再有一行代码引用它。

Custom Elements

近年来掀起浪潮的一项技术是 Custom Elements:浏览器内置的一个组件库,也就是说用户不需要下载、解析和编译额外的框架。

从 2014 年开始,我们基于 v0 规范创建了一些定制元素。然而,当时标准仍处于变化中,我们并没有投入很多。直到 2017 年 Web Components v1 规范发布,并且在 Chrome 和 Safari 上实现, 我们才开始更大规模采用Custom Elements

在 jQuery 迁移过程中,我们在寻找适合提取出 custom elements 的模式方法。例如,我们把用来展示模态对话框的 facebox 转换成 <details-dialog> 元素

我们追求渐进增强的一般哲学也延伸到 custom elements。这意味着我们尽可能多地保留标记中的内容,并且只在上面添加行为。例如, <local-time> 默认显示原始时间戳,并可升级为转换时间到本地时区。而 <details-dialog>, 当它内嵌于<details> 元素中时,无需 JavaScript 即可交互,但是可以通过增强可访问性来获得升级。

下面是一个如何实现自定义元素<local-time>的示例:

// local-time 元素显示用户当前时区的时间
// 
//
// 用法示例:
//   <local-time datetime="2018-09-06T08:22:49Z">Sep 6, 2018</local-time>
//
class LocalTimeElement extends HTMLElement {
  static get observedAttributes() {
    return ['datetime']
  }

  attributeChangedCallback(attrName, oldValue, newValue) {
    if (attrName === 'datetime') {
      const date = new Date(newValue)
      this.textContent = date.toLocaleString()
    }
  }
}

if (!window.customElements.get('local-time')) {
  window.LocalTimeElement = LocalTimeElement
  window.customElements.define('local-time', LocalTimeElement)
}

我们期待采用的 Web Components 的一个方面是Shadow DOM。Shadow DOM 的强大特性有可能为 web 释放大量可能性,但这也使得 polyfill 更加难以实现。因为现在的 polyfill 会导致性能损失,即使对于操作与 web components 无关的DOM的代码,我们也不可能在生产中使用它。

Polyfills

这些是帮助我们过渡到使用标准浏览器特性的 polyfill。我们只在绝对必要的情况下才尝试使用这些 polyfill,也就是对过时浏览器提供单独的兼容性 JavaScript 压缩包。

猜你喜欢

转载自blog.csdn.net/2301_79354153/article/details/134841235