之前谈到过很多次数据驱动的理解,这次通过实际项目检验了一下自己的想法。

相关文件

《前端数据驱动的价值》

《前端数据驱动的陷阱》

项目详设

详设的重要性

对于复杂一点的项目,做一个详细设计非常重要。有人会疑惑,前端还需要详设吗?
根据我的经验,详设非常重要,非常体现能力。
对于一个新人,详设能够给开发做一些提前准备。
对于一个老手,详设可以提前预见一些隐藏的坑。
对于一个高手,详设需要达到随便给一个有点经验的人,都能直接写代码。

在某种程度上,开发者详设在整个开发周期中占得比重越大,能力就越强。
新人可能只占5%,高手肯能占到50%以上(架构完全想清楚了,然后剩下用代码去实践设计)。

react详设的步骤

  1. 吃透业务,这个不管用什么选型都很重要
  2. 顶层数据结构,model必须梳理清晰,model需要能够完整的覆盖业务
  3. 业务React Component中每一个的props,state布局,props,state中每一项的用处,计算方式,与顶层数据结构的映射函数
  4. 业务React Component中每一个的Action对于的model改变
  5. model上面添加和后端的关联

基本上上面梳理清楚了,后面就可以直接写代码了。

道和术

网上看到很多讲解redux+react的实践思路,设计模型。感觉都是实践方式上面的讲解。

比较经典的一张图:
redux

今天我这边想换一种思路去解释他。

数据驱动+react实践的一个前提

相信react的性能!
相信react的性能!
相信react的性能!

快照概念

model能力超级强大,请记住,每个model都必须能够对应一种页面状态(能够像恢复快照一样)。model和页面状态存在一种一一对应的关系。

如下图:

redux

每一个M都和下面的页面对应,用M1可以render出第一个页面,M2可以render第二个页面。

当用户有交互行为时,通过action改变M1M2,这时大家注意了

慢动作:

用户对M1的页面一做了一个操作(action)让M1产生了改变M',这时M1变成了M2,对应的页面也由页面一刷新成了M2对应的页面二。同理,M2通过交互变成了M3,页面也会刷新M3对应的页面三。

注意我强调了刷新两个字。

核心就是页面的行为使得数据改变,数据渲染出数据改变后相应的页面。

这个就是我所理解的数据驱动。

为了达到上面目的,其实我们有意忽略了性能问题。就是用户每次操作都会重新渲染数据,生成对应的新页面。

那么性能问题如何解决,这时react就出场了,性能上,我们需要借助react的虚拟dom,去比较每一次页面修改的最小diff,然后重新渲染diff部分。所以我上面提到,你需要相信react的性能。

说实话,如果没有这种最小diff的处理能力,这种完全的数据驱动性能问题非常大。

从上面看,代码其实可以分为两大部分:

  1. render: 根据model写渲染逻辑,这部分就交给react,大家仔细看看react的生命周期,都是围绕render的
  2. change model: 根据用户的action,修改model的数据,这部分交给redux的模式

数据驱动副产物

单元测试

某种程度上,前端是非常难去写单侧的,因为涉及到dom,哪怕是时间允许,单侧的使用度也不是很高。
对于数据驱动这种模式,至少从数据层,可以规避dom,做一层数据变化的效验(这个和写服务端单侧差不多)。然后有精力和时间,可以加一层model-to-dom逻辑的测试。

用户行为回放

回答上面那个图片,通过model可以记录一个页面的快照,那么如果对于单个用户,单个终端,按照时间轴记录一连串的model,我们就可以回放用户的操作行为。
以及利用大数据去批量分析用户行为数据。

数据驱动的思考

这种模式某种程度上,是为了提高开发效率,减少页面的复杂度(参考《前端数据驱动的价值》),减少开发的复杂度。

想想5-6年前,还是多页面时代,每个模块都是一个页面,数据都由后端去套模板。然后用户每个操作基本上都会触发一些刷新。数据驱动和有点类似,只是借用react在单页面上实现了。前端也承担了更多的数据处理工作。

React组内开发规范

下面是我们部门总结的内部开发规范(试行版本),欢迎提意见。

部门FE React 组件开发规范

适用范围

部门FE 所有基于React开发的(包含fcui2)组件,欢迎提意见。

要求

  • 必须:表示绝对要求这样做。
  • 必须不:表示绝对不要求这样做。
  • 应该/建议:表示一般情况下应该这样做,但是在某些特定情况下可以忽视这个要求。
  • 应该不/不建议:表示一般情况下不应该这样做,但是在某些特定情况下可以忽视这个要求。
  • 可以:表示这个要求完全是可选的,你可以这样做,也可以不这样做。

基本概念 [basic-concepts]

  • 实例化生命周期
    • getDefaultProps
    • getInitialState
    • componentWillMount
    • render
    • componentDidMount
  • 更新期生命周期
    • getInitialState
    • componentWillMount
    • render
    • componentDidMount
  • 运行期生命周期
    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
    • render
    • componentDidUpdate
  • 销毁期生命周期
    • componentWillUnmount

核心依赖 [deps](针对内部UI库)

  • 必须在UI内只依赖React,underscore。
  • 必须不在UI内部任何地方使用jQuery等直接操作DOM的库

JSX书写 [jsx]

参考:Airbnb的style guide

  • 必须命名JSX文件为.jsx.js。
  • 必须使用PascalCase作为文件名。
  • 必须只包含一个React Component在一个JSX文件中。
  • 必须令文件名与所包含的React Component名字相同。
  • 必须只能使用React.createClass()来创建一个React Component。

    虽然ES6 Class和pure function都可以创建React Component,
    但ES6 Class不能使用mixin做扩展,与目前的扩展方法冲突;
    Pure function较难掌握和管理。

  • 必须使用JSX语法来生成组件的DOM片段。

  • 必须不能在JSX中出现React.createElement()
  • 必须遵守下面示例中的DOM片段对齐方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// bad
<Foo superLongParam="bar"
anotherSuperLongParam="baz" />

// good
<Foo
superLongParam="bar"
anotherSuperLongParam="baz"
/>

// if props fit in one line then keep it on the same line
<Foo bar="bar" />

// children get indented normally
<Foo
superLongParam="bar"
anotherSuperLongParam="baz"
>
<Quux />
</Foo>

  • 必须在DOM片段中使用双引号"

    Why?JSX attributes can’t contain escaped quotes, so double quotes make conjunctions like "don't" easier to type.

    Regular HTML attributes also typically use double quotes instead of single, so JSX attributes mirror this convention.

1
2
3
4
5
6
7
8
9
10
11
// bad
<Foo bar='bar' />

// good
<Foo bar="bar" />

// bad
<Foo style={{ left: "20px" }} />

// good
<Foo style={{ left: '20px' }} />
  • 必须在自关闭标签前加一个空格。
1
2
3
4
5
6
7
8
9
10
11
12
// bad
<Foo/>

// very bad
<Foo />

// bad
<Foo
/>

// good
<Foo />
  • 必须书写propTypes,规定每个可接受属性的类型,并对propTypes加以jsdoc说明。

  • 必须使用camalCase来命名props。

1
2
3
4
5
6
7
8
9
10
11
// bad
<Foo
UserName="hello"
phone_number={12345678}
/>

// good
<Foo
userName="hello"
phoneNumber={12345678}
/>
  • 必须当props的值为字面值true时,省略={true}
1
2
3
4
5
6
7
8
9
// bad
<Foo
hidden={true}
/>

// good
<Foo
hidden
/>
  • 必须在DOM片段前后加一对括号(),当DOM片段在一行以上时。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// bad
render() {
return <MyComponent className="long body" foo="bar">
<MyChild />
</MyComponent>;

}

// good
render() {
return (
<MyComponent className="long body" foo="bar">
<MyChild />
</MyComponent>
);

}

// good, when single line
render() {
const body = <div>hello</div>;
return <MyComponent>{body}</MyComponent>;
}
  • 必须将组件写成自关闭标签,当组件没有children时。
1
2
3
4
5
// bad
<Foo className="stuff"></Foo>

// good
<Foo className="stuff" />

  • 必须将关闭标签另起一行,当组件有多个props时。
1
2
3
4
5
6
7
8
9
10
// bad
<Foo
bar="bar"
baz="baz" />

// good
<Foo
bar="bar"
baz="baz"
/>
  • 必须将bind handler到this的动作放到构造函数中。

    Why? A bind call in the render path creates a brand new function on every single render.

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
// bad
class extends React.Component {
onClickDiv() {
// do stuff
}

render() {
return <div onClick={this.onClickDiv.bind(this)} />
}
}

// good
class extends React.Component {
constructor(props) {
super(props);

this.onClickDiv = this.onClickDiv.bind(this);
}

onClickDiv() {
// do stuff
}

render() {
return <div onClick={this.onClickDiv} />
}
}

  • 必须以如下的顺序排列JSX文件中的方法。

    1. displayName
    2. propTypes
    3. contextTypes
    4. childContextTypes
    5. mixins
    6. statics
    7. defaultProps
    8. getDefaultProps
    9. getInitialState
    10. getChildContext
    11. componentWillMount
    12. componentDidMount
    13. componentWillReceiveProps
    14. shouldComponentUpdate
    15. componentWillUpdate
    16. componentDidUpdate
    17. componentWillUnmount
    18. clickHandlers or eventHandlers like onClickSubmit() or onChangeDescription()
    19. getter methods for render like getSelectReason() or getFooterContent()
    20. Optional render methods like renderNavigation() or renderProfilePicture()
    21. render

更多的通用组件规范 [general-guide]

[基本的JSX书写规范] (#jsx-jsx)基础上,更多的通用的React组件开发规范。

1
2
3
4
5
6
7
8
9
10
11
12
13
// bad
render() {
var cleverFunction = function () {};
// ...
}

// good
{
cleverFunction() {},
render() {
// just use this.cleverFunction
}
}
  • 不建议在运行期生命周期中使用时间复杂度O(n2)及以上阶的算法。
  • 必须不允许出现观察者模式,如自定义addEventListener方法,或on, fire等。
  • 必须只能通过以下2种方法设置组件内部状态:
    • 通过父组件的render方法,改变子组件的props。
    • 通过子组件的setState方法。
  • 必须不允许为组件提供setXXX方法来改变其内部状态,除非该setXXX方法中仅包含一个setState调用,且完成了一个充分复杂的state转换。
  • 必须为所有回调在getDefaultProps给出空函数默认值_.noop
  • 可以提供与组件内部数据结构紧密相关的操作方法。这些方法可以实现为一个纯函数,即只依赖其所有的参数来得到其结果。这些方法可以放在组件的static域中。

微信公众号

前端修炼

我是如何优化网站首页性能的一篇文章中提到过95分位值的概念。下面从最近实际数据看看95分位值对于性能优化的参考价值。

真实数据

最近优化有了一些效果,就正好借着具体的实例数据来看看95分位值的意义。

看下面一张图:

性能平均值

性能平均值

从这个曲线中可以看出3月11号数据绿色的线升高了,出了点问题。

性能50分位值

50分位值

50分位值在3月11号和3月7,8号都有一些抖动,但是不是特别明显。

性能80分位值

80分位值

80分位值和平均值类似,能够看到3月11号的一次波动

从上面几个数据看,其实我们能够看出来的就是3月4号开始有一个明显的下降(呵呵,优化的部分成果)。

另外一个就是感觉3月11号有一个异常点。最后排查也确实是出现了服务器的问题,后端有接口响应问题。

那么回过头来看看我们期待的95分位值,这些天记录的情况:

95分位值

图中可以明显看出3月4号的优化效果,项目是3月3号上线。
第二个问题点就是很明显的看出绿色的线在3月11号有一个明显的突刺,必然是哪里出了问题。

然后第二个地方就是3月8日也有一个明显的波动,这个暂时没有找到原因,后端也没有反馈过异常。但是这个问题需要记录,如果再次出现,需要发现其中的规律。

95分位值举例

收集100个数据,从小到大排列,95分位值就是取出第95个用户的数据做统计。 50分位值就是第50个人的数据。

95分位值意义

第一点: 从上面图中可以看出,在性能统计中,95分位值的波动最明显,能够放大问题。
平均值,50分位值的图形如果不是特别关注,波动其实并不明显。

第二点: 从高T们的历史经验看,如果能够保证95分位值的稳定性,就能很好的体现系统的稳定性。

微信公众号

前端修炼

无法用语言准确表达思维时,就用公式;一个不行,那就两个

—— 李老师

本文假设读者已了解react的基本概念,并有少量react开发实践。如果没有,请先阅读
http://www.ruanyifeng.com/blog/2015/03/react.html

当前我们如何开发业务?

react1

备注:微信不支持公式,所以我这边截图。
补充一下f表示一次用m生成v的渲染方法
g是界面发生交互时,对m做修改的方法

当前业务模块之间如何通信?

  1. 回调模式:callback
  2. 观察者模式:on / fire (addEventListener / removeEventListener)
  3. 伪总线模式:fcContextChange

三种模式没有本质区别:

  1. 回调模式,一次创建一条通信链路,此链路工作时不与其他链路发生干扰。
    react2
  2. 观察者模式,一次创建多条通信链路,每条链路工作时与其他链路发生干扰。
    react3
  3. 伪总线模式,是一种特殊的观察者模式,信息的发出者是一个固定的模块。

在目前系统中,只存在兄弟通信和父子通信,不存在跨树通信,即孩子不能跟叔叔直接通信。如果要跟叔叔通信,必须借助祖先中转。

现在开始简化模型:

a. 假设系统中模块之间的链路能双向通信,这样可以将有向链路换成无向链路;
b. 假设任意两个模块之间都可以存在链路,但只允许存在一条链路,这样之前不同的链路类型下降成单条链路的一个参数,并且将祖先中转缩短成直接通信;
c. 把系统中的模块看作顶点,模块间通信链路看作边,则系统变成了无向图。
react4

这是模型简化后的链路数量,而目前我们系统中的链路数比这多得多,因为我们允许节点间有多条链路,而且链路是单向通信的。

我们开发业务时很大一部分工作量就是创建各种通信链路,随着系统复杂性不断增加,图越来越复杂,代码逻辑就没人能看懂了,因为数据在系统中的流动是没有规律的。

React基本原则2:在React中,数据流是单向的。数据总是自顶而下流动,内部不允许出现反向数据流。React简化通信的方法相当粗暴,既然管理不好通信,那么干脆禁止通信。

如何用React满足我们的通信需求?React的做法是:
a. 把整个应用抽象成一个数据集;
b. 每个子模块使用数据集的一部分做渲染,涉及渲染的数据通过props向下传递;
c. 每个子模块都可以直接修改最顶层的数据集。

现在,系统中通信链路条数变成了n,系统复杂度完全在我们掌控之中。当然这样做也是有弊端的,所有数据放在一个model中,这个model必然很庞大,维护起来需要一定技巧,相应地出现了flux和redux等框架专门用于管理model,目前我们没有引入,准备使用ER.model,加强命名规范,来临时解决这一问题。

react5

最关键,上图绿色链路的创建过程是半自动化的,只需要把修改model的回调函数mode.dispatch通过props一层层传递下去。当然,绿色链路的创建可以利用react.context做成全自动,但react官方对context的使用有疑虑,并且API在将来可能会修改,所以暂时没有引入。

Note

Context is an advanced and experimental feature. The API is likely to change in future releases.

Most applications will never need to use context. Especially if you are just getting started with React, you likely do not want to use context. Using context will make your code harder to understand because it makes the data flow less clear. It is similar to using global variables to pass state through your application.

If you have to use context, use it sparingly.

Regardless of whether you’re building an application or a library, try to isolate your use of context to a small area and avoid using the context API directly when possible so that it’s easier to upgrade when the API changes.

当前我们如何测试ui和业务模块?

答案是测试基本靠手。怎么测试交互?目前有基于web driver的OneUX SDK方案。这个方案用脚本模仿了鼠标键盘操作,允许我们实现自动化测试。
但问题不在这里,而是工作量的问题。模拟操作的方式,归根到底是QA把手动测试过程用脚本记录下来。如果测试一次性通过,手动测试和写脚本的工作量是一致的。

react7

在React中,自动化测试变得非常简单。还记得React基本原则1么?props + state是渲染界面view的唯一依据。因此,用户在页面的交互行为,最终都转化成props或state的变化。

react8

react9

面试时经常问的一个问题是,你最近或者做过的项目中挑战最大的内容,然后顺着其中的技术点挖一些更深入的内容,看看候选人了解多少,理解程度。

然后突然想了想我做的有什么难度比较大的项目,可以给大家分享下。

今天介绍下我一年前完成的一个两个独立系统的模块打通过程,换句话说就是A系统可以直接使用B系统的业务模块。

背景介绍

A系统是一个古老的系统,B系统是一个新做的系统,(原本是希望A系统的功能全部迁移到B系统上的,但是由于各种原因搁置了)当时结果导致两个系统在同时使用和升级。

随着业务的开发,很多需求在A和B系统之间产生了关联性,例如B系统上线了一个新产品,然后为了增加入口和使用量,需要在A系统上可以直接调用新产品的某个功能。
直观的解决方案是A系统上面复制粘贴一份B系统上面这个产品的功能,但是问题就来了,未来维护起来怎么办,同时维护两份代码?以后这样的需求多了怎么办,重复代码越来越多?

另外一个解决办法就是A,B两个系统间的融合。当然这样评估的一个前提是A和B虽然是两个系统,但是大家开发模式,底层依赖还是相似的。

需求分析

需求拆解

上面是这两个系统的基本架构,拆分成线上线下两个部分。

A网站有两个业务模块A1A2

B网站有三个业务模块,B1,B2,B3

现在的需求是B1这个模块不仅能在B系统中运行,而且可以共享到A系统中。

目标如下图:

需求拆解

那么问题来了:

需求拆解

问题一

支持业务代码的底层dep和common不一样。
随着时间的推移,dep依赖第一个是引入的库有些差别,第二个更加严重是是大部分库的版本已经不一样了。

业务模块的底层就像是生态环境,如果生态环境差别越大,那么这两个生态融合在一起的困难也就越大。

问题二

B1这个模块在A系统中调用时,如何找对路径。
举个例子:
B系统中有一个 module/b/c/d.js;
B1中require了一个 module/b/c/d,在B系统中是可以正确访问到的。

但是如果在A系统中调用到B1的require的module/b/c/d。它是会找到A系统中的module/b/c/d.js;

这只是其中的一个例子,由于系统中的AMD模式,路径的寻找也是一个坑点。

当然对于上面的问题可以通过map方式(esl.js中有个map,require.js应该也有对应的)。当时是用过另外一种方式解决的。

问题三

编译两个问题,
一是路径问题,当时坑我最深的。
二是业务模块独立打包,A系统中应该是仅仅需要B1这个业务,而不是所有B系统中的代码。

问题四

如何对开发人员透明,这个很关键。如果系统融合了,一线的开发人员需要为此做很多事情,就比较失败了。

所以整个调用过程如何让开发无感知,B系统中正常开发一个模块,A系统通过一个api就能直接使用了,也不需要区分线上线下。

具体步骤

主要问题抛出了,后面就是看如何一一对应的解决问题了。

当然了上面介绍的以外,还有很多细节问题,例如融合后样式冲突这种很多细节上面的坑。只是这些没有上面四个问题那么严重。

第一步,底层打平

  1. dep打平(1)A系统中补充B系统中新增的库,相对简单
  2. dep打平(2)A和B系统中有差异的库,都升级到同样版本,这个非常坑,由于很多使用的是公司或者部门内部的库,升级不想外部jquery一样对开发透明,他们的升级绝对不是透明的,哪怕是百度最火的echart。所以解决升级库的兼容问题是一个工作量很大的活。
  3. common基础依赖合并。找出不同内容互补,然后相同又冲突的内容,修改打补丁方式,虽然有坑,但是经理了dep的升级,觉得还ok。

完成上面三步后的一个期望的成果是,B1模块以及依赖的内容可以直接复制到A系统中,在A里面可以直接调用打开。

注意一些细节就是初始化是否相同,A和B如果初始化不一样,可能在A系统调用B1时,B1有些内容缺少初始化过程。(这种就是属于细节上的坑了)

第二部,处理path路径

当时处理思路比较简单,B1模块前面统一加上B1这个前缀,这样就能够在require时找到指定的路径了。

这个地方的难点在于打包编译,因为破坏了默认的打包规则,而且根据一些打包的扩展配置也无法达到期望。

当时解决的方式是仔细的研究了打包编译流程,很大程度上面重写了或者说hack了默认的打包规则。

其实打包编译都研究到这里了,也就顺便把问题三解决了。当时的对于打包的研究已经可以安装需要随意打包了。

这里的预期效果就是B1模块在B自己的系统中,A中可以调用打开了

第三部,对开发人员透明

完成上面步骤后,有一个遗留问题是,线上打开的方式和线下打开的方式不一样,对于开发人员非常不友好。

所以这一步应该是优化过程,让这个功能更加完善,所以写了一个简单的api,开发人员只需要调用api即可,不需要考虑线上和线下逻辑。这个复杂的逻辑包含到了api中处理。

项目难点

我个人认为这个项目的难点:

  1. 对于两个系统的架构要非常熟悉
  2. 对于打包编译需要非常熟悉,因为一旦涉及到线上和线下,编译就是必须要了解的内容
  3. 对于系统底层代码要熟悉,因为在升级dep过程中,很多版本兼容bug必须要定位到底层代码中
  4. 对于AMD需要熟悉,当时定位甚至发现了esl.js和require.js在某种场景的细微区别

目前状态

在最近一年中,A和B两个系统以及融合在一起了。

微信公众号

前端修炼

我是如何优化网站首页性能的

最近接到一个任务,首页性能优化。

目标:95分位值下

  1. 看到页面框架主体内容6s(优化前10s左右),优化提升40%
  2. 看到操作详细内容9s(优化前12s左右),优化提升25%。

侧面看出我们系统的庞大程度吧,这个不值得骄傲,项目比较悠久,历史包袱比较沉重,后面计划node同构方式去重构,但是现阶段需要一个低成本,短时间的方案去提高现有性能作为过渡。

95分位值解释

95分位值目前是我们看性能指标的一个重要参考点。

举例:收集用户打开的时间,从快到慢排列,比如是100个用户数据,95分位值就是取出第95个用户的数据做统计。 50分位值就是第50个人的数据。

为什么是95%,因为跟进高T的优化经验,95分位值的数据取点最能放大问题。50,80的取点暴露的问题不明显。

当我把最慢的那一批人的性能优化好了,哪些快的自然就解决了。

优化难点

  1. 面试经常问到的页面优化点,例如图片合并,js合并,css合并,js压缩等等都已经做了。常规优化点没有什么空间可以优化。
  2. 代码比较老。注释上面都是12-13年的代码。
  3. 改动需要尽量的少,功能点不能改,时间比较紧张,QA没有人力支援,所以需要代码改动比较小的情况下(修改必须可控),不重构的情况下挖掘优化点。

任务拆分

优化性能这种任务其实比较难制定计划,除非经验特别丰富。

第一步:是梳理代码

当然也不是看所有的老业务代码,看的重点是看各个模块的加载逻辑,展现逻辑,看入口即可。

第二部:删遗留代码

大概了解整个首页的初始化流程后,梳理了简单的逻辑后,发现第一个任务,删代码。

梳理大概结构后,目测有大量下线功能的代码任然遗留在系统中,之前的下线逻辑应该是仅仅屏蔽了入口,而没有清理代码。

所以我第一个具体的工作就是找出下线的业务代码并将他清理,不完全统计,清理代码量开发环境下至少5W行。

清理代码好处很多:

  1. 减少无用代码的初始化消耗
  2. 减少静态资源
  3. 让代码更加清晰,减少无用代码的干扰

删除无用代码其实是个脏活,吃力不讨好,删除的代码如果还有地方引用,那么删除了就是引入了一个bug。

删除代码这个工作又没有什么显性的收益,还费工费时。其实就是一个里子的工作,把大家看不到的地方做好。

第三部:优化初始化逻辑

尽量让不是完全依赖的ajax并行,减少串行。当前系统有两个展示模块有串行关系。梳理业务后,找出首页加载的默认逻辑,将串行调整成为并行。

这里是修改代码的地方之一,修改越少越好,因为一旦修改多了,就不好控制了,就需要QA介入,那么整个项目的周期就会大大延长。

第四部:ajax预取

这个是上一个项目经验积累,在加载模块静态资源前,可以并行的请求这些模块的ajax内容。原本逻辑是,加载完毕各个模块的静态资源,然后模块内部开始加载静态资源需要的ajax。这样就避免不了静态资源的请求和静态资源里面ajax的请求形成了一个串行关系。
预取的一个明显优势是,ajax可以提前

节省的时间 = min(ajax请求时间,静态资源加载时间)

这里是修改的另外一个地方:同理,这个地方没有业务逻辑,所以需要和业务完全解耦着做。

第五部:优化打包合并

打包这部分难度比较大,优化空间也相对较多。
这里分了两部分:

一:首屏不展示部分按需加载

目前看很多应该按需加载的内容全部都合并到了一起,放在首屏加载了。例如点击一个按钮出来一个操作页面弹框。其实这个弹框的代码首屏展示是完全不需要。

当时注释:

代码加起来压缩完大概200kb左右,没必要拆的太细,如果代码达到500KB以上,再进一步考虑细拆

实际情况是,这个部分的代码压缩混淆后达到了1.1MB(坑爹啊)。

这种情况就是当初开发人员设想是美好的,后续业务开发人员没有意识或者了解到当初的规定,业务越来越多,代码也就越来越多。

这种情况其实比较常见,因为打包合并这种其实是尽量对业务人员透明的,这种合并后的内容,其实在开发环境体现不出来,只有刻意的压缩代码和优化时才能注意到。

容易忽视的部分就是容易出现问题的地方。

二: 打包合并重复的部分删除

还是上面的原因,打包合并对于开发人员和开发环境透明,很长一段时间后,会发现大量打包重复,比较明显的就是底层依赖库个个文件重复引用。这种不会引发bug,但是会影响首屏静态资源的加载和静态资源的解析。

一和二的成果很明显:静态资源网络压缩后(gz),1.9mb优化到了1.1mb,整体提升了42%。

三: 疑难散文件清理

之前优化过几次散文件,由于成本比较大,遗留了一些散文件。这次就是集中处理了一下,散文件其实是比较严重的,一个散文件就会浪费一个请求。95分位值下,一个散文件可能就是100ms的影响。所以不要小看散文件对于性能的影响。

成果是减少5个js静态资源请求,2个css请求。

优化感受

这些优化本周会上线,期待数据会比较好看。如果本周效果不是很明显,后面的优化空间其实就非常小了,暂时不考虑cdn,依赖后端这些方法,仅仅从静态资源出发。

优化其实就是一个没有那么明确计划的任务,往往有可能对着页面一遍一遍看加载流程或者把源代码挨个扫一遍找找感觉,一个突发奇想,一个奇淫技巧,一个业务展示效果的调整就能达到。

后续看看是否达标,性能不达标的话,还会有《我是如何优化网站首页性能的(番外篇)》。/(ㄒoㄒ)/~~

优化感受2

优化就是细节完善,举个例子:

  1. 有时一个js散文件可能多消耗50ms,但是一旦出现10个,20个影响就叠加起来了。
  2. 一个底层库被重复打包了,可能多了几千行,静态资源增加了几k,加载上可能就是几ms,加上与解析几ms。但是一旦重复的静态资源多了,问题就来了。上万行,上百K的资源都是拖慢系统的根源。难以想象我这次粗滤的清理了一下,清理了0.6MB的资源。

优化没有止境,这一版上线后,肯定还有很多遗留的地方,后续看看效果,继续优化。

ps:最后发现自己整理完这么多,也没有做什么,感觉后面还需努力

微信公众号

前端修炼

react融合进系统的体验

引入的背景

在一个庞大的商业系统中引入react这种数据驱动的模式。
希望能够一点点重构去替换以前的模块,逐步的将系统重要部分底层框架替换成react。

同事实践的心得

以下内容都摘自同事使用后的一些感想

心得一

从过程化开发向面向数据的开发转化。后者要求开发者对数据结构和算法和业务需求本身要有理解。React开发的核心是设计一套数据结构使其既方便业务用户界面的展示又能方便的实现业务功能——表现为一组操作数据结构的算法。这与传统的前端开发很不一样,偏激一点的说,相比以前用$搬弄DOM节点,我觉得这样的前端开发更“严肃”了。我希望能够推动团队迎接,适应,实现这个变化,这对于一个技术人员,而不仅仅是前端技术人员都是有益的。

心得二

1.React单向数据流原则是核心中的核心,即整个系统中只存在自上至下的数据流,反向数据流通过绑定自动完成,不存在兄弟间横向数据流。

2.控制数据流属于最强的开发规范,必定会给开发业务的同学带来巨大的思维挑战,从系统整体质量和维护性来看,必须牺牲业务开发的编程自由度。目前就是自由度太高,导致出现五花八门的业务实现,代码根本没法看。

心得三

其实之前我们也是数据驱动视图的,生命周期时用初始化数据渲染了视图,自然这时有了一层数据到视图的映射逻辑关系。数据改变之后绑定视图映射关系会写在model onchange handler中。

这样会有一些问题:

1.数据到设图的映射逻辑关系可能写了两遍,而且会发散在template, action, view等各个地方

2.初始化时数据和视图的关系是同步的,但是初始化之后这两者就可能就不一致,很可能handler没写用jquery直接操作DOM了。

初级阶段觉得React的好处是,把映射关系收敛到render方法中,有一层封装让我们直接操作DOM变得更难。数据如果在任何时候都能表达视图,是有很多事情可以想象的。

心得四

我们需要在React方面思考的技术问题,有下面这些点:

UI组件应当有稳定一致的开发规范。

UI组件应当有充分的UT 。(并尝试是否可以为业务组件加UT)

UI组件乃至业务组件内的数据结构是否应该有一个统一的模式(如immutable或者更轻量的模式),使得对于数据结构的任意位置的修改,都可以有事件冒出做一些统一的处理。

多个兄弟的组件之间的通信有什么范式?

父子组件之间双向通信有什么范式?

目前实现了ER-React,即一个React模块对外表现为一个ER模块。未来在此基础上,将一个ER-React模块的父模块实现为React后,是要脱掉ER-React的ER,变为React-React呢?还是实现为React-ER-React呢?

按照React的开发模式,随着我们自下而上的重构业务,很自然的,下面的组件的“Model”部分会逐渐“上浮“与上层的组件的Model合并成为一个更大的Model。如此往复,我们自然会形成“整个应用只有一个大Model”的局面。我们需要在这一切发生之前想明白这个“大Model”内部要如何组织,会以何种形式存在,并以何种形式和各个组件交互。

类似,从View的角度看,我们最终会形成“整个应用就是一个大的React组件”。对于每一个业务动作,整个应用都会重新render。这个render的性能迟早我们需要关心。如何控制这个render的性能使之不会影响用户体验?

react引入的意义

活力

引入后我觉得最重要的成就就是让一个系统拥有升级换代的活力。就像注入新鲜血液一样,系统能够跟上时代的变化。

对于一个庞大的商业系统而言,系统底层的稳定性是一个很重要的点。不过如果能在在系统上面做一些侵入性的改造,让一个稳定的系统充满活力还是很有意义的。

首先对于业务开发人员,很明显,他们在原有系统上面开发了这么久以后,对于新技术的引入是非常欢迎的,他们是非常乐意去学习新技术的。

提高开发效率

这个是写给老板们看的,花这么大力气去引入一个新技术对于公司的收益就是提高开发人员的效率。

当然这个提高的效率的前提是对于开发人员有更高的要求的。

提高系统的健壮性

react的模式是可以在某种程度上面融入UT的。
以及一个很好的数据驱动模式维护性和扩展性是比现有系统强的。

回放用户行为

数据驱动好处就是可以通过数据记录用户的页面状态,用数据就能恢复页面快照,需要分析用户行为,只需要收集到页面的数据变化流即可。

react引入的挑战

  1. 数据驱动最合适的是从根部重构。但是目前我们只能从叶子模块一点点往根部重构。其实是一个反向过程。
  2. 数据驱动模式对于开发人员要求比较高,能不能设计一种模式降低要求,避免出现不同水平的开发者开发出层次不齐的业务模块。
  3. 引入新的模式一个缺点就是以前模式成为了技术债务。因为一个系统存在多种模式,意味着新人学习成本会增加很多。多种模式的共存,如果维护不好,也会出现一种很混乱的现象。

微信公众号

前端修炼

前端数据驱动的陷阱

年前一篇文章《前端数据驱动的价值》聊了一下数据驱动的一些看法。

其中数据驱动的核心在于整个系统中对于数据的架构,设计,维护。对于数据的处理直接决定了系统的稳定性,可维护性,可扩展性。但是这里的数据维护也是相当复杂和难搞的一块。

精选评论

引用nightiresegmentfault对于《前端数据驱动的价值》的评论:
我觉得非常好所以完整复制过来

数据驱动肯定不是从 flux/redux + react 才开始的
上者特别强调单向数据流,所以才给人造成“由它开始”的错觉

单向数据流有一个前置依赖,那就是 Single Data Source,也就是你的“提线木偶”所描述的那样

然而在你的文章里却把它写成了 Data Driven 的前置依赖

问题来了:只有单向数据流才算数据驱动吗?

肯定不是的,其实老牌的 Angular/Ember/……等等都是一样的,无非就是对待 Data 的方式各有不同罢了;刨除这些细微差别,它们都是从 “DOM 驱动” 转向 “数据驱动” 的代表作品

只是它们并没有把这个的概念提炼的深入人心,如此说来,React 一家是功不可没的,不可变数据 + 单向数据流让数据驱动真正成为了“理念”,而不只是“概念”

然而,单向数据流也不是十全十美的,对于 UI 编程来说,有些地方的确是双向绑定来得更“漂亮”一些,比如:表单

所以 React 也适时的加入了双向绑定;不过这并不妨碍数据驱动这一理念得以贯彻

Single Data Source 也不是万能灵药,如果 A B C 三个模块都是各自独立开发的,之后因为需求而要合并在一起,怎么办?在它们之上再来一个 SDS?那这样还算不算是 SDS 呢?(或者反过来,不是 SDS 的话难道就不行吗?)

这个问题其实也还在发展中,这会儿我也给不出答案,只是客观描述一番罢了。

数据驱动的陷阱

对于一个真正完美的数据驱动,就如同《前端数据驱动的价值》中的例子,所有业务全部由一个store驱动,维护好store就是维护好了整个系统。

但是对于这个一笔带过的维护store其实技术含量非常高。

下面分业务场景和情况描述

新项目

这种情况是比较幸运的,开始的模型中设计好业务数据结构,设计好未来可扩展点。

当然,即使是新产品,也会遇到麻烦点,因为每一个参与开发的人,必须要全局的理解整个数据结构。

看看数据难搞的地方,举个例子:

模式A:

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
var store = {
userInfo: {
name: 'test',
age: '20'
},
planNum: 2,
plans: {
'plan1': {
name: 'plan1',
price: 2.00,
unitNum: 1,
unit: {
'unit1': {
name: 'unit1',
price: 1.22,
keywordNum: 2,
keyword: {
'keyword1': {
name: 'word1',
price: 22.00
},
'keyword2': {
name: 'word2',
price: 21.00
}
}
}
}
},
'plan2': {
name: 'plan2',
price: 3.00,
unitNum: 0,
unit: {}
}
}
}

上面是一种最简单的数据结构,清晰的展现了planunitkeyword直接的关系,简单直白。

但是如果当层级关系越来越深,这个里面增删改查信息就是一个麻烦点,可以说上面结构的可扩展性不强。

那么换下面一种结构是否更加合适:

模式B:

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
39
40
41
42
43
44
var store = {
userInfo: {
name: 'test',
age: '20'
},
planNum: 2,
plans: {
'plan1': {
name: 'plan1',
price: 2.00,
unitNum: 1,
unit: [unit1]
},
'plan2': {
name: 'plan2',
price: 3.00,
unitNum: 0,
unit: []
}
},
units: {
'unit1': {
plan: 'plan1',
name: 'unit1',
price: 1.22,
keywordNum: 2,
keyword: [keyword1]
}
},
keywords: {
'keyword1': {
plan: 'plan1',
unit: 'unit1',
name: 'word1',
price: 22.00
},
'keyword1': {
plan: 'plan1',
unit: 'unit1',
name: 'word2',
price: 21.00
},
}
}

个人感觉这种模式更加适合,便于查找和更新,那么问题来了,先抛开哪种数据结构更合适问题,假如开发初期模式A是ok的,随着业务的复杂,发现模式A越来越难维护,经过重新设计,发现转换到模式B更加合适,能明显提高开发和维护效率,那么怎么办?

个人还没有实践过,但是经验上看感觉是会导致大面积重构。即使重构完成,假如后期发现更好的数据模式呢?

所以可以看出初期的数据结构决定了未来架构,看上去像不像后端开发,数据库的设计很重要。

既然越来越像后端设计模式,那么是否会模仿当时的前后端分离策略,底层数据结构和业务展现通过api实现呢?个人感觉这样有点问题过于复杂化。那么是否可以加一个中间数据转换层去处理数据呢?这样在底层数据结构变换时,也能避免直接影响业务模块,中间层做一个适当的解耦,展示内容和数据结构没有什么关联,关联的仅仅是数据信息。

结论一:初期的数据结构设计会是未来的不定时炸弹,迟早会面对,需要有相应策略

模块合并

再来考虑评论中的一个问题:

Single Data Source 也不是万能灵药,如果 A B C 三个模块都是各自独立开发的,之后因为需求而要合并在一起,怎么办?在它们之上再来一个 SDS?那这样还算不算是 SDS 呢?(或者反过来,不是 SDS 的话难道就不行吗?)

复杂的系统中确实会遇到这种问题,直接举例:

模块C,主要负责修改level:

1
2
3
4
5
6
7
8
9
var store = {
moduleA: {
'plan1': {
name: 'plan1',
price: 22.00
},
level: 2
}
}

模块D,主要负责修改auth:

1
2
3
4
5
6
7
8
9
var store = {
moduleB: {
'plan1': {
name: 'plan1',
price: 22.00
},
auth: 0
}
}

现在的逻辑是假如两个独立开发好的模块需要合并到一起,他们都有各自模块的内部数据结构,假如模块C除了修改level,还修改了plan1的name会怎么样?为了保证数据统一,需要去找到模块D中的plan1信息,并且修改。假如他们都合并到上面一个例子模块A中怎么办,是否还需要继续同步修改。如果漏掉一个同步,展示上,以及后面的处理都会引发各种bug。

当然,如果不用数据驱动,可能这种模块合并也是需要从展示层级各处同步修改信息的,也会特别麻烦。只是相比较而言,数据驱动的这种合并同步并没有给我们带来很清晰简单的处理方式。

结论二:数据驱动下,数据的重复和同步是一个坑,要尽量避免

不适合的场景

然而,单向数据流也不是十全十美的,对于 UI 编程来说,有些地方的确是双向绑定来得更“漂亮”一些,比如:表单

评论里面提到的这部分应该是单向数据流的一个痛点,如果模块里面严格执行单向数据流,对于这种表单验证来说,是非常痛苦的。

例如:

1
2
3
var form = {
value: 2.00
}

对应一个输入框,输入框只能限制输入1-100的2位小数。整个验证过程单向数据驱动就会特别麻烦。

一个简单的例子,在使用react实现表单验证就比较麻烦。

结论三:对于某些具体的模块,单向数据流不是万灵药,整体架构单向数据驱动,具体模块可以根据场景选择最合适的

总结

个人感觉数据驱动对于开发人员的要求其实有一些增加,开发是需要更多的全局观,对于业务需要更加的了解。这个其实算是缺点,因为从工程化的角度,这种模式提高了开发成本,提高了开发人员的学习成本。

那么优点呢,应该是系统的稳定性,可维护性,健壮性会更好。

总之没有银弹,每种模式都有适合的场景。以上就是对于数据驱动的一点点看法。仅供参考。

微信公众号

前端修炼

数据驱动应该是从flux/redux + react这种模式开始流行的。

他的背后不仅仅是数据驱动这么简单,在复杂的系统中,我觉得它解决了一个很关键的问题就是模块间的交互/通信。有很多文章拿他和mvc/mvvm去比较,我个人觉得没有特别的可比性,因为解决的问题不同。

以往处理模式

一个稍微复杂点的例子:

模块

假如有这么一个页面,我们按照以往模式开发,首先模块化开发,拆分成A,B,C 三个模块,然后每个模块有自己的子模块。

如果需求简单还比较好解决,每个模块中自己解决自己的逻辑,解耦的非常清晰。父子之间的关系也非常明确。

  1. 例如销毁C模块,会自动销毁它的子模块C1C101
  2. 模块间的关系也很清晰,B1不会和B2有直接关系,他们之间需要通过B模块去传递。同理,B模块A模块也没有直接关系,他们都需要通过外层页面去处理关系。

但是假如有这么一个需求,A2的显示和B2(用户交互)以及C101(用户交互)相关怎么办。

按照这种模式,它的解决方案是:

B2如果发生改变,通知B模块B模块在通知页面页面调用A模块C模块C模块调用C1C1调用C101获取C101的数据处理,页面调用A模块A模块再调用A2,再结合一下从C101获取的数据,改变它的展示。

是不是看着很绕,从图上看是这么个关系:

模块

图中仅仅显示了其中一个复杂交互,假如我们再多两个模块间关联的逻辑:

  1. B1B2模块影响A2模块(图中黄线)
  2. C1影响B1模块(图中白线)

如下图:

模块

3个复杂一点的交互,整个模块间的通信已经变成蜘蛛网了,重要的是,每一条关系线都需要开发者维护的,不仅影响开发效率,而且不好维护,容易引发bug,假如后期加新需求或者调整需求,开发成本都是比较高的。

可见,对于复杂的交互,或者模块间关系复杂时,这种依赖父子关系的通信,是一个很大的障碍。

但是我们怎么办,拒绝模块化开发吗?那样页面设计起来耦合度更大,更加不可维护。

首先一点,模块化开发是一个不可逆的趋势,然而在这种趋势下,解决模块化通信是一个非常重要的点。

模块间通信其他方案

在那个时候,我考虑最多的就是如何去解决模块之间的通信,如何让模块之间交互更加轻松,模块之间更加独立。

方案一:

当时考虑的一个方案是使用一个全局的event(全局的on和fire)。这样模块之间就不用依赖父子关系了。模块和模块间是可以之间交互的。

但是这样会有一些弊端:

  1. 事件名称如何定义,保证不重名
  2. 事件是否会重复的on
  3. 模块和模块之间会因为事件产生一些耦合
  4. 当交互特别复杂时,也会比较麻烦,还是上面的例子,B2通知C2改变后,C2还需要通知C101获取一次数据,来确认改变

整体来看:

优势: 摆脱了模块间父子层级关系,可以简单的跨模块通信

劣势: 依然需要维护复杂的模块间关系,只是可以绕过父子依赖

方案二:

全局共享一个model + component模式。这种其实已经非常趋向与数据驱动了。每个模块都是共享全局的model,然后每个component都可以被全局获取到到,里面的功能属性可以直接被使用。

其实这种模式已经比较理想,页面上面的任何component都可以被直接调用到并且使用。

个人觉得缺点就是:
多了一个全局可调用component的功能。如果砍掉他可以实现完成数据驱动,如果模块调用时,使用多了直接获取component的功能,还是需要在模块间维护好和其他模块间的交互逻辑。

数据驱动

先看一个图,我感觉可以很好的体现数据驱动

提线木偶

提线木偶:他的特点就是每个动作都是,头,手臂,脚,金箍棒都是由操作的人手决定的,头和手臂直接没有任何关系。

数据驱动也可以这么理解,页面上面所以的展示都是由数据决定的,和页面其他地方没有任何关系。

再来看看上面那个例子如果加上数据驱动的设计思想。

数据驱动

页面之间每个模块,不用关心父子模块之间的关系,每个独立的模块都是由一个全局的model决定。

回到上面那个麻烦的场景。当B2改变时,它会修改model中对应的数据(效验C101数据,结合B2的改变,修改A2的数据),然后A2的业务模块跟进A2的数据改变。

这种设计的核心是每一个模块的改变,全部都交给model处理。

然后model里面会和个个模块一一对应,每个模块无需关注其他模块的变化,只需要关注model里面对应自己数据的变化即可。所以模块间关系链条会显得非常简单。

重点在于,当交互逻辑不断增加时,这个关系链条依然不会增加,因为模块只和model里面对于的数据相关联。

当然,这种模式也无法去省略复杂的业务逻辑,只是业务逻辑全部都会聚集在model中。可以理解为页面上所有的操作都是对数据的操作。然后每个模块只需要监听关注的数据改变即可,这个监听关系就是图中唯一的一条关系线。

换一个理解,我们将直接的模块和模块直接的耦合关系全部转移到了数据中去体现。而数据的维护是远远比模块更好维护的。

Model如何对应View

还是上面页面为例子:

model

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
39
40
41
var page = {
a: {
isShow: true,
children: [{
a1: {
isShow: true
}
},
{
a2: {
isShow: true
}
}]
},
b: {
isShow: true,
children: [{
b1: {
isShow: true
}
},
{
b2: {
isShow: true
}
}]
},
c: {
isShow: true,
children: [{
c1: {
isShow: true,
children: [{
c101: {
isShow: true
}
}]
}
}]
}
}

isShow 表示展示的意思。这个状态对应文章第一个图片。

模块

当数据改变时,例如model发生变化如下:

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
39
40
41
var page = {
a: {
isShow: true,
children: [{
a1: {
isShow: true
}
},
{
a2: {
isShow: false
}
}]
},
b: {
isShow: true,
children: [{
b1: {
isShow: true
}
},
{
b2: {
isShow: false
}
}]
},
c: {
isShow: true,
children: [{
c1: {
isShow: false,
children: [{
c101: {
isShow: true
}
}]
}
}]
}
}

对应下面这样:

数据驱动

换一个理解就是每一种数据状态对应一种页面的展示状态。页面想展示成什么样子,需要数据处理成什么样子。数据是这个页面的核心。

数据驱动开发关注点

第一点数据结构的处理,因为数据决定了整个页面的展示,数据结构开始的设计非常关键,数据结构的可扩展性决定了页面的可扩展性,如果开始数据模式不好,后期维护也会非常难受。

第二点是处理好模块和数据中对应的关系。

可以看到数据驱动的难点和关键点就是数据结构的设计。而这个也是很考验开发者能力的。数据结构的好坏直接决定了后期业务开发的质量。

数据驱动和mvc/mvvm的关系

文章开头说了,从我的角度理解数据驱动这种模式和mvc并没有什么竞争关系,在具体的实践中,每一个模块可以是一个mvc或者mvvm,模块的内部处理交给模块自己,可以是mvc,或者单例也可以。数据驱动主要是处理模块之间的一种逻辑。

那么为什么数据驱动和react这种结合的更加好了?因为react更进一步是讲模块内部也实现一个数据驱动,模块内部的数据改变了,模块的状态会跟着改变。

微信公众号

前端修炼

当扔给你一个bug时,你会怎么办

当你的老大给你分配一个bug时,你会怎么想?

如果简单,之前遇到过还好,如果完全没有头绪呢,一看就很麻烦呢?

估计第一反应就是what the F** ……

呵呵,其实我最近就干了这些事,分配了一些感觉比较麻烦bug给组里面的同学。

从技能角度

因为从我的经验上看,解决bug是一个迅速提高技术的捷径。你想想如果项目中没有遇到任何bug,或者比较麻烦的点,你会主动debug到项目依赖的框架中吗?即使你有目的的去了解框架,你会像定位bug那样有目的性吗?

定位bug其实就是要求你在一定时间内,去有目的性的(开始没有目的也要逐步梳理出眉目)研究框架中某一些技术点。

假设耗费了很大的力气还没有找到问题,你在求助其他人时,你也会很认真的看他怎么定位bug,看看自己定位的思路和他的差距在哪里。这个差距就是你快速学习的内容。

从项目角度

在项目上,有一个比较重要的衡量标准:

快速定位问题和解决问题的能力

我觉得这个在技术行业,非常重要。直接决定了你是否能独立承担项目。能力越强,越能承担负责的项目。

微信公众号

前端修炼