仅用于站内搜索,没有排版格式,具体信息请跳转上方微信公众号内链接
React曾经以简洁、灵活的组件化理念征服了无数前端开发者,成为构建现代Web应用的事实标准。然而,在它大获成功的背后,也有越来越多开发者开始感到困惑,甚至抓狂——这个曾被誉为“前端终极解法”的框架,真的还那么“简单”吗?
在这篇长文中,作者以切身经历,从Angular的崛起讲到React的“疯狂演化”,层层剖析React在状态管理、架构设计、Hooks使用等方面所带来的实际困扰。他并非只是吐槽,而是试图从更深层的角度理解:为什么我们写前端会变得这么复杂?到底是React的错,还是整个Web开发模式本身早就出了问题?
作者|mbrizic编译|苏宓
出品|CSDN(ID:CSDNnews)
那些年我写的Angular
在我还只是个初级程序员、四处接活的日子里,我靠写Angular. JS谋生。那时候,它算是一门非常出色的技术。绝对是当时最大的JS框架,更重要的是,它可能是第一个真正意义上的Web开发框架。在那之前,大多都是“库”(libraries)——提供一堆函数供你使用;而Angular不仅给了你工具,还提供了整个搭建Web应用的结构和范式。
当然,好也只是相对而言。Angular之所以显得好,是因为它的前辈们都不行。当时也有些单页应用框架,比如Backbone、Knockout,但它们都没留下太大印象。真正被Angular干翻的对手,其实是jQuery。
虽然jQuery本质上只是HTMLDOMAPI的一层包装(而那时候的DOMAPI确实挺糟糕的),但如果你想做个复杂点的网页,它几乎就是事实标准。它的用法非常直接:你在JS里手动创建HTML元素、控制行为、挪来挪去,靠一堆命令式代码让网站变得“像应用”。
这对于简单项目这没问题,但你可以想象,一旦项目规模变大,就会变成维护者的噩梦。而这正是当时大家普遍遇到的问题。也不能怪jQuery,只能说用户的需求在变,他们想要的交互越来越复杂,而开发者只能硬着头皮用jQuery去实现。哪怕它早就不适合干这活了。
这时候Angular出现了,一切豁然开朗。
你终于可以专注于编写UI和业务逻辑了,而不用再去手动拼HTML。这真的是一次颠覆性的进步:第一次,有了能做“大型”交互式Web应用的正经工具。它带来的一些“魔法”特性包括:
A)组件化。虽然它的命名有点怪怪的,叫“directives”,但本质就是你可以定义一个HTML+JS组合的UI片段,然后在多个地方复用。
B)双向绑定。你定义一个变量,它一旦发生变化,界面上所有用到它的地方都会自动更新。这套机制当时用起来非常顺手。但后来,有人开始挑毛病,说这种“数据可以随处流动”的方式太混乱,于是大家开始推崇“单向数据流”(从上往下传)。听起来确实更“工程化”,但一落地反而让开发变得更复杂,还引发了一连串关于状态管理的争论,最后的结果就是——我们全都在用Redux了。
我的第一份工作就是把一个庞大、难以维护的jQuery应用重构成Angular项目,整个过程还算顺利,效果也挺不错。
然而,不怎么美好的是——几年后我又不得不把那些相同的页面再用Angular2重写一遍。幸好我在公司打算第三次用React重写时,及时离职了。
React登场
后来我有机会接触了React,甚至在一两个项目里真正用了起来。
我还记得第一次看到它时,那种眼前一亮的感觉。当时大家还在用的是Angular2——它完全重写了初代Angular,结果是模板代码量翻倍、强制使用TypeScript、数据单向绑定、响应式/可观察者模式……单拎出来都不错,但组合在一起后,开发体验异常复杂,构建速度慢、运行也慢。
React就像钟摆一样,把前端框架摇回了“简单主义”的方向,大家自然一拥而上。那段时间,React的确还保持着“简单”,于是它一路走红,成了构建SPA(单页应用)的首选库。
细心的人可能会发现,我在这里开始称它为“库”而不是“框架”了,可见它当时确实更轻量。但你光靠一个库,是不可能搞定完整应用的。你得用好几个库去填充功能缺口,还得自己制定代码结构。React的“自带啤酒”哲学就是:你想喝啥都得自己带——于是你其实在半手工搭一个属于你自己的“框架”,并承担所有由此带来的后果。
结果就是——没有两个React应用是完全相同的。每个项目都像是从互联网上拼凑了一堆库、DIY出来的定制“微型框架”。
我当时不幸参与的几个React项目,全都让我产生了一个念头:就算是Angular2,也比这玩意儿强。JSX本身没啥问题,看着也算稳,但它周围那一圈东西……完全是一锅粥。
于是我跑路了,转身去写Java后端,这大概已经说明一切。
就在我以为可以逃离的时候……
有人说,人的知识都是“开关式”的,一共有两种情况——一是你懂了,另一种就是你没懂。我显然属于后者,因为我最近又把自己拉回了React。
当然,这只是个业余项目,不像正式上线的产品那样复杂,所以我也没完全体验到React的“全部威力”。但即便如此,这次的使用过程还是不仅验证了我对它的低预期,甚至比我想的还要离谱。
React给人的感觉真的很疯狂——我不明白为什么没人在认真讨论这件事。
架构、组件、状态
我们先从React所“推崇”的架构讲起。就像前面说的,React只是一个库,它并不会强迫你做任何事情,但由于JSX的存在,它隐含的限制导致一些模式成为了“默认架构”。在很久以前,我们常讨论MVC、MVVM、MVP这些架构,其实只是同一个套路的不同变体。
那么React属于哪种?
我觉得它都不是,反而是一个全新的范式,可以称作“组件式架构”。
乍一看,这逻辑很清晰。你有组件,组成一个从上往下的树形结构,于是应用就搭好了。React内部会帮你处理状态更新与UI同步,挺简单的。
但不知从哪一刻开始,这一切就开始变得“过于聪明”了。对于一个自称“UI库”的东西,React拥有大量复杂的术语;而对一个跟函数式编程毫不相关的项目来说,它却偏偏借来了大量函数式的术语。
这篇文章中,我们就从“状态”开始说吧。如果你有一个自顶向下的组件树,按理说也应该自顶向下传状态。但现实中组件数量繁多、颗粒度很小,结果你会花费大量时间和代码,只为了把某些数据“接力传递”到需要的地方。
这个问题通过用Reacthooks把状态“偷偷塞”进组件里解决了。我倒是没听谁抱怨过,但你们是认真的吗?你们的意思是——任何组件都可以随便用整个应用里的任何状态?更夸张的是,任何组件都能修改状态,然后影响到其他组件的显示?
这怎么能通过代码审查?这根本就是全局变量,只不过加了一层“复杂一点的修改流程”伪装。甚至连“流程”都谈不上,就是一种仪式感。因为你根本无法防止别人在任何地方随意变更状态。大家真的觉得,只要给这种做法取个聪明名字(比如叫reducer),它就突然一下变成了好架构了吗?
所以说,如果“自上而下传递状态”和“旁加载状态”这两种做法都很烂,那到底该怎么解决这个问题呢?说实话,我也没什么好办法。唯一能想到的就是:也许我们根本就没法优雅地解决它,那说明整个“组件化架构”可能一开始就是个错误,我们不该把它捧成所谓的“最佳设计典范”,然后就停止了探索和创新。也许这次真的轮到再发明一个新的JS框架,试试有没有更好的思路了。
ReactHooks
接下来我们继续聊那些“令人怀疑怎么能过审的设计”——ReactHooks。不可否认,它们很有用,但它们的存在本身让我一头雾水。
我甚至懒得吐槽“组件是纯函数”这种说法,因为Hooks根本就不是纯函数,它们是一个个有状态的小黑盒。更糟的是,Hooks可以组合,你可能堆好几层,全是黑盒,层层嵌套,状态分布四面八方。
不过算了,我主要还是想吐槽一下useEffect。按理说,“副作用”这个概念应该很好理解:你改了个状态,然后需要做点额外的事,比如发个网络请求。理论上,这种把“核心业务逻辑”和“sideeffect”区分开的做法,听起来挺有道理的。可问题是,在实际开发中,你真的能把它们分得那么清楚吗?
我最不满的一点就是——大家把useEffect当成“组件挂载后执行点什么”的工具来用。我知道React从类组件转向Hooks的时候,useEffect是最接近componentDidMount的替代方案。但这不就是个赤裸裸的“黑魔法”吗?
你用一个“副作用”的钩子来初始化组件?如果你只是从里面发个API请求,那确实算是副作用。可问题是,那个API请求……它还要更新组件的状态。也就是说,一个看起来很无害的useEffect副作用钩子,实际上在管理组件状态。没人觉得这事儿离谱吗?
更疯狂的是:如果你还想基于这个状态再做点别的操作,你就得……再写一个useEffect,依赖前一个useEffect设置的那个状态。
这段代码,来自一家刚被收购、估值数千万美元的公司的线上产品。我这里稍作改编,把里面真实的业务实体替换成了“house”和“cat”这样更好理解的例子。你可以先看看,试着猜猜这段代码到底是按什么顺序执行的。准备好了吗?答案在下面这张图里:
所以就是这样——原本可以用简单命令式代码写清楚的状态变化流程,现在被拆散成两个异步函数,而你仅能靠每个函数底部那个“依赖数组”来猜它们的执行顺序。而实际的理解方式,居然是从下往上“反着读”。
我记得以前大家还在嫌JavaScript的Promise太繁琐、then链太多,再往前还有“回调地狱”……但说真的,不管哪个,都比这清晰!
我理解这类问题也许可以通过两个方式缓解:
a)抽出去放进单独文件(只是在藏问题),
b)用Redux或类似方案重构(但我经验有限,不敢下结论)。
“最佳实践模式”
以上种种,组合起来看就很丑,完全背离了React“HelloWorld”示例中所承诺的那种简洁优雅。但等等,我还没吐槽完。
我最近读了一个熟人写的博文,标题叫《最常见的React设计模式》。原本没抱太大期望,结果读下来还是被震惊到了:就为了在屏幕上渲染一个列表,模式居然可以复杂成这样,每一步都要极高的认知负担。
最讽刺的是:文章里居然完全没提“这些写法是不是太绕了”。这一切复杂性都被默认接受了。看上去大家真的就是这么写UI的,没人觉得有什么不对劲。
然后,有些人还不满足,还要写“CSS-in-JS”,然后还真有人为此发工资。好吧,我承认JSX的出现打破了“关注点分离就是文件分离”的迷思,把HTML和JS写在一起确实也没啥问题。但你连CSS也硬塞进来,还要强类型支持?这是不是有点太过分了?
为什么会变成这样?
如果我只是简单地说React是“疯了”,然后拍拍屁股走人,那也太容易了。既然我们是理性的人类,那不如稍微深入一点,试着理解这个问题。
我又回忆起了第一份工作,回到当年那个负责“从jQuery迁移到Angular”的项目,还有其中一位资深同事。他是那种典型的后端大神,架构师级别,对软件开发有着深厚理解和威望。
我印象最深的不是他的技术方案,而是他每次看我们前端代码时的表情。他看着我们的Angular应用,总是一脸懵逼:“你们到底在干嘛?为什么要搞得这么复杂?”
问题不在我们——我们也是一群认真做事的开发者。但在一个传统后端程序员的眼里,当时整个Angular生态看起来就像一场灾难。
今天,我的年龄大概跟他当年差不多,而我也正在写一篇博客,吐槽AngularReact有多“疯”。有些轮回,大概是逃不掉的吧。
但我们试着站得更高一点,理解这背后的本质。
首先,我认为我们可以达成一个共识:大多数Web应用,本就不该做成Web应用。
很多人一开始不需要用到SPA,但最终还是选择SPA的方式,因为他们觉得可能以后会用到,所以不如一开始就选SPA。
但我想说的是,其实你用React并不是“没代价”的。只是我们太习惯了“默认就用单页应用(SPA)”的思维方式,反而忘了更简单的替代方案有多轻松。比如,用一个传统的、纯粹的服务端渲染页面,复杂度比React低太多太多了——没有前后端通信的额外开销,前端代码非常轻量;如果你的后端是强类型的,UI代码也可以一起受益;你可以对整个前后端同时做重构;页面加载更快;而且因为有些组件对所有用户都是一样的,你完全可以只渲染一次,然后缓存起来……这些优势数都数不完。
当然,你会因此失去那种“产品经理一个想法,前端立马能实现”的灵活交互能力。但这也不完全成立——我认为,如果你愿意用点原生JS做“渐进增强”,可以走得很远,直到真的复杂到“非React不可”的程度才再引入它。
所以,我说React之所以被用,是因为大家过去就已经用了。惯性,是种强大的力量。但这仍然不能解释为什么React写出的代码,会变得这么不可理喻。
我的答案,其实并不是为了抨击React——而是为Angular、jQuery乃至所有曾经的框架辩护:
因为这类代码糟糕的根本原因,是“构建一个任意组件都能影响任意其他组件的交互式UI”本身,就是软件工程里最复杂的事之一。
你回想一下我们日常接触的其他系统:厨房水龙头有两个输入(冷水和热水),一个输出(流水);电钻可能就一个按钮,影响一个电机;烤箱最多四五个旋钮,对应几个加热元件,已经足够复杂危险了。
但WebUI呢?输入和输出都可能是无限多个。你怎么可能指望能写出“干净的代码”来支撑它?
所以,我这整篇关于React的吐槽……其实根本不是React的错。也不是Angular的错,更不是jQuery的错。简单来说,无论你选用哪种技术栈,最后都不可避免地会面对构建交互式UI的复杂性压垮一切的现实。
这个问题怎么解决?我也不是天才,给不出终极方案,但可以随便聊聊思路。如果我们把网页看成“输入”和“输出”的系统,或许可以从源头上做减法——减少输入输出的数量。
先说“输入”。是的,我的意思就是:能少放按钮就少放按钮。虽然现实中这个事我们常常控制不了,但真的得说清楚:功能越少,代码越简单,维护越容易。
这话听着像废话,但真有产品经理意识到这点吗?他们知道一个额外的按钮,可能会让维护成本增加30%,bug概率提高5%吗?没人统计过,但我相信它是真的。
为什么我提议后端引个Redis,你说“要控制技术复杂度”;可当产品随口提了个“全局筛选器,可以影响任意页面”的需求,你就老老实实写出了一坨十年都甩不掉的烂代码?
别再随便加按钮了,求你们了。甚至能不能删掉一些?
再说“输出”。我写这篇文章时突然意识到一件事:服务器端渲染其实就是只保留一个输出。
用户每操作一次,就重新渲染整个页面。听上去笨,但其实非常纯粹:没有前端状态管理,代码复杂度直接砍半。如果做得到,真的太值了。
当然,完全没有JS不现实,还是得加点交互。但我觉得更合理的方式是:只在最必要的地方,用最小的JS代码。
我甚至想给这种做法起个名字:“交互小岛(islandsofinteractivity)”。一查才发现这个词早被用过了,虽然人家说的是Preact、SSR、manifest之类的技术,我怀疑讲的根本不是一回事。人类总有办法把简单事搞复杂。
但我真心觉得——以我们今天的网络带宽,完全可以在SSR页面里嵌一个小型React应用,只负责一个局部的交互“岛屿”。这种组合,一点也不可怕,甚至我打算下个项目就这么干。
所以,我想尝试一种未经验证的前端开发方式:
全站用服务器渲染,只有必要的交互才引入React或其他库。
不管怎么说,都不会比现在更混乱了。
好啦,今天的内容分享就到这,感觉不错的同学记得分享点赞哦!
PS:程序员好物馆持续分享程序员学习、面试相关干货,不见不散!
点分享
点收藏
点点赞
点在看