React全家桶:react-starter-kit之二

之前

好像也没有太多值得一提的地方,但是react-starter-kit这个当前相当先进(激进)同构技术栈各个难题的克服,还是给了我很大的满足的感。在这个过程中,不仅仅是mongodb、mongoose、express、react和redux的单个技术点,同时也是各个技术点的交叉和融合。特别是GraphQL,实在是给我留下了极其深刻的映像和震撼。

Antd

Antd可以说是难得的国产精品。
说起国产的技术产品,怎么说呢,在早些时候requireJS比较火的时候,阿里出过SeaJS,但是坑很多,如果使用SeaJS,现有的各种模块面临极其繁琐的重新封装。
阿里的Alice当初也尝试过,但是也没有用起来。
百度的开源的产品让人最深刻的Echarts和Ueditor,但是除了这两个,其他的出的开源产品基本都是处于无维护和没人用的状况。
腾讯没啥重量级开源产品,但是其团队成员做的ArtTemplate,artDialog等,其实体验还真的不错。

Antd算是阿里推出的一项重量级开源产品。

好了,不扯太多。我们先在项目内部集成antd。

首先是安装

1
npm install antd --save-dev

如何使用这里就不说了,这是API的活儿了。
不过当你弄完以后,打开使用了antd的页面,你会在控制台看到一下提示:

antd-plugins-error

提示是说用的未编译版本,请使用指定插件来限制打包体积。好吧,然后打开network选项卡看看到底大了多少。。。
结果是main.js达到坑爹的8.9M,使用前则是5M左右。考虑到react-starter-kit是各种框架集成,antd一个人用这么多体积确实是太大了。所以老实把插件安装起来好了。

安装:

1
npm install babel-plugin-antd --save-dev

安装完毕之后要进行一些配置。打开tool/webpack.config.js

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
{
test: /\.jsx?$/,
loader: 'babel-loader',
include: [
path.resolve(__dirname, '../node_modules/react-routing/src'),
path.resolve(__dirname, '../src'),
],
query: {
// https://github.com/babel/babel-loader#options
cacheDirectory: DEBUG,
// https://babeljs.io/docs/usage/options/
babelrc: false,
presets: [
'react',
'es2015',
'stage-0',
],
plugins: [
["antd"],
'transform-runtime',
...DEBUG ? [] : [
'transform-react-remove-prop-types',
'transform-react-constant-elements',
'transform-react-inline-elements',
],
],
},
}

代码挺长,但是其实无非是plugins数组里面加一个[“antd”],
到这里babel-plugin-antd基本就能用了,github上面的文档说css也可以实现模块化,但是按文档操作会导致编译报错,这里就先不折腾了。直接将css文件复制到publice目录,然后在view/index.html里面引入。

Redux & react-redux

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。通过它可以有条不紊的维护当前越来越复杂的应用状态。Redux虽然不像GraphQL那样可以让人眼前一亮,但是对深谙开发流程但是对状态维护苦恼的开发者来说它是救市良药。具体的大家可以去看看 《Redux中文文档》

react-redux是一个将redux和react整合起来的package,它是胶水。但是需要说明的是:redux专注于状态维护,和React本质上是没有任何关系的。

当写到这里时候,我将默认读者已经阅读过中文文档,对Redux用途和redux三大基础概念:action,reducer和store有了基础认识。

react-redux

react-redux提供两个关键模块:Provider和connect。

Provider

Provider这个模块是作为整个App的容器,在你原有的App Container的基础上再包上一层,它的工作很简单,就是接受Redux的store作为props,并将其声明为context的属性之一。

这里就列举一下react-starter-kit里面Provider的设置,打开 src/components/App/App.js,可以很清楚的看到render函数的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
render() {
if (this.props.error) {
return this.props.children;
}

const store = this.props.context.store;
return (
<Provider store={store}>
<div>
<Header />
{this.props.children}
<Feedback />
<Footer />
</div>
</Provider>
);
}

Redux的store作为props,并将其声明为context的属性之一,子组件可以在声明了contextTypes之后可以方便的通过this.context.store访问到store——但是这里store本质上给connect用的。

Provider本质上为为子组件提供整体的舞台。

connect

conect正如其名,连接了react和redux。

这里回首一下redux的流程:store保存整体状态,通过调用store.dispatch一个action,来调用对应action的reducer,最终redux会根据action来更新state(应用的状态,而非react组件的state)。

通过connect,React-redux应用中,store中维护的state就是我们的应用state,一个React组件作为View层,做两件事:render和响应用户操作。于是connect就是将store中的必要数据作为props传递给React组件来render,并包装action creator用于在响应用户操作时dispatch一个action。

如果这样说还是不太能理解,那么那就直白一些,redux的store可以视为一个可以保存状态的内存盘,而connect则赋予react组件随处修改store的能力。

实例

如果这样还是没有明白,我们来实际做个小功能,一个基于antd页码组件的翻页小模块。大致效果如图:
preview

这里用到的antd里面的tabs组件和Pagination组件还有Spin组件(显示加载中)。

这是一个比较常见的功能,这里简单说一下功能点:

  1. tabs切换选项卡显示
  2. 最新选项卡显示一个标签云+分页展示模块。
  3. 点击页码时候进行内容切换
  4. 切换每页条数时候更新数量
  5. 内容和页码组件的其他融合

为了简单,这些就都放在home路由的组件里面了。
不过开始说redux之前,我们先把路铺好。这里需要提前准备的有:

  1. api接口
  2. 相对tabs组件和Spin组件

api

首先说说api,分页功能需要整理总条目数。我们调整了news的api数据源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
router.all('/news', (req, res) => {
const pageSize = req.body.pageSize||req.query.pageSize||10;
const currentPage = req.body.currentPage||req.query.currentPage||1;
const index = (currentPage-1)*pageSize;

News.count({}, function(err, count) {
News
.find()
.skip(index)
.limit(pageSize)
.exec(function(err,news){
res.json({
code:200,
news:news,
count:count
});
});
});
});

不算复杂,仅仅是添加了一个count字段,然后一个skip来和limit来实现获取指定分页数据。我们这里使用GraphQL来做数据接口。实际实现就不具体提及。因为这里主要是讲react-redux。
最终我们发送这样一个片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
`
{
news(currentPage: $ {
currentPage || this.props.currentPage
}, pageSize: $ {
pageSize || this.props.pageSize
}) {
title, link, contentSnippet
}, tags {
name
}, count {
count
}
}
`

返回这样一个结构的结果:

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
{
"data": {
"news": [
{
"title": "测试标题1",
"link": "http://www.baidu.com",
"contentSnippet": "测试摘要测试摘要"
},
{
"title": "测试标题2",
"link": "http://www.baidu.com",
"contentSnippet": "测试摘要测试摘要"
}
],
"tags": [
{
"name": "javascript"
},
{
"name": "css"
}
],
"count": {
"count": 17
}
}
}

相对tabs组件和Spin组件

接下来整理其他相对独立不需要进行状态交互的模块.下面是Home组件的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
28
var Home = React.createClass({
...
render: function() {
let content;
if(this.state.newsLoading){
content = <Spin />;
}else{
content = (
<!-- 这里是列表组件 -->
)
};
return (
<div className={s.root}>
<div className={s.container}>
<Tabs type="card">
<TabPane tab="最新" key="1">
<CloudTags tags={this.state.tags||[]} />
{content}
<PaginationContainer />
</TabPane>
<TabPane tab="热门" key="2">选项卡二内容</TabPane>
<TabPane tab="更新" key="3">选项卡三内容</TabPane>
</Tabs>
</div>
</div>
)
}
});

this.state.newsLoading===true时候显示Spin组件,否则显示列表。至于Tabs组件这个,貌似标签结构已经足够清晰,就不说了。
需要说明的是,为了尽量减少请求,标签云的数据也一起请求过来了,这也是GraphQL的一个优势展现。

这两步走完,可以提起react-redux正文了。

react-redux

开始之前,还是希望大家可以去看看这篇文章: React和Redux的连接react-redux,对react-redux有相对基础认识

react-starter-kit的Redux处理

首先,我们需要一个Provider(如果你不知道这是啥,还是先去看看刚才提到的文章)。这个模块是作为整个App的容器。

我们看看react-starter-kit是怎么规划这个模块的:
首先是 routes/index.js ,这个文件里面是整个应用的路由规则规划。我们看关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
path: '/',
children: [
home,
<!-- ...略 -->
error,
],
async action({ next, render, context }) {
const component = await next();
if (component === undefined) return component;
return render(
<App context={context}>{component}</App>
);
},
};

这段代码的意思是,所有的component,最后输出的时候,都必须放到line12中这个APP容器中去。
这个APP容器的关键代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
render() {
if (this.props.error) {
return this.props.children;
}
const store = this.props.context.store;
return (
<Provider store={store}>
<div>
<Header />
{this.props.children}
<Feedback />
<Footer />
</div>
</Provider>
);
}

总体来说,就是通过这种方式把Provider全局使用起来了,所有的路由,都会被APP这个容器包裹起来,方便后面是用connect来链接react和redux。

connect

接下来我们来处理状态相关的东西。
当前的应用状态是这样的:
redux state

在开始之前还是说一下各个组件之间的数据交互。

  1. 列表数据从来自一个API,里面包含news的列表,同时也包含了条目总数。
  2. 条目总数会传到页码组件,同时,页码组件会绑定当前页,页码和条目总数。当组件改变当前页和pageSize会触发news更新
  3. news更新如果总数添加了,又会将总数更新一次传到页码组件里面更新页码。

很显然,如果各个组件之间太独立,那么手工传值就会非常让心心烦,也不方便维护,但是如果放到全局的store内部,自动传入到各个组件,那么就变得非常还用且容易维护了。

先来处理一下页码组件,这是在Home文件夹下单独建立的文件PaginationContainer.js:

1
2
3
4
export default connect(state => ({
currentPage: state.runtime.currentPage,
newsTotalItem:state.runtime.newsTotalItem,
}), {setRuntime:setRuntimeVariable})(PaginationContainer);

这个算是将react和redux连接的关键代码,意思是将store上两个属性,映射到currentPage和newTotalItem两个props上,然后react里面就可以直接使用this.props.currentPage,和this.props.newsTotalItem了。setRuntime则是将一个方法也放到对应props上,这样可以使用这个方法来更新store。后面可以看到,我们之后更新store里面的数据都是使用的这个方法。例如onChange和onShowSizeChange都是这样。

当点击不同页码,会触发onChange,而更改每页数量,会触发onShowChage。当这些钩子被触发之后,会更新store内部数据,最后反应到Home组件,被News对应的列表接受到,最终更新页面.

然后是对应的Home.js;

所有的逻辑都在这里面了。这里来详细说一说。
Home这个文件应该从生命周期讲起。

在这些周期里面最先执行的是getInitialState,它返回了react的初始state,这里就是放了一个tags和news的数组,用来存放需要遍历的内容,而newsLoading这个状态主要是在news载入前产生loading的效果,这个很常见了。

接下来是componentDidMount,当react组件载入完毕后执行它,它会运行restNewsState函数,其实就是从api里面获取tags,news和count这三个关键数据。
我们来看看这个函数成功后的回调里面的关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
//总条目数
self.props.setRuntime({
name:"newsTotalItem",
value:data.count.count
});


//内部状态
(context||this).setState({
news:data.news,
tags:data.tags,
newsLoading:false
})

首先是将count的数量放到store的runtime对象的newsTotalItem上。因为这个数据会共享到页码组件。然后是更新内部的news,tags,newLoading,因为这几个数据是不需要共享的,所以直接放到react内部的state上去。

但是当我们更新内部的state时候会产生一个副作用,那就是会触发componentWillReceiveProps,但是我们也不能不管,因为不用这个钩子,那么会导致PageSize改变,currentPage改变之后我们的news List不会被更新。
所以这个时候需要用到shouldComponentUpdate,这个钩子在componentWillReceiveProps之前运行,它返回true,那么才会进行更新,也就是运行componentWillReceiveProps,所以我们在这里对状态进行对比即可,如果没有变更,那么我们返回false即可,这里的对比使用了lodash的isEqual,lodash确实很好用.

最后使用connect将Home包裹起来返回export之即可。

总结

这篇文章其实是长话短说,很多细节都略过了,比如tags其实也应该是react-redux中的一环,比如count的GraphQL查询该怎样写。甚至,react和redux的连接,我也没有写出理论来,只是引用了一篇文章来偷偷懒。
但是个人还是觉得够了react和redux的连接,需要对redux有了解,需要对react-redux有了解,我认为我做的只是最后一环,如何在react-starter-kit里面给他来一个最后的实践。
难者不会,会着不难,这是一个通行万道的钥匙。有了第一次的成功,其实一切无非水到渠成。这也是本文的目的。