【天天快播报】尤雨溪解读 2022 Web 前端生态趋势
2022-09-08 16:53:54来源:大前端技术之路
尤大大从下面的三个前端领域的不同层次来展开了介绍:
开发范式&底层框架(注:大家比较熟悉的Vue、React这些框架层面)工具链(注:像webpack这样的构建工具)上层框架(注:例如Next.js、Nuxt.js)正式分享之前,尤大大提出声明:“本分享只代表讲着个人观点,因为自己是框架和构建工具的作者,肯定会包含利益相关和个人的偏见,但是分享中会尽可能做客观的看法,大家多多多包涵”,下面就让我们饱享这顿“美味”吧!
【资料图】
下面的内容是根据尤大大的分享进行了一定的抽离和少许的个人总结,如果内容出现歧义可以在评论区留言!
开发范式&底层框架方面趋势过去几年中影响最大的开发范式层面的变化肯定就是我们的React Hooks随着他的推出可以说是启发了很多组件逻辑表达和逻辑复用的新范式,在React生态中彻底取代了Class Components,包括现在其实很少能够在React中看到Class Components了,
不仅如此,其实在其他的框架中React Hooks也产生了很大的影响,比如说我们Vue推出的Vue Composition API组合式API,还包括受到React Hooks的启发的Svelte3,更有SolidJS他是语法上相似于React Hooks实现上更相似于Vue Composition API。
随着 React Hooks 的推广和开发者对其的广泛使用,他开发中的一些体验问题也逐渐被正视,这里不可回避的一些体验问题的根本原因有以下几点:
Hooks执行原理和原生JS的心智模型的差异:因为React Hooks是通过把组件的代码每一次更新都进行重复调用来模拟一些行为,从而导致反直觉的一些限制;不可以条件式的调用React force;Stale Closure的心智负担:如果你不传正确的依赖数组,那么就会产生过期闭包;必须手动声明use Effect依赖;如何‘正确’使用use Effect是个复杂的问题;需要useMemo/useCallback等手动优化,否则的话就会不知不觉的导致一些性能问题;尤大大表示作为竞争框架的作者,对 React Hooks 框架的看法可能相对更直接一些,但这些也并非尤大大一个人的看法,而是近年来 React 社区和 React 团队也已经意识到的问题,当然 React 团队针对这些问题也在做改善的努力,据代表性的改善从下三个方面:
基于依赖追踪范式在上面的这些改进之前,其实很多 React 的社区成员也包括一些本身就不适用 React 的用户来说,虽然 React Hooks 产生了重大的影响但是大家也意识到了他的一些问题,反而是一些跟 React Hooks 相似的一些逻辑组合能力,另一方面基于依赖追踪的范式开始重新得到了重视;比如在 React 内部的 Recoil ,当然在社区之外就有更多了比如:
我们可以看一下就基于依赖追踪的范式而言上面三个方案的代码:
SolidJS
//状态const [count,setCount] = createSignal(0)//副作用createEffect(() => console.log(`${count()`}//状态更新setCount(count() + 1)
能够看出其实 SolidJS 和 React Hooks 非常相似
副作用中的 createEffect 跟 React 中的 use Effect 其实是类似的,但是 createEffect 并不需要去声明依赖,在调用 count 函数的时候其实帮你收集了依赖;状态更新的时候我们也并不需要用到 useCallback 这种额外的方式去创造函数来去传递给我们的事件侦听器;这些都是非常符合直觉的;Vue Composition API
//状态const count = ref(0)//副作用watchEffect(() => console.log(count.value))//状态更新count.value++
其实Vue中使用的Composition API跟SolidJS本质上的内部实现几乎是一样的,只不过SolidJS看起来更像是React,而Vue是通过一个 ref 对象,对象上的 value 机可以读也可以写,在读和写之中就会自动的追踪和更新依赖。
Ember Starbeam
//状态const count = Cell(0)//副作用DEBUG_RENDERER.render({render: () => console.log(count.current)})//状态更新count.set(prev => prev + 1)
Ember Starbeam中的这个Cell其实就和Vue中的refapi 几乎是一样的,暴露出count为当前的值和set方法来进行状态的更新
基于依赖追踪范式—共同点上面提到的三种基于依赖追踪的范式他们的共同点有什么呢?
同时以依赖追踪为一等功名概念的框架中,本身组件的设计肯定也是跟依赖追踪有紧密的结合,所以组件的更新渲染也会有自动的依赖追踪,也就是说组件的更新会更精确,而不再依赖于一个状态从父组件到子组件一层层传递下去,而是每一个即使是深层嵌套的组件也可以自发的更新,整体上的性能会更好。
在react生态中的Recoil这样的方案,虽然也提供了依赖自动的依赖追踪和一定程度的逐渐的更新优化,但是因为他们仍然是需要在React Hooks的这个大的体系中使用的,所以在很多其他的方面依然会受制于hooks的问题,那么Hooks本身在这些方案之外,还是会存在过期闭包等等 user fact 这些问题。
React Hooks确实是启发了一个新范式的时代,但是慢慢的我们也发现他自己自身存在的一些问题,当然React团队正在试图解决这些问题,同时在React体系之外,开始有一些其他的具有同等的逻辑组合能力,但同时避免了React Hooks这些问题的这些方案存在,也渐渐的收到了前端社区的重视。
基于编译的响应式系统不过即使是基于依赖追踪的方案,我们也可以进行一些基于编译时的这个优化,那这里首当其冲的就是Svelte3
Svelte
Svelte3从一开始就是一个编译时优化方案,上面就是Svelte组件中的一个使用状态的代码,我们看到他跟他的状态就是这个javaScript的这个let这样声明一个变量,就是一个响应式的状态,那么你要更新这个状态就直接去操作这个变量就可以,
副作用是用一个神奇的编译式的魔法,也就是这个$,这个$的一个label,这其实是javaScript的一个label语法来声明,$之后的这个语句会自动去追踪count这个变量的变化,当count变化的时候,这个语句就会自动重新执行,那么我们可以看到这个跟我们之前的这个几个代码范例,他所达成的目标其实是一致的,只是他使用编译的手段使代码变的更加简洁,但也正是因为简洁所以存在下面的限制:
Vue Reactivity Transform
也正是受到上方的限制的启发,Vue 在3.2的时候引入了一个实现性的功能Vue Reactivity Transform响应式转换 ,下面就是 Vue 转化后的一段代码:
还是一个简单的变量声明,但是我们用一个 $ref 这样的一个函数,这个函数其实是一个编译时的一个宏的概念,这个函数并不是真实存在的,只是给编译一个提示,那编译器通过编译之后就会把它转化成我们之前看到的基于真实的 ref 的代码。
但是在使用时候,体验就变成了只是声明一个函数,然后使用这个变量和更新这个变量就跟使用一个普通javaScript变量没有区别。同时这个语法因为在声明的时候会显式的声明,说哪个变量是响应声,哪个变量不是响应式。
所以这个语法可以在嵌套的函数中使用,也可以在 TS/JS 文件中使用,他并不限制于 Vue 文件,所以这是一个更加朴实的编译响应式模型。
Solid -labels
在Solid的生态中,其实也受启发于Vue Reactivity Transform,他的社区用户做的一个Solid-label,也是基于Solid的响应式方案,然后再做一层编译式的优化,那么可以看到跟Reactivity Transform能够达成的效果是非常相似的。
那最终的目的就是让大家可以用更简洁的代码去表达组件逻辑,同时又不放弃这个逻辑组合,像React Hooks那样进行自由的逻辑组合的这些能力啊。所以说这也是一个很有意思的探索方向。
统一模型的优势和代价优势:和Svelte
相比,Vue的Reactivity Transform
和Solid \-labels
都属于统一模型,也就是他不受限于组件上下文,它可以在组建内使用,也可以在组建外使用,优势就是有利于长期的重构和复用,因为很多时候我们的大型项目中的逻辑复用都是在我们一个组件写着写着发现这个组件变得很臃肿,很大的时候我们才开始考虑要把逻辑开始重新组织抽取复用,那么由于Svelte
的语法只能在组件内使用,就使得把逻辑挪到组件外成为一个代价相当大的一个行为,并不是一个简单把文把这个逻辑拷贝复制出去,而是需要进行一次彻底的重构,
因为组件外用的是完全一套不同的系统,但是像用Reactivity Transform
和Solid \-labels
这样的方案呢,我们就可以把组件内的这些逻辑原封不动的直接拷贝到组件外,然后把它包在一个函数里面,抽取就完成了,那么这样重构时的这个代价就非常小,也就更鼓励团队的这样的优化,对于长期的维护性更有帮助。
代价:因为我们需要显示的去声明响应式的变量,所以它会有一定程度的底层实现的抽象泄露,也就是说,用户其实是需要先了解底层的响应式模型的实现,然后才能更好地理解这个语法糖是如何运作的,而不像Svelte
组建中的这个语法,即使你完全不了解他底层如何运作的也可以,几乎可以零成本的上手,这就是一个长期的可维护性和一个初期的上手成本之间的一个平衡和取舍。
讲完了状态管理,我们在还可以聊一聊关于基于编译的运行时优化,编译的运行时优化又是三个主要的代表,如上图所示,那首先我们可以看一下不同的这个策略:
Svelte的这个代码生成策略相对更更繁琐一些,而Solid是基于先生成一个基本的HTML字符串,然后在里面找到对应的DOM节点进行绑定,而Svelte是通过生成一这个命令式的一个一个节点,然后把节点拼接的这些javaScript代码,但这个策略就导致掉同等的这个组件源码之下Svelte的每个组件的编译输出会更臃肿,所以虽然大家感觉Svelte是以轻量出名的,
但其实我们会发现在相对大型的项目中,在项目中组建超过15个之后,Svelte的整体的打包体积优势就已经几乎不存在了,那么当组建超过50个,甚至是达到100个的时候,所有的体积会越来越越来越臃肿。
而相对于而言,我们可以看到Vue和Solid的编译这个输出啊,整体的这个曲线就平缓很多,所以其实在越大型的项目中。反而是Svelte的体积优势反而是一个劣势,据我所知,Svelte团队也有在想要优化这一方面的,可能会在下一个大版本中才能实现,那么我们也会拭目以待。
同时尤大大提出Solid的编译性能确实是非常的猛,其实在我们的 Vue 引入了很多编译时的优化以后我们的性能已经比Svelte好了,但是离Solid还是有一定的距离。
Vue Vapor Mode(input)就上面提及到的编译时性能优化,其实我们的 Vue 在早期的时候也做了这方面的探索,如还在试验中的一个项目Vue Vapor Mode。
那同样的这个只有单文件组件输入,我们现在是通过把模板编译成虚拟DOM的一个渲染函数来进行运行时的实现。但是因为模板是一个编译源,所以我们也可以选择在另一个模式下把它编译成不同的输出,也就是一个更类似于Svelte输出。
这里这个输出的代码只是一个示例代码。并不一定是最终的代码,也不是你需要书写的代码,它完全是一个编译器的输出啊,它的整体的思路就是一次性生成这个模板的静态结构、静态节点,然后再去生成命令式的,找到动态节点,并对把它跟状态进行响应式的绑定的这样一些代码,这个策略本质上就是Solid
所采用的策略,
那么其实呢,这个策略可以被所有的模板引擎所使用,我们也在探索某个版本的Vue
当中会引入一个可选的这样的一个模式,把模板编译成这样的,性能更优的,运行时的这个体积也更小的一个模式,当然这不会是一个破坏性更新,因为我们的目标是可以让你渐进式的去使用这个功能。
关于原生语言在前端工具链中的使用尤大大提出下面几个见解:
工具链的抽象层次最早的打包工具,包括brow/webpack/rollup他们都是专注于打包的,他们的抽象层次相对低,当你想要用这些工具去做一个真正的应用的时候,你需要使用大量第三方插件,以及大量的配置来达到一个满足你自己要求的最终的形态。
那么在这个基础上就产生了像Parcel/Vue-cli/CRA,这样的一些所谓的脚手架,更高抽象层次的这些工具,这些工具的特点是他们的抽象层的高,也就说他们专注于应用,专注于解决一个完整的应用方案呢,它的相对而言的缺点就是它是一个比较复杂、比较庞大的一个黑盒儿。
当你需要去进行自定义的定制的时候,你就会不可避免的遇到一些问题,比如说你跟他默认的功能产生一些意见上的冲突的时候,你就会比较痛苦。
那么我们现在做的这个新项目Vite其实可能有一些同学已经在用了,其实我们是在思考过这个抽象层次的问题之后才决定的他要走一个怎么样的路线,也就是说Vite的CLI它是专注于应用层次啊,它的抽象层次高,它有很多的开箱记,就是事先帮你寄配置好的功能,那么大部分的情况下,你开箱即用就可以达到跟Parcel/Vue-cli/CRA几乎同等的这些功能啊,但是我们的API层面啊,这个可能用到的同学会少一些,
但是它的API层面其实是专注于支持上层框架,我们这个抽象层次会更低一点,我们只解决一些所有的够meta framework都必须要解决的问题,但是对于上层框架,你用什么,我们并不会做过多的限制,反而是要做的更尽可能的灵活,能够支持任何上层框架的用例,所以这也是为什么Vite现在几乎成为了下一代的meta framework共同的一个基底层选择。
基于 Vite 的上层框架我们看到上面这么多的上层框架都在基于Vite说明我们Vite走的路线还是相对成功的。
上层框架 Meta FrameworksJS全栈的意义是什么 ?如果我们讲到这个Meta Frameworks,也就是最典型的例子,也就是NextJS 、NuxtJS、以及现在React社区中的新秀Remix等等,那么当我们思考这样类型的JS全栈的时候,我们做全栈的意义是什么?
那么相信在国内很多大企业的朋友都知道,因为我们可以用同一个语言去做前后的连接,我们可以做一些纯前端和纯后端都各自做不到的事情,或者说之前需要很复杂的联调才能达成的一些事情,那么JS全栈可以更好的去完成一个语言让我们可以把前后打通。那么我们能够打通什么呢?
数据的前后端打通类型的前后端打通JS全栈的代价一些新的全栈框架,现在在试图去改善的一些问题首先。我们现有的这些前端框架,比如说像主流的像React、Vue我们在做了服务端渲染之后,还需要在前端要进行一次所谓的注水,也就是Hydrate在追寻的过程中,我们要确保在客户端和前端有同样的数据,所以其实虽然我们的数据已经用于渲染HTML,
这些数据理论上在HTML里面已经都用过了,但是我们还得再把这个数据再发送一次,一起发送到前端,让前端去进行Hydrate这样一个过程。因为没有这个数据,我们在前端就没有办法保证Hydrate的正确性啊。
在客户端,有些组件它可能在客户端是不,需要交互的是静态的,但是他在服务端用到了动态的这个数据,但这个组件依然会被发到服务端,它依然会可能产生这个javascript运行时的代价啊,以及缓慢的这个Hydrate会影响页面的交互指标,也就是 time to interactive。
有一些比较复杂的庞大的项目,他可能这个注水的过程会把页面卡顿,以至于虽然能看到页面,但是没法交互,要等个一秒钟才能交互等等,会产生这样的问题。
社区探索的方向社区现在新一代的这些全栈框架都在试图解决这些问题啊,比如说像React提出了server only components其实从这个定义上,我们就发现他是没有一个全栈框架,围绕一个全栈框架去做,其实用户是没有办法简单地使用的一个概念,所以React server only components其实是一个必须要全站才能做的概念,Next 当然也会去做,然后,其实 Nuxt 最近也开了一个server only components的一个提案,所以说这个已经就是说server only components其实不仅仅是一个React独有的概念,在很多其他的框架中,我们可能慢慢都会出现类似的这个类似的东西。
还有一个方向就是减少注水,hydration的这个成本,那么也就是局部的注水,或者也叫island architecture就像大海中一个小岛,只有这些小岛去对他进行注水,让他交可交互啊。那么比较代表性的就是astro、isles和生态里面的fresh这些框架。
然后呢,还有一个探索方向,就是所谓的fine-grained+resumabl hydration,就是细粒度懒加载,这个数据其实是Qwik这个框架所发明的,Quick的作者就是Misko Hevery,也就是Angular的原作者,离开Google之后,现在新开发的这个框架啊,那么Qwik它主打的就是说它的特点就是不需要再把数据重新发送一遍。
他是直接在生成的渲染的html里面嵌入所需的数据从而使得客户端的js可以直接在html里面获得所要的数据,甚至是可以跳过一些需要执行的js步骤,直接跳到一个已经完成的状态上面去,这就是所谓的resumable,也是一个比较值得关注的一个方向。
以及我们的 Vue 生态里面生态里面有一个我们的VitePress,我们其实探索的是一个在我们页面的核心内容:其实是静态的MD文件的前提下如何做高效率的hydration那么我们做的是所谓的hydration就是整个的外部的这个一个框架内容外包着的这一层ui是动态的,然后呢在内部静态的里继续进行局部的注水,然后这样的话,我们依然可以获得一个单页应用的体验,但又获得很好的客户端注水的性能。