深圳幻海软件技术有限公司 欢迎您!

GitHub:我们为什么会弃用jQuery?

2023-02-26

最近,我们将jQuery完全从GitHub.com的前端代码中移除了,这标志着我们数年来逐步移除jQuery这个渐进式的过程终于结束了,这对我们来说是一件里程碑式的事件。这篇文章将介绍过去我们是如何依赖上jQuery的,随着时间地推移,我们意识到不再需要它,但到***我们并没有使用另一个库或框架取代

最近,我们将 jQuery 完全从 GitHub.com 的前端代码中移除了,这标志着我们数年来逐步移除 jQuery 这个渐进式的过程终于结束了,这对我们来说是一件里程碑式的事件。这篇文章将介绍过去我们是如何依赖上 jQuery 的,随着时间地推移,我们意识到不再需要它,但到***我们并没有使用另一个库或框架取代它,而是使用标准的浏览器 API 实现了我们所需要的一切。

早期,jQuery 对我们意义重大

GitHub.com 在 2007 年底开始使用 jQuery 1.2.1,那是谷歌发布 Chrome 浏览器的前一年。当时还没有通过 CSS 选择器来查询 DOM 元素的标准方法,也没有动态渲染元素的样式的标准方法,而 Internet Explorer 的 XMLHttpRequest 接口与其他很多 API 一样,在浏览器之间存在不一致性问题。

jQuery 让 DOM 操作、创建动画和“AJAX”请求变得相当简单,基本上,它让 Web 开发人员能够创建更加现代化的动态 Web 体验。最重要的是,使用 jQuery 为一个浏览器开发的代码也适用于其他浏览器。在 GitHub 的早期阶段,jQuery 让小型的开发团队能够快速进行原型设计并开发出新功能,而无需专门针对每个 Web 浏览器调整代码。

基于 jQuery 简单的接口所构建的扩展库也成为 GitHub.com 前端的基础构建块:pjax和 facebox。

我们将永远不会忘记 John Resig 和 jQuery 贡献者创建和维护的这样一个有用的基本库。

后来的 Web 标准

多年来,GitHub 成长为一家拥有数百名工程师的公司,并逐渐成立了一个专门的团队,负责 JavaScript 代码的规模和质量。我们一直在排除技术债务,有时技术债务会随着依赖项的增多而增长,这些依赖项在一开始会为我们带来一定的价值,但这些价值也随着时间的推移而下降。

我们可以将 jQuery 与现代浏览器支持的 Web 标准的快速演化进行比较:

  •  $(selector) 模式可以使用 querySelectorAll() 来替换;
  •  现在可以使用 Element.classList 来实现 CSS 类名切换;
  •  CSS 现在支持在样式表中而不是在 JavaScript 中定义可视动画;
  •  现在可以使用 Fetch Standard 执行 $.ajax 请求;
  •  addEventListener() 接口已经足够稳定,可以跨平台使用;
  •  我们可以使用轻量级的库来封装事件委托模式;
  •  随着 JavaScript 语言的发展,jQuery 提供的一些语法糖已经变得多余。

另外,链式语法不能满足我们想要的编写代码的方式。例如: 

$('.js-widget')  
  .addClass('is-loading')  
  .show() 
  • 1.
  • 2.
  • 3.

这种语法写起来很简单,但是根据我们的标准,它并不能很好地传达我们的意图。作者是否期望在当前页面上有一个或多个 js-widget 元素?另外,如果我们更新页面标记并意外遗漏了 js-widget 类名,浏览器是否会抛出异常会告诉我们出了什么问题?默认情况下,当没有任何内容与选择器匹配时,jQuery 会跳过整个表达式,但对我们来说,这是一个 bug。

***,我们开始使用 Flow 来注解类型,以便在构建时执行静态类型检查,并且我们发现,链式语法不适合做静态分析,因为几乎所有 jQuery 方法返回的结果都是相同的类型。我们当时之所以选择 Flow,是因为 @flow weak 模式等功能可以让我们逐步将类型应用于无类型的代码库上。

总而言之,移除 jQuery 意味着我们可以更多地依赖 Web 标准,让 MDN Web 文档成为前端开发人员事实上的默认文档,在将来可以维护更具弹性的代码,并且可以将 30KB 的依赖从我们的捆绑包中移除,加快页面的加载速度和 JavaScript 的执行速度。

逐步解耦

虽然定下了最终目标,但我们也知道,分配所有资源一次性移除 jQuery 是不可行的。这种匆匆忙忙的做法可能会导致网站功能出现回归。相反,我们采取了以下的策略:

1. 设定指标,跟踪整一行代码调用 jQuery 的比率,并监控指标走势随时间变化的情况,确保它保持不变或下降,而不是上升。

2. 我们不鼓励在任何新代码中导入 jQuery。为了方便自动化,我们创建了 eslint-plugin-jquery(https://github.com/dgraham/eslint-plugin-jquery),如果有人试图使用 jQuery 功能,例如 $.ajax,CI 检查将会失败。

3. 旧代码中存在大量违反 eslint 规则的情况,我们在代码注释中使用特定的 eslint-disable 规则进行了注解。看到这些代码的读者,他们都该知道,这些代码不符合我们当前的编码实践。

4. 我们创建了一个拉请求机器人,当有人试图添加新的 eslint-disable 规则时,会对拉取请求留下评论。这样我们就可以尽早参与代码评审,并提出替代方案。

5. 很多旧代码使用了 pjax 和 facebox 插件,所以我们在保持它们的接口几乎不变的同时,在内部使用 JS 重新实现它们的逻辑。静态类型检查有助于提升我们进行重构的信心。

6. 很多旧代码与 rails-behavior 发生交互,我们的 Ruby on Rails 适配器几乎是“不显眼的”JS,它们将 AJAX 生命周期处理器附加到某些表单上: 

// 旧方法  
  $(document).on('ajaxSuccess', 'form.js-widget', function(event, xhr, settings, data) {  
    // 将响应数据插入到 DOM 中  
  }) 
  • 1.
  • 2.
  • 3.
  • 4.

7. 我们选择触发假的 ajax* 生命周期事件,并保持这些表单像以前一样异步提交内容,而不是立即重写所有调用,只是会在内部使用 fetch()。

8. 我们自己维护了 jQuery 的一个版本,每当发现我们不再需要 jQuery 的某个模块的时候,就会将它从自定义版本中删除,并发布更轻量的版本。例如,在移除了 jQuery 的 CSS 伪选择器之后(如:visible 或:checkbox)我们就可以移除 Sizzle 模块了,当所有的 $.ajax 调用都被 fetch() 替换时,就可以移除 AJAX 模块。

这样做有两个目的:加快 JavaScript 执行速度,同时确保不会有新代码试图使用已移除的功能。

9. 我们根据网站的分析结果尽快放弃对旧版本 Internet Explorer 的支持。每当某个 IE 版本的使用率低于某个阈值时,我们就会停止向它提供 JavaScript 支持,并专注支持更现代的浏览器。尽早放弃对 IE 8 和 IE 9 的支持对于我们来说意味着可以采用很多原生的浏览器功能,否则的话有些功能很难通过 polyfill 来实现。

10. 作为 GitHub.com 前端功能开发新方法的一部分,我们专注于尽可能多地使用常规 HTML,并且逐步添加 JavaScript 行为作为渐进式增强。因此,那些使用 JS 增强的 Web 表单和其他 UI 元素通常也可以在禁用 JavaScript 的浏览器上正常运行。在某些情况下,我们可以完全删除某些遗留的代码,而不需要使用 JS 重写它们。

经过多年的努力,我们逐渐减少对 jQuery 的依赖,直到没有一行代码引用它为止。

自定义元素

近年来一直在炒作一项新技术,即自定义元素——浏览器原生的组件库,这意味着用户无需下载、解析和编译额外的字节。

从 2014 年开始,我们已经基于 v0 规范创建了一些自定义元素。然而,由于标准仍然在不断变化,我们并没有投入太多精力。直到 2017 年,Web Components v1 规范发布,并且 Chrome 和 Safari 实现了这一规范,我们才开始更广泛地采用自定义元素。

在移除 jQuery 期间,我们也在寻找用于提取自定义元素的模式。例如,我们将用于显示模态对话框的 facebox 转换为<details-dialog>元素(https://github.com/github/details-dialog-element)。

我们的渐进式增强理念也延伸到了自定义元素上。这意味着我们将尽可能多地保留标记内容,然后再标记上添加行为。例如,<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)  

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

我们很期待 Web 组件的 Shadow DOM。Shadow DOM 的强大功能为 Web 带来了很多可能性,但也让 polyfill 变得更加困难。因为使用 polyfill 会导致性能损失,因此在生产环境中使用它们是不可行的。