xile611

xile611 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

xile611 赞了文章 · 2017-01-13

使用cross-env解决跨平台设置NODE_ENV的问题

更多文章,请在Github blog查看

问题

在搭建公司新的前端工程的架构中,需要在在package.jsonscripts标签下配置一系列命令,如下所示:

"scripts": {
    "clear": "rm -rf build&& mkdir build",
    "start": "npm run clear&& NODE_ENV=development webpack-dev-server --host 0.0.0.0 --devtool eval --progress --color --profile",
    "deploy": "npm run pre&& npm run clear&& NODE_ENV=production webpack -p --progress"
  },

上面配置中的的&&最开始使用的是;,后来发现;在windows环境中无法正常运行,于是改成了*unix和windows都兼容的&&。但是公司的部分使用windows的同事在运行npm start的时候,依然会报错:

'NODE_ENV' 不是内部或外部命令,也不是可运行的程序
或批处理文件。

npm ERR! Windows_NT 6.1.7601
npm ERR! argv "D:\\nodejs\\node.exe" "D:\\nodejs\\node_modules\\npm\\bin\\npm-cli.js" "start"
npm ERR! node v4.0.0-rc.5
npm ERR! npm  v2.14.2
npm ERR! code ELIFECYCLE
npm ERR! yy-ydh-web@1.0.7 start: `npm run clear&& NODE_ENV=development && webpack-dev-server --host 0.0.0.0 --devtool ev
al --progress --color --profile`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the yy-ydh-web@1.0.7 start script 'npm run clear&& NODE_ENV=development && webpack-dev-server --host
0.0.0.0 --devtool eval --progress --color --profile'.
npm ERR! This is most likely a problem with the yy-ydh-web package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR!     npm run clear&& NODE_ENV=development && webpack-dev-server --host 0.0.0.0 --devtool eval --progress --color
 --profile
npm ERR! You can get their info via:
npm ERR!     npm owner ls yy-ydh-web
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR!     D:\workspace\node_modules\yy-ydh-web\npm-debug.log

简单来说,就是windows不支持NODE_ENV=development的设置方式。

但是,又不想放弃这样简单方便的方式,于是只好求助于Google了。

解决方式

功夫不负有心人,在万能的google上,我找到了解决方法:cross-env
这个迷你的包能够提供一个设置环境变量的scripts,让你能够以unix方式设置环境变量,然后在windows上也能兼容运行。

使用方法:

  • 安装cross-env:npm install cross-env --save-dev
  • NODE_ENV=xxxxxxx前面添加cross-env就可以了。

赶紧试试吧!

查看原文

赞 115 收藏 156 评论 13

xile611 赞了文章 · 2017-01-11

R统计绘图(1): ggplot2入门指南

前言

ggplot2是R语言最流行的第三方扩展包,是RStudio首席科学家Hadley Wickham读博期间的作品,是R相比其他语言一个独领风骚的特点。包名中“gg”是grammar of graphics的简称,是一套优雅的绘图语法。Wickham Hadley将这套语法诠释如下:

一张统计图形就是从数据几何对象(geometric object,缩写geom)的图形属性(aesthetic attribute,缩写aes)的一个映射。此外,图形中还可能包含数据的统计变换(statistical transformation,缩写stats),最后绘制在某个特定的坐标系(coordinate system,缩写coord)中,而分面(facet)则可以用来生成数据不同子集的图形。

这个解释读起来还是有点抽象。我们举个具体的例子来解读这个概念。假设现在我们要对一批连续取值的数据绘制直方图。首先,要定义清楚需要几个分组或者每个分组的区间,根据分组定义统计落在这个分组里的个数,这个步骤就是把data变为stats。然后,需要选定表达数据的几何对象,这个例子选用的是条块bar,这个步骤就是选geomgeom有一堆属性需要设定,比如x、y、颜色等,称为aes,哪个aes由哪个stats指定,需要指定一个映射关系mapping,即指定谁对谁。知道谁对谁后,还需要知道怎么个对法,需要由scale决定,比如stats的color字段取值为1应该对到什么颜色上,取值为2应该对到什么颜色上。这些完成了以后,统计图形的主体部分就成形了,但是假如我们希望在直方图上,再画一个概率密度曲线图,怎么办?ggplot2的思想非常精妙,把上面的主体部分称为一个图层layer,一个统计图形可以拥有多个图层,每个图层叠加起来形成我们要的效果。接下来,再选定一个坐标系统coord,一张统计图形plot就做好了。假如我们有多组数据,每组数据都要按照相同的方法画一张图,每张图重复敲代码很繁琐,就可以使用分面facet快速绘制多张统计图形。这个过程用图形总结如下:

clipboard.png

我们可以看到ggplot2相比其他绘图系统的几个特性:

  • 标准化:任何一个统计图形遵循相同的绘图流程,所以语法高度统一;

  • 面向数据:上面的绘图流程只与数据有关,与数据无关的绘图细节封装在单独的theme()方法里,数据相关绘图与数据无关绘图分离;

这两大特性解放了数据分析师的思维,做到绘图时所思即所见,非常优雅高效。下面我们来逐步剖析每个元素的内容。

数据

ggplot2接受的输入数据一般是data.frame,这是一个表格型结构,每一行是一个观测(observation),每一列是一个变量(variable)。R语言内置了许多著名的数据集,本文选取其中的iris进行讲解。iris中文名是鸢尾花,有四个属性,分别是Sepal.Length(花萼长度),Sepal.Width(花萼宽度),Petal.Length(花瓣长度),Petal.Width(花瓣宽度),以及一个类别标签Species。我在网上找了一个图片,做个标注,方便朋友理解。

clipboard.png

我们可以使用str()查看数据集的结构,用summary()对每一个变量进行统计。

str(iris)

clipboard.png

summary(iris)

clipboard.png

Hadley对data.frame提出了一个是否tidy的概念,抽象来讲就是一个变量必须有自己独立的一列,一个观测必须有自己独立的一行,每个取值必须有自己独立的一个单元格。为了便于理解,我们从R for Data Science这本书截取出这个图进行解释:

clipboard.png

左边的数据是tidy的,右边的数据是不tidy的,通过另一个包tidyr可以轻松完成二者的转换。ggplot2的数据要求是tidy的。

统计图形基本要素

几何对象geom

几何对象,说的直观一些,就是你选择什么几何图形来表示这组数据。ggplot2提供了众多几何对象geom_xyz()供大家选择。举两个常见的例子,geom_point()用于表示两个连续变量之间的关系,几何形状是点;geom_bar()用于表示x轴为离散变量,y轴为连续连续变量之间的关系,几何形状是条块。完整的几何对象请下载RStudio公司总结的ggplot2 cheetsheet

几何对象需要解决一个问题,即相同数据的几何对象位置相同,是放在一个位置相互覆盖还是用别的排列方式。ggplot2的几何对象有一个position选项,用于指定如何在空间内布置相同取值的集合对象。dodge为并排模式;fill为堆叠模式,并归一化为相同的高度;stack为纯粹的堆叠模式;jitter会在X和Y两个方向增加随机的扰动来防止对象之间的覆盖。

统计变换stats

在ggplot2里,几何对象与统计变换往往是一一对应的。每个统计变换需要通过一个几何对象来展现;每个几何对象的展现依赖统计变换的结果。举个简单例子,以下两行代码的效果是一样的:

ggplot(iris) + geom_bar(aes(x=Sepal.Length), stat="bin", binwidth = 3)
ggplot(iris) + stat_bin(aes(x=Sepal.Length), geom="bar", binwidth = 3)

图形属性aes

每个几何对象都有自己的属性,这些属性的取值需要通过数据提供。数据与图形属性之间的映射关系称为mapping,在ggplot2中用aes()进行定义。常见的图形属性有:xysizecolorgroup。图形属性的任意一项都可以用数据的某一个变量来表示。

标尺scale

前面提到aes()设定了数据与图形属性的映射关系,但是数据怎么映射为属性,这就是标尺(Scales)的功能。对于任何一个图形属性,如xyalphacolorfilllinetypeshapesize,ggplot2都提供以下四种标尺:

  • scale_*_continuous():将数据的连续取值映射为图形属性的取值

  • scale_*_discrete():将数据的离散取值映射为图形属性的取值

  • scale_*_identity():使用数据的值作为图形属性的取值

  • scale_*_mannual():将数据的离散取值作为手工指定的图形属性的取值

举个例子

group_iris <- iris %>% group_by(Species) %>% dplyr::summarise(avg_sepal_length=mean(Sepal.Length))
str(group_iris)
p <- ggplot(group_iris) + geom_bar(aes(x=Species, weight=avg_sepal_length, fill=Species))
p

clipboard.png

p + scale_fill_manual(
  values = c("skyblue", "royalblue", "navy"), # mannual类scale特有的选项,指定图形属性的取值范围
  limits = c("setosa", "versicolor", "virginica"), # 数据的取值范围
  breaks = c("setosa", "versicolor", "virginica"), # 图例和轴要显示的分段点
  name = "Species",  # 图例和轴使用的名称
  labels = c("set", "ver", "vir")  # 图例使用的标签
)

clipboard.png

除了上述四大类通用的标尺,特定的图形属性还有一些专门的标尺类型。对于xy类图形属性,有如下几种特殊的标尺:

  • scale_x_date(labels=date_format("%m/%d"), breaks=date_breaks("2 weeks"))

  • scale_x_datetime()

  • scale_x_log10()

  • scale_x_reverse()

  • scale_x_sqrt()

对于colorfill类的图形属性,有如下几类特殊标尺:

  • scale_fill_brewer(palette="Blues"):根据调色盘生成颜色标尺,可用的调色盘可以通过RColorBrewer::display.brewer.all()命令查看;对于具体的一个调色盘,可以通过RColorBrewer::brewer.pal(n=4, name="Blues")查看具体某个名字调色盘的n个配色值。

  • `scale_fill_grey(start=0.2, end=0.8, na.value="red"):灰度标尺

  • scale_fill_gradient(low="red", high="yellow"):双色渐变标尺

  • scale_fill_gradient2(low="red", high="blue", mid="white", midpoint=25):三色渐变标尺

  • scale_fill_gradientn(colours=terrain.colors(6)):n色渐标尺,其他的调色盘有rainbow()heat.colors()topo.colors()cm.colors()以及RColorBrewer包的调色盘。

对于shape类的图形属性,我们可以手工指定形状:scale_shape_manual(values=c(3:7)。每个形状用数字表示,根据下图可以选择自己需要的形状。

clipboard.png

图层layer

ggplot2的绘图过程有点像Photoshop,有一个图层的理念,每个图层可以有自己的图形对象和图形属性,通过+将不同图层叠加起来生成最后的统计图形。如果将数据定义在ggplot()中,那么所有图层都可以共用这个数据;如果将数据定义在geom_xyz()中,那么这个数据就只供这个几何对象使用。

坐标系coord

ggplot2默认的坐标系是笛卡尔坐标系,可以用如下方法指定取值范围:coord_cartesian(xlim=c(0,5), ylim=c(0,3))。如果想要让x轴和y轴换位置,比如将柱形图换成条形图,可以使用coord_flip()函数。coord_polar(theta="x", direction=1)是角度坐标系,theta指定角度对应的变量,start指定起点离12点钟方向的偏离值,direction若为1表示顺时针方向,若为-1表示逆时针方向。

掌握了数据、几何对象、图形属性、图层和坐标系的概念后,我们就可以开始绘制常见的统计图形了。

练习

Kaggle数据挖掘竞赛里有一个经典的探索性分析例子,对iris数据集进行了各种形式的可视化,帮助人通过直观的图形更深地理解特征与label的关系。Kaggle官网给出了Python版本的实现。本节用R对该notebook的代码进行重现。

library(ggplot2)

# Make scatter plot of Sepal.Length and Sepal.Width
p.scatter <- ggplot(iris) + geom_point(aes(x=Sepal.Length, y=Sepal.Width))
p.scatter

clipboard.png

# One piece of information missing in the plots above is what species each plant is
p.scatter <- ggplot(iris) + geom_point(aes(x=Sepal.Length, y=Sepal.Width, color=Species))
p.scatter

clipboard.png

# Boxplot to explore numeric variable
p.box <- ggplot(iris) + geom_boxplot(aes(x=Species, y=Petal.Length))
p.box

clipboard.png

# One way we can extend this plot is adding a layer of individual points on top of it
p.box.jitter <- p.box + geom_jitter(aes(x=Species, y=Petal.Length))
p.box.jitter

clipboard.png

# A violin plot combines the benefits of the previous two plots and simplifies them
# Denser regions of the data are fatter, and sparser thiner in a violin plot
p.violin <- ggplot(iris) + geom_violin(aes(x=Species, y=Petal.Length))
p.violin

clipboard.png

# A final plot useful for looking at univariate relations is the kdeplot,
p.density <- ggplot(iris) + geom_density(aes(x=Petal.Length, colour=Species)) 
p.density

clipboard.png

分面

分面,就是分组绘图,根据定义的规则,将数据分为多个子集,每个子集按照统一的规则单独制图,排布在一个页面上。ggplot2提供两种分面模式:facet_grid()facet_wrap()

我们先来看一下facet_grid()的效果。

library(tidyr)
library(dplyr)
# 将数据变为tidy的
tidy_iris <- iris %>% 
  gather(feature_name, feature_value, one_of(c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width")))

p.box.facet <- ggplot(tidy_iris) + geom_boxplot(aes(x=Species, y=feature_value)) + facet_grid(feature_name~Species)
p.box.facet

clipboard.png

可以看到facet_grid()是一个二维的矩形布局,每个子集的位置由行位置变量~列位置变量的决定,在上面的例子中就是每一个Species的取值作为一行,每一个feature_name的取值作为一列。

再来看一下facet_wrap()的效果。

p.box.facet <- ggplot(tidy_iris) + geom_boxplot(aes(x=Species, y=feature_value)) + facet_wrap(~feature_name+Species, scales="free")
p.box.facet

clipboard.png

facet_wrap()生成一个动态调整的一维布局,根据~位置变量1+位置变量2+...来确定每个子集的位置,先逐行排列,放不下了移动到下一行。scales="free"让每个子图的坐标系适合自己的数据,便于在有限的空间里充分展示子图的细节,但也失去了不同子图之间比较的作用,需要谨慎使用。

题外话:复杂布局

分面的特点是可以快速生成多个子图,每个子图的生成方式是一样的,因此只需要指定分组的规则即可。但是有时候我们希望绘制多个子图,每个子图的生成方法却不一样,这个时候分面就不起作用了,需要使用grid包提供的布局功能。下面我们用ggplot2和grid的布局实现一个较为复杂的统计图形效果:

library(grid)
# Show bivariate scatter plot and univariate histogram
p.hist.len <- ggplot(iris) + geom_histogram(aes(x=Sepal.Length))
p.hist.wid <- ggplot(iris) + geom_histogram(aes(x=Sepal.Width)) + coord_flip()
grid.newpage()
pushViewport(viewport(layout = grid.layout(3, 3)))
print(p.scatter, vp=viewport(layout.pos.row=2:3, layout.pos.col=1:2))
print(p.hist.len, vp=viewport(layout.pos.row=1, layout.pos.col=1:2))
print(p.hist.wid, vp=viewport(layout.pos.row=2:3, layout.pos.col=3))

clipboard.png

在做数据分析时,我们经常需要观察变量自身与变量之间的两两关系。这个过程中需要绘制大量的图表,且每个业务的数据分析都需要这么做,因此算是一种重复性比较大的工作。我们可以使用GGally包来快速完成这个探索性分析的任务。

library(GGally)
# Another useful seaborn plot is the pairplot, which shows the bivariate relation
# between each pair of features
# 
# From the pairplot, we'll see that the Iris-setosa species is separataed from the other
# two across all feature combinations

ggpairs(iris, aes(colour=Species), alpha=0.4) # R could be better!!

clipboard.png

题外话:其他练习

Kaggle数据挖掘竞赛剩下的例子是绘制Parallel coordinate graph、Andrews Curve、radviz,前两个的实现参考如下,最后一个暂时没找到对应的方法。

# Parallel coordinate graph & Andrews Curve
# 修改自:http://cos.name/2009/03/parallel-coordinates-and-andrews-curve/
# 轮廓图的思想非常简单、直观,它是在横坐标上取n个点,依次表示各个指标(即变量);横坐标上则对应各个指标的值(或者经过标准化变换后的值),然后将每一组数据对应的点依次连接即可
# 调和曲线图的思想和傅立叶变换十分相似:
# 根据三角变换方法将 n 维空间的点映射到二维平面上的曲线上,其中x取值范围为[-pi,pi]。

# Another multivariate visualization technique pandas has is parallel_coordinates
# Parallel coordinates plots each feature on a separate column & then draws lines
# connecting the features for each data sample

p.paral <- ggplot(cbind(iris %>% gather(feature_name, feature_value, one_of(c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width"))), id=1:nrow(iris))) + geom_line(aes(x=feature_name, y=feature_value, group=id, colour=Species))
p.paral

clipboard.png

# One cool more sophisticated technique pandas has available is called Andrews Curves
# Andrews Curves involve using attributes of samples as coefficients for Fourier series
# and then plotting these
andrews_curve <- function(data, x_col, y_col, step=pi/30){
  x = as.matrix(data[, x_col])
  t = seq(-pi, pi, pi/30)
  m = nrow(x)
  n = ncol(x)
  f = matrix(0, m, length(t))
  for(i in 1:m) {
    f[i,] = x[i,1]/sqrt(2)
    for(j in 2:n) {
      if (j%%2 == 0)
        f[i, ] = f[i, ] + x[i, j] * sin(j/2 * t)
      else f[i, ] = f[i, ] + x[i, j] * cos(j%/%2 * t)
    }
  }
  colnames(f) <- t
  label <- data[, y_col]
  id <- c(1:nrow(f))
  res <- cbind(as.data.frame(f), label, id)  %>%
    gather(x, y, -label, -id, convert = TRUE)
}

iris.andrew <- andrews_curve(iris, x_col=c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width"), y_col="Species")
p.andrew <- ggplot(iris.andrew) + geom_line(aes(x, y, group=id, color=label))
p.andrew

clipboard.png

# A final multivariate visualization technique pandas has is radviz
# Which puts each feature as a point on a 2D plane, and then simulates
# having each sample attached to those points through a spring weighted
# by the relative value for that feature

# 暂时没能力实现

其他元素

主题

所有与数据不相关的图形控制细节都放在theme()这个函数里。ggplot2内置了一些常见的主题:theme_bw()theme_classic()theme_grey()theme_minimal()。如果需要更多的主题可以安装ggthemes包,也可以自定义主题。

图例

ggplot2可以设定图例的位置:theme(legend.position="bottom"),其他选项有top、left和right。

每个图形属性都会有一个图例,图例的类型共有三种:colorbar为颜色条,适合连续变量;legend为键值对,适合有限取值的变量;none,将一个图形属性的图例设置为none,则不显示这个图形属性的图例。

标签

常用的绘图标签有:

  • ggtitle("New Plot Title"):指定图形名称

  • xlab("New X label"):指定x轴标签

  • ylab("New Y label"):指定y轴标签

  • 图例标签需要使用scale_*()namelabels选项进行指定

关于作者:丹追兵,数据分析师一枚,编程语言python和R,使用Spark、Hadoop、Storm、ODPS。本文出自丹追兵的pytrafficR专栏,转载请注明作者与出处:https://segmentfault.com/blog...

查看原文

赞 6 收藏 7 评论 1

xile611 发布了文章 · 2016-06-20

React实践 - Component Generator

我们开发一个新产品的时候,通常会先抽象出一些公用的组件,然后通过这些组件来拼装成页面。不知道大家有没有发现,这种开发方式带来的问题是一个团队内经常会有这样的场景:

A 已经开发了一个 XX 表格模块,B 要开发一个类似的 YY 表格模块,然后 B 通常是去把 A 的代码 copy 一下,修改一些东西;或者不巧 B 不知道 A 已经开发 XX 表格,然后 B 又得一行行的写一些类似的代码。

造成这种问题的原因简单的说就是:组件抽象的粒度太单一。接下来我们会通过两个例子来讲述问题及我们如何解决这样的问题的。

一个简单的组件 - Switch

首先我们看一个简单的 Switch 组件,如果一个产品中有常用的两种切换功能:

Switch

如果使用之前封装的基础组件组件 Switch 来实现,我们需要如下调用:

<Switch
 className="switch"
 activeIndex={this.state.activeIndex}
 onChange={::this.handleSwitchChange}
>
 <SwitchItem>趋势</SwitchItem>
 <SwitchItem>列表</SwitchItem>
</Switch>

这种组件抽象方式(实现省略)好处就是通用性强,但带来一些问题:

  • 每个人都需要维护选项的展示名称和顺序之间的关系

  • 调用代码较长,有冗余

于是,我们对这类组件进行了重构,希望让每个组件使用更加简单,只需要关系具体的状态即可。具体的做法就是开发一个 Generator —— generateSwitch 来生成常用的切换组件:

export const generateSwitch = (name, options) => {
  const propTypes = {
    className: PropTypes.string,
    activeKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    onChange: PropTypes.func.isRequired,
  };

  const Switch = (props) => {
    ...

    return (
      <span className={classes}>
        {
          options.map((entry, index) => (
            ...
          ))
        }
      </span>
    );
  };

  Switch.propTypes = propTypes;
  Switch.displayName = name;

  return Switch;
};

export const ASwitch = generateSwitch('ABSwitch', [
  { name: 'AA', key: 'a' },
  { name: 'BB', key: 'b' },
]);

export const BSwitch = generateSwitch('CDSwitch', [
  { name: 'CC', key: 'c' },
  { name: 'DD', key: 'd' },
]);

这种做法就可以解决上面说的问题:

  • 比常见的切换组件使用更加便利,调用代码一行就够了,而且能够起到统一参数的作用;

  • 对外暴露生成函数 generateSwitch 也能保证通用性。

更复杂的例子 - 业务模块

下面以一个表格业务为例,常见的表格模块如下:

RankModule

在开发这个模块的时候,虽然每个小区块我们都抽取了相应的组件,如 Selector, Table, Switch, Pagination 等。但是把这些组件串起来也有很多逻辑,如果每个类似的模块都重复写,任何一个小的逻辑发生变化,都可能需要修改所有的模块实现。所以这时候我们想做的事情就是:这个模块本身也是一个组件,我们只需要通过一些配置生成不同的模块,而不是重复的 copy 代码,然后修改一些差异的地方。

在这里碰到的一个问题是,我们整个系统是使用 Redux 来管理数据的,整个项目的 Store 结构如下:

Store

每个业务模块会去 connect 相应的数据以及 actions ,每个模块都有相应的 reducer。并且每个卡片的 action 也需要做到全局唯一,所以我们给模块的 UI Component 以及 reducer 分别开发了相应的 Generator

首先来看 UI Component 的 Generator

function generateAbcModule({pageName, moduleName}) {
  const ACTION_PREFIX = `${pageName}/${moduleName}`;
  const LOAD = ACTION_PREFIX + 'LOAD';
  ...

  function load(url, params, id) {
    return (dispatch, getState) => {
      const state = getState();
      ...

      return dispatch({
        type: LOAD,
        ....
      });
    };
  }

  @connect((state, props) => {
    const moduleState = state[pageName][moduleName];

    return {
      ...moduleState,
    };
  }, {
    load,
  })
  class AbcModule extends Component {
    ...
  }

  return AbcModule;
}

通过代码发现,我们把 actionCreators 与 UI 放在了一起,然后通过 pageNamemoduleName 来唯一地标识一个模块,拼装这两个参数作为 action 的前缀,从而达到每个模块的 action 是全局唯一的。

接下来我们是 reducerGenerator

function generateAbcModuleReducer({pageName, moduleName, defaultIndexes}) {
  const ACTION_PREFIX = `${pageName}/${moduleName}/`;
    const LOAD = ACTION_PREFIX + 'LOAD';

    const initialState = {
      indexes: defaultIndexes,
      ...
  };

  return function AbcModuleReducer(state = initialState, action) {
    switch (action.type) {
      case LOAD:
        return {
          ...state,
          isLoading: true,
          ...
        };
      ...
    }
  };

类似的,reducer Generator 也是通过 pageNamemoduleName 来唯一地标识一个模块。当然每个模块可能会有不同的 initialState,这个也可以通过 generateAbcModuleReducer 的入参来设置。

总结

上面这种使用 Generator 来封装业务模块的方法,能够在一定程度上减少重复代码,加快开发速度,不过如果业务发展的很快,有可能会导致业务模块组件 props 泛滥 的问题。

Mod

以上面的排行卡片为例,可变的东西就非常多,相应的就需要很多的 props 来配置,所以我们也需要根据具体的业务来把握是否要进行抽象。

查看原文

赞 1 收藏 13 评论 0

xile611 发布了文章 · 2016-05-05

React 实现 Table 的思考

Table 是最常用展示数据的方式之一,可是一个产品中往往很多非常类似的 Table,但是我们碰到的情况往往是 Table A 要排序,Table B 不需要排序,等等这种看起来非常类似,但是又不完全相同的表格。这种情况下,到底要不要抽取一个公共的 Table 组件呢?对于这个问题,我们团队也纠结了很久,先后开发了多个版本的 Table 组件,在最近的一个项目中,产出了第三版 Table 组件,能够较好的解决灵活性和公共逻辑抽取的问题。本文将会详细的讲述这种 Table 组件解决方案产出的过程和一些思考。

Table 的常见实现

首先我们看到的是不使用任何组件实现一个业务表格的代码:

import React, { Component } from 'react';

const columnOpts = [
  { key: 'a', name: 'col-a' },
  { key: 'b', name: 'col-b' },
];

function SomeTable(props) {
  const { data } = props;

  return (
    <div className="some-table">
      <ul className="table-header">
        {
          columnOpts.map((opt, colIndex) => (
            <li key={`col-${colIndex}`}>{opt.name}</li>
          ))
        }
      </ul>
      <ul className="table-body">
        {
          data.map((entry, rowIndex) => (
            <li key={`row-${rowIndex}`}>
              {
                columnOpts.map((opt, colIndex) => (
                  <span key={`col-${colIndex}`}>{entry[opt.key]}</span>
                ))
              }
            </li>
          ))
        }
      </ul>
    </div>
  );
}

这种实现方法带来的问题是:

  • 每次写表格需要写很多布局类的样式

  • 重复代码很多,而且项目成员之间很难达到统一,A 可能喜欢用表格来布局,B 可能喜欢用 ul 来布局

  • 相似但是不完全相同的表格很难复用

抽象过程

组件是对数据和方法的一种封装,在封装之前,我们总结了一下表格型的展示的特点:

  • 输入数据源较统一,一般为对象数组

  • thead 中的单元格大部分只是展示一些名称,也有一些个性化的内容,如带有排序 icon 的单元格

  • tbody 中的部分单元格只是简单的读取一些值,很多单元格的都有自己的逻辑,但是在一个产品中通常很多类似的单元格

  • 列是有顺序的,更适合以列为单位来添加布局样式

基于以上特点,我们希望 Table 组件能够满足以下条件:

  • 接收一个 对象数组所有列的配置 为参数,自动创建基础的表格内容

  • thead 和 tbody 中的单元格都能够定制化,以满足不同的需求

至此,我们首先想到 Table 组件应该长成这样的:

const columnOpts =  [
  { key: 'a', name: 'col-a', onRenderTd: () => {} },
  { key: 'b', name: 'col-b', onRenderTh: () => {}, onRenderTd: () => {} },
];

<Table data={data} columnOpts={columnOpts} />

其中 onRenderTdonRenderTh 分别是渲染 td 和 th 时的回调函数。

到这里我们发现对于稍微复杂一点的 table,columnOpts 将会是一个非常大的配置数组,我们有没有办法不使用数组来维护这些配置呢?这里我们想到的一个办法是创建一个 Column 的组件,让大家可以这么来写这个 table:

<Table data={data}>
  <Column dataKey="a" name="col-a" td={onRenderTd} />
  <Column dataKey="b" name="col-b" td={onRenderTd} th={onRenderTh} />
</Table>

这样大家就可以像写HTML一样把一个简单的表格给搭建出来了。

优化

有了 Table 的雏形,再联系下写表格的常见需求,我们给 Column 添加了 widthalign 属性。加这两个属性的原因很容易想到,因为我们在写表格相关业务时,样式里面写的最多的就是单元格的宽度和对齐方式。我们来看一下 Column 的实现:

import React, { PropTypes, Component } from 'react';

const propTypes = {
  name: PropTypes.string,
  dataKey: PropTypes.string.isRequired,
  align: PropTypes.oneOf(['left', 'center', 'right']),
  width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  th: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
  td: PropTypes.oneOfType([
    PropTypes.element, PropTypes.func, PropTypes.oneOf([
      'int', 'float', 'percent', 'changeRate'
    ])
  ]),
};

const defaultProps = {
  align: 'left',
};

function Column() {
  return null;
}

Column.propTypes = propTypes;
Column.defaultProps = defaultProps;

export default Column;

代码中可以发现 th 可以接收两种格式,一种是 function,一种是 ReactElement。这里提供 ReactElement 类型的 th 主要让大家能够设置一些额外的 props,后面我们会给出一个例子。

td 的类型就更复杂了,不仅能够接收 functionReactElement 这两种类型,还有 int, float, percent, changeRate 这三种类型是最常用的数据类型,这样方便我们可以在 Table 里面根据类型对数据做格式化,省去了项目成员中很多重复的代码。

下面我们看一下 Table 的实现:

const getDisplayName = (el) => {
  return el && el.type && (el.type.displayName || el.type.name);
};

const renderChangeRate = (changeRate) => { ... };

const renderThs = (columns) => {
  return columns.map((col, index) => {
    const { name, dataKey, th } = col.props;
    const props = { name, dataKey, colIndex: index };
    let content;
    let className;

    if (React.isValidElement(th)) {
      content = React.cloneElement(th, props);
      className = getDisplayName(th);
    } else if (_.isFunction(th)) {
      content = th(props);
    } else {
      content = name || '';
    }

    return (
      <th
        key={`th-${index}`}
        style={getStyle(col.props)}
        className={`table-th col-${index} col-${dataKey} ${className || ''}`}
      >
        {content}
      </th>
    );
  });
};

const renderTds = (data, entry, columns, rowIndex) => {
  return columns.map((col, index) => {
    const { dataKey, td } = col.props;
    const value = getValueOfTd(entry, dataKey);
    const props = { data, rowData: entry, tdValue: value, dataKey, rowIndex, colIndex: index };

    let content;
    let className;
    if (React.isValidElement(td)) {
      content = React.cloneElement(td, props);
      className = getDisplayName(td);
    } else if (td === 'changeRate') {
      content = renderChangeRate(value || '');
    } else if (_.isFunction(td)) {
      content = td(props);
    } else {
      content = formatIndex(parseValueOfTd(value), dataKey, td);
    }

    return (
      <td
        key={`td-${index}`}
        style={getStyle(col.props)}
        className={`table-td col-${index} col-${dataKey} ${className || ''}`}
      >
        {content}
      </td>
    );
  });
};

const renderRows = (data, columns) => {
  if (!data || !data.length) {return null;}

  return data.map((entry, index) => {
    return (
      <tr className="table-tbody-tr" key={`tr-${index}`}>
        {renderTds(data, entry, columns, index)}
      </tr>
    );
  });
};

function Table(props) {
  const { children, data, className } = props;
  const columns = findChildrenByType(children, Column);

  return (
    <div className={`table-container ${className || ''}`}>
      <table className="base-table">
        {hasNames(columns) && (
          <thead>
            <tr className="table-thead-tr">
              {renderThs(columns)}
            </tr>
          </thead>
        )}
        <tbody>{renderRows(data, columns)}</tbody>
      </table>
    </div>
  );
}

代码说明了一切,就不再详细说了。当然,在业务组件里,还可以加上公共的错误处理逻辑。

单元格示例

前面提到我们的 tdth 还可以接收 ReactElement 格式的 props,大家可能还有会有点疑惑,下面我们看一个 SortableTh 的例子:

class SortableTh extends Component {
 static displayName = 'SortableTh';

 static propTypes = {
    ...,
    initialOrder: PropTypes.oneOf(['asc', 'desc']),
    order: PropTypes.oneOf(['asc', 'desc', 'none']).isRequired,
    onChange: PropTypes.func.isRequired,
 };

 static defaultProps = {
   order: 'none',
   initialOrder: 'desc',
 };

 onClick = () => {
   const { onChange, initialOrder, order, dataKey } = this.props;

   if (dataKey) {
     let nextOrder = 'none';

     if (order === 'none') {
       nextOrder = initialOrder;
     } else if (order === 'desc') {
       nextOrder = 'asc';
     } else if (order === 'asc') {
       nextOrder = 'desc';
     }

     onChange({ orderBy: dataKey, order: nextOrder });
   }
 };

 render() {
   const { name, order, hasRate, rateType } = this.props;

   return (
     <div className="sortable-th" onClick={this.onClick}>
       <span>{name}</span>
       <SortIcon order={order} />
     </div>
   );
 }
}

通过这个例子可以看到,thtd 接收 ReactElement 类型的 props 能够让外部很好的控制单元格的内容,每个单元格不只是接收 data 数据的封闭单元。

总结

总结一些自己的感想:

  • 前端工程师也需要往前走一步,了解用户习惯。在写这个组件之前,我一直是用 ul 来写表格的,用 ul 写的表格调整样式比较便利,后来发现用户很多时候喜欢把整个表格里面的内容 copy 下来用于存档。然而,ul 写的表格 copy 后粘贴在 excel 中,整行的内容都在一个单元格里面,用 table 写的表格则能够几乎保持原本的格式,所以我们这次用了原生的 table 来写表格。

  • 业务代码中组件抽取的粒度一直是一个比较纠结的问题。粒度太粗,项目成员之间需要写很多重复的代码。粒度太细,后续可扩展性又很低,所以只能是大家根据业务特点来评估了。像 Table 这样的组件非常通用,而且后续肯定有新的类型冒出来,所以粒度不宜太细。当然,我们这样写 Table 组件后,大家可以抽取常用的一些 XXXThXXXTd

最终,我把这次 Table 组件的经验抽离出来,开源到 https://github.com/recharts/react-smart-table,希望开发者们可以参考。

查看原文

赞 7 收藏 36 评论 0

xile611 发布了文章 · 2015-12-15

React源码剖析系列 - 玩转 React Transition

过去一年,React 给整个前端界带来了一种新的开发方式,我们抛弃了无所不能的 DOM 操作。对于 React 实现动画这个命题,DOM 操作已经是一条死路,而 CSS3 动画又只能实现一些最简单的功能。这时候 ReactCSSTransitionGroup Addon,无疑是一枚强心剂,能够帮助我们以最低的成本实现例如节点初次渲染、节点被删除时添加动效的需求。本文将会深入实现原理来玩转 ReactCSSTransitionGroup。

初窥 ReactCSSTransitionGroup

在介绍 ReactCSSTransitionGroup 的用法前,先来实现一个常规 transition 动画,要实现的是删除某个节点的时候,让该节点的透明度不断的变大。

handleRemove(item) {
  const { items } = this.state;
  const len = items.length;

  this.setState({
    items: items.reduce((result, entry) => {
      return entry.id === item.id ? [...result, { ...item, isRemoving: true }] : [...result, item];
    }, [])
  }, () => {
    setTimeout(() => {
      this.setState({
        items: items.reduce((result, entry) => {
          return entry.id === item.id ? result : [...result, item];
        }, [])
      });
    }, 500);
  });
},

render() {
  const items = this.state.items.map((item, i) => {
    return (
      <div
        key={item.id} onClick={this.handleRemove.bind(this, item)}
        className={item.isRemoving ? 'removing-item' : ''}>
        {item.name}
      </div>
    );
  });

  return (
    <div>
      <button onClick={this.handleAdd}>Add Item</button>
      <div>
        {items}
      </div>
    </div>
  );
}

同时我们在 CSS 中需要提供如下的样式

.removing-item {
  opacity: 0.01;
  transition: opacity .5s ease-in;
}

相同的需求,使用 ReactCSSTransitionGroup 创建动画会是怎么的呢?

handleRemove(i) {
  const { items } = this.state;
  const len = items.length;

  this.setState({
    items: [...items.slice(0, i),  ...item.slice(i + 1, len - 1)]
  });
},

render() {
  const items = this.state.items.map((item, i) => {
    return (
      <div key={item} onClick={this.handleRemove.bind(this, i)}>
        {item}
      </div>
    );
  });

  return (
    <div>
      <button onClick={this.handleAdd}>Add Item</button>
      <ReactCSSTransitionGroup transitionName="example">
        {items}
      </ReactCSSTransitionGroup>
    </div>
  );
}

在这个例子中,当新的节点从 ReactCSSTransitionGroup 中删除时,这个节点会被加上 example-leave 的 class,在下一帧中这个节点还会被加上 example-leave-active 的 class,通过添加以下 CSS 代码,被删除的节点就会有动画的效果。

.example-leave {
  opacity: 1;
  transition: opacity .5s ease-in;
}

.example-leave.example-leave-active {
  opacity: 0.01;
}

从这个例子,我们可以看到 ReactCSSTransition 可以把开发者从一大堆动画相关的 state 中解放出来,只需要关心数据的变化,以及 CSS 的 transition 动画逻辑。

后面将会仔细分析 ReactCSSTransitionGroup 的源码实现。在看代码之前,大家可以先看 官网的文档,对 ReactCSSTransitionGroup 的用法进一步了解。看完之中,可以想想两个问题:

  • appear 动画和 enter 动画有什么区别?

  • ReactCSSTransitionGroup 子元素的生命周期是怎样的?

ReactCSSTransitionGroup 模块关系

ReactCSSTransitionGroup 的源码分为5个模块,我们先看看这5个模块之间的关系:

Relation

我们来整理一下这几个模块的分工与职责:

  • ReactTransitionEvents 提供了对各种前缀的 transitionend、animationend 事件的绑定和解绑工具

  • ReactTransitionChildMapping 提供了对 ReactTransitionGroup 这个 component 的 children 进行格式化的工具

  • ReactCSSTransitionGroup 会调用 ReactCSSTransitionGroupChild 对 children 中的每个元素进行包装,然后将包装后的 children 作为 ReactTransitionGroup 的 children 。

从这个关系图里面可以看到,ReactTransitionGroupReactCSSTransitionGroupChild 才是实现动画的关键部分,因此,本文会从 ReactTransitionGroup 开始解读,然后从 ReactCSSTransitionGroupChild 中解读怎么实现具体的动画逻辑。

ReactTransitionGroup 源码解读

下面我们按照 React 生命周期来解读 ReactTransitionGroup

初次 Mount

  • 在初始化 state 的时候,将 this.props.children 转化为对象,其中对象的 key 就是 component key,这个 key 与 children 中的元素一一对应,然后将该对象设置为 this.state.children

getChildMapping

  • 在初次 render 的时候,将 this.state.children 中每一个普通的 child component 通过指定的 childFactory 包裹成一个新的 component,并渲染成指定类型的 component 的子元素。在下面的源码中也可以看到,我们在创建过程中给每个 child 设置的 key 也会作为 ref,方便后续索引。

render: function() {
  var childrenToRender = [];
  for (var key in this.state.children) {
    var child = this.state.children[key];

    if (child) {
      childrenToRender.push(React.cloneElement(
        this.props.childFactory(child),
        {ref: key, key: key}
      ));
    }
  }
  return React.createElement(
    this.props.component,
    this.props,
    childrenToRender
  );
}
  • 初次 mount 后,遍历 this.state.children 中的每个元素,依次执行 appear 动画的逻辑。

Appear

更新 component

当接收到新的 props 后,先将 nextProps.childrenthis.props.children 合并,然后转化为对象,并更新到 this.state.children。计算在 nextProps 中即将 leave 的 child,如果该元素当前没有正在运行的动画,将该元素的 key 保存在 keysToLeave。

对于 nextProps 中新的 child,如果该元素没有正在运行的动画的话(也许会疑惑,一个刚进入的元素怎么会有动画正在运行呢?下文将会解释),将该元素的 key 保存在 keysToEnter。从这里也能看出来,本来在 nextProps 中即将 leave 的 child 会被保留下来以达到动画效果,等动画效果结束后才会被 remove。

componentWillReceiveProps

component 更新完成后,对 keysToEnter 中的每个元素执行 enter 动画的逻辑,对 keysToLeave 中的每个元素执行 leave 动画的逻辑。由于 enter 动画的逻辑和 appear 动画的逻辑几乎一模一样,无非是变成执行 child 的componentWillEntercomponentDidEnter 方法。

leave 动画稍有不同,看下面源码可以看到,在 leave 动画结束后,如果发现该元素重新 enter,这里会再次执行 enter 动画,否则的话通过更新 state 中的 children 来删除相应的节点。这里也可以回答,为什么对刚 enter 的元素,也要判断该元素是否正在进行动画,因为如果该元素上一次 leave 的动画还没有结束,那么这个节点还一直保留在页面中运行动画。

另外,大家有没有注意到一个问题,如果 leave 动画的回调函数没有被调用,那么这个节点将永远不会被移除。

if (currentChildMapping && currentChildMapping.hasOwnProperty(key)) {
  // This entered again before it fully left. Add it again.
  this.performEnter(key);
} else {
  this.setState(function(state) {
    var newChildren = assign({}, state.children);
    delete newChildren[key];
    return {children: newChildren};
  });
}

至此,我们看到 ReactTransitionGroup 没有实现任何具体的动画逻辑。

ReactCSSTransitionGroup

搞清楚 ReactTransitionGroup 的原理以后,ReactCSSTransitionGroup 做的事情就很简单了。简单地说, ReactCSSTransitionGroup 调用了 ReactTransitionGroup ,提供了自己的 childFactory 方法,而这个 childFactory 则是调用了 ReactCSSTRansitionGroupChild 。

_wrapChild: function(child) {
  // We need to provide this childFactory so that
  // ReactCSSTransitionGroupChild can receive updates to name, enter, and
  // leave while it is leaving.
  return React.createElement(
    ReactCSSTransitionGroupChild,
    {
      name: this.props.transitionName,
      appear: this.props.transitionAppear,
      enter: this.props.transitionEnter,
      leave: this.props.transitionLeave,
      appearTimeout: this.props.transitionAppearTimeout,
      enterTimeout: this.props.transitionEnterTimeout,
      leaveTimeout: this.props.transitionLeaveTimeout,
    },
    child
  );
}

下面来看 ReactCSSTransitionGroupChild 是怎么实现节点的动画的。以 appear 动画为例,在 child.componentWillAppear 被调用的时候,给该节点加上 xxx-appear 的 className ,并且在一帧(React 里是写死的17ms)后,给该节点加上 xxx-appear-active 的 className ,最后在动画结束后删除 xxx-appear 以及 xxx-appear-active 的 className。

componentWillAppear

enter、leave 动画的实现类似。到这里源码就解读完了,其中,还有一些细节要去注意的。

隐藏在 key 里的秘密

在源码解读的过程中,我们发现 ReactTransitionGroup 会将 children 转化为对象,然后通过 for...in... 遍历。对于这一过程,会不会感到有所疑虑,ReactTransitionGroup 怎么保证子节点渲染的顺序。

对于这个问题,React 的处理过程可以简化为下面的代码,测试结果显示,当 key 为字符串类型时,for...in... 遍历的顺序和 children 的顺序能够保持一致;但是当 key 为数值类型时,for...in... 遍历的顺序和 children 的顺序就不一定能够保持一致,大家可以用下面这段简单的代码测试一下。

function test (o) {
  var result = {};
  for (var i = 0, len = o.length; i < len; i++) {
    result[o[i].key] = o[i];
  }
  for (var key in result) {
    if (result[key]) {
      console.log(key, result[key]);
    }
  }
}

因此,我们知道 ReactCSSTransitionGroup 所有子 component 的 key 千万不要设置成纯数字,一定要是字符串类型的。

transitionend 之殇

在 React 0.14 版本中,React 已经表示将在未来的版本中废弃监听 transitionend、 animationend 事件,而是通过设置动画的 timeout 来达到结束动画的目的,有没有想过 React 为什么要放弃原生事件,而改用 setTimeout

事实上,原因很简单,transitontend 事件在某些情况是不会被触发。在 transitionend 的 MDN文档 中有这么几行文字:

In the case where a transition is removed before completion, such as if the transition-property is removed, then the event will not fire. The event will also not fire if the animated element becomes display: none before the transition fully completes.

  • 当动画元素的 transition 属性在动画完成前被移除了,transitionend 事件不会被触发

  • 当动画元素在动画完成前,display 样式被设置成 "none",这种情况 transitionend 事件不会被触发

  • 当动画还没完成,当前浏览器标签页失焦很长的时间(大于动画时间),transitionend 事件不会被触发,直到该标签页重新聚焦后 transitionend 事件才会触发

正是由于 transitionend 不会触发,会导致隐形 bug,可以看其中一个 bug

总结

  • appear 动画是 ReactCSSTransitionGroup 组件初次 mount 后,才会被添加到 ReactCSSTransitionGroup 的所有子元素上。

  • enter 动画是 ReactCSSTransitionGroup 组件更新后,被添加到新增的子元素上。

  • ReactCSSTransitionGroup 提供创建 CSS 动画最简单的方法,对于更加个性化的动画,大家可以通过调用 ReactTransitionGroup 自定义动画。

参考资料

查看原文

赞 8 收藏 50 评论 3

认证与成就

  • 获得 16 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-07-28
个人主页被 299 人浏览