GitHub:为何我们最终决定放弃使用jQuery?

发表时间: 2018-09-19 14:49

【CSDN编者按】7月17日,GitHub改版并放弃了jQuery, 这对于GitHub来说,绝对是一件可以载入公司史册的大事。今天的文章中,四位参与改版和弃用JQuery的GitHub工程师,将介绍最初GitHub使用jQuery的历史背景、和后来不再需要jQuery的原因,并讲解GitHub如何在不引入其他库、或框架的情况下,通过标准浏览器API,来实现他们需要的功能的。

为什么最初需要jQuery

我们最近刚刚完成了一个里程碑,成功地从GitHub.com的前端代码的依赖中去掉了jQuery。这标志着这项一点一滴持续了多年的jQuery解耦合工作的完成,以及我们终于可以完全删除这个库了。

GitHub.com在2007年末引入了jQuery 1.2.1作为依赖。当时距离Google发布Chrome浏览器的第一版,还有一年的时间。

当时没有什么标准的方法,通过CSS选择器,来查询DOM元素,也没有标准的方式,来实现元素的视觉动画,而由Internet Explorer倡导的XMLHttpRequest接口,也像许多其他API一样,在各种浏览器上的实现不一致。

而jQuery使得操作DOM、定义动画和实现“AJAX”请求,变得十分简单。简单来说,它使得Web开发者可以创建更现代、更动态的效果。

最重要的是,通过jQuery在一种浏览器上实现的功能,基本上也能在其他浏览器上运行。

在GitHub的早期,许多功能才刚刚起步,有了jQuery,我们的小团队才能迅速地建立原型、并推出新功能,而不需要为每种Web浏览器调整代码。

我们还把jQuery简单的接口,作为蓝图来构建扩展库,这些库(pjax,
https://github.com/defunkt/jquery-pjax和Facebox,
https://github.com/defunkt/facebox),后来成了GitHub.com前端的其他部分的组成部分。

我们会永远感谢John Resig和其他jQuery贡献者们,创建并维护了这个十分有用、并且在历史上十分重要的库。

后来的Web标准

多年以后,GitHub成长为拥有数百名工程师的公司,还逐渐组成了一个独立的团队,专门负责我们发送到浏览器上的JavaScript代码的尺寸和质量。

我们一直在监视技术债务,而有些技术债务的原因,是那些曾经有价值、但后来随着时间的发展而失去了价值的依赖。

而对于jQuery,我们将它与现代浏览器中迅速发展的Web标准做了比较,结果发现:

  • $(selector) 可以简单地用querySelectorAll()替换;
  • CSS类名切换,可以通过Element.classList实现;
  • CSS现在支持在样式表中定义视觉动画,无需使用JavaScript;
  • $.ajax请求可以用Fetch标准实现;
  • addEventListener()接口已经十分稳定,足以跨平台使用;
  • 我们可以用一个轻量级的库,来封装事件代理模式;
  • jQuery提供的一些语法糖,已随着JavaScript语言的发展,而变得多余。

而且,链式语法并不能满足我们直观地书写代码的需要。例如:

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

这种语法很容易编写,但以我们的标准来看,它并不能很好地传达作者的意图。作者希望页面上只有一个JS-Widget元素、还是有多个?

而且,如果我们修改网页代码时,一不小心删掉了JS-Widget类名,浏览器会产生异常并告诉我们发生了错误吗?

默认情况下,如果类名不匹配,jQuery会静默地忽略整个表达式,但在我们看来,这种行为与其说是功能,不如说是个Bug。

最后一点,我们想使用Flow(https://flow.org/)进行标注,从而在构建时实现静态类型检查。

但我们得出结论,链式语法并不能很好地适应静态分析,因为几乎所有jQuery的函数的返回值,都是同一种类型。

我们选择Flow、而不是其他库的原因是因为当时像@flow weak模式等特性可以让我们逐渐地、有效地给大量几乎没有任何类型的代码添加类型。

总的来说,jQuery解耦合,意味着我们可以更依赖于Web标准,将MDN Web文档,作为事实上的前端开发标准,方便以后维持代码的灵活性,并最终从打包文件中,去掉一个30KB的依赖,提高页面加载速度、和JavaScript的执行时间。

增量解耦合

即使确定了最终目标,我们也不能简单地,把所有资源都花在,使用原生JS重写jQuery的事情上。

一旦发生什么事情,这种急功近利,会导致许多网站功能倒退,从而不得不花更多时间去解决。我们必须要这样做:

设定好度量标准,跟踪jQuery调用次数和全部代码行数的比例,并随时监视该度量,保证它不变或减小,而不会增加。

我们不鼓励在任何新代码中使用jQuery。为了使用自动化减轻工作量,我们创建了eslint-plugin-jquery(
https://github.com/dgraham/eslint-plugin-jquery#readme),如果任何人尝试使用jQuery功能(如$.ajax),它就会造成CI检查失败。

旧代码中有大量的ESLint规则违反,这些违反我们都使用eslint-disable规则在代码注释中标注出来了。这样就能尽快进行代码审查、并集思广益。

许多旧代码显式地耦合了Pjax和Facebox这两个jQuery插件的外部接口,因此我们在使用原生JS,替换这两者的实现时,尽力保持接口不变。静态检查让我们能更信心地进行重构。

许多就代码都与rails-behaviors(
http://josh.github.io/rails-behaviors/)有接口,后者是我们在Ruby on Rails和JS之间的适配器。这种接口会为特定的表单,添加一个AJAX生命周期处理函数。

 // LEGACY APPROACH $(document).on('ajaxSuccess', 'form.js-widget', function(event, xhr, settings, data) { // insert response data somewhere into the DOM })

为避免不得不用新方法,一次性重写整个网站,我们采用了触发伪“*AJAX*”生命周期事件的方式,使这些表单,能像以前一样继续异步提交内容,只不过内部使用的是fetch()。

我们定制了一个jQuery,一旦我们认为某个模块不再需要,就把它从定制版本中删掉,使jQuery更灵巧。

例如,在删除最后一个jQuery专用的CSS伪类(:visible、:CheckBox等),我们就删掉了Sizzle模块(https://sizzlejs.com/);在使用fetch()替换了最后一个$.ajax调用之后,就删掉了AJAX模块。

这样做有两个目的,一是加快JavaScript执行速度,一是确保新功能不会使用被删掉的功能。

根据网站访问分析的结果,只要有可能,我们就会删掉支持旧Internet Explorer版本的部分。当某个IE版本的使用率,降到某个阈值之下,我们就不会再为其提供JavaScript,从而得以专注于,支持更多现代浏览器。

提早去掉IE8~9的支持,使得我们可以使用更多的原生浏览器功能,不用再勉强进行Polyfill。

作为构建GitHub.com前端的新方法的一部分,我们尽可能采用基础的HTML来实现功能,只把JavaScript用作渐进式增强。

这样,即使Web表单和其他UI元素上使用了JS,它们也能在禁用了JavaScript的浏览器中运行。一些情况下,我们可以删掉整个旧有行为,不用再使用原生JS重写。

通过这些方法(以及多年来积累的其他方法),我们得以逐渐地减小对jQuery的依赖,直到没有一行代码使用它。

自定义元素

近几年人们谈论得最多的一项技术就是自定义元素,它是个浏览器原生的组件库,也就是说用户无需下载、解析或编译任何框架。

我们从2014年起,就根据v0规格,建立了一些自定义元素。但是,由于当时的Web标准依然不明确,所以我们并没有深入研究。

直到2017年,Web组件的v1规格发布,而且Chrome和Safari都开始支持,我们才开始在大范围内使用自定义元素。

在jQuery迁移过程中,我们寻找适合提取成自定义元素的部分。例如,我们将使用Facebox显示对话框的代码,改成了<Details-Dialog>元素。

渐进式增强的思想,也应用到了自定义元素中。就是说,我们尽可能保持标签的内容,仅在标签不能实现的地方,添加新的行为。

例如,<local-time>默认会显示原始的时间戳,然后增强翻译成本地时区内的时间;而<details-dialog>如果嵌入到<details>元素中,那么即使没有JavaScript,本身也是交互式的,但会利用提高可用性的功能进行增强。

下面是实现自定义元素<local-time>的例子。

 // The local-time element displays time in the user's current timezone// and locale.//// Example:// <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组件功能之一,就是Shadow DOM。

Shadow DOM的强大功能,可以给Web带来许多可能性,但也使得它很难polyfill。

因为现在的polyfill方式,会给那些操纵与Web组件无关的DOM的代码,也造成大量性能损失,所以还不适合在生产环境中使用。

Polyfill

我们在转换成标准浏览器功能的过程中,使用了下面这些polyfill。我们尽量仅在绝对必须时——即需要兼容旧版本浏览器时——才使用polyfill。

  • github/eventlistener-polyfill;
  • github/fetch;
  • github/form-data-entries;
  • iamdustan/smoothscroll;
  • javan/details-element-polyfill;
  • jonathantneal/closest;
  • kumarharsh/custom-event-polyfill;
  • marvinhagemeister/request-idle-polyfill;
  • mathiasbynens/Array.from;
  • mathiasbynens/String.prototype.codePointAt;
  • mathiasbynens/String.prototype.endsWith;
  • mathiasbynens/String.prototype.startsWith;
  • medikoo/es6-symbol;
  • nicjansma/usertiming.js;
  • rubennorte/es6-object-assign;
  • stefanpenner/es6-promise;
  • webcomponents/template;
  • webcomponents/URL;
  • webcomponents/webcomponentsjs;
  • WebReflection/url-search-params;
  • yola/classlist-polyfill。

原文:
https://githubengineering.com/removing-jquery-from-github-frontend/

作者:mislav, koddsson, muan, keithamus

译者:弯月,责编:胡巍巍