今天的文章中,四位参与改版和弃用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显示对话框的代码,改成了元素。
渐进式增强的思想,也应用到了自定义元素中。就是说,我们尽可能保持标签的内容,仅在标签不能实现的地方,添加新的行为。
例如,默认会显示原始的时间戳,然后增强翻译成本地时区内的时间;而如果嵌入到元素中,那么即使没有JavaScript,本身也是交互式的,但会利用提高可用性的功能进行增强。
下面是实现自定义元素的例子。
// The local-time element displays time in the user's current timezone // and locale. // // Example: // Sep 6, 2018 // classLocalTimeElementextendsHTMLElement{ staticgetobservedAttributes() { return['datetime'] } attributeChangedCallback(attrName, oldValue, newValue) { if(attrName ==='datetime') { constdate =newDate(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。
网友评论文明上网理性发言 已有0人参与
发表评论: