最近买了尤雨溪大大的Live:不吹不黑聊聊前端框架,这场Live让我的前端思维到了前所未有的高度:当我们身为前端开发萌新,在前端人才金字塔的浮动与挣扎中思考该学什么框架、该如何入门前端、又遇到学习瓶颈怎么办的时候,正是这些业界大牛们用自己的行动引导着我们,有如尤大所说:多思考场景需求,多看看技术到底做了怎样的取舍,现在把相关的东西作为笔记整理下来,希望对前端开发有兴趣的同学都可以去支持一下尤大
组件可以是函数
想象一下整个应用是一个大的函数,函数里面可以调用别的函数,每一个组件是一个函数,一个组件可以调用其他的函数,整个一个树状结构
组件是有分类的
- 纯展示型的组件,数据进,DOM出,直观明了
- 接入型组件,在React场景下的container component,这种组件会跟数据层的service打交道,会包含一些跟服务器或者说数据源打交道的逻辑,container会把数据向下传递给展示型组件
- 交互型组件,典型的例子是对于表单组件的封装和加强,大部分的组件库都是以交互型组件为主,比如说Element UI,特点是有比较复杂的交互逻辑,但是是比较通用的逻辑,强调组件的复用
- 功能型组件,以Vue的应用场景举例,路由的router-view组件、transition组件,本身并不渲染任何内容,是一个逻辑型的东西,作为一种扩展或者是抽象机制存在
JSX和模版的对比
JSX本质就是Javascript,它完全获得了Javascript的灵活度,它最大的价值在于书写功能型组件的时候比纯模版更好,但是模版在展示型和其他的用例上也是不差的,模版会让你更少的把逻辑放在视图里面,展示型的组件虽然在逻辑上比较简单但是在样式上还是具有一定的复杂度
代码的拆分(colocation)
把应该放一起的东西放一起,比如说在vue的单文件组件里面我们把模版、样式和javascript的逻辑都是放在一起的,目前主流框架都是这么做的,如果这概念扩展开来,组件的文档也是可以和组件的其他东西放一起的,组件化之后和传统的Separation of concern就有区别了,传统的Separation of concern是以语言为单位作切分,组件是以组件本身作为一个切分的抽象
变化侦测和渲染机制
- 渲染机制
现代的前端框架里面渲染这块最重要的是声明式(Declarative),相比较之下的概念是命令式(Imperative),Imperative最直接的例子是我们使用Jquery时候拿到一个选择器"直接干",使用命令去进行操作,直截了当,但是很快就会遇到维护性的问题,英文里面有个词叫做jQuery Spaghetti,Spaghetti的意思是意大利面,这个词的意思就是说你的代码写到后面会像一坨意大利面一样,维护起来很困难,声明式的好处就是说我们直接描述说数据和DOM结构之间的映射关系应该是怎么样的,不需要我们手动去做这些操作
React里面有一个等式 view = render(state)
,这里render就是我们在React里面写的render函数,vue的模版其实也是编译成渲染函数的,所以模版和JSX之间的本质是相似的,输入state,输出DOM,理想情况下我们就是描述了这样一种关系,那输入变了输出也会跟着变,我们不需要顾虑输入和输出之间发生了什么事情,具体到底层实现可以是Virtual DOM,但并不一定得是Virtual DOM,可以是细粒度的绑定
- 变化侦测
用过vue的朋友知道vue的数据是响应式的,vue会把你传递的数据进行转化,转化过后当你改变一些属性的值的时候,vue就会进行相应的更新,附上尤大相关的演讲PPT,里面有详细的过程:
ppt
Live提问:
问:一直有一个疑问,以前<div onclick="clickHandler"></div> 被人诟病,为啥 vue 的声明式写法就是推崇的?
答:HTML里面这个onclick里面的Javascript的作用域是全局的,当你在vue里面这么写的时候是有很明确的Javascript作用域的,你的绑定以及你的method所能触及的影响范围是设定好的,这个跟全局的Javascript有本质的区别,另外当我们在vue或者Reac里面这么写的时候你的Javascript逻辑和你的模版或者说你的JSX是在一起的,可以联想我们之前提到的colocation的概念,所以并不会造成一个维护上的困难,但是如果你在全局的Html里面这样直接裸写,你完全不知道你这段Javascript可能会引用到哪里的变量或者是调用的哪里的方法
简单的总结,变化侦测主要分为两种:
pull
所谓pull,系统不知道数据什么时候变了,它需要一个信号去告诉它说数据有可能变了,在这个系统才会去进行一次比较暴力的比对,在React里面的表现是Virtual Dom Diff,在Angular里面就是整个脏检查的流程,能够这么做的前提是现在Javascript足够快,虽然有浪费但是性能上也可以接受push
相比之下,vue的响应式数据或者RXJS的数据机制,在数据变动之后立刻就可以知道数据变动了,而且一定程序上我们会知道哪些数据变了,这样就可以进行相对更细粒度的更新,pull的这种更新是最粗粒度的,所以在大型应用里面我们要帮助系统来减少一些无用功,但是push的形式也有它的缺陷,粒度越细,你的每一个绑定都会需要一个observabel/watcher,这样会带来相应的内存以及依赖追踪的开销,所以在vue2里面选择的是一个比较中等粒度的方案,在组件级别是push,每一个组件是一个响应式的watcher,当数据变动时候我们可以对组件进行更新,在每个组件内部则是用Virtual Dom进行比对,push和pull之间的本质区别是在于用侦测成本换取一定程度的自动优化
状态管理
状态管理这个概念其实也是在FB提出了Flux之后才搬到台面上来讲,Flux在经历了初期的混乱竞争之后慢慢的合流到了Redux上,vux在一定程度受到了Redux的影响,状态管理的本质是从源事件(source event)映射到状态的迁移和改变,然后在映射到UI的变化,声明式的渲染已经帮我们解决了从状态到UI的映射,这一块,所以状态管理这些库他们做的实际上是如何管理将事件源映射到状态变化的过程,如何将这个映射的过程从视图组件中剥离出来,如何组织这一部分代码来提高可维护性,是状态管理要解决的本质问题
把 Vue 当 Redux 用
把 Vue 当 MobX 用
现在的状态管理方案还面临一些其他的共同的尴尬,一个是组件的局部状态和全局状态如何区分,现在是局部状态和全局状态并没有很明显的区分,另一个是全局状态和服务端数据之间,现有的方案是把服务端抓过来的数据塞到store里面去
路由
路由是只有在大型的单页应用才会遇到的一个问题,传统的路由思想是比较有侵入式的,每个路由有自己的数据模型,有自己的模板等,但是当Reac和vue出现之后人们发现把路由和组件解耦是可行的并且还更加灵活,比如Reac直接用不带路由是完全没问题的,另一个启示是,如果从组件出发去思考路由,本质上就变成了把一个url映射到组件树结构的一个过程,url到组件的映射会有一些小的分歧,我们到底是应该从url出发,还是从这个状态出发,其实本质是一样的,因为url就是一个序列化的状态。
当实际在SPA中去做一个你会发现路由会涉及到许多其他问题,比如说hash模式和history模式如何兼容,重定向,别名,懒加载,然后最复杂的是跳转,路由之间的跳转需要提供各种"钩子",然后这些"钩子"里面又可能做异步操作,"钩子"里面也有可能取消这次跳转,使得这次跳转无效等等。
整体来说现在主要的路由方案都有点相似,比较有意思的是最新的reat-router4,他推崇的是一种用组件本身来做路由的一种思路,这里很大程度上利用了上述第四大组件"功能型组件",在父组件里面声明式的渲染其他组件,跟传统的路由组件方案的区别是"去中心化",他不是把整个路由表写在一个地方,是分散的写在各个组件里头,这样做的好处是灵活性非常好,但是也有一些问题,首先,集中式的路由表对于理解整个应用的结构是有帮助的,另一方面,去中心化的路由对于跳转的管理会弱一些,他对于跳转的管理是直接用组件的生命周期去做的。
web路由和app路由的区别:
目前web路由整体思路上是一样的,将url映射到组件树,从一个url跳转到另一个url,我们把新的url push到历史的stack里面去了,但是stack前一个位置所对应的位置是被我们丢弃掉的,我们从一个状态迁移到另一个状态我们整个应用界面迁移到另一个状态了,原生应用上的跳转就像一叠卡片一样,新的界面会盖在现有的界面上,当你退回去的时候只是把当前的卡片拿掉,之前的卡片就会出现,web用的路由方案做app会比较别扭。
CSS方案
主流的 CSS 方案
- 跟 JS 完全解耦,靠预处理器和比如 BEM 这样的规范来保持可维护性,偏传统
- CSS Modules,依然是 CSS,但是通过编译来避免 CSS 类名的全局冲突
- 各类 CSS-in-JS 方案,React 社区为代表,比较激进
- Vue 的单文件组件 CSS,或是 Angular 的组件 CSS(写在装饰器里面),一种比较折中的方案
比较 CSS 方案时首先要明确场景的问题,如果应用逻辑已经组件化了,是一个比较复杂应用的开发,传统的 CSS 方式可维护性就有问题了
react-css-in-js
反对css-in-js的文章
传统 css 的一些问题:
- 作用域
- Critical CSS
- Atomic CSS
- 分发复用
- 跨平台复用
css-in-js有很多不同的方案,这些方案各自解决了上述的一些问题,但是并不完美:
- CSS Modules,Inline-Styles,vue的单文件组件里面直接加一个scoped都可以解决这个问题
- 所谓的Critical ,比如说我们直出一个页面,可能我们整个应用有几十个页面,但是我们直出的永远是第一个页面,如果没一个页面都有一个对应的CSS的话,理论上渲染首屏我们只需要首屏的CSS就够了,这就是所谓的Critical CSS,在服务端渲染尤为重要,解决的办法是在服务端渲染的时候侦测到渲染要用到哪些CSS,css-in-js和vue2.3+有一个运行时的功能,在编译过程里面可以把CSS的插入跟组件的生命周期挂钩,同样可以起到收集Critical CSS的效果
Live提问:
问:在vue里 使用CSS Modules 会不会比 使用 scoped 好?
答:我个人觉得没有什么本质的区别,scoped的成本会更低一点,CSS Modules会有一定的运行时的代价,因为需要用动态的class绑定
- Atomic CSS的概念:比如说我们有两条CSS规则,一条是color:red,一条是color:green,我们写两个button的样式,一个按钮是红的,一个按钮是绿的,原子类的话就会把color:red单独拆成一个类:A,把color:green单独拆成一个类:B,然后所有button共享的再拆成一个类:C,然后红色的button可以说是AC,绿色的button是BC,总而言之就是把尽可能多的共享的一些单独的规则都拆成一个很小很小的类,这样出来的最后的结果是你的CSS可压缩性更好了,可以变得更小,对应于css-in-js里面的Style Chunk
- 分发复用的论点是说css-in-js都是Javascript,所以可以跟普通的Javascript模块一样直接发包到npm上去复用,确实Javascript比纯CSS更容易去组合复用,但是css你也可以发到npm上然后webpack直接引用,这一点上并不算完全的优势
- 跨平台复用:VueX里面就是把静态的css在parse之后编译成Javascript,就可以跨平台复用了
构建工具
构建工具解决的其实是几方面的问题:
- 任务的自动化
- 开发体验和效率(新的语言功能,语法糖,hot reload 等等)
- 部署相关的需求
- 编译时优化
虽然 Vue 本身用 flow,但建议使用 TypeScript 的 flow,主要从开发体验、生态完善度上考虑
服务端数据通信
长久以来我们传统的做法都是围绕Rest,服务端如果暴露的是一个比较标准的Rest API,那我们客户端就可以直接拿一个fetch直接去抓,或者围绕Rest来做一个资源的抽象/封装,特定的应用会遇到比较复杂的场景,一种是,数据直连,数据之间有大量的关联性,另一类是有实时推送同步的需求,这种情况下传统的Rest做法会比较痛苦。
Vue.js 服务器端渲染指南
跨平台渲染
从前端框架的角度去看,跨平台渲染的本质是在设计框架的时候要让框架的渲染机制和DOM解耦,这里面有很多种实现方式,并不一定需要Virtual Dom,本质上只要把框架更新时候的一些节点操作封装起来,你就可以做到跨平台,一个原生的渲染引擎,比如 React Native 和 VueX本质都是在底层针对每个平台有一个适配的渲染引擎,只要把渲染引擎暴露的结点操作的 API,跟框架运行时对接一下,就可以实现将框架里面的代码渲染到原生的目的。这里的解耦很清晰,这也是为什么能看到 NG 可以接 React Native,VueX 可以跑 Vue 文件,VueX 可以跑在 NativeScript 上等等。
新规范
- Web Component
Web Component 和类 React、Angular、Vue 组件化技术谁会成为未来? - WebAssembly
是面向 Web 的通用二进制和文本格式,可以跑在浏览器里面。但是在目前的形势下,WebAssembly 暂时还操作不了 DOM,对于框架的影响暂时比较有限,待观望
总结
总结一下吧,我们聊了很多东西,可能比较杂,但我希望大家发现其中一些共性的东西:技术方案都是先有问题,再有思路,同时伴随着取舍。在选择衡量技术的时候,尽量去思考这个技术背后是在解决什么问题,它做了怎样的取舍。这样一方面可以帮助我们更好的理解和使用这些技术,也为以后哪天你遇到业务中的特殊情况,需要自己做方案的时候打好基础。