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 压缩包。

微信公众号:1024 译站
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271