深入聊聊 JavaScript 框架
The following article is from 抖音前端技术团队 Author 陈敏亮
一、前言
关于 JS 框架部分能聊的内容比较多,我相信大家对某个框架的使用、原理等知识是比较容易找到资料来学习的,鉴于此这部分内容将会从另一个视角出发:通过回顾 JS 框架的发展历程,和大家一起探讨框架的本质以及 JS 框架不断变化背后的驱动力。正所谓「鉴往知来」,希望大家能对 JS 框架有更全面的认知,能够把握变化背后不变的逻辑,更好的应对未来新的变化。
二、什么是框架
在讲清楚一件事情之前,我们需要先准确定义它,这一部分我们先定义清楚什么是框架以及什么是 JS 框架。
先看一个通用意义上「框架」的的定义:
框架(framework)是一个框子——指其约束性,也是一个架子——指其支撑性。是一个基本概念上的结构,用于去解决或者处理复杂的问题。
个人非常喜欢这个定义,简洁而准确,框架的核心就是「约束性」和「支撑性」。
如何理解「约束性」
框架是解决复杂领域问题的系统性方法,对于复杂问题而言,往往缺失「银弹」,大家看问题的角度会有差异,每个框架背后都有自己的理念,当你选择某个框架就需要遵循它的逻辑和规则,在规则之内解决问题,这是「约束性」。
如何理解「支撑性」
复杂问题通常可以拆解成一系列小问题,问题的整体解法则是如何解决它们中的所有或者部分,框架的「支撑性」体现在它提供了问题的定义和一系列解决问题的方法,基于它能够将针对各个子问题的解法更加有序的组织起来,最终形成整体的解法,「支撑性」即有序组织。
如何定义 JS 框架
解决前端特定领域问题的代码组织框架,约束性体现为需要基于约定的结构来组织代码,支撑性体现为基于框架内置的机制或能力高效的实现功能。
按这个定义 JS 框架覆盖了方方面面,例如解决 Web 整体实现的前端应用框架、游戏引擎框架、后端服务框架等等,但需要指出的是大家通常认为的 JS 框架指的是前端应用框架,这也是这部分文章介绍的重点。
再顺道说一下框架和库的区别,JS 库通常是解决系统构建中某个子问题的代码实现,它和框架的关系往往是被调用者(库)和调用者(框架)的关系。
JS 框架要解决什么问题
一直以来 Web 前端技术发展非常迅速,新技术更是层出不穷,但这些技术发展的背后始终围绕着一条主线——如何更高效的构建 Web 人机交互界面。复杂 Web 应用的构建首先要考虑的是如何更合理的组织代码,这关系到协作效率、可维护性、可扩展性等等,本质是对生产效率的持续追求。
再进一步拆解来看,我们的问题域主要包括视图构建、视图状态管理、用户交互、服务端交互等,在复杂场景下,随着表达内容和人机交互多样性的增加,如何兼具效率、体验等具有较大的挑战,而这也恰恰是 JS 框架所要解决的问题。
三、JavaScript 框架的发展
从 Tim Berners-Lee 在 1989 年的提议开始到 2019 年 3 月 12 日,WWW 迎来了它的 30 岁生日, 这 30 年无疑是人类科技进步最快的 30 年,Web 也从最初的提议变成了人类社会的基础设施。
从技术角度看,Web 在满足人们越来越多线上化需求的同时,Web 前端也在变得越来越复杂:更强大的功能、更丰富多样的内容、更复杂的运行环境等等,从最初的小制作到现在的大规模复杂应用,不断推动开发者们寻求更高效的前端构建方式,这也正是前端技术快速发展背后的动力。
从 Web 前端的形式看,大致可以将 30 年划分为三个时期:只读 Web 时期、可交互 Web 时期、Web 应用时期。
只读 Web 时期(1990~2000),Web 的核心功能是图文内容的线上化,只有少数用户使用并且只能浏览内容。从技术角度看,这个时代是 Web 标准的起源阶段,同时由于单一的内容表现需求,尚不存在诞生 JS 框架的土壤。
可交互 Web 时期(2000~2010),越来越多的人通过 Web 满足需求,Web 内容形态也从单一的只读图文展示演变成功能丰富的可交互形式,这种演变也促使 Web 开发领域演化出新的社会分工角色 —— Web 前端工程师。与此同时,有了 JS 框架的土壤之后,形形色色的前端库和框架也逐渐诞生,但此时它们更多专注于解决某个方向的问题:例如兼容性或者组件化等等;
Web 应用时期(2010 至今),对于很多行业而言,Web 服务已经属于基础设施,大多数人已经依赖 Web 能力来满足需求。为了追求更好的体验以及更高的效率,Web 服务的构建也像客户端应用靠齐,JS 框架的发展则逐步覆盖到高效构建 Web 前端相关的整个领域。
只读 Web 时期
1989 年,为了让学校和科研机构间能够更高效的共享信息,Tim Berners-Lee 提出了「WWW」提议并且在次年发明了第一个 web 浏览器 - WorldWideWeb。而在随后的 1991 年,HTML 的第一个版本 - 「HTML Tags」诞生,目标是文本、图片等信息的在线展示以及互联互通。
第一个商业化浏览器的诞生则是在 1993 年,来自 Netscape 的 Netscape Navigator。W3C 的诞生则要到 1994 年,由 Tim Berners-Lee 创办。
直至 1995 年,Web 上所有内容仍然是纯静态,但随着 Web 需求的快速增长,逐步有些场景需要用户交互的能力,因此在 1995 年 9 月,Netscape 发布了 Javascript 的第一个版本 - LiveScript,并在 3 个月后改名为 JavaScript。
这一时期除了 WWW 的起源之外,另一件值得一提的事是史上第一次「浏览器大战」,在这场大战中,Netscape 在前期以先发优势拿下 80% 的市场份额,微软 IE 则后来居上,以和 Windows 捆绑的方式逐步赢回优势,到 2000 年微软 IE 市场份额占比超过 80%。
有意思的是在第二次「浏览器大战」中,IE 又败下阵来,后起之秀 Chrome 借着更高的性能、更好的标准兼容性以及更快的迭代速度攻城掠地,至 2019 年占领 70% 以上的市场份额,IE 市场份额则降至 10% 左右。
对开发者而言,「浏览器大战」中由于各方对标准的支持程度有差异,甚至为了实现差异化会特意引入新特性,因此开发者需要投入大量的工作来解决兼容性问题,这也为后续的 Javascript 库/框架的产生埋下伏笔。
可交互 Web 时期
进入 2000 年,互联网加速发展,随着电商、社交等新平台的推出,越来越多用户加入互联网,此时的互联网用户群已经逐步由最初的小众用户发展为大众用户,各大平台也越来越重视 Web 用户体验,开始在 Web 用户交互方面增加资源投入。
另一方面,浏览器通过持续的升级也具备更多的能力来支撑更好的用户交互,例如异步请求能力的引入等。以上需求和供给两侧为 JavaScript 库/框架的发展提供了土壤。
这个阶段,Web 主流的渲染方式是后端渲染,JavaScript 的重点工作是对页面实现局部交互能力,例如表单验证、异步提交、图片轮播、Tab 切换等等,这一时期的 JavaScript 库/框架围绕完善用户交互相关基础设施发展,主要包括组件化、兼容性以及工具库。
组件化
与其他编程语言中的人机交互界面构建能力相比,在 Web 中缺乏丰富、标准化的 UI 组件库。因此在这个阶段涌现出一批以组件化为主的框架,其中比较有代表性的是 DoJo(2005)、ExtJS(2007)。这些框架的特点是提供了 All in One 的组件库,组件类型丰富,当然交互形式也非常厚重,典型的桌面客户端风格。
兼容性
「浏览器大战」的大背景下,浏览器厂商各自为战,于是不论是 BOM、DOM 还是 JavaScript 层面都需要做大量的兼容性工作,于是 jQuery(2006) 便在这一背景下诞生。
jQuery 一方面解决了大量兼容性问题,另一方面提供了非常便捷的基于 CSS 选择符的 DOM 获取方式以及 DOM 操作能力,此外还重新封装了异步请求的 API,这些能力在以命令式(imperative)编程范式为主的时期是非常高频的操作,因此 jQuery 的出现大大提升了前端编程体验和效率。
<script>
$( "div span:first-child" )
.css( "text-decoration", "underline" )
.hover(function() {
$( this ).addClass( "sogreen" );
}, function() {
$( this ).removeClass( "sogreen" );
});
</script>
当然,随着 jQuery 很多能力从事实标准演进为正式标准,浏览器开始提供原生支持,再者前端编程模式从命令式向声明式(declarative)演进,针对 DOM 的获取与操作大大减少,jQuery 的使用率也随之快速下降。此外,随着 Chrome 在「浏览器大战」中胜出,兼容性问题已经有了非常大的改善,当然移动端除外。
工具库
这一时期原生 JavaScript 仅支持一些较「原始」的编码能力,对比其他成熟的编程语言而言在编程效率和体验上都存在巨大差异,各类工具库基于「原始」能力进行再封装,能较大程度缩小差异。工具库类的代表有 PrototypeJS(2005)、YUI(2006)、Mootools(2007),它们主要针对面向对象、事件、异步请求、数组操作等方面进行了封装。
/*PrototypeJS 代码示例*/
// 面向对象
var FirstClass = Class.create( {
// The initialize method serves as a constructor
initialize: function () {
this.data = "Hello World";
}
});
// 异步请求
Ajax.Request = Class.create( Ajax.Base, {
// Override the initialize method
initialize: function(url, options) {
this.transport = Ajax.getTransport();
this.setOptions(options);
this.request(url);
},
// ...more methods add ...
});
// DOM 与事件
$$('#items li').each( function(item) {
item.observe('click', function(event) {
doSomethingWith(event.target);
});
});
// 数组遍历
myArray.each(function(item) {
// Your code working on item here...
});
当然,按上述三个方面并不是能够特别准确的去给众多 JavaScript 库/框架进行归类,很多采用分层以及可拆解的设计,能够同时兼顾各个方面的需求,就如以下 YUI 的整体架构。
Web 应用时期
最近 10 年,随着前端投入的持续增加,前端团队的话语权也越来越重,同时大家也在持续探索更高效的前端研发模式,例如这期间逐步发展的前后端分离、前端工程化等,尤其是前后端分离的协作模式给前端开发带来了更高的自由度和空间,前端的工作职责从最初的局部动态化、局部可交互逐步扩张到整站的前端构建。
这种变化直接带来了至少两方面的挑战,一是如何解决多人协作问题,针对规模越来越大的前端工程,多人协作是必然,而协作的关键是明确的分工和合作;二是代码的可维护性问题,在前端框架出现之前,局部动态 UI 通常采用前端模板加命令式编程范式来实现,这种方式对于轻量的 UI 构建是可接受的,但在应对大规模前端功能构建时,很容易写出可维护性极差的代码。
这些是构建 GUI 应用时的典型问题,在 Web 前端出现之前的其他平台的 GUI 构建中也有类似的问题,解决的方式是引入合适的架构模式,将看似乱做一团的代码分门别类,各自关注各自职责范围内的工作,这些架构模式包括 MVC、MVP、MVVM 等等,背后的核心理念是「关注点分离」。
在 Web 前端面临类似的挑战时,自然而然的是借鉴传统 GUI 构建的经验,通过框架的形式引入新的架构模式来解决,在这一背景下前端也逐步出现了真正意义上的「框架」。
2010 年发布的 Backbone.js 引入了视图和数据模型的概念,但它并不是标准的 MVC 实现,没有明确的控制器概念,而是由视图承担部分控制器职责,但这些并不重要,重要的是前端 GUI 构建有了新的思路和选择。
另外一点值得一提的是,Backbone.js 仍然采用了命令式编程范式,这在前端 GUI 构建中与后续逐渐流行的声明式编程范式而言,编程效率上会有明显的差距。
/*Backbone.js 代码示例(部分)详细前往 https://backbonejs.org/docs/todos.html*/
var AppView = Backbone.View.extend({
el: $("#todoapp"),
statsTemplate: _.template($('#stats-template').html()),
events: {
"keypress #new-todo": "createOnEnter",
},
initialize: function() {
this.input = this.$("#new-todo");
this.listenTo(Todos, 'add', this.addOne);
this.footer = this.$('footer');
this.main = $('#main');
Todos.fetch();
},
render: function() {
var done = Todos.done().length;
var remaining = Todos.remaining().length;
if (Todos.length) {
this.main.show();
this.footer.show();
this.footer.html(this.statsTemplate({done: done, remaining: remaining}));
} else {
this.main.hide();
this.footer.hide();
}
this.allCheckbox.checked = !remaining;
},
addOne: function(todo) {
var view = new TodoView({model: todo});
this.$("#todo-list").append(view.render().el);
},
clearCompleted: function() {
_.invoke(Todos.done(), 'destroy');
return false;
}
});
var App = new AppView;
});
同年 AngularJS 发布第一个版本,和 Backbone 的最大不同,一方面它充分借鉴了微软 WPF 的 MVVM 架构模式,引入 VM 概念,支持视图和数据的双向绑定;另一方面它采用了声明式的视图构建模式,大大减少了原生 DOM 操作。
此外,AngularJS 提供的功能非常全面(路由管理、组件化、视图模板、状态管理、后端交互、事件管理、动画等),基本达到开箱即用的状态,使用它能够非常高效的构建 SPA 类应用。
当然,它的缺点也非常明显,引入了过多的概念导致上手成本非常高,此外它的「大而全」是优点的同时也是缺点,对于非 SPA 类 Web 场景而言过于臃肿。总体而言,AngularJS 对于解决特定场景中的前端架构问题是非常有效的,能够非常显著的提升前端协作效率。
在 AngularJS 发布后的第四年和第五年 React 以及 Vue 陆续发布,React 发布时的目标是优化视图构建,但随着整个生态的完善,React 已经有充分的能力支撑大规模的前端应用开发。Vue 则借鉴了 AngularJS 的架构模式,并且同样采用了声明式的视图构建方式。
相比 AngularJS,React 和 Vue 的最大优势是更加的轻量和灵活,能够适应更多的场景,此外上手成本也要低得多,尤其是 Vue,这也让后两者在最近几年能够碾压 AngularJS。
纵观最近 10 年前端框架的变迁,有两点得以验证,一是声明式编程范式相比命令式在前端 GUI 构建中的成功,背后的关键逻辑是研发效率的显著优势;二是渐进式框架相比 All in One 「全家桶」式框架的成功,渐进式框架有更好的灵活性,面对前端场景的变化有更强的适应性。
四、鉴往知来
以上走马观花的概览了最近 30 年前端框架的发展,整体演进脉络是清晰的,Web 前端从无到有,从有到逐步走向专业化,技术上的关注点和挑战也在不断变化,从解决最初的 DOM 操作效率、兼容性、组件化等问题,再到提升大规模前端的协作效率等,那些能够准确把握并且有效解决问题的框架/库都获得了成功,至少是阶段性的成功。顺着这条脉络往前看,前端框架的后续发展仍然取决于前端领域在未来将面临何种挑战。
Web vs Native
在智能手机普及的初期,各平台的移动端也曾经以 Web 为主,当时各种 Web 服务的 m 站是必备之一。随着智能手机的普及,移动 APP 具备明显的路径、性能、能力等方面的优势,迅速成为移动化的首选。在这个过程中,Web 前端很大程度上淡出移动应用的核心研发工作,后来随着 hybrid 开发模式的成熟,Web 前端又逐步参与到一些移动应用的开发中。
再看未来,逐步由流量运营到以用户运营为核心的时期,以 Native 为主导的模式预计仍然会是主流,而且用户体验变得更加重要,尤其是用户长停留的应用。在这一背景下,Web 和 Native 如何更好的协作,如何取长补短,在效率和体验上能够进一步突破天花板将是长期的挑战。
智能终端
前面更多谈论的是 PC 端和移动端,随着 5G 的完善和普及,IoT 会迎来加速发展,届时在移动端和 PC 端两端之外将会出现更多形式各异的终端,这类终端的 GUI 需求如何解决,是否可以采用 Web 技术来提效,现有 Web 框架在 IoT 设备上是否是最佳选择?随着 IoT 的爆发,这些问题会是前端框架不得不考虑的问题。
低代码化
另外一个看得见的趋势是低代码化(low/no code)的普及,不久的将来大量的模式化的 Web 开发工作极大可能将通过低代码化的方式来解决,例如运营、管理类的平台就是典型的模式化 Web 平台,这类平台在当前的前端研发工作中占有很大的比例,也是当前各类前端框架的重要应用场景,一旦这类需求由低代码化的方式来实现,那么具体的实现使用何种框架将变得不那么重要。
在这个背景下,很可能会诞生一些专有框架来更高效的解决低代码化的领域问题。而通用类框架的场景会更多的集中在非模式化的场景中,在这类场景中对框架的灵活性、定制化等会有更高的要求。
- EOF -
觉得本文对你有帮助?请分享给更多人
推荐关注「前端大全」,提升前端技能
点赞和在看就是最大的支持❤️