当前位置: 首页 > 科技观察

游鱼溪解读2022Web前端生态趋势

时间:2023-03-19 18:12:57 科技观察

游达达介绍了以下三个不同层次的前端领域:开发范式&底层框架(注:Vue、React等框架层次大家都很熟悉)工具链(注:webpack等构建工具)在官方分享上层框架之前(注:如Next。本文作者肯定会包含利益和个人偏见,但我会在分享中尽量做到客观,请多多包涵”,下面让我们一起享用这顿“美味”吧!以下内容是在游达分享的基础上进行了一定的抽象和个人的一点总结,内容如有歧义,可以在下方留言评论区!开发范式&底层框架趋势这几年最有影响的开发范式变革一定是我们的ReactHooks了,它的推出可以说启发了很多新的范式组件逻辑表达和逻辑重用。在React生态系统中完全取代了ClassComponents,包括现在在React中很少见到ClassComponents。不仅如此,ReactHooks在其他框架中也产生了很大的影响,比如我们的VueCompositionAPI推出的VueCompositionAPI,包括受ReactHooks启发的Svelte3,以及语法类似于ReactHooks的SolidJS等等实现上类似于VueCompositionAPI。随着ReactHooks的推广和开发者的广泛使用,其开发中的一些经验问题也逐渐被人们认识。这里不可避免的一些体验问题的根本原因如下:Hooks执行原理与原生JSModel的思维差异:由于ReactHooks每次更新时通过重复调用组件代码来模拟一些行为,从而导致一些反直觉的行为限制;不可能有条件地调用Reactforce;StaleClosure的精神负担:如果你没有传递正确的依赖数组,将会产生一个过期的闭包;useEffect依赖必须手动声明;如何“正确”使用useEffect是一个复杂的问题;需要手动优化useMemo/useCallback等,否则会在不知不觉中导致一些性能问题;游达表示,作为竞品框架的作者,对ReactHooks框架的看法可能相对更直接,但这些并不是游达的观点,但是React社区和React团队这几年也意识到了当然,React团队也在努力改善这些问题。根据具有代表性的改进,主要有以下三个方面:基于依赖跟踪范式。在上述改进之前,其实很多React社区成员也包括一些不适用React的用户,虽然ReactHooks产生了不小的影响,但大家也意识到了它的一些问题。而是一些类似于ReactHooks的逻辑组合能力。另一方面,基于依赖追踪的范式开始被重新找回。注意;比如React里面的Recoil,当然社区外还有更多例如:我们可以从依赖跟踪范式的角度来看一下上面三种方案的代码:SolidJS//statusconst[count,setCount]=createSignal(0)//副作用createEffect(()=>console.log(`${count()`}//状态更新setCount(count()+1)可以看出SolidJS和ReactHooks非常相似sideeffectsReact中的createEffect其实和React中的useEffect类似,只是createEffect不需要声明依赖,它实际上是在调用count函数的时候为你收集依赖;state更新的时候我们不需要使用useCallback.创建函数的方式来来去去的事件监听器;这些非常直观;VueCompositionAPI//stateconstcount=ref(0)//sideeffectswatchEffect(()=>console.log(count.value))//状态更新count.value++其实Vue中使用的CompositionAPI差不多和SolidJS的内部实现一样,但是SolidJS看起来更像React,而Vue使用的是ref对象,对象上的值机可以读也可以写。在读写过程中,会自动跟踪更新依赖EmberStarbeam//statusconstcount=Cell(0)//sideeffectsDEBUG_RENDERER.render({render:()=>console.log(count.current)})//Statusupdatecount.set(prev=>prev+1)EmberStarbeam中的Cell其实和Vue中的refapi差不多,暴露了count作为当前值和set方法来进行state的更新基于依赖跟踪范式——上面提到的三种基于依赖跟踪的范式有什么共同点?同时,在以依赖跟踪为一等概念的框架中,其自身组件的设计必须与依赖跟踪紧密结合,因此组件的更新渲染也会有自动依赖跟踪,也就是说组件更新会更准确。不再依赖一个状态从父组件到子组件逐层传递,每个组件,即使嵌套很深,也可以自发更新,整体性能会更好。React生态中类似Recoil的解决方案也提供了自动依赖跟踪和一定程度的渐进式更新优化,但是由于在ReactHooks这个大系统中仍然需要用到,所以在很多其他的方面还是会受制于hooks的问题,所以Hooks本身除了这些解决方案之外还是会存在过期闭包等用户事实的问题。ReactHooks确实启发了一个新时代的范式,但慢慢的我们也发现了它自身的一些问题。当然,React团队正在努力解决这些问题。同时,在React系统之外还有一些其他等价物。逻辑组合能力,但同时,这些避免ReactHooks问题的解决方案存在,并逐渐受到前端社区的关注。基于编译的响应式系统但是即使是基于依赖跟踪的方案,我们也可以基于编译进行一些优化。这里首先是Svelte3SvelteSvelte3从一开始就是一个编译时优化方案。以上是Svelte组件之一。使用state的代码,我们可以看到他和他的state是javascriptletdeclareavariable,这是一个responsivestate,那么如果要更新state可以直接操作变量,副作用是使用一个神奇的编译魔法,就是这个$,这个$的一个标签,其实就是javaScript的一个标签语法来声明。$后面的语句会自动跟踪变量count的变化。当计数发生变化时,会自动重新执行这条语句,那么我们可以看到,这和我们之前的代码示例一样,他达到的目的其实是一样的,只是他使用了编译让代码更简洁,但是也正是因为它的简洁,所以有如下限制:VueReactivityTransform也是受到了上述限制的启发。Vue在3.2引入了一个实现函数VueReactivityTransform响应式变换。下面是Vue改造后的一段代码:还是一个简单的变量声明,但是我们用的是$ref这样的函数。这个函数在编译时其实就是一个宏的概念。这个功能实际上并不存在。它只是给编译一个提示。编译器通过编译后,会将其转换为我们之前看到的实际基于ref的代码。但是在使用的时候,体验就变成了只是声明一个函数,然后使用这个变量,更新这个变量就和使用普通的javaScript变量没什么区别了。同时,这个文法会显式声明哪个变量是响应声音,哪个变量不是响应类型,因为在声明的时候会显式声明。所以这种语法可以用在嵌套函数中,也可以用在TS/JS文件中。它不限于Vue文件,所以这是一个更简单的编译响应模型。Solid生态中的Solid-labels实际上是受到了VueReactivityTransform的启发。他的社区用户做的一个Solid-label也是在Solid的基础上做的响应式方案,然后做了一层编译优化,所以可以看到和ReactivityTransform能达到的效果很像。最终的目的是让大家在不放弃这种逻辑组合的情况下,能够用更简洁的代码来表达组件逻辑,像ReactHooks那样自由组合逻辑的能力。所以这也是一个值得探索的有趣方向。统一模型的优势和成本优势:Vue的ReactivityTransform和Solid\-labels相对于Svelte,属于统一模型,即他不限于组件Context,可以在组件内部或外部使用。好处是有利于长期的重构和复用,因为很多时候我们大型项目中的逻辑复用都是写在我们的某个组件里面的。我们发现这个组件变得非常臃肿,直到它很大的时候才开始考虑重新组织、提取和重用逻辑。由于Svelte的语法只能在组件内部使用,它使得逻辑移到了组件之外。成为一个代价高昂的行为并不是简单的将逻辑复制出文本,而是需要进行彻底的重构,因为组件使用了完全不同的体系,但是像ReactivityTransform和Solid\-labels这样的解决方案,我们可以直接把组件里面的逻辑拷贝到组件外面,然后用一个函数包裹起来,提取完成,那么这样重构的成本就非常小了,鼓励团队这样优化,就是更有利于长期可维护性。成本:因为我们需要显式声明响应式变量,所以会对底层实现有一定程度的抽象泄漏。也就是说,用户其实需要了解底层响应式模型的实现,才能更好的理解这个语法糖是如何工作的,不像Svelte中的语法,即使不了解底层是如何工作的,也可以以几乎零成本开始。这是一个长期可维护性与初始入门成本之间的平衡和权衡。基于编译的操作是优化。说完状态管理,我们还可以谈谈基于编译的运行时优化。基于编译的运行时优化是三个主要代表,如上图所示。首先,我们可以看一下不同的策略:Svelte的代码生成策略相对比较繁琐,而Solid是基于先生成一个基本的HTML字符串,然后在里面找到对应的DOM节点进行绑定,而Svelte是生成一个command的一,然后将节点拼接在一起创建这些JavaScript代码,但是这种策略导致在这个组件相同的源代码下,Svelte的每个组件的编译输出比较臃肿,所以虽然大家觉得Svelte是出了名的轻量级的,但实际上我们会发现,在一个比较大的项目中,超过15个项目形成后,Svelte的整体打包体积优势几乎不存在,所以当超过50个甚至100个以上的时候,所有的体积都会变多并且更加臃肿。对比一下,我们可以看到Vue和Solid的编译输出。整体曲线要平缓很多,所以其实在更大的项目中。相反,Svelte的尺寸优势是劣势。据我所知,Svelte团队也在努力优化这方面,可能会在下一个大版本中实现,我们拭目以待。同时,您指出Solid的编译性能确实很强。其实我们的Vue引入了很多编译优化之后,我们的性能已经比Svelte好,但是和Solid还有一定的距离。VueVaporMode(input)指的是上面提到的编译时性能优化。其实我们的Vue在早期也有这方面的探索,比如VueVaporMode,这个项目还在测试中。同样只有单文件组件输入,我们现在在运行时通过将模板编译成虚拟DOM的渲染函数来实现。但是由于模板是编译源,我们还可以选择以另一种模式将其编译为不同的输出,即更像Svelte的输出。这里输出的代码只是一个示例代码。它不一定是最终的代码,也不是你需要编写的代码。它完全是编译器的输出。它的总体思路是一次性生成模板的静态结构体和静态节点,然后生成命令式是的,找到动态节点并以响应式的方式绑定到状态上。这个策略本质上就是Solid采用的策略。事实上,这个策略可以被所有的模板引擎使用。我们也在探索某个版本的Vue,会引入一种可选模式,将模板编译成这个,性能更好,运行时体积更小。模式,当然这不会是一个破坏性的更新,因为我们的目标是让你逐步使用这个特性。前端工具链中工具链中原生语言的使用前端工具链中原生语言的使用特别提出了如下见解:工具链的抽象层次最早的打包工具,包括brow/webpack/rollup,都是以Packaged为主,抽象层次比较低。当你想使用这些工具做一个真正的应用时,你需要使用大量的第三方插件和大量的配置才能最终达到一个符合你自己要求的形态。然后在这个基础上,一些所谓的脚手架像Parcel/Vue-cli/CRA,这些抽象层次比较高的工具,特点就是抽象层次高,也就是说,他们专注于Application,专注于解决问题一个完整的应用解决方案,它的相对缺点是它是一个相对复杂和庞大的黑盒。当你需要定制定制的时候,难免会遇到一些问题。例如,当你对它的默认功能有一些意见冲突时,你会更加痛苦。嗯,我们现在在做的新项目Vite,可能真的会有一些同学在用。其实他走什么样的路线,我们其实是在思考了这个抽象层次,也就是Vite的CLI之后才决定的。它专注于应用程序级别。它具有高度的抽象。它有很多开箱即用的注释。它是帮助您提前发送配置的功能。在大多数情况下,您可以开箱即用地达到与Parcel/Vue相同的水平。-cli/CRA功能差不多,但是在我们的API层面,可能用这个的同学比较少,但是它的API层面其实是专注于支持上层框架的,我们的抽象层次会低一些。我们只是解决一些所有元框架都必须解决的问题,但是对于上层框架,你用什么,我们不会做太多的限制,而是尽可能的灵活,能够支持任何上层的用例框架,所以这就是为什么Vite几乎成为下一代元框架的共同基础层选择。基于Vite的上层框架,我们看到上面这么多上层框架都是基于Vite的,可见我们在Vite走的路线是比较成功的。上层框架MetaFrameworksJS全栈意义何在?如果说这个MetaFrameworks,最典型的例子,就是NextJS,NuxtJS,还有现在React社区的菜鸟Remix等等,那么当我们想到这类JS全栈的时候,我们做全栈是什么意思?嗯,相信国内很多大公司的朋友都知道,因为我们可以用同一种语言来回连接,所以可以做纯前端和纯后端都做不了的事情,或者需要之前要很复杂。有些东西只有通过联调才能实现,那么JS全栈才能更好的完成一门语言,让我们前后端打通。那么我们能通过什么?数据的前后端连接到该类型的JS全栈的成本现在一些新的全栈框架试图先改善一些问题。我们现有的前端框架,比如主流的React、Vue,在做服务端渲染之后,需要在前端进行一次所谓的注水,即在追求Hydrate的过程中,我们必须确保客户端和前端有相同的数据,所以其实我们的数据虽然已经被用来渲染HTML了,理论上HTML中已经用到这些数据了,但是我们要重新发送这个数据,再发送给前端端在一起,让前端走Hydrate的过程。因为没有这些数据,我们没办法保证前端Hydrate的正确性。在client端,有些组件可能不在client端,需要交互的是静态的,但是他在server端用的是动态数据,但是这个component还是会发送到server端,可能还是生成这个javascriptruntime的成本,缓慢的Hydrate会影响页面的交互索引,即timetointeractive。对于一些复杂庞大的项目,注水过程可能会卡住页面,导致页面虽然可以看到,但是无法交互,交互需要一秒等等,都会造成这样的问题。社区探索的方向社区现在正在尝试用新一代的全栈框架来解决这些问题。例如,React提出了仅限服务器的组件。其实从这个定义我们发现它并没有一个全栈框架,围绕一个全栈Stack框架,其实用户是没有办法简单使用一个概念的,所以Reactserveronlycomponents其实是一个概念全站必须实现的,Next肯定会做,而且,事实上Nuxt最近也开了一个serveronlycomponents的提案,所以这已经意味着serveronlycomponents不仅仅是React独有的概念,在很多其他框架中,我们可能逐渐会有类似这样的东西。另一个方向是减少注水。补水的代价是局部注水,或者岛式建筑就像海中的小岛。只有这些小岛才能把它灌满水,让它互动起来。那么比较有代表性的就是生态中的astro、isles和fresh的框架。那么,还有另外一个探索方向,所谓fine-grained+resumablhydration,也就是细粒度懒加载。这些数据实际上是由Qwik框架发明的。Quick的作者是Angular的原作者MiskoHevery。离开谷歌后,现在是新开发的框架,所以Qwik的主要特点是不需要重新发送数据。他直接在生成的渲染html中嵌入需要的数据,让客户端的js可以直接获取html中需要的数据,甚至可以跳过一些js需要执行的步骤,直接跳转到完成状态上面去,这就是所谓可续传,也是一个更值得关注的方向。而在我们的Vue生态中,生态中有我们的VitePress。我们真正探索的是我们页面上的一个核心内容:如何在实际为静态MD文件的前提下进行高效的水化。那么我们做的就是所谓的注水,就是把这层ui外包出去的整个外部框架内容是动态的,然后在内部静态继续进行局部注水,然后这样,我们还是可以得到单页应用的体验,但获得良好的客户端注水性能。