React 与 Redux 在生产环境中的实践总结
React 与 Redux 在生产环境中的实践总结
前段时间使用 React 与 Redux 重构了我们360netlab 的 开放数据平台。现将其中一些技术实践经验总结如下:
Universal 渲染
Universal (“同构”现在是公认的不准确的叫法)渲染是指在服务端与客户端使用一套代码进行渲染的技术。它所带来的优势如下:
- 与实现服务端渲染的传统应用相比,Universal 渲染中的客户端渲染减少了网络请求(主要是模板和静态资源的请求),提高了页面间切换的速度,可以看到页面之间的切换都是瞬间完成的。
- 与实现客户端渲染的传统 SPA(比如 Angular1.x 搭建的单页面应用)相比,Universal 渲染的服务端渲染提升了首屏加载速度,无须等待庞大的 Javascript 脚本加载完成后进行渲染,因此也无须使用欢迎界面了。
- 与使用不同语言实现服务端渲染+客户端渲染的应用(指的是后端语言为 Java、Python、PHP、前端语言为 JavaScript 的应用)相比,由于 Universal 渲染使用同一套代码(前后端均为 JavaScript),因此至少减少了一半的代码量。
Universal 渲染非常复杂,需要权衡的东西很多。不过这都是值得的,真正让网站达到了快如鬼魅的速度!顺便引用一句话:
According to research at Google, the difference of just 200 milliseconds in page load performance has an impact on user behavior.
根据 Google 的调查,在一个页面的加载过程中,仅仅200毫秒的差异就可以影响用户的行为。
延迟渲染
很多人抱怨 React 并没有大家说的那么快,其实 React 只是便于优化性能,在没有经验的新手手中,React确实可能会很慢。但如果你对 React 非常了解,那么快如鬼魅便不是虚言。React 性能优化的方法很多,网上也有无数的文章对其进行介绍(选择 React 的另一好处:活跃的社区),常见的方法主要是,使用不可变数据,快速进行变更检查,以避免不必要的重新渲染。但我们还要介绍一种方法——延迟渲染。
延迟渲染类似于分页或瀑布流,就是在一个有大量数据页面中,先渲染一部分,等用户滚动下去后,再进行渲染。
延迟渲染除了可以提升性能之外,还可以过滤掉不需要在服务端渲染的代码(服务端可没有re-render),以减少 Universal 的难度。
延迟渲染的方法很多,实现的轮子也很多,不再赘述了。
减小重量
在 React 与 Redux 的项目中,不可避免要引入一些第三方的库,因此最终打包的脚本重量很容易达到 500-800kb 以上(gzip 压缩前)。尽管首屏渲染速度不会受此影响(因为我们实现了 Universal 渲染中的服务端渲染,而浏览器又是自上而下解析的),但我们依然希望这个脚本的重量能够更小。现将一些可行的办法列举如下:
改变库的调用方式
写过NPM的包的同学很清楚,一个包通常会有一个入口文件,我们将所有的模块都放在这个入口文件中,以便其他开发者调用。但是如果仅仅只用了一个包中很少一个模块,那么从入口文件调用就会导致增加了很多多余的模块。为此,我们应该改变一些库的调用方式,来避免这种情况,比如:
React Bootstrap 应该这么调用:
import IndexLink from 'react-router/lib/IndexLink';
import Navbar from 'react-bootstrap/lib/Navbar';
import Nav from 'react-bootstrap/lib/Nav';
import NavItem from 'react-bootstrap/lib/NavItem';
不应该这么调用:
import { IndexLink, Navbar, Nav, NavItem }from 'react-router';
React Router 应该这么调用:
import Route from 'react-router/lib/Route';
import IndexRoute from 'react-router/lib/IndexRoute';
不应该这么调用:
import { Route, IndexRoute } from 'react-router/lib/Route';
这种改进方式所带来的效果非常明显,至少能减少100kb的重量。
除此之外,Bootstrap的样式文件也应该进行自定义,并去除一些不用的模块。最终我们项目中所有的样式文件合并后也只有22kb(gzip 压缩后)。
代码分割
使用 webpack 1.x 的 require.ensure
可以轻易实现代码分割。分割的对象主要有俩个:
- 路由组件
- 只在个别页面使用的大型第三方库
路由组件的分割意义不大,因为我们写的代码几乎很少(这也正是使用库和框架的意义),即便按需加载,也不会带来太多的提升。而且,原本打包成一个文件,可以进行代码去重,但分割后就无法实现这个功能了(当然,如果你将公共库提取出来了,这个问题就不存在了)。不过,如果你的项目非常庞大,也可以试试。
分割只在个别页面使用的大型第三方库是有意义的。比如,我们项目中一些页面使用了很重的 Highcharts,但也有很多页面不需要它,如果不对其进行代码分割,就会连累不使用 Highcharts 的页面。所以应该对只在个别页面使用的大型第三方库进行分割。方法如下:
将这些库使用 require.ensure
封装成 Promise:
export const loadHighcharts = () => new Promise((resolve)=> {
require.ensure([], (require)=> {
if (!window.Highcharts) {
window.Highcharts = require('highcharts');
}
resolve(window.Highcharts);
}, 'highcharts');
});
然后,在组件中调用:
import React, { Component, PropTypes } from 'react';
import { loadHighcharts } from '../Map/load';
class Chart extends Component {
componentDidMount() {
loadHighcharts()
.then(Highcharts => {
...
this.chart = Highcharts.chart(this.container, config);
});
}
componentWillUnmount() {
this.chart.destroy();
}
render() {
return (
<div
ref={(c) => {
this.container = c;
}}
style={{
height: 400,
minWidth: 310,
margin: '0 auto',
textAlign:'center'
}}
>
<i className="fa fa-spinner fa-spin fa-2x fa-fw"/>
</div>
);
}
}
启用 gzip 压缩
启用 gzip 压缩的效果更加明显,往往能减少 70% 的重量,最终我们项目的代码重量一共只有130kb(包含了React Bootstrap、React Rouer、Highcharts在内的N多重量级第三方库,另外还有所有的页面代码在里面)。这个方法比较常见,不再赘述。
减小重量的方法就先聊到这吧!
使用更少的样板代码发起异步action
很多人说 Redux 代码多,开发效率低。其实 Redux 是可以灵活使用以及拓展的,经过充分定制的 Redux 其实写不了几行代码。今天先介绍一个很好用的 Redux 拓展—— redux-amrc。它可以帮助我们使用更少的样板代码发起异步action。
一般情况下,为了清楚地记录异步的过程,我们需要使用 三个 action 来记录状态变化。通常,我们的代码会是这样:
export const USER_REQUEST = 'USER_REQUEST'
export const USER_SUCCESS = 'USER_SUCCESS'
export const USER_FAILURE = 'USER_FAILURE'
使用了 redux-amrc 后,再也不用写这么多action了,甚至连处理这些action的reducer都不用写,你只需要把异步以Promise的形式传给 redux-amrc就行了:
import { ASYNC } from 'redux-amrc';
/**
* 这个action创建函数可以帮你自动发起 LOAD 和 LOAD_SUCCESS,
* state.async.[key] 将会变为 'success'
*/
function success() {
return {
[ASYNC]: {
key: 'key',
promise: () => Promise.resolve('success')
}
}
}
/**
* 这个action创建函数可以帮你自动发起 LOAD 和 LOAD_FAIL,
* state.async.loadState.[key].error 将会变为 'fail'
*/
function fail() {
return {
[ASYNC]: {
key: 'key',
promise: () => Promise.reject('fail')
}
}
}
更多的使用方法,请参考官网文档。
与 传统的DOM操作相结合
React 强调声明式构建用户界面,但在一些情况下,往往还是操作 DOM 来得快。事实上,在 React 中操作 DOM 也很方便。现将一些场景列举如下:
使用Canvas
有时候我们需要使用 Canvas 画个多边形什么的,尽管已经有很多封装 Canvas 的 React 库了,但命令式的 Canvas 画法也非常方便,可以直接在React 中使用:
比如,这么一个画多边形的方法:
/**
* 使用canvas画多边形
* @param c:canvas context
* @param n:多边形的边数
* @param r:多边形的半径
* @param color:线条颜色
*/
function drawHexagon(c, n, r, color) {
const context = c;
const x = context.canvas.width / 2;
const y = context.canvas.height / 2;
const ang = (Math.PI * 2) / n; // 旋转的角度
context.save();// 保存状态
context.fillStyle = 'transparent';// 填充颜色
context.strokeStyle = color;// 填充线条颜色
context.lineWidth = 1;// 设置线宽
context.translate(x, y);// 原点移到x,y处,即要画的多边形中心
context.moveTo(0, -r);// 据中心r距离处画点
context.beginPath();
context.rotate(ang / 2);// 旋转
for (let i = 0; i < n; i += 1) {
context.rotate(ang);// 旋转
context.lineTo(0, -r);// 据中心r距离处连线
}
context.closePath();
context.stroke();
context.fill();
context.restore();// 返回原始状态
}
可以这么在 React 中用:
class Hexagon extends Component {
static propTypes = {
title: PropTypes.string.isRequired,
content: PropTypes.array.isRequired
};
componentDidMount() {
const context = this.canvas.getContext('2d');
const sin60 = Math.sin(Math.PI / 3);
drawHexagon(context, 6, 80 / sin60, '#D9DADB');
}
render() {
return (
<div>
<canvas
width="190px"
height="170px"
ref={(c) => {
this.canvas = c;
}}
/>
</div>
);
}
}
其实任何基于 DOM 的操作方法都可以这么玩!你可以把 componentDidMount()
当成 jQuery 的 $(document).ready()
方法。
实现图表
无论你之前使用的是 D3 还是 Highcharts,几乎都是基于 DOM 来完成图表的绘制的,在 React 中,如果你不想使用一些封装好的库,也可以操作DOM,方法和在 React 组件中画 Canvas 一样:
- 渲染一个div
- 使用 ref 属性获取DOM
- 操作DOM
需要注意的是,如果你的库不够智能,那么你需要在 React 组件 卸载时 销毁操作DOM 产生的对象,以防止内存泄露。
实现返回顶部
返回顶部这个功能也操作了 BOM 和 DOM,它与 React 的结合方法如下:
- 使用 React 渲染一个
a
标签,并添加返回顶部的点击事件 - 在组件挂载和卸载时分别添加和移除
window
的scroll
事件来显示或隐藏返回顶部的a
标签
import React, { Component } from 'react';
class ScrollLink extends Component {
constructor() {
super();
this.state = { linkStyle: { display: 'none' } };
}
componentWillMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll = () => {
const top = window.pageYOffset || document.documentElement.scrollTop;
this.setState({ linkStyle: { display: top > 100 ? 'block' : 'none' } });
};
scrollToTop = () => {
const scrollTo = (element, to, duration) => {
if (duration <= 0) return;
const _element = element;
const difference = to - _element.scrollTop;
const perTick = (difference / duration) * 10;
setTimeout(() => {
_element.scrollTop += perTick;
if (_element.scrollTop === to) return;
scrollTo(_element, to, duration - 10);
}, 10);
};
scrollTo(document.body, 0, 100);
};
render() {
const styles = require('./index.scss');
return (
<a
className={styles.scrollLink}
onClick={this.scrollToTop}
style={this.state.linkStyle}
>
<i className="glyphicon glyphicon-arrow-up"/>
</a>
);
}
}
export default ScrollLink;
React 与 Redux 的实践经验还有很多,由于篇幅原因,在后续的博文中我们再一一介绍。
教程源代码及目录
如果您觉得本博客教程帮到了您,就赏颗星吧!
React 与 Redux 在生产环境中的实践总结的更多相关文章
- 生产环境中使用Docker Swarm的一些建议
译者按: 实践中会发现,生产环境中使用单个Docker节点是远远不够的,搭建Docker集群势在必行.然而,面对Kubernetes, Mesos以及Swarm等众多容器集群系统,我们该如何选择呢?它 ...
- .NET跨平台之旅:在生产环境中上线第一个运行于Linux上的ASP.NET Core站点
2016年7月10日,我们在生产环境中上线了第一个运行于Linux上的ASP.NET Core站点,这是一个简单的提供后端服务的ASP.NET Core Web API站点. 项目是在Windows上 ...
- 理解Docker(6):若干企业生产环境中的容器网络方案
本系列文章将介绍 Docker的相关知识: (1)Docker 安装及基本用法 (2)Docker 镜像 (3)Docker 容器的隔离性 - 使用 Linux namespace 隔离容器的运行环境 ...
- .NET跨平台之旅:生产环境中第2个跑在Linux上的ASP.NET Core站点
今天我们在生产环境中上线了第2个跑在Linux上的ASP.NET Core站点.这是一个简单的Web API站点,通过命令行的方式调用安装在Linux服务器上的程序完成操作.之前用的是nodejs,现 ...
- 【原】Storm Local模式和生产环境中Topology运行配置
Storm入门教程 1. Storm基础 Storm Storm主要特点 Storm基本概念 Storm调度器 Storm配置 Guaranteeing Message Processing(消息处理 ...
- 生产环境中CentOS7部署NET Core应用程序
NET Core应用程序部署至生产环境中(CentOS7) 阅读目录 环境说明 准备你的ASP.NET Core应用程序 安装CentOS7 安装.NET Core SDK for CentOS7. ...
- [virtualenv]生产环境中使用virtualenv
virtualenv 对于python开发和部署都是好工具,可以隔离多个python版本和第三方库的版本,这里作者总结了几个常用python服务怎么样结合virtual部署 原文链接 Python 中 ...
- Kubernetes 在生产环境中常用架构
Kubernetes 在生产环境中常用架构 首先,我们来梳理下Kubernetes生产架构,其设计适用于绝大多数环境.如下图所示 在该架构中,我们可以将其分为四层,如下: Client层:即Kuber ...
- Dubbo Mesh 在闲鱼生产环境中的落地实践
本文作者至简曾在 2018 QCon 上海站以<Service Mesh 的本质.价值和应用探索>为题做了一次分享,其中谈到了 Dubbo Mesh 的整体发展思路是“借力开源.反哺开源” ...
随机推荐
- POJ2125 Destroying The Graph
题目链接:ヾ(≧∇≦*)ゝ 大致题意: 给出一个有向图D=(V,E).对于每个点U,定义两种操作a(u),b(u) 操作a(u):删除点U的所有出边,即属于E,操作花费为Ca(u). 操作b(u):删 ...
- 洛谷P3385 【模板】负环(DFS求环)
洛谷题目传送门 HNOI爆零前回刷模板题 非常不正经的题目,目前并没有合适的优秀算法,就算是大家公认的dfs(还是不要强行叫dfs-spfa吧,概念应该不一样,这就是暴力dfs松弛答案) 但是对于随机 ...
- JavaScript -- 标签 , Break 和 Continue 语句
break 语句用于跳出循环. continue 用于跳过循环中的一个迭代. 标签引用,break 语句可用于跳出任何 JavaScript 代码块. demo: <!DOCTYPE html& ...
- 2019.3.16 noiac的原题模拟赛
RT,这太谔谔了,我不承认这是模拟赛 但是虽然是搬了三道题,题目本身也还能看,就这么着吧 (怎么机房里就我一道原题都没做过啊 T1 CF24D Broken Robot 比较简单地列出式子之后,我们发 ...
- Luogu 1613 跑路(最短路径,倍增)
Luogu 1613 跑路(最短路径,倍增) Description 小A的工作不仅繁琐,更有苛刻的规定,要求小A每天早上在6:00之前到达公司,否则这个月工资清零.可是小A偏偏又有赖床的坏毛病.于是 ...
- 【CF884D】Boxes And Balls k叉哈夫曼树
题目大意:给定一个大小为 N 的集合,每次可以从中挑出 2 个或 3 个数进行合并,合并的代价是几个数的权值和,求将这些数合并成 1 个的最小代价是多少. 引理:K 叉哈夫曼树需要保证 \((n-1) ...
- 关于NIO一些优化
1. 使用NIO开发web服务,传输文件内容,可以使用FileChannel.transferTo(position,count,socketChannel)来提升性能: 经过测试,确实能提升10% ...
- PHP多条件模糊查询
所使用的方法:$sqlArr=array();array_push();implode(); 原理, 一.建立sql语句前半句,并且建立一个空数组. 二.根据条件是否为空来判断是否向数组中添加元素.如 ...
- python deamon(守护)线程的作用
stackoverflow 上的解释 某些线程执行后台任务,例如发送keepalive数据包,或执行定期垃圾收集,或任何.这些仅在主程序运行时有用,并且一旦其他非守护程序线程退出就可以将其关闭. 没有 ...
- js监听浏览器tab窗口切换
js监听浏览器tab窗口切换 ——IT唐伯虎 摘要:js监听浏览器tab窗口切换. if (document.hidden !== undefined) { document.addEventLis ...