基于Redux的应用程序中最常见的state结构是一个简单的JavaScript对象,它最外层的每个key中拥有特定域的数据。
然而,BFF可以是来自不同Domain的数据库合并后返回到前端的。
思考后端的操作逻辑,微服务的场景,按照业务的Restful API,Redux的这种设计模式导致这种CRUD和Service的冲突
在后端开发中,我们设计数据库或者对象模型,通常会根据领域模型来建立不同的数据表和对象,以反映我们对客观现实的抽象,这种抽象在MVC的世界里通常由Model表示。
而在前端开发中,我们一般会从UI的角度出发,去设计前端展示所需要的视图对象模型,我们也称之为View Model。
而往往在大多数情况下,后端的数据模型Model和前端的数据模型View Model是不对等的。
当一个应用的生命周期启动,应用会调用后台的API获取后端数据,然后以某种方式转换成前端所需数据模型,最后展示在应用的页面上。
作为一个后端的开发人员,通常以什么样的方式暴露后端的资源给别的应用使用呢?
如果需要API的通用度比较高,一般最简单直接的方法是开放领域资源的CRUD操作(你可以是RESTful API也可以是别的方式)。
1 2 3 4 5 6 7 8 |
|
我们目前就暂且以这种方式为例,目测这种方式暴露的API比较常见,后面我们再讨论其他的。
让我们再回到Redux中,基于Redux的应用程序中,比较常见的state结构是一个简单的JavaScript对象,它最外层的每个key中拥有特定域的数据,这其实是Redux官方文档上的一句话,我在其他的一些博客上也看到了采用基于业务领域的方式组织state结构。
1 2 3 4 5 6 7 8 9 10 11 |
|
在combineReducer的帮助下,构建上面这个结构,其实就是拆分成多个reducer,拆分之后的reducer独立负责管理该特定切片state的更新。好处是,它提供了一种非常直接的代码逻辑拆分管理的方式,职责独立,物理位置独立(文件独立)。
有了这样一层结构,我们该如何操作它呢?
现在我们假设有这样一个操作,它既需要更新zoos下面的数据,又需要更新animals下面的数据。
如果放在后端代码中,你会怎么做?
我的思路会是写一个函数A,在这个函数里面调用zoos和animals的service或者repository的方法完成更新操作,那么,真正使用的时候,只需要调用这个函数A即可,思路很明朗直接。
在combineReducer和redux结合情况下,我们就需要转换一下思路了,不同业务领域下的数据被放置到了不同的reducer,而你能做的只是发送action。
combineReducer神奇的地方就是,被发送的action会被所有的reducer接收到。(有点像发布订阅模式)
这样一个过去直觉上同步有序的操作过程,在redux中,被分发到多个拆分之后的reducer中,每个reducer都去响应这个action,在需要的情况下独立的更新他们自己的切片state,最后组合成新的state。
后端的业务数据被存储在了redux的store中,然而它是以后端model的形式保存在那。我们的前端页面需要的数据模型,一般和后端model不完全一样,也许是多个后端model组合在一起才得到可以使用的view model,这种情况很常见。
这个时候,我们就需要从后端数据模型中计算衍生数据,得到我们最终需要的View Model。
一般的做法是在mapStateToProps中进行,mapStateToProps中的state是根节点上的state,所以可以拿到所有的领域数据,此时我们就能根据它衍生出我们需要的视图模型。
而在这里一般都会推荐使用reselect库来做,好处是:
第一,能够借机将这个计算衍生数据的逻辑拆分到另一个模块中
第二,它能帮你记住之前的计算过的数据,避免二次计算,同时避免无意义的重新渲染(关于什么时候重新渲染,在前面已经介绍过了)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
这么分析下来,每个部分的职责都很清晰,开发的模式也比较明确,然而理想和设计在业务不复杂的时候都很美好,现实往往比它们骨感很多,这一篇文章只是给了一个简单的例子,没有分析复杂的场景。
在下一篇,我们可以在此基础上分析开发中的复杂场景,一起思考下怎么样管理是合适的。同时,我们也来思考另一种新的前端架构BFF下的Redux应该怎么管理,当后端API不再是资源的CRUD,而是面向业务的API操作时,Redux又应该怎么管理state。
]]>前面提到应用的数据类型有三种:领域数据,应用状态,UI状态。
其中,UI状态看上去更多是存放在React组件里面。
那是不是领域数据和应用状态都应该归Redux管理呢?不一定,为什么呢?我是从这角度思考的:
Redux本身存储的是一个全局数据,即被共享的数据。那么,我们就会问,不会被共享的数据应该存在哪呢?
首先,你当然仍然可以存储在Redux里面,那么当前情况会是,数据被存放在Redux state树形结构的某一片区域中,被某一个路由下的某一个组件所使用。你一定会问,这样做有什么好处?
1.单一的数据源,你很放心的将数据存在那里,打开Redux Debug Tool就能看到(心里长叹一口气,数据还在还在)
2.将同类型(比如:领域数据)的数据统一类聚在一起,方便你统一管理,让你拥有绝对的上帝视角
3.某一天,设计师说,在一个离原组件很遥远的位置加一个按钮,当点击时,要改变这个数据,此时你可以很轻松的发送一个action并写一个reducer的case操作它,就完成了(数据操作和组件的关系是低耦合的)
等等其他我没有想到的好处
如果我不存放在Redux中,应该放哪里?可以是组件里面。那么同样的问题,放在组件里面有什么好处?
1.Redux中存放的都是被共享的数据,相比存放所有的数据,redux的state结构会小一些
2.组件的数据和状态是自管理的,无论你把我放在哪,我都能坚强的活着
3.某一天,我被页面的多个位置用到,你不需要在Redux里面配多个数据空间和action以供我的分身使用,我自带装备,我为自己“带盐”
4.我还能配合高阶组件(用高阶函数做数据加载部分),让你的代码看上更加装逼
等等其他我没有想到的好处
如果博客里面可以发表情,此时我特别想发一个拍脑袋的表情,请问,看完这一段,我胡乱分析的结果,是不是感觉有些不知所措?我到底应该放在哪呢?
正如官方文档里面FAQ的结果
问:必须将所有state都维护在Redux中吗? 可以用React的setState()方法吗?
答:没有 “标准”。作为一名开发者,应该决定使用何种 state 来组装你的应用,每个 state 的生存范围是什么。在两者之间做好平衡,然后就去做吧。
所以说,看情况(It depends)永远是正确的答案。
当然官方文档也给了一些将怎样的数据放入Redux的经验法则:
1.应用的其他部分是否关心这个数据?
2.是否需要根据需要在原始数据的基础上创建衍生数据?
3.相同的数据是否被用作驱动多个组件?
4.能否将状态恢复到特定时间点(在时光旅行调试的时候)?
5.是否要缓存数据(比如:数据存在的情况下直接去使用它而不是重复去请求他)?
我个人感觉这些经验法则是有些道理的,和我们前面讲解和分析的套路差不多,可以作为参考。
首先,我个人认为一定不是将所有的数据都放在Redux里面,但是至于什么样的数据该放在哪里,那就需要看是什么样的使用场景?你需要询问自己几个问题(如上面说问),分析它的好处和坏处,是否满足你的需求,然后做出判断。
我们下一节,来看redux state的组织结构(shape),以及action,reducer如何配合state的更新。
]]>在进行深入探讨之前,我先确保大家的理解是一致的,因为这部分是客观的,而之后的内容是相对主观和有争议的。
1.当组件的某个操作dispatch了一个action,所有的reducer都会接收到:All reducers will be invoked when an action is dispatched?,combineReducers用法中也有讲到
2.当combineReducers发现有任意一个reducer返回了新的state,会通知所有和redux关联(connect)的组件准备更新,请检查自己是否要更新
3.每一个通过connect构建的组件,其mapStateToProps中的state,是combineReducers合并的state,也就是每个组件都能拿到所有reducer中的state(曾经有遇到过有人误以为是跟它action相关的reducer的state)
Redux的三大设计原则之一,单一数据源,定义了整个应用的state被储存在一棵object tree中,并且这个object tree只存在于唯一一个store中。这样一个顶层的状态树,会拥有一个全局的视角,掌握着整个应用的状态。
单一数据源的设计原则在许多程序设计的领域都是准确的,但仅仅用一个JavaScript对象来存储整个应用的状态,总会让人感觉某一天这个对象一定会特别臃肿,上帝实在太忙,要关注的东西太多。
很自然的,我们就会去思考,物尽其用,到底什么样的数据应该放在Redux中,什么样的数据应该放在别处来管理。
大多数应用会处理多种类型的数据和状态,通常可以分为以下三类:领域数据(Domain data),应用状态(App state),UI状态(UI state)
领域数据也就是业务相关数据,一般和你的后台业务系统数据相关联,是领域数据的数据来源,但他们不一定直接对等。
应用状态和UI状态有时候不容易分清,应用状态是描述应用无限循环的生命周期中的某一种存在(中间)状态,而这样一个应用状态可能会导致一个或者多个UI的状态变化。比如:用户登录,是一个应用状态,它可能导致导航栏的UI状态改变。
UI的状态,自然是描述UI的改变,但不一定是由应用状态的变化导致。比如:页面上tab的切换。
根据上面的应用数据和状态的分类,好像让我们对这样一个问题有些头绪。不过在你下任何判断之前,我们在从另外一个维度继续思考一下。
从前面我们就了解到,Redux存储的数据,在任何一个与之关联的组件中都能拿到,也就是说,Redux存储的是一个全局的数据。
反之,React本身也有一个state,而它所关注的只是组件本身以及它的子组件,兄弟和父组件它都不关心。
除了Redux和React,就没有别的位置保存数据了?当然不是,比如:cookie,local storage。这些也是保存数据的关键位置,毕竟当页面刷新后,Redux和React中的state就丢失了,还需要从后台重新加载。
现在还不是时候讨论Redux里面存什么,我们反向推理,用排除法,先看看React里面应该存在什么。
React中的state存在于组件当中,那我们就需要思考组件的特性,它能独立,也许还能自治,一般都高可重用,这是它的部分典型特性。基于它这些特性,也就决定了它的state也必须满足这些要求,组件的state是服务于组件本身的,这些state能够在不收外部干预下就自我管理,这些state当组件被用在任何位置时,都能适应。一个典型的例子:UI状态。
当然,上面是我对React组件state理解的一个抽象描述,能够一定程度下知道我的思考。还有什么原则,能够帮助决策什么样的state可以放在组件中,Dan Abramov在他的Twitter发了这样一张图片也具有不错的指导意义:
1.如果这个数据可以从props中计算得到,那么就不应该放在state中
2.如果这个数据在render方法中不被使用,那么就不应该放在state中
今天先写到这里,后续还会继续讨论redux的state和reducer的设计思考。
]]>没有异步的情况下,Redux配合React很容易理解的(Action->Reducer->CombineReducers->React-Redux->Component),简单回顾下:
1.在组件里面dispatch(发出)一个action对象(带上类型和数据)
2.action对象被传递到reducer的入口,reducer根据类型给到不同的switch分支,然后根据带入的数据操作state,返回新的state
3.redux发现有新的state,配合React-Redux,通知所有component,告诉组件请注意你自己要不要更新,然后各自判断各自更新
一个被传递到组件里的disptach
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
action被传递到reducer的入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
组件被通知请查看你是否需要更新,connect发现todos变了,所以要更新这个connect嵌入的组件
1 2 3 4 5 |
|
异步世界其实没什么可怕的(又不是异世界),看下面一个React里面用fetch实现的数据异步加载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
过程很简单,我们需要考虑三种页面状态:请求开始和进行中,请求成功,请求失败,然后分别设置组件的state。
按照“没有Redux的异步世界”的思想,在Redux里面,我们仍然可以依葫芦画瓢的进行异步的Redux的操作。
首先,一个异步请求都需要dispatch至少三种action,对应至少三个不同的状态:
1.通知reducer请求开始
2.通知reducer请求成功
3.通知reducer请求失败
1 2 3 |
|
如果没有Redux异步中间件,那么你的做法和没有Redux时是类似的,你需要在mapDispatchToProps那传入三个disptach,将异步的fetch逻辑放在组件里面实现,Redux本身仍然是处理同步的state操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
因为Redux官网推荐,我们就以Redux-Thunk为例。
Redux-Thunk刚刚引入的时候,往往容易让使用者有些感到混乱,一个原因是函数式编程的嵌套写法,第二个是和Redux之前dispatch函数做的事情不一样了。
其实,没有Redux-Thunk我们已经可以处理异步请求,只不过异步逻辑不在Redux里面,而是在组件里面,如果我们加入Redux-Thunk会有什么不同呢?
我在上面篇文章讲过,中间件的作用是在dispatch的附近做一些额外的操作,让Redux拥有不同的能力,Redux-Thunk中间件的能力,可以让action creater,不用返回一个action对象,而是一个函数,这个action创建的函数就成为一个thunk。
(关于Thunk函数的含义:编译器的”传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做Thunk函数。)
这个函数并不需要保持纯净,它还可以带有副作用,包括执行异步API请求。这个函数还可以dispatch action。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
你看其实代码差不多,只不过,因为Redux-Thunk,你可以将异步的处理逻辑,从组件里面拿出来,将它放在一个和Redux其他代码更加的内聚的位置,也许是action的存放位置actions.js,而组件里面只需要dispatch一个thunk。
关于Redux的异步世界,暂时更新到这里,Redux里面处理异步的中间件有好多,我就不一个个分析了,你肯定很关心这个,《Redux异步方案选型》
]]>middle这个词很重要,它是指,这个件(ware),被放在(穿插)于某一个已存在操作的过程当中(middle)。
我这么平淡直白无水准的解释,你应该能get到吧,如果你熟悉Java Web开发,你可能第一时间会想到Java Servlet Filters,当然也许你比较年轻,你对Spring熟悉,你可能会立刻想到AOP(面向切面编程),它们可以完成日志,审计,authentication, authorization等。
那么,再往后面走,如果你使用过Express或者Koa等服务端框架,在这类框架中,middleware是指可以被嵌入在框架“接收请求到产生响应过程之中”的代码。例如,Express或者Koa的middleware可以完成添加CORS headers、记录日志、内容压缩等工作。
回过头来,看Redux的中间件,同样,它肯定是指穿插在Redux的某一个过程当中,那么问题来了,这个ware可以在哪里穿插,或者哪里有穿插操作的需要呢?我们来逐一分析,以下内容,参考阮一峰的文章:
(1)Reducer:纯函数,只承担计算State的功能,不合适承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。
(2)View:与State一一对应,可以看作State的视觉层,也不合适承担其他功能。
(3)Action:存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。
其实,和其他服务端框架类似,它被嵌入到Redux“接收请求到产生响应过程之中”,位于action被发起之后,到达reducer之前,也就是store.dispatch()附近。
我在看官方文档的时候,看到一个非常有趣的概念,猴子补丁(monkey patching),大概的意思是指在运行时动态修改模块、类或函数,通常是添加功能或修正缺陷。
通过这种方式,可以在代码的运行过程中,给store.dispatch打补丁,增加额外的功能,比如log state和action,代码如下:
1 2 3 4 5 6 |
|
通过上面这样一段代码,我们就在Redux原来的store.dispatch附近添加了我们自己代码,当我们再次调用store.dispatch,它就会打印log了,不过monkey patching本质上是一种hack,“将任意的方法替换成你想要的”。
如果,我们想给dispatch加另一个补丁,那就在它的前面,或者后面,在加上一段类似的代码呗。
真实情况下,我们肯定不用这样写代码,那Redux提供了applyMiddlewares的方式,所以就不需要我们像上面那样写,那它是怎么做的呢?
1 2 3 4 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
所有中间件被放进了一个数组chain,通过compose,将多个中间件合并,从右到左执行。中间件可以拿到getState和dispatch这两个方法。
注:compose: 将多个函数合并成一个函数,从右到左执行。例如:
1
|
|
-4 * 2 + 1 再求绝对值
仔细看下来之后,中间件就没有那么神秘了,下一篇文章,我们来介绍下Redux的异步中间件,比如:Redux-Thunk,看懂这一个,其他的都差不多。
]]>我尝试用简(kuo)单(hao)易(li)懂(mian)的词解释
1.可以给你的React应用带来性能提升(不用深度对比)
2.简单的编程和调试体验,少出bug(不会操作共享对象)
3.数据更容易追踪,推导(保留previewState可以对比)
Talk is shit, show me your money.
光说还是差了点,虽然很通俗了,但是还是看下面一段代码,我用一个React初学者,特别容易犯的一个错误来说明这三个问题,请看题板:
1 2 3 4 5 6 |
|
1 2 3 4 5 |
|
请问,第一种写法有什么问题?你有20秒的时间思考并作答,对不起,由于我没法遮挡住答案,请自觉闭眼思考。
答案:就是上面说的三点,举个例子:shouldComponentUpdate(nextProps, nextState),需要对比nextState和this.state的区别来决定是否渲染,但此时this.state已经不是以前的state了状态了,特别是PureComponent的shallow compare,直接导致组件不重新渲染,会出bug
因为不用就会出bug,因为你就会问“为什么UI不更新”,因为Redux和React-Redux都使用了浅比较。
具体行为体现在两处,一个在按照Domain区分的combineReducers那,一个在使用数据被connect包裹的组件那:
1.Redux的combineReducers方法浅比较它调用的reducer的引用是否发生变化
比如:
1
|
|
combineReducers会遍历所有这些键值对,判断每一个reducer执行返回结果后的引用是否发生变化(所以是浅比较),如果其中一个变化了,就会设置一个标志位hasChanged为true,当遍历结束,如果hasChanged为true,则返回新的conbineReducers合并的总的state,否则返回旧的那个state(这个新旧的state在React-Redux中会用到)。
这就是为什么在reducer当中要使用Object.assign({},state,{..}}来返回一个新的state,如果你直接操作state,并返回它,combineReducers会认为它没有改变。
1 2 3 4 5 6 7 8 9 10 |
|
2.React-Redux的state和mapStateToProps
React-Redux的connect方法生成的组件,会假设包装的组件是一个“纯”(pure)组件,即给定相同的props和state,这个组件会返回相同的结果。做出这样的假设后,React-Redux就只需检查根state对象或mapStateToProps的返回值是否改变。如果没变,包装的组件就无需重新渲染。
我们回忆一下,上面说道,combineReducers会决定是否返回新的根state,而每次调用React-Redux提供的connect函数时,它之前储存的根state对象的引用,会与当前传递给store的根state对象之间进行浅比较。如果相等,说明根state对象没有变化,也就无需重新渲染组件,甚至无需调用mapStateToProps。
如果不相等,则connect会调用mapStateToProps来,并查看最后传给组件的props是否被更新。同样,这里也是一个浅比较,它要对比的是这个对象的第一层引用是否变化。
1 2 3 4 5 6 7 |
|
即,这个对象的todos是否变化。
1 2 3 |
|
比如,如果在todos的reducer返回的state没有变化,那么这里的todos也就是没有变化,因此组件就不需要渲染。
mapStateToProps因为它特殊的作用,很容易出现一种反模式,我们需要注意,就是像下面这样写:
1 2 3 4 5 6 7 8 9 10 |
|
todos对象永远拿到的都是新的对象{},而不是直接由reducer里面返回的对象,所以组件一定会重新渲染,无论state是否变化。
React-Redux这样设计是有道理的
因为Redux本身的CombineReducers只会决定最根节点的state有没有变化,也就是这个{ todos: myTodosReducer, counter: myCounterReducer }里面的存不存在变化,然后决定返回新的或者旧的,而每一个connect的组件拿到这个新的根state,首先判断state有没有变化,然后判断这个变化和它想要的数据(一个或者多个reducer)有没有关,如果没有关系,当然就不用重新渲染。
React和Redux之所以要求使用Immutable,原因的初衷是性能,避免深度对比。对于我们使用React和Redux的开发人员,除了关注性能,更是因为需要遵循React和Redux的编程规范来写出正确的代码。
]]>Reconciliation有的人翻译成“协调算法”,有的人翻译成“一致性对比”,在没有官方答案之前,我认为直译可能会比较准确,它的作用是React用来区分一棵节点树和另一棵节点树的算法,以确定哪些部分需要更改。
Reconciliation是通常被理解为“虚拟DOM”背后的算法。
简单描述就是:当您渲染React应用时,会生成描述该应用的节点树并将其保存在内存中。然后将该节点树刷新到渲染环境 - 例如,在浏览器应用程序的情况下,它会转换为一组DOM操作。当应用程序更新(比如,通过setState)时,会生成一棵新树。对比得到新树与前一棵树的区别,以计算需要更新渲染应用的操作。
React团队在2016年7月公开发布React Fiber,React新的核心算法,React Fiber的目标是提高其对动画,布局和手势等领域的适用性。它的特征是增量渲染:能够将渲染工作分割成块并将其分散到多个帧中。
按照官方文档的说法,Fiber Reconciler的主要目标:
这个项目持续的2年之久,蕴含着过去多年来Facebook不断改进的工作成果。该架构可向后兼容,彻底重写了React的协调(Reconciliation)算法。他们还专门做了一个网站叫:http://isfiberreadyyet.com/
27 September 2017
我们都知道DOM只是React可以渲染的渲染环境之一,另外一个就是React Native。(这就是为什么“虚拟DOM”有点用词不当)。
它可以支持如此多环境的原因是因为React的设计使是Reconciliation和渲染是分开的阶段。Reconciler执行计算树的哪些部分已经改变; 渲染器然后使用该信息实际更新呈现的应用。这种分离意味着React DOM和React Native可以在共享由React核心提供的相同Reconciler的同时使用它们自己的渲染器。
Fiber在React 16中首次登场,发布时间是2017年9月26号,那么意味着,在这之前,有另外一套Reconciliation的算法。React现在把它命名为Stack Reconciler。它存在于React 15及更早版本的实现中。
Stack Reconciler犯了一个单线程或者存在UI主线程环境下的“禁忌” - 用同步的方式来处理整个组件树。
Virtual DOM diff会一次性处理整个组件树,重点在于,Stack Reconciler始终会一次性地同步处理整个组件树。因为整个过程都是在内存中完成,所以当组件树比较小的时候的,不会感觉到问题,但是,当组件树比较庞大的时候,就会出现卡顿(掉帧)的情况。
单线程进入到栈中,要从栈从退出来,才能响应其他用户操作
按照时间片段的方式执行
参考:Lin Clark - A Cartoon Intro to Fiber - React Conf 2017
首先,不变的地方是,diff节点或者说判断两个节点是否相同的方式没有变:
1.不同的元素类型
每当根元素具有不同类型时,React就会销毁旧的树并从头开始构建新树。从a到img ,或者从Article到Comment,从Button到div – 这些都将导致全部重新构建。
1 2 3 4 5 6 7 |
|
2.相同DOM元素类型,有不同的属性
当比较两个相同类型的React DOM元素时,React检查它们的属性(attributes),保留相同的底层DOM节点,只更新发生改变的属性(attributes)。
1 2 3 |
|
3.子元素递归
默认情况下,当递归一个DOM节点的子节点时,React只需同时遍历所有的孩子基点同时生成一个改变当它们不同时。
例如,当给子元素末尾添加一个元素,在两棵树之间转化中性能就不错:
1 2 3 4 5 6 7 8 9 10 |
|
React 会比较两个 li.first 树与两个 li.second 树,然后插入 li.third 树。
如果在开始处插入一个节点也是这样简单地实现,那么性能将会很差。例如,在下面两棵树的转化中性能就不佳。
1 2 3 4 5 6 7 8 9 10 |
|
React 将会改变每一个子节点而没有意识到需要保留 li.Duke 和 li.Villanova 两个子树。这种低效是一个问题。
为了解决这个问题,React 支持一个 key 属性(attributes)。当子节点有了 key ,React 使用这个 key 去比较原来的树的子节点和之后树的子节点。例如,添加一个 key 到我们上面那个低效的例子中可以使树的转换变高效:
1 2 3 4 5 6 7 8 9 10 |
|
现在 React 知道有’2014’ key 的元素是新的, key为’2015’ 和’2016’的两个元素仅仅只是被移动而已。
关于key的使用,这个key需要在它的兄弟节点中是唯一的就可以了,不需要是全局唯一。而且必须唯一可以表示该节点,比如:不能使用index。
据说,会影响到生命周期的调用,就目前我从官方网站上看到的,还没有正式提出哪些变化,所以可以暂时不用慌张。
本来只是想借助Reconciliation来讲最后一点关于key的使用,不知道怎么写成了介绍Reconciliation的历史过程,所以Copy Paste的比较多。不过,从整体看来,Fiber引入时间片的异步更新,确实改进不少页面渲染的性能问题。
另外,Dan Abramov: Beyond React 16 | JSConf Iceland 2018有简单介绍关于这个Fiber的异步更新功能启用后的页面渲染速度演示,但目前这个功能还没有开启。
]]>面试官:“你能说一下React的生命周期函数调用过程吗?”
我以及和我一样的人:“大哥,是不是我背出来,你就录用我?不是的话,你给我10秒钟,网不卡的话,我立马Google给你答案”
手贱,给你们扩皮了一份。
Mounting
constructor()
static getDerivedStateFromProps()
componentWillMount() / UNSAFE_componentWillMount()
render()
componentDidMount()
Updating
componentWillReceiveProps() / UNSAFE_componentWillReceiveProps()
static getDerivedStateFromProps()
shouldComponentUpdate()
componentWillUpdate() / UNSAFE_componentWillUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
Unmounting
componentWillUnmount()
Error Handling
componentDidCatch()
你那么问别人问题,既不实际,也很尴尬,别人还觉得你没水平。不如我们结合实际问些问题:
1.组件需要做一次网络请求来获取数据,请问应该怎么写?组件有一些事件订阅放在哪个位置比较合适?为什么?
2.在哪些生命周期函数里面我不应该调用setState?如果调用了,会导致什么样的问题?
3.如果我这么写constructor来初始化对象会有什么问题?
1 2 3 4 5 6 |
|
4.你以前的经历中,用到了哪些生命周期函数?遇到过什么奇怪的问题没有?
等等,这样会显得你比较有水平,如果朋友们还问过其他类型的问题?请不惜赐教!我会好好收藏的。
官方文档其实就是不错的和最准确的 the-component-lifecycle
另外推荐看 https://reactarmory.com/guides/lifecycle-simulators
另外,放心,我这些面试问题都没有给答案,不会被套路的,大不了,我换个方式问。
]]>首先,简单说一下shouldComponentUpdate的作用(如果你已经知道,请不要跳过,帮助我审查下有没有描述错误)
1 2 3 |
|
extends React.Component和写Functional Stateless Component(它不能复写shouldComponentUpdate),shouldComponentUpdate默认都是返回true。
这意味着,当props或者state更新时,该组件一定会调用render方法。
也就意味着,React一定会去对比该组件节点上的VisualDOM,但是,不一定会去更新真实DOM,因为reconciliation的结果可能是相等的。(一致性对比,对比新返回的元素是否和之前的元素是否一样)
如果,你将shouldComponentUpdate复写,返回false,那么componentWillUpdate(),render()和componentDidUpdate()都不会被调用,那么该组件不会更新。
当shouldComponentUpdate返回true,这个过程是向叶子节点传递的,比如:父节点返回true,它有两个叶子节点A和B,那么A和B会被要求执行mount或者update的生命周期,如果A的shouldComponentUpdate返回false,B返回true,那么只有B更新。
React官方的ShouldComponentUpdate In Action讲解的很清楚。
听起来貌似很有道理,谁不希望减少无谓的计算,提高性能。
然而,我又看到这样一句话:React team called shouldComponentUpdate an “escape hatch”(逃生出口)instead of “turbo button”(涡轮增压按钮)。在 github issue Stateless functional components and shouldComponentUpdate上也有人这么说。 听上去总结起来,有两个原因:
1.维护自定义的shouldComponentUpdate成本太高,有可能加了一个新的prop,但是忘记更新shouldComponentUpdate的代码,导致bug
2.也许shouldComponentUpdate的比较计算逻辑比起直接重新render更加浪费性能
那这就尴尬了,这到底是写还是不写呢?当被问到这个问题的时候,永远都有一个正确但不受人欢迎的答案:“Well, it depends”(好吧,看情况)。
与其思考这个没有人能够给出准确答案的问题,不如我们思考怎么样结合对shouldComponentUpdate的理解,合理的写组件,设计合理的状态结构树。
大家对React.Component和Functional Stateless Component比较熟,一个就是extends React.Component,一个就是函数,前面也说了,它们的shouldComponentUpdate永远返回true。
React里还有一个顶级的组件API:PureComponent。 这个PureComponent,对shouldComponentUpdate有一个默认实现,官方称为shallow的prop和state对比。啥意思呢?就是帮你对比this.props和this.state上的第一层叶子节点的引用。
如果,某一个叶子节点里面深层的一个元素改变了,而该叶子节点本身的引用没变,shouldComponentUpdate是检查不出来的。
什么类型的组件比较适合写成PureComponent呢?比如:基础组件Button,它本身的属性就相对简单,完全可以和普通HTML的button元素相似,这样就可以将组件的属性拍平一层展现,用PureComponent正好满足shallow对比。
一切不分析性能瓶颈而做的性能优化,都是无用功,shouldComponentUpdate不一定是你的性能瓶颈,但是,我们在这里讨论shouldComponentUpdate,为React组件的更新的问题开了一个头,后面介绍Redux和Object.assign还会在提到组件(不)更新的问题。
周五了,祝大家周末愉快。
]]>1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
构造函数(或者构造器),这个概念对于熟悉基于类的面向对象语言的朋友们肯定烂熟于心,但是对于JavaScript而言,这个概念往往容易让人困惑。
在JavaScript的世界里,构造函数和普通函数没有什么区别,你一样的可以像普通函数一样调用它,但如果通过new关键字来调用函数,该函数就成为了构造函数,this指针就会指向新创建的对象。
比如:
1 2 3 4 5 6 7 8 9 |
|
更多具体的解释请参考我以前的一篇博客:JavaScript渐入佳境 - 构造函数、new、原型。
上面是一个ES5的例子,然而,当我们写React代码时,我们会用到ES6语法,会用到class,constructor以及super关键字,他们的作用是什么?
我们先看下面一个跟React无关的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
简单来说,class的作用就是定义Shape和Rectangle两个function(这就是被人用烂的词,语法糖),extends的作用是定义函数Rectangle的prototype和proto属性来实现原型链的继承,super的作用是在Rectangle函数中执行Share函数,并绑定this指针。
建议查看Babel的编译结果,它是更准确的ES5转义:babel链接
如果以后有人问我,JavaScript和Java有什么关系,我不会说它们没关系,我会告诉那个人,ES6抄袭Java的语法范式。
我们再来回到React上,看ES6和Babel编译后的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
当我们创建一个组件时,如果不需要在constructor里面做任何初始化的操作时,我们是不需要复写constructor的,因为Babel编译后,会将整个arguments都绑定上this指针后传递给被Rectangle的原型(React.Component),并执行,它替我们将constructor中super()的操作做了,如上所示。
如果有需要在constructor中做初始化的操作时,那么必须带上super(props)并放在最前面,因为它的作用是调用Rectangle的原型(React.Component),并绑定this指针。
那么,哪些初始化的操作可以在constructor里面做呢?原则上只有一个,那就是初始化state。
1 2 3 4 5 6 |
|
有三个问题:
1.为什么不用this.setState()来执行?
原因:1.this.state够直接了,你还要怎样?2.this.setState()是一个异步执行的函数,执行完之后,组件的响应式重新渲染(render),你这第一次渲染都还没开始呢。3.在这里this.setState()是一个空指令,这么写,不会任何起作用,不信你可以试试。
2.那么能不能在constructor里面执行网络请求来初始化数据?
我问过许多来面试的候选人,你的网络请求会放置在什么时候执行,我印象中确实有人回答我说在constructor中。听起来,在constructor中获取数据来初始化挺合理的。而且确实有人问:Stack Overflow
然而,我们在官方文档上看到这样一句话:避免在构造函数中引入任何有副作用的代码(比如data fetching或者DOM manipulation)或执行订阅的操作,如果有,请在componentDidMount里执行。
关于这个位置的理解,我常常这样解释,就像你用jQuery写代码一样,你一定会等到document ready之后,才开始操作DOM或执行网络请求(也是为了操作DOM),否则,很有可能遇到undefined的情况。虽然,React这里也许和jQuery不一样,但我认为它的理由是相似的。(如果不对,请纠正我)。
3.那么可不可以传递props到state呢?非常像Java的写法。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这么写,没有说完全错误,但是你需要注意的是,要同时实现getDerivedStateFromProps()在React 16中,或者16之前的componentWillReceiveProps,来保证当父组件更新时,props能有传递到state,因为constructor只会执行一次。
如果出现这种使用场景,我们需要思考一下,能否将state的控制向上提升,将Shape组件仅仅作为Presentional Component,这样减少在不同的两个位置(父组件和它自己)来控制组件的状态。
官方文档说,在constructor里面只做两件事情,初始化state,和绑定event的handler函数的this指针到组件对象本身。既然是this指针这么困惑的话题,我再啰嗦一句这里做了什么:
当函数(这里指这个组件)就成为了构造函数,该函数中this指针就会指向新创建的对象,也就是constructor里面的this就是指向的它自己(该组件的实例),那么this.handleClick = this.handleClick.bind(this);就能保证在handleClick函数里面的this指针,无论handleClick被传递到了哪里,可能被基础DOM元素button使用,也可能被子组件传递到别的位置,handleClick里面的this指针都能指向该组件的(如果是下面的例子就是Toggle),这样里面的this.setState才能起作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
别小看一个constructor函数,这里面的知识点可多了。总结一下,就是只干这些事情,其他的别干:
1 2 3 4 5 |
|
我们就从React官方网站的首页开始我们的思考。先看它的小标题:
A JavaScript library for building user interfaces。
React从一开始就将自己到底是一个什么样的存在,定义的非常的清楚。看清楚,我不是一个框架,我就是一个JavaScript的库,那我是干什么用的呢?构建用户的界面(UI),其他的乱七八糟的事情我不管。
有人可能会问,其他乱七八糟事情的是什么?比如:页面的路由,网络请求,逻辑控制器(Controller),服务等等,是不是听起来挺像前几年某A打头的框架做的事情,这里我就不点名了,大家心里都清楚,没有谁对谁错,此一时彼一时的。
为什么React是这样的一个定义呢?关于这一点,我们可以在Pete Hunt在2013年5月份写的一篇博客Why did we build React?中看到一些insight。比如: 简单摘录一段:
React isn’t an MVC framework.
React is a library for building composable user interfaces. It encourages the creation of reusable UI components which present data that changes over time.
鄙人简单理解和翻译:我不是MVC框架。React是一个用于构建可组合用户界面的库。它鼓励创建可重用的UI组件,以呈现随时间变化的数据。Pete Hunt将React的目的说的很透彻。
React官网也通过这样一句话,给自己了一个清晰的定位,并且在这个清晰的定位下,给出下面三个基本特性:Declarative,Component-Based和Learn Once, Write Anywhere。我们一个个来看:
Declarative,声明式的,嗯呐嗯呐,这是什么意思?相信大家对“声明”这个词比较了解,比如:声明一个变量,声明一个函数。
要理解它,首先要引入另外一个东西,叫做Imperative Programming(命令式编程)。声明式编程和命令式编程,都是一种编程范式,那么他们的区别是什么?简单来说就是what和how的区别。
命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
声明式编程:告诉“机器”你想要的是什么(what),让“机器”想出如何去做(how)。
命令式编程应该大家都比较好理解,比如:操作几个变量,最后计算出你想要的结果,这里的重点在于你通过指令操作它们得到结果。那么声明式呢?举个例子,比如,SQL语句:
1
|
|
你没告诉SQL该怎么去搜索,只是告诉它要找到名字是React的库,对吧?
React就是采用声明式的编程范式思想,你只需要设计在不同状态下,组件应该是长什么样子,React自己会帮助完成组件的更新。它的最直接明白的对比(反面教材),就是通过jQuery操作DOM来更新UI。
React的这种开发模式和有限状态机的思想是一致的,在预知所有状态的条件下,去规划你的代码,也因此衍生了Redux, MobX这样的状态管理库。
基于组件的,这个相对比较好理解,组件是什么?对数据和方法的简单封装。它应该具备具有独立性,封装性,可重用性,职责单一,有自己的状态等等。React组件就封装了自己的状态来构建复杂的UI的组件。
Since component logic is written in JavaScript instead of templates, you can easily pass rich data through your app and keep state out of the DOM.
官方网站上说:组件的逻辑是用JavaScript编写,而不是模板,所以你轻松的传递数据到应用,并且让状态不和DOM打交道。
如果你十分好奇,这里的“而不是模板”是什么意思?Pete Hunt的那篇文章其实说的很清楚,传统的Web应用是通过HTML或者模板引擎(比如后端模板引擎:JSP,HAML等,前端模板引擎:handlebar,ejs等)来构建UI的,而React使用有完整功能编程语言来渲染视图。
其实,我是不太认同的,JSX不算模板,VueTemplate不算模板?不过JSX允许你用JavaScript的方式做一些逻辑的处理,而不像JSP需要些JSTL和ctag的的逻辑标签,如果我的理解有误,请务必纠正我。
这个,看看就行,官方网站这里特指的是React Native可以开发移动端的应用,不过大家也不要太天真,你理解成React和React-Native的思想和语法是融会贯通的就行了,不要真的以为可以很轻松的将组件在React和RN之间移植,否则你会被鄙视的。
另外提一点,在ElectronJS的帮助下,可以通过React开发桌面应用,这个倒是真可行。
你看,首页的信息量其实挺大吧,认证阅读和思考,其实收获不少,总结下来就是:我是一个用来实现基于状态的UI组件的JavaScript库(妈呀,有点绕)。
]]>1.对resource下的资源进行处理
2.对war包中其他资源,比如:jsp文件进行处理
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 8 |
|
比较简单,一眼就看完了,就不多介绍了。
]]>延迟操作?网络请求?回调函数?它们统称为“异步操作”。
因为:
习惯了jQuery的回调
1 2 3 4 5 6 7 8 |
|
习惯同步的Get方法
1 2 3 |
|
当有一天
AngularJS通过Service返回一个Promise的时候,我们仍然将Service命名为UserService,但此时返回是一个Promise,而不是User本身。
1 2 3 |
|
JavaScript对象创建的方法有两个:字面量和new关键字
通过new关键字创建一个Promise,并传递一个函数作为参数
1 2 3 |
|
Promise中业务代码的执行有两个结果:成功(resolve)或者 失败(reject)
成功调用resolve
1 2 3 4 5 6 |
|
失败调用reject
1 2 3 4 5 6 7 8 9 10 11 12 |
|
这是常见的Promise教程顺其自然的语法讲解,resolve会将传入的参数传递给then的回调函数,reject会传递给catch或者then的第二个参数。
这个时候,我们思考一个问题:我们一直说Promise是解决异步操作的,那么上面的代码中,哪一部分是异步的呢?
先思考下,异步操作中,到底哪一步是异步,比如,ajax调用:代码顺序(同步)执行,发现了一个ajax操作,顺序(同步)执行它,ajax发出一个网络请求,这个网络请求操作交给了浏览器,当网络请求返回,调用对应的callback函数。
真正的异步操作是指这个回调函数,它并没有在JavaScript代码顺序(同步)执行的过程中被调用,而是在晚一些时候才被执行。
那么对于上面的Promise,构造函数传入的函数,是顺序执行的。在这个Promise的传递函数中,没有进行任何的异步操作(比如网络请求),而是顺序执行的,直接调用resolve或者reject将状态设置为成功或者失败。
但是当运行promise.then或者promise.catch,即便当时promise的状态已经是确定的,then和catch里面的函数仍然是异步执行。
过去,我们都是使用开源的Promise网络请求工具库,比如Fetch,Axios。今天我们来自己通过ES6 Promoise和XHR实现一个Promise网络请求工具。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
其实非常简单,你只要记得在fetchData执行完之后,你需要一个promise,那么fetchData中就需要通过new关键字创建并返回,剩下的就是将XHR的操作放在传入的构造函数中。
先看代码:
1 2 3 4 5 6 7 8 9 |
|
Promise.resolve(1)是Promise提供的快速创建一个Promise的方法。
这里,我们通过代码反向推导,promise可以调用then或者catch方法,当我们看到then方法后面可以继续调用then方法时,就可以明白,then方法也返回了一个promise,这个promise的then方法中的函数接收到的参数是上一个then方法中的函数return的结果。
假设现在我们来实现( 1 + 1 ) * 2 = 4
1 2 3 4 5 6 7 8 9 10 11 |
|
那么,这里很容混淆的时候,以前你可能会认为.then方法之所以可以chain,是因为then的函数中返回了一个promise,但其实不是这个原因。
那么,如果真的返回了一个promise,结果是什么呢?答案是:
如果你返回类似于promise的内容,下一个then()则会等待,并仅在promise产生结果(成功/失败)时调用
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
第一个log在1秒后打印,第二个log在5秒后打印。
链式调用请求书列表中每本书的详细内容,并返回JSON数据
https://raw.githubusercontent.com/benweizhu/es6-promise-workshop/master/data/books.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Promise.all: 接收一个promise对象的数组作为参数,当这个数组里的所有promise对象全部变为resolve或reject状态的时候,它才会去调用.then方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
Steve Souders在2007年提出这样的“性能黄金法则”,我猜测当他看到React这样一项技术之后,一定会觉得自己的这个法则居然如此的准确,可能甚至觉得这个比例不够极致。(虽然此组件非React组件,但是我还是忍不住想笑)
所以,今天我们就来聊聊,React应用在产品环境下的性能优化问题。
在开始做任何的优化之前,你需要知道痛点在什么地方?既然Steve说80%~90%时间花在了下载页面中的 所有组件 上,那么就从了解项目的模块组成开始。
1.Webpack运行时的输出
在没有任何外部力量帮助的情况下,我们可以直接阅读Webpack的输出
1
|
|
可以查看模块在哪个分块中出现,帮助我们查找分块中重复的依赖。
2.bundle-size-analyzer
webpack-bundle-size-analyzer是我个人比较喜欢的模块大小分析工具,使用起来非常简单,输出也非常清晰。
3.webpack-bundle-analyzer
webpack-bundle-analyzer在github上star人数更多,功能也相对更加齐全(fancy)。
1.Vendor代码分离
代码分离是Webpack核心功能之一,典型的做法是将第三方依赖代码从应用代码中抽离出来,这样可以利用浏览器的缓存来提高性能(减少下载次数)。
Webpack官方文档有非常详细的介绍: code-splitting-libraries,我就不在这里赘述,下面是一个简单代码样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
2.CSS代码
也许你还会想要的就是将CSS文件分离,原因是一样的。官方文档也给出了非常详细的介绍:code-splitting-css,所以同样也不赘述了。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
3.React-Router按需分离
当应用逐渐变得复杂后,你会发现,仅仅将代码分离为vendor和app两个bundle,远远是不够的,要么vendor.js文件特别大,要么app.js文件特别大,这个时候你一定会想到,要按需加载(异步加载)。
ES2015 Loader spec中定义了一个import()方法来在运行时动态加载ES2015的模块,代码如下:
1 2 3 4 5 6 7 8 |
|
Webpack会将import()方法看做一个“代码分离点”,将被加载的模块放在一个单独的文件块中。
那么,如果你的应用采用了React-Router,我们就可以根据路由,按需加载所使用的组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Webpack会根据分离点生成对应的JS文件
4.大文件异步加载
笔者遇到过这样的需求,采用了某图表库来做地图绘制,但是地图库JS文件或者JSON文件特别的大,即便压缩之后也有400+KB。所以,我将该依赖放在应用(SPA)的首页,并采用了异步加载,这样,首屏加载速度不会依赖于它,而用户从首页到需要使用该地图的部分还存在一些操作过程,所以留存了一些时间来异步加载地图数据。
1 2 |
|
当首页需要的JS加载完成之后,才开始加载:
Webpack的官方文档有详细的说明,对于产品环境的构建应该运行webpack -p:production-build。
1
|
|
此时,Webpack会做几件事情,我们也需要根据这些事情做相关的配置:
1.对JS代码进行压缩
–optimize-minimize 标签会对JS代码用UglifyJsPlugin做压缩,并根据Webpack中配置的devtool配置SourceMap
2.SourceMap
即便在产品环境下,仍然建议使用SourceMap,方便产品环境的bug定位,但是对于开发环境和产品环境,我们需要使用不同力度的SourceMap,才能既方便开发也兼容产品环境性能。
1 2 3 4 |
|
官方提供了7种Devtool,而且有更详细的关于devtool的配置,请详见 devtool。 3.Node环境变量production
将redux的中间件和开发环境使用的devtool通过变量分离 –define process.env.NODE_ENV=“‘production’” 标签会以下面的方式使用DefinePlugin:
1 2 3 4 5 6 7 8 9 |
|
这个时候,就可以在产品代码里面获取到此环境变量。这个时候我们要做的就是根据环境变量的不同,来进行不同的配置,比如:这样写log
1
|
|
对于产品环境就等价于:
1
|
|
此时UglifyJS插件就会将它去除掉。
又比如:在react-redux开发中,我们一般都会配置开发插件DevTool,或者log中间件,但其实,在产品环境中,我们不需要,这个时候就需要根据环境变量来动态配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
清理无用的JS代码,真实导入有用的模块。配置起来非常简单:
1
|
|
除了从产品环境模块架构上优化,Babel也在编译阶段优化React应用性能作出了巨大贡献。
1.transform-react-constant-elements插件
transform-react-constant-elements,自从React0.14版本,我们可以将React元素以及他们的属性对象当做普通的值对象看待。这时候我们就可以重用那些输入是immutable的React元素。
1 2 3 |
|
1 2 3 4 5 |
|
从而减少对React.createElement的调用。
2.transform-react-inline-elements插件
transform-react-inline-elements,自从React0.14版本,可以将React元素内联为对象,Babel将React.createElement方法替换成babelHelpers.jsx来转换元素为对象。
1
|
|
1 2 3 4 5 6 |
|
3.其他开源Babel插件
除了以上两个官方插件,在开源世界还有许多其他Babel插件可以优化代码,而且非常实用,这里留给大家自己去探索: transform-react-remove-prop-types, transform-react-pure-class-to-function, babel-react-optimize(综合所有优化的插件,此处应该有掌声)
除了利用工具和构建,以及模块按需加载,来提高产品环境下的代码性能,最最基础的还是开发在平时写代码的需要注意的一些基础原则
1.只导入需要的包
以Lodash为例,比如:如果你只用到isEqual,那么就不要把整个lodash都引入
1
|
|
通过babel-plugin-lodash和lodash-webpack-plugin来缩小应用所需要的lodash的模块
用高阶函数Flow来代替Chain,以避免将整个ladash都加载。
2.使用ESLint
合理的使用ESLint,除了帮助团队指导代码风格,也可以告诉你如何正确的写React应,比如,当组件是纯presentational组件时,就应该使用PureComponent或者纯函数组件,这些eslint都会告诉你。
3.利用React官方的Perf工具
1 2 3 |
|
使用Gzip压缩倒不是React应用才有的性能优化策略,但还是要提一下,因为确实有用。
1.Nginx服务器端配置
我猜测大部分的情况下,都会用Nginx来部署静态资源,斗胆提供一个nginx的gzip配置。
1 2 3 4 |
|
2.手动压缩
另外一种方式,就是我们自己手动压缩Gzip,这样可以减少Nginx编码带来的性能消耗,Webpack插件 compression-webpack-plugin可以做到。
1.服务器端渲染如何
有人会说,服务器端渲染如何? 这个要看情况。服务器端渲染一般主要用来处理首屏渲染性能(注意是首次加载)和搜索引擎爬虫问题。如果你的JS文件特别大,那么服务器端渲染能够,让用户在加载完HTML和CSS之后立刻看到页面。如果不是首次加载,那么其实JS是可以缓存在客户端的,所以即便不用服务器端渲染,之后也不会很慢。
相对的缺点是:配置起来比较麻烦,但如果是一劳永逸的事情,还是值得一做的。
更多关于是否应该进行服务器端渲染,服务器端渲染的好处 以及如何进行服务器端渲染?请查看相关文章。
2.ServiceWork
渐进式 Web 应用程序思想(PWA)最近可火了,2017年ThoughtWorks技术雷达将“Progressive Web Applications”放在了试验阶段。
简单介绍什么是service worker:
在2014年,W3C公布了service worker的草案,service worker提供了很多新的能力,使得web app拥有与native app相同的离线体验、消息推送体验。 service worker是一段脚本,与web worker一样,也是在后台运行。 作为一个独立的线程,运行环境与普通脚本不同,所以不能直接参与web交互行为。native app可以做到离线使用、消息推送、后台自动更新,service worker的出现是正是为了使得web app也可以具有类似的能力。
Github: Google sw-toolbox, sw-precache-webpack-plugin和offline-plugin
3.Preload
Preload 作为一个新的web标准,旨在提高性能和为web开发人员提供更细粒度的加载控制。Preload使开发者能够自定义资源的加载逻辑,且无需忍受基于脚本的资源加载器带来的性能损失。
1
|
|
作为新的标准,浏览器兼容性是你有必要考虑的一个方面:
github: preload-webpack-plugin
文章内容有点长,但我相信这些都是干货是值得一读的,前端产品环境性能优化确实是一个说不完的话题,前端技术更新迭代也没有多少其他计算机技术能够匹敌的,这也对前端开发工程师(全栈开发工程师)的技术敏感度和追求新技术的态度有很高的要求。
作者:Benwei,ThoughtWorks高级咨询师,全栈开发工程师,《实战Gradle》译者
转载原文地址: http://benweizhu.github.io/blog/2017/05/12/react-redux-production-optimisation/
参考文献:
1.https://hackernoon.com/optimising-your-application-bundle-size-with-webpack-e85b00bab579
2.https://brotzky.co/blog/code-splitting-react-router-webpack-2/
3.http://www.jianshu.com/p/f4054b2dcc6e
4.http://2ality.com/2015/12/webpack-tree-shaking.html
5.https://hackernoon.com/how-i-built-a-super-fast-uber-clone-for-mobile-web-863680d2100f
6.http://andrewhfarmer.com/server-side-render/
VR(Virtual Reality)即虚拟现实,这个作为由美国VPL公司创始人拉尼尔在20世纪80年代初提出的一个概念,在16,17年成为了除AI(在此感谢“阿法狗”)之外,最为接近风口的技术行业。
雷总说过“站在台风口,猪都能飞上天”,何况这只猪还有点本事。
回想一下,在这个风口上,哪些人已经参与进去了?VR设备厂家(VR眼镜),视频拍摄设备厂家(360度全景视频拍摄相机),视频制作工作室,游戏工作室,还有CCTV5体育台(忍不住笑出声),最后还有我们普罗大众(整个生态链上的消费者)。
有时候,我还真的很羡慕那些做游戏开发的程序员,一边拿着高工资,一边实现着小时候的梦想,偶尔通宵紧急修bug也情有可原,看看他们现在又多了个玩具。
光羡慕可不行,我要充分发挥自己的能动性为最具程序员群众基础的Web程序员谋福利。
我们除了作为一个普通的消费者参与到VR的风口中,当真没有别的办法了?ThoughtWorks技术雷达第16卷(2017年)告诉你,答案就是Web VR。
什么是Web VR
下面快速的引用一下本期技术雷达对Web VR的描述:
Web VR是一组可让你通过浏览器访问VR设备的实验性JavaScript API。它已经获得了技术社区的支持,并有正式版本和每日构建的版本可用。如果你想在浏览器中构造VR 体验,那么WebVR将会是一个不错的开始。此项技术以及相关补充工具,例如 Three.js,A-Frame,ReactVR,Argon.js和Awe.js都能够为浏览器带来AR体验。除了互联网委员会标准以外,该领域中的各种工具也将有助于促进AR和VR更广泛的应用。
WebVR更主要的是一种开放标准,目的是能够从浏览器给用户带来VR体验。 –webvr.info
今天最主要的目的就是和大家一起快速的浏览一下三个github上比较火的开源Web VR技术。
github: https://github.com/mrdoob/three.js/
光看这个名字,就能深深的感受它,和3d,和VR,和Web有着非比寻常的相关系。Three.js其实不是一个很新的东西,2010年的4月就已经发布了它的第一个开源版本R1(至今有7年了)。它是一个JavaScript 3D库,提供Canvas,SVG,CSS3D的渲染方式,但更多的是封装了底层的WebGL图形接口,以提供简化、高效的三维图形程序开发。
一个Three.js VR例子(可惜需要兼容性的浏览器,Android的Chrome,HTC Vive,Gear VR等)
github: https://github.com/aframevr/aframe/
A-Frame相对Three.js要更新一些,第一个开源版本发布于2015年12月。它是由Mozilla旗下的VR研究团队MozVR推出的开源框架,A-Frame旨在帮助开发者更轻松的开发在浏览器中运行的高性能响应式的VR体验。
和Three.js不同,A-Frame是纯粹的VR Web框架,而且它与现代Web开发的趋势结合更加紧密,使用Web开发者熟悉的HTML标签来创建WebVR场景,提供自定义的语义化标签,降低学习成本。你只需要仅仅几行代码就可以创建一个VR场景,如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
因为A-Frame基于DOM,他可以和现有其他现代Web框架结合。比如:A-Frame-React。
A-Blast - Mozilla基于A-Frame研发的VR游戏(请在Wifi环境下打开)。
超强的场景Inspector工具 - A-Frame Inspector
A-Frame提供一个场景查看工具A-Frame Inspector,可以让你改变场景,操作组件。
Mozilla都建立了自己的专门的VR团队来专注于A-Frame的研发,你想其他的巨头们就不会蠢蠢欲动,Facebook就是其中一个。
github: https://github.com/facebook/react-vr
一个好消息是React于2017年04月19日正式推出ReactVR(即正式开源),即去年第一次在Twitter上公布React VR项目已过去10个月了。
React的优势在于它已经在广大人民群众中打下坚实的基础,并且拥有了一群忠实的粉丝。从技术角度上谈,React VR使用了一个简化版的OVRUI库,其内部使用的是我们上面已经介绍的Three.js(即通过WebGL来渲染场景)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
性能黄金法则由《高性能网站建站指南》的作者Steve Souders在2007年提出。在2012年,他重新发表了一篇博客《The Performance Golden Rule》,分析并统计排名前10,10个在10000排名左右网站的加载时间,并计算了在HTTP Archive上被抓取到的50000个网站的前后端耗时占比,而最终验证了自2007年提出的这个理念的准确性。
]]>
图标库(选择阶段) -> 图标使用(开发阶段)
图标设计(设计阶段) -> 图标导出(沟通阶段) -> 图标使用(开发阶段)
第一种方式是一般是小公司或者独立开发者的工作流程。而对于大型组织或公司,因为拥有更完善的团队和资源,一般是第二种方式,能够获得更多自主权和建立企业VI(Visual Identity,企业视觉识别)的能力。
但无论是哪种方式,都包括两个角色:设计师和Web开发,只是第一种工作方式中,设计师是不可见的。
设计阶段通常是由不了解Web开发的设计师们来完成的,他们会根据产品的需要,绘画出满足需求的图标。
ThoughtWorks官网Contact with us图标
然后交给Web开发人员使用,为什么要先介绍图标的使用,而一笔跳过导出过程呢?原因很简单,因为我们需要先知道服务的对象是谁,才知道如何正确的为它服务。
1.使用图片
直接将设计师画好的图标,以PNG格式的图片一个个分离导出,这是最直观的图标打包方式。
1688DPL中台图标库
它的优点是:(1)能够使用彩色的图标(2)能够支持大部分浏览器;缺点是:(1)图标大小是固定的(不能根据场景自由缩放)(2)Retina屏幕需要两倍图。
开发人员拿到这样的图标,通常会需要先合成为一张图片,以方便制作雪碧图,这个过程可以由开发人员自己完成,也可以由设计师(设计师可以根据源文件中心导出一张包含所有图标的PNG文件)。
制作雪碧图的工具有很多,我比较常用的在线雪碧图工具是:Sprite Cow,或者NodeJS平台下的构建工具插件,如:webpack-spritesmith。
2.直接使用svg
使用SVG(可缩放矢量图形),W3C标准,最看好的Web端图形解决方案。它能提供如裁剪路径、Alpha通道、滤镜效果等复杂渲染能力,具备传统图片没有的矢量功能,还可以被记事本等阅读器、搜索引擎访问。
设计师可以轻松的在设计绘图软件(AI,PS)的帮助下导出SVG格式的图标/图片。
但目前,国内svg还并没有被非常广泛的使用,原因是它的兼容性,不能够很好的兼容旧的IE版本和一些Android原生浏览器。
Can I use svg?
百度2017年前三个月的浏览器使用统计,目前国内还有超过20%的用户仍在使用IE8,9甚至是IE7。
3.IconFont
IconFont是目前最为流行的图标解决方案,顾名思义,它就是字体文件,你可以用任何一个字体编辑工具打开它,如果你打开某一个查看,你会发现它就是一些路径,这些路径可以用AI,PS,Sketch等软件来绘制。
IconFont的优点在于能够用CSS控制样式,无限缩放而不失真,支持IE7+,兼顾屏幕阅读器,不过缺点是不能支持彩色(拥有多种颜色的图标)图标。获得IconFont的方式也很简单,设计师将图标通过AI/PS转成SVG文件,然后由开发人员通过工具(在线或者本地)转换为IconFont,比如:国外的icomoon.io,国内的iconfont.cn,开源构建工具插件有gulp-iconfont等等。
“产生适合Web开发的图标”是我们今天要关注的重点。
1.使用图片的方式
如果开发人员直接使用图片,则相对简单,设计师只需要针对普通屏幕和Retina屏幕准备两套图(单倍图和两倍图)。
以国内某著名的中文小说阅读网站为例,会针对不同的设备使用不同倍数的logo图片,以保证在如Retina屏幕下的清晰度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
2.使用SVG
关于转换成SVG,这里就要引荐一下Sara Soueidan在Generate London 2015 Conference上的演讲《Sara Soueidan: SVG for Web Designers (and Developers)》(YouTube视频需要翻墙),如果不方便,Sara Soueidan有一篇博客《Tips for Creating and Exporting Better SVGs for the Web》更详细的讲解关于SVG导出的内容,当然,还有一篇国内的翻译文章《创建和导出SVG的技巧》,最后在推荐一篇Adobe工程师michael chaize写的关于AI导出SVG的文章《Export SVG for the web with Illustrator CC》。
不过,我觉得看视频更直观,顺便领略一下这位优秀的 阿拉伯女性前端开发工程师(兼自由作家和演讲人) 的风采。
博客和视频中谈到了多个点导出SVG需要注意的地方,篇幅限制,这里简单描述三个tip:
(1)选择适合绘画的画板。
你有在网页上嵌入过SVG吗,给它指定一个高度和宽度,然后发现它其实比你指定的尺寸要小?开发人员常常会遇到这样的问题。
大多数情况下,这是因为SVG视窗中有一定大小的白色空白的空间。视窗是按照你在样式表中指定的尺寸显示的,但是它里面有额外的空白——在图形周围——使得你的图片看起来好像“缩水”了,因为这块空白是占空间的,在视窗里面。为了避免这种情况,你需要确保你的画板是刚刚好放下里面的图像的,不要大太多。
画板的尺寸就是导出的SVG的视窗的尺寸,所有画板上的空白都会最终变成视窗中的白色空白。
对于没有AI工具的开发,可以在下面的SVGO优化选项中选择“Prefer viewBox to width/height”。
(2)选择合适的导出选项
上面的图片中展示的选项是推荐的生成适合Web使用的SVG的。如果你不想使用Web字体,可以选择把文本转换成轮廓。
如果SVG中包含大量的文字,这个选项output fewer tspan elements可以很大程度降低svg的大小。
(3)优化SVG
通常是建议在把SVG从图形编辑器中导出后,再用单独的优化工具来进行优化。比如:删除无用Comments和Metadata,简化代码,简化单个路径等。推荐的第三方工具:NodeJS工具svgomg,AI插件SVG-NOW,Sketch插件Svgo-compressor等,请参考Sara Soueidan的文章《Useful SVGO[ptimization] Tools》。
3.IconFont
前面提到IconFont一般是由SVG通过工具转换而来,而如果开发最终需要使用IconFont展示图标,则对于导出的SVG有一些特殊要求。我在本文的前面一小节,已经介绍了几款IconFont的转换工具,每一款工具其实都有详细的文档说明SVG绘制的规则,尽管不尽相同,但有一些基本原则是一致的:
(1)将文字转换为路径
(2)不可以使用图片(字体只是路径)
(3)修剪画板(trimming to art boundaries)(前面已经介绍过)
(4)将描边转化为闭合图形
(5)简化无用的节点
等等
更多关于IconFont的绘画规则,请参考:Iconfont.cn文档,Icomoon文档,gulp-iconfont文档,fontello文档。
无论是开发还是设计师,最重要的还是沟通,借用Sara Soueidan的一句“设计师和开发者应该成为好朋友”。
]]>