【第1269期】基于React实战分享WeatherApp
前言
有时候跟童鞋聊,会听到这样的吐槽,我对着书或网上的视频写代码都挺顺的,但为什么自己写的时候总是”难产“呢?那可能就是你敲的姿势不对了。今日早读文章由来自IBM@alivebao投稿分享。
@alivebao, IBM前端工程师.负责PC端开发,喜欢折腾前端框架及工程化方面的内容,喜欢学习新技术
目录
React新的前端思维方式
React基础
编写一个React实例
从Flux到Redux
中间件
React新的前端思维方式
1.1 create-react-app
这一章首先介绍了工具create-react-app , 通过该工具我们能快速创建一个react应用框架。
首先是安装:
npm install --global create-react-app
安装完成后执行:
create-react-app weather_app
至此便在当前目录下创建了一个react应用。进入weather_app,执行npm start
即可启动应用(我这里装的版本是v1.3.0)
先来看一下应用的目录结构:
|
|--node_modules/
| |--...
|
|--public/
| |--...
|
|--src/
| |--...
|
|--package.json
|
|...
其中 node_modules为依赖, public是一些静态资源。
在 src的index.js中有:
ReactDOM.render(<App />, document.getElementById('root'));
也就是说,程序启动后,将 public/index.html中id为root的节点渲染为src/App.js中定义的组件App。
那么,通过修改或替换App.js,就可以运行我们自己定义的组件了
1.2 JSX
JSX是JS的扩展,可以在JS中编写HTML。在我们的程序中增加一个Weather_App的应用组件并将其显示在主页面中:
// Weather_App.js
import React, { Component } from 'react';
class Weather_App extends Component {
render() {
return (
<div className="weather-app">
<div>Hello world</div>
</div>
);
}
}
export default Weather_App;
修改index.js,在主页面中引入:
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Weather_App from './Weather_App';
ReactDOM.render(<Weather_App />, document.getElementById('root'));
个人认为JSX中最重要的地方有两点:
在return语句中,返回的DOM节点只能有一个根节点,也就是说顶层节点不能有两个(否则会报错):
// 错误示例
return (
<div>Node 1</div>
<div>Node 2</div>
)需要注意JS和HTML中的关键字冲突。以Weather_App为例,我们不能在里面写class=xxx,而应该将class替换为className
PS:我刚试了下可以直接在return用class。。版本号:
"dependencies": {
"react": "^16.3.2",
"react-dom": "^16.3.2"
},
然后网上搜了下,说React16允许DOM传属性了,所以这么操作也行, 但是,官方不建议这么搞,打开控制台可以看到还是弹出了一个Warning
1.3 其他
另外书中还谈到了React工作方式的优点 - 函数式编程思维:
UI = render(data)
也就是说开发者专心处理数据源就好了,渲染的细节交给React去处理。这个在之后的几章里能逐渐感受到,这里就不多说了
附一下个人对纯函数的理解:
给定该函数固定的参数,函数执行完成后不会改变该参数;且当输入的参数相同时,输出也永远是相同的
比如这俩就不满足纯函数:
// 改变了输入
(a) => {
a += 1;
}
// 输出不固定
() => {
return Math.random();
}
维基百科还提到了另外一点:
该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等
这里我的理解是说这个函数不能改变其他东西(比如说全局变量),也就是说只负责输出。
React基础
prop和state
React组件里的数据分为两种-prop和state,这两种数据改变即可能引起组件的渲染(只是可能,不是一定会修改的)。
prop和state的主要区别
prop和state的主要区别在于:
prop是由外部传入的,是组件无法修改的
state是用于记录组件内部的状态的,因此组件可以修改
比如我们的应用需要一个组件用于显示某地温度,该组件接受一个指定地址的参数,根据该地址调用接口获取当地气温。
那么这里的地址location就是一个prop,气温 tempature就是一个state。
我们创建两个新的组件 - WeatherSelecter(下拉框,选择地址) & WeatherPanel(显示面板,显示选择的地点及其温度),并将这俩组件引入到WeatherApp。
首先是WeatherPanel:
// WeatherPanel
import React, { Component } from 'react';
class WeatherPanel extends Component {
constructor(props) {
super(props)
this.state = {
temperature: 'NA'
}
this.getTemperature = this.getTemperature.bind(this);
}
getTemperature() {
const mockTemperature = Math.random() * 100;
this.setState({
temperature: mockTemperature
})
}
render() {
const {location} = this.props;
return (
<div className="weather-panel">
<div>{location}的温度是: {this.state.temperature}</div>
<button onClick = {this.getTemperature}>Get Temperature</button>
</div>
);
}
}
export default WeatherPanel;
我们通过点击getTemperature模拟获取温度的过程
这里要注意的地方:
bind - JS中的this的坑,在组件中定义的方法都需要通过bind函数指定this(当然,也可以使用箭头函数)
setState - 只有在构造函数初始化时能直接给state赋值,其他地方都要通过setState去操作。组件执行这个方法后才会刷新
porps - 通过this.props.NAME 使用外界传递进来的属性(属性不可修改)
JSX的render的return中使用变量 - 通过大括号 {} 进行引用
接下来是WeatherSelecter,地址location同样可以当成一个props传递进来:
// WeatherSelecter
import React, { Component } from 'react';
class WeatherSelecter extends Component {
constructor(props) {
super(props)
}
render() {
const {locationGroup} = this.props;
return (
<div className="weather-selecter">
<select>
{
this.props.locationGroup.map((locationObj) => {
return <option key={locationObj.key}>{locationObj.name}</option>
})
}</select>
</div>
);
}
}
export default WeatherSelecter;
这里locationGroup是一个对象数组,通过map显示出来。option中有一个key属性,这个主要是做性能优化的,之后的章节会提到
WeatherApp修改如下:
import React, { Component } from 'react';
import WeatherSelecter from './WeatherSelecter'
import WeatherPanel from './WeatherPanel'
import {arrLocation as LocationGroup} from './WeatherLocationGroup'
class WeatherApp extends Component {
render() {
return (
<div className="weather-app">
<WeatherSelecter locationGroup={LocationGroup} />
<WeatherPanel location='undefined' />
</div>
);
}
}
export default WeatherApp;
这里存在两个问题:
如何对传递进来的props进行检测 - 也就是说组件如何预期传递捡来的属性的类型,以及需要的属性没穿进来的时候,组件该如何处理
组件间如何通信 - selecter里选中的值,是何如传递到panel里的
对于问题1,可以通过定义PropTypes解决。以WeatherPanel为例,预期输入的属性是一个名为location的字符串,可以这么写:
// WeatherPanel
...
import PropTypes from 'prop-types';
...
WeatherPanel.propTypes = {
location: PropTypes.string.isRequired
}
PS: React.proptype在Reactv15.5已经弃用了,使用的话需执行 npm install —save prop-types手动安装一下依赖
增加这些之后就会对props进行检查了 - 如果location不存在,或其类型不是string,程序就会直接报错
书中建议props的检测放在开发环境里,在发布代码的时候就不要这么操作了(毕竟即增加了代码量,对用户又没什么意义)
关于组件间的通信,这里暂时通过父组件来完成。
父组件WeatherApp向 WeatherSelecter中传递一个回调函数,当selecter的内容改变时通知父组件,从而重新渲染:
// WeatherApp.js
...
// 新增方法locationUpdae
locationUpdate(locationName) {
this.setState({
selectedLocation: locationName
})
}
...
// 传给WeatherSelecter
<WeatherSelecter locationGroup={LocationGroup} locationUpdate={this.locationUpdate}/>
WeatherSelecter:
// WeatherSelecter.js
...
// 修改render的内容, select的value改变时调用WeatherApp的setState更新整个WeatherApp
// WeatherSelecter里加个onChange方法再在constructor写遍bind太麻烦了,这里直接使用箭头函数
render() {
const {locationGroup, locationUpdate} = this.props;
return (
<div className="weather-selecter">
<select onChange={(event) => {locationUpdate(event.target.value)}}>
{
this.props.locationGroup.map((locationObj) => {
return <option key={locationObj.id} value={locationObj.name}>{locationObj.name}</option>
})
}</select>
</div>
);
}
效果图:
PS: 多个组件同步数据挺麻烦的,以后学习了Flux/Redux就方便多了
组件的生命周期
组件在生命周期中可能会经历三个过程:
装载(Mount),即组件第一次在DOM树种渲染的过程
更新(Update),即组件重新渲染的过程
卸载(Unmount),即组件从DOM树种删除的过程
装载
组件装载过程中会经历以下阶段:
constructor -> getInitialState -> getDefaultProps -> componentWillMount -> render -> componentDidMount
constructor是组件类的构造函数,我们在这里完成组件的初始化工作(设置state以及通过bind绑定成员函数的this环境)
getInitialState和getDefaultProps值在React.createClass这种写法中生效,但这种写法已被Facebook官方逐步废弃
render是整个React组件中最重要的函数,组件通过该函数的返回值进行渲染。 render函数不做实际的渲染动作,它只是返回一个JSX描述的解构,最终由React来操作渲染过程
componentWillMount和componentDidMount的调用分别发生在render前后,这里需要注意componentDidMount函数。
在WeatherSelecter和WeatherPanel的class中分别打印一下生命周期流程:
...
componentWillMount() {
console.log('component WeatherXXX WillMount')
}
componentDidMount() {
console.log('component WeatherXXX DidMount')
}
render(){
console.log('component WeatherXXX render')
}
打印效果如图:
为什么两个组件的componentDidMount会在最后才统一执行?
在组件的生命周期中,当componentDidMount被调用时,组件一定是已经被渲染出来了的
而render函数调用只是返回了该组件的结构描述,并是立刻渲染的。
具体的渲染时机由React决定,而React库要拿到所有组件的render后才能决定如何渲染
通过这点,也就知道了,当需要对组件的DOM进行操作时,这类操作需放在componentDidMount这一阶段(比如使用jQuery选择某组件id等)
更新
组件的更新过程会经历以下阶段:
componentWillReceiveProps -> shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate
这里最重要的函数是shouldComponentUpdate
父组件的render被调用时,被包含在render中的子组件就会开始更新过程,但从提升性能的角度来看,子组件没有必要每次都更新。
shouldComponentUpdate(nextProps, nextState)返回一个boolean,为false时表示组件没有必要更新,我们可以通过修改这个函数避免无意义的更新。
先给WeatherApp加个强制更新的按钮,点击按钮会强制刷新WeatherApp:
// WeatherApp.js
...
render(){
...
<button onClick= {() => {this.forceUpdate()}}>Force Update</button>
</div>
}
渲染完成后点击Force Fresh,可在控制台看到WeatherSelecter和WeatherPanel都重新走了一遍render:
然后我们修改WeatherPanel,当WeatherPanel的props.location或state.temperature没变化时,该组件不更新:
shouldComponentUpdate(nextProps, nextState) {
return (nextProps.location !== this.props.location) ||
(nextState.temperature !== this.state.temperature);
}
再点Force Update,可以看见只有WeatherSelecter执行了render,WeatherPanel并没有重新渲染:
组件被更新后,其DOM被重绘了,如果需要在重绘后再在组件上做一些DOM相关的操作,则可以在componentDidUpdate中进行
编写一个React实例
书上从第三章开始介绍Flux/Redux, 本文在这章打算先用React构建出一个天气预报应用,在之后的章节中在将其改造成Redux
效果图:
Git Log: 81005f12b7244a98f50314aa36d8028ce245c11f
3.1 实现
这个应用中,各React组件分解如图:
也就是说,在这里应用被这样分解了:
WeatherApp = WeatherHeader + WeatherPanel
= (<div>{header}</div> + WeatherLocationSelecter) + (WeatherSelectedStatus + WeatherCalenderSelecter)
那么各组件应该完成什么功能?分组件来看的话:
WeatherHeader部分负责选择城市,当城市切换时,应用发起网络请求获取相应城市的天气信息
WeatherSelectedStatus是一个纯负责展示的组件
WeatherCalenderSelecter负责展示未来两天的信息,通过点击可在WeatherSelectedStatus中展示当日的具体信息的
获取信息
WeatherHeader负责选择城市 & 获取天气信息,那么在该组件成功获取天气信息后,如何通知应用进行更新?
这里通过在WeatherApp添加一个更新天气的回调函数,然后将该函数作为props传递给WeatherHeader。WeatherHeader中的WeatherLocationSelecter获取信息成功后,调用该回调函数通知系统进行重绘:
//WeatherApp.js
...
locationIdUpdate(locationId, dailyInfo) {
this.setState({
daily: dailyInfo,
selectedLocationId: locationId
})
...
绘制WeatherSelectedStatus
这个很简单,WeatherSelectedStatus 是一个傻瓜组件,预设获取的数据类型,完成render即可。
傻瓜组件:React中专心负责绘制工作的组件被称为傻瓜组件
WeatherCalenderSelecter和WeatherLocationSelecterStatus的交互
在这个天气预报应用中,我们通过WeatherLocationSelecter获取指定城市未来三天的天气信息,并调用WeatherApp传递给它的回调函数重绘应用。WeatherLocationSelecte获取到的数据结构实际上是一个对象数组:
[{
// dailyInfo of day1
...
}, {
// dailyInfo of day2
...
}, {
// dailyInfo of day3
...
}]
WeatherSelectedStatus接受数组的第一个对象,绘制当天的天气
WeatherCalenderSelecter接受整个数组,绘制未来天气信息
为了能达到选择WeatherCalenderSelecter中某天信息,切换WeatherSelectedStatus中显示内容的目的,实际上就是要通过点击CalenderSelecter中的各item切换WeatherSelectedStatus中接受到的天气对象。
所以采用和1中类似的方案,在WeatherPanel中增加一个回调函数传递给WeatherCalenderSelecter,点击不同item时切换WeatherPanel传递给WeatherSelectedStatus的内容即可。
最后附一张解析图:
注:这里用的API是心知天气提供的天气接口
3.2 组件性能优化
书上在第四章的最后提到通过插件React Pref可检测React组件渲染的性能问题
React的官方文档表示在React 16之后插件react-addons-perf已废弃,可通过浏览器自带的性能分析工具直接分析
Chrome -> F12 -> Performance -> User Timing
这里头能直接看到React事件,下图是我连点几次第一个日期选项按钮的截图:
可以看到每次点击时,WeatherSelectedStatus每次都走了一遍update过程
第二章中提到过,在React组件的生命周期分为Monut -> Update -> Unmount三步,而Update这一过程可分为:
componentWillReceiveProps -> shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate
通过修改shouldComponentUpdate可以避免无意义的组件更新,从而达到提升性能的目的。修改组件的更新规则,改为WeatherSelectedStatus只在currentDayInfo变化时才update:
// WeatherSelectedStatus
shouldComponentUpdate(nextProps) {
return JSON.stringify(nextProps.currentDayInfo) !== JSON.stringify(this.props.currentDayInfo)
}
再次连点几次第一个日期选项按钮,发现这次WeatherSelectedStatus只调用到shouldComponentUpdate就停止了:
然后,我们试着重复点击北京这个按钮时,能看到整个组件都重复渲染了,这里最简单的优化方式是直接避免掉没意义的网络请求= =:
// WeatherLocationSelecter
...
// 1. 在构造函数里加一个state,用于记录上次选中的id
...
constructor(props) {
...
this.state = {
currentSelectedId: undefined
}
...
}
...
// 2. 发起网络请求前,比较想要请求的id是否与上次记录的id一致
...
locationIdUpdate(locationId) {
if(this.state.currentSelectedId === locationId) {
return
}
...
}
...
另外,为了最大程度避免短时间内多次重复请求已得到的数据,还可以在第一次请求后把数据缓存到浏览器,并加上过期时间
3.3 项目代码组织方式
当前代码组织图如下,可以看到是一堆文件全都丢在src下面:
文件可以按角色或功能进行组织
按角色进行组织(MVC框架)
|
|--controllers/
| |--...
|
|--models/
| |--...
|
|--views/
| |--...
|
|...
按功能进行组织
书中提到,在React中,这种是一种更为适用的组织方式。在这里,我们把各个功能模块放入对应文件间中,并在每个文件夹中增加相应的index文件导出本模块的文件,供其他模块进行调用。
以WeatherSelectedStatus为例,新建文件夹WeatherSelectedStatus, 将x.js和x.css放入其中,并增加index.js:
import view from './WeatherSelectedStatus'
export {view}
在需要使用WeatherSelectedStatus的地方,我们可以使用以下方式获取:
import {view as WeatherSelectedStatus} from './WeatherSelectedStatus'
这么做的好处在于,无论WeatherSelectedStatus如何修改,通过index对外暴露的接口都不会改变,使用时直接安装上面的import方式进行导入即可。
最终文件结构如图所示:
从Flux到Redux
之前的解决方案存在两个问题:
数据源可能不统一
过多层次的钩子函数传递
当两个组件依赖同一个数据状态时,我们应该怎么做?是两组件各自维护一个数据状态吗?还是将状态抽至上一层组件?
再看一下上一章的组件分解图:
在上图的WeatherSelectedStatus和 WeatherCalenderSelecter中,两个组件都需要展示天气信息。
如果两个组件分别维护各自的天气信息状态,那么也就是点击切换城市按钮时,让两个组件都去发更新天气状态的网络请求。
这么做显然是不合适的,除去效率低不说,当某个组件请求出错,导致两个组件数据不一致时,该以哪个为准?
因此,最好的方式是将天气信息抽象至上一层组件。在这里,我们做到了统一数据源,将天气信息daily作为最顶层组件 WeatherApp的state。
这里为什么不把天气信息放在 WeatherPanel?
考虑这么一种情况:当我们点击WeatherHeader 中更新天气信息的按钮时,如何将得到的新的天气信息传递给 WeatherPanel
因此,我们需要把天气信息放在WeatherApp中。
但这么做又导致了另外一个问题: 过多层的钩子函数传递
WeatherLocationSelecter获取天气信息成功后,需要调用WeatherApp传递进来的钩子函数 locationIdUpdate ,这个函数的作用是更新WeatherApp中的selectedId 和 daily
然而WeatherHeader并不需要这个函数,将函数传递给它的唯一目的,就是在于将这个函数传递给子组件WeatherLocationSelecter
4.1 Flux
Flux框架结构如图所示:
在Flux框架中,数据存储在store中,数据的改变由action进行触发。
当dispather收到发来的action后,若该action是已注册过的类型则对其进行处理,处理完成后发送通知,通知监听该action的各类组件执行自己的回调函数。
接着我们来看如何使用Flux能够避免以上问题
使用Flux,我们需要Dispatcher、Action和Sotre
首先定义一个Dispatcher,用于接收和处理其他组件发送来的信息:
import {Dispatcher} from 'flux'
export default new Dispatcher()定义Action,也就是组件发送的信息类型:
// ActionTypes
export const UPDATELOCATION = 'updateLocation'
// Actions
import * as ActionTypes from './ActionTypes'
import AppDispatcher from '../AppDispatcher'
export const updateLocation = (locationId) => {
AppDispatcher.dispatch({
type: ActionTypes.UPDATELOCATION,
locationId: locationId
})
}
这里通常将使用两个js,一个用于存放信息类型(ActionTypes),另一个用于定义action的构造函数,也就是通过该函数发送的信息(Actions)
这么做的原因在于,store会对不同类型的Action操作也不同,有单独导入action的必要
定义一个WeatherStore,用于存储天气信息:
let locationId = undefined
let daliyInfo = {}
const WeatherStore = Object.assign({}, EventEmitter.prototype, {
getDailyInfo: function() {
return daliyInfo
},
emitChange: function() {
this.emit(CHANGE_EVENT)
},
addChangeListener: function(cb) {
this.on(CHANGE_EVENT, cb)
},
removeChangeListener: function(cb) {
this.removeListener(CHANGE_EVENT, cb)
}
})
这里让WeatherStore继承EventEmitter的方法,该Store接受到 action: UPDATELOCTION时,更新天气信息dailyInfo,并在更新完成后调用emitChange,通知所有注册在其上的组件:
AppDispatcher.register((action) => {
if(action.type === ActionTypes.UPDATELOCATION) {
if(daliyInfo.locationId === action.locationId) {
return
}
daliyInfo.locationId = action.locationId
daliyInfo.daily = "Getting data ..."
WeatherStore.emitChange()
let requestCode = undefined
LocationGroup.forEach((val) => {
if(val.id === daliyInfo.locationId) {
requestCode = val.code
}
})
const requestURL = `/v3/weather/daily.json?key=${CustomConfig.key}&location=${requestCode}&language=zh-Hans&unit=c&start=0&days=3`
fetch(requestURL)
.then((response) => {
if(response.status !== 200) {
daliyInfo.daily = "Getting data Failed!"
throw new Error('Fail to get response with status ' + response.status)
}
response.json().then((responseJSON) => {
daliyInfo.daily = responseJSON.results[0].daily
WeatherStore.emitChange()
})
}).catch((error) => {
daliyInfo.daily = "Getting data Failed!"
WeatherStore.emitChange()
})
}
})
修改需要监听消息的组件
这里以WeatherPanel为例:
// WeatherPanel
...
onChange() {
this.setState({
dailyInfo: WeatherStore.getDailyInfo().daily
})
}
componentDidMount() {
WeatherStore.addChangeListener(this.onChange)
}
...
在WeatherPanel挂载后,声明监听WeatherStore发出的通知。当WeatherStore的dailyInfo更新完成后,调用emitChange,就会调用WeatherPanel中的onChange,更新WeatherPanel中的dailyInfo
WeatherHeader类似,在组件中增加onChange,注册在WeatherStore上:
import React, { Component } from 'react'
import {view as WeatherLocationSelecter} from '../WeatherLocationSelecter'
import {arrLocation as LocationGroup} from '../utils'
import WeatherStore from '../WeatherStore'
import './WeatherHeader.css'
class WeatherHeader extends Component {
constructor(props) {
super(props)
this.state = {
selectedId: undefined
}
this.onChange = this.onChange.bind(this)
}
onChange() {
this.setState({
selectedId: WeatherStore.getDailyInfo().locationId
})
}
componentDidMount() {
WeatherStore.addChangeListener(this.onChange)
}
componentWillUnmount() {
WeatherStore.removeChangeListener(this.onChange)
}
render() {
const selectedId = this.state.selectedId;
let title = undefined
LocationGroup.forEach((val) => {
if(val.id === selectedId) {
title = val.name;
}
})
return (
<div className="weather-header">
<div className="weather-title">{title}</div>
<WeatherLocationSelecter/>
</div>
);
}
}
export default WeatherHeader;
可以看到,修改成flux框架后的应用,即实现了单一数据源的目标(所有的数据都存在store中,数据的更新通过action传递),
也避免了无意义的钩子函数的传递(store更新数据后,直接让各组件调用自己的onChange函数,从store中取数据)
4.2 Redux实例
Redux是FLux的一种实现,这里主要有两个地方需要注意:
Redux只有一个store - 在上节Flux实现的实例中,虽然我们也只用了一个Store,但Flux中,应用可以拥有多个Store;Redux规定应用只能拥有一个store
Reducer - 数据的改变通过纯函数Reducer完成
先来复习一下reduce函数:
let a = [1, 2, 3, 4].reduce(function reducer(sum, item) {
return sum + item
}, 0)
console.log(a) // 10
数组在这里根据传进的reducer函数对所有元素进行操作,其中sum是上次操作的结果,item是本次操作的对象。
应用到Redux中,有:
redcucer(state, action)
也就是说根据action和state产生状态,产生的结果完全由这俩参数决定,这里需要注意:
reducer是纯函数, 绝对不能改变state和action这两个参数
回顾之前在flux-WeatherStore里的行为:
这种操作就直接改变了WeatherStore的值,在Redux中是不被允许的,在Redux中,我们应该通过reducer直接返回一个新的state。
接下来我们把之前的Flux改成Redux实例:
npm install --save redux
修改action,不再通过Dispather派发,而是直接返回一个action对象
import * as ActionTypes from './ActionTypes'
import AppDispatcher from '../AppDispatcher'
export const updateLocation = (locationId) => {
return {
type: ActionTypes.UPDATELOCATION,
locationId: locationId
}
}
创建Store和reducer,其中store用于存储数据,reducer用于定义对数据的处理方式:
// store
import {createStore} from 'redux'
import reducer from './Reducer.js'
const initValues = {
daily: undefined,
locationId: 0
}
const store = createStore(reducer, initValues)
export default store
// reducer.js
...
export default (state, action) => {
switch(action.type) {
case ActionTypes.UPDATELOCATION:
let responseJSON = {...}
return {...state, daily: responseJSON.results[0].daily, locationId: action.locationId}
default: return state
}
}
在reducer中,我们的处理方式暂时为同步处理,即一旦接收到action后就返回具有随机天气情况的state,而不去异步发出网络请求获取天气情况
这里不执行异步请求是因为reducer作为一个纯函数,接受到请求后是直接执行并同步返回state,从而引发其他组件渲染的,没有提供异步操作的机会。
异步请求需要通过中间件进行操作。具体方法会在下一章进行介绍
这里注意,reducer直接返回了一个state对象,而不是改变传入的参数state
修改view
以WeatherPanel为例,我们可以通过store的getState获取store的state:
...
getOwnState() {
return {
selectedCalender: 0,
dailyInfo: store.getState().daily
}
}
...
向store注册回调函数
...
componentDidMount() {
store.subscribe(this.onChange)
}
componentWillUnmount() {
store.unsubscribe(this.onChange)
}
...
修改WeatherLocationSelecter里的方法locationIdUpdate,点击地址时由store发出action:
locationIdUpdate(locationId) {
store.dispatch(Actions.updateLocation(locationId))
}
由于只有一个Store,APPDispatcher也没有存在的必要了,可以直接删掉。
dispatch的方法合并到了store中,对action的处理由reducer进行定义
4.3 容器组件和傻瓜组件
在Redux框架下,React组件主要负责两个功能:
与Store进行交互,读取store的数据以及发送action
根据props和state渲染界面
所以可以把组件进行拆分,让父组件负责与store的业务关系(容器组件),子组件专心负责渲染(傻瓜组件)
以我们的应用为例,WeatherLoactionSelect中发送更新天气信息的action这一逻辑可抽至其父组件WeatherHeader中。
点击WeatherLocationSeleceter中的button时,通过props调用父组件发送action,这样就可以让LocationSelecter组件专心于界面的渲染:
import React, { Component } from 'react';
class WeatherLocationSelecter extends Component {
render() {
const {LocationGroup, locationIdUpdate, selectedId} = this.props
return (
<div className="weather-selecter">
{
LocationGroup.map((locationObj) => {
return <button className={locationObj.id === selectedId ? 'selected' : ''} key={locationObj.id} onClick={() => locationIdUpdate(locationObj.id)}>{locationObj.name}</button>
})
}
</div>
);
}
}
export default WeatherLocationSelecter;
写到这里的时候发现前面有部分写的不规范的地方…
WeatherPanel中仍存有state - selectedCalender, 应该将其存放至store中,并把calendar的更新作为一个action:
// store
import {createStore} from 'redux'
import reducer from './Reducer.js'
const initValues = {
daily: undefined,
locationId: 0,
calenderId: 0
}
const store = createStore(reducer, initValues)
export default store
// action
...
export const updateCalender = (calenderId) => {
return {
type: ActionTypes.UPDATECALENDER,
calenderId: calenderId
}
}
// Reducer
...
case ActionTypes.UPDATECALENDER:
return {...state, calenderId: action.calenderId}
...没有写明白容器组件和傻瓜组件 - WeatherHeader中的render仍做了显示title的逻辑处理,可以把它分成WeatherTitleWrapper + WeatherTitle的组件
WeatherPanel中的CalenderSelecter和Selectedstatus也可以拆成 Wrapper Component(focus on deal with store)+ UI(focus on render)
至此,我们的应用结构如图所示:
其中WeatherHeader和WeatherPanel为容器组件,负责与store打交道;WeatherLocationSelecter/WeatherCalender/WeatherSelectedStatus均为傻瓜组件,专注于UI渲染工作
修改后的代码提交记录为:
Gitlog: b8f8e41ed0ee4c1563400ad3032d790c442dfdd8
4.4 组件context
组件WeatherHeader和WeatherPanel中都直接导入了store,但在许多情况下,我们并不能确定store的具体存放位置,应该避免这种做法。
然而如果采用从应用顶层导入store,并将其作为props传递给子组件的方式,同样会存在另一个问题:
当需要store的组件位置很深时,我们需要一级级的通过中间层的组件将store传递给最底层的子组件,即使中间层的组件不需要store
为此,React提供了一个叫Context的功能,它可以提供一个上下文环境,使得所有组件均可访问该环境中存储的对象。
首先写一个Provider,让该Provider提供context。Provider需要提供 getChildContext方法,并定义其 childContextTypes
import {Component} from 'react'
import PropTypes from 'prop-types'
class Provider extends Component {
getChildContext() {
return {
store: this.props.store
}
}
render() {
return this.props.children
}
}
Provider.childContextTypes = {
store: PropTypes.object
}
export default Provider
并将该Provider包裹最顶层的应用组件WeatherApp:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import WeatherApp from './WeatherApp';
import store from './Store.js'
import Provider from './Provider'
ReactDOM.render(
<Provider store={store}>
<WeatherApp />
</Provider>,
document.getElementById('root')
)
这样,当组件需要使用该store时,即可直接通过 this.context.store 的方式获取:
// WeatherHeader
...
class WeatherHeader extends Component {
...
constructor(props, context) {
super(props, context)
...
}
getOwnState() {
return {
selectedId: this.context.store.getState().locationId
}
}
...
}
WeatherHeader.contextTypes = {
store: PropTypes.object
}
export default WeatherHeader;
这里需要注意两点:
由于使用了context,需要在构造函数中引入该参数
和Provider一样,需要定义contextTypes
4.5 react-redux
react-redux主要做了两件事:
提供了Provider - 直接从’react-redux’中导入{Provider}即可
之前的代码中有很多组件都存在大量的重复代码(subscribe/onChange/getOwnState),通过react-redux的connect可以避免这种方式
各Wrapper组件的主要工作包括两点:
从store中获取state,将其传递给包裹的傻瓜组件
将store的dispatch封装成store,将其传递给包裹的傻瓜组件
react-redux的connet的使用形式为:
connect(mapStateToProps, mapDispatchToProps)(UIComponent)
connect接受两个函数作为参数,并返回一个接受傻瓜组件为参数的函数,该函数的最终返回结果为一个class
这么说有点绕,也就是说,
函数mapStateToProps代替了容器组件中传递state的功能
mapState(state, ownProps) - state为store中的state,ownProps为组件的属性
函数mapDispatchToProps代替了容器组件中传递dispatch的功能
mapDispatcher(dispath, ownProps) - 调用dispatch(xxx)即可发送action
通过使用connect,可以直接替代容器组件的功能,并不再需要重复先前的各种冗余的代码,大大减少了代码量
以之前的WeatherSelectedStatusWrapper + WeatherSelecetedStatus为例,可以直接删了Wrapper,将SelectedStatus改为:
import React, { Component } from 'react'
import {connect} from 'react-redux'
import './WeatherSelectedStatus.css'
function mapState(state) {
return {
currentDayInfo: state.daily[state.calenderId]
}
}
class WeatherSelectedStatus extends Component {
render() {
const {currentDayInfo} = this.props
const {text_day, code_day, high, low, wind_scale, wind_direction, wind_direction_degree, wind_speed} = currentDayInfo
return (
<div className="selected-status">
<div className="status">{text_day}</div>
<div className="detail">
<div>
<img alt="status-img" src={require('../img/status_icon/' + code_day + '.png')} />
<span>{low} ~ {high}°C</span>
</div>
<div>
<div>风力等级: {wind_scale}</div>
<div>风向角度(0~360): {wind_direction} { wind_direction_degree}</div>
<div>风速(km): {wind_speed}</div>
</div>
</div>
</div>
);
}
}
export default connect(mapState)(WeatherSelectedStatus);
中间件
上一章中,在使用redux后,我们将请求网络数据的过程改成了本地实时返回数据。
这是因为Reducer作为一个纯函数,接受到action后必须立即返回状态,无法执行异步操作。
为此,Redux中提供了中间件。所谓中间件,是作为action在派发给Reducer前,对action进行预处理的一种机制:
在这里,我们可以使用Redux提供的中间件redux-thunk进行异步处理
redux-thunk
一般情况下,action必须存在Type属性,由Reducer进行处理。
但当action的类型为function,在经过redux-thunk时,thunk会截获该action并执行function。
thunk处理的function可接受两个参数:
diapatch: 即store的dispath,当thunk处理完成后,可通过dispatch发送新的action
getState: 可通过该方法获取store中存储的state
修改Store.js,增加中间件redux-thunk:
import {createStore, compose, applyMiddleware} from 'redux'
import reducer from './Reducer.js'
import thunkMiddelware from 'redux-thunk'
const initValues = {
daily: undefined,
locationId: 0,
calenderId: 0
}
const middlewares = [thunkMiddelware]
const storeEnhancers = compose(applyMiddleware(...middlewares))
export default createStore(reducer, initValues, storeEnhancers)
修改action中发送的内容,将其改为函数形式:
// 增加用于发送网络请求结果的action
export const fetchDataSuccess = (daily, locationId) => {
return {
type: ActionTypes.FETCHDATASUCCESS,
locationId: locationId,
daily: daily
}
}
export const fetchDataStarted = (locationId) => {
return {
type: ActionTypes.FETCHDATASTARTED,
daily: 'Loading...',
locationId: locationId
}
}
export const fetchDataFailed = (locationId) => {
return {
type: ActionTypes.FETCHDATAFAILED,
daily: 'get data failed!',
locationId: locationId
}
}
// 获取天气信息
export const fetchData = (locationId) => {
return (dispatch, getState) => {
if(getState().locationId === locationId) {
return
}
let requestCode = undefined
LocationGroup.forEach((val) => {
if(val.id === locationId) {
requestCode = val.code
}
})
if(!requestCode) {
dispatch(fetchDataFailed(locationId))
return
}
dispatch(fetchDataStarted(locationId))
const requestURL = `/v3/weather/daily.json?key=${CustomConfig.key}&location=${requestCode}&language=zh-Hans&unit=c&start=0&days=3`
fetch(requestURL)
.then((response) => {
if(response.status !== 200) {
dispatch(fetchDataFailed(locationId))
return
}
response.json().then((responseJSON) => {
dispatch(fetchDataSuccess(responseJSON.results[0].daily, locationId))
}).catch((error) => {
dispatch(fetchDataFailed(locationId))
})
})
}
}
至此,当时,redux-thunk可捕获该action Actions.fetchData(locationId),并在获取数据过程中发送相应的action,通知Reducer返回对应的state:
// Reducer.js
import {ActionTypes} from './action'
export default (state, action) => {
switch(action.type) {
case ActionTypes.UPDATECALENDER:
return {...state, calenderId: action.calenderId}
case ActionTypes.FETCHDATASTARTED:
return {...state, daily: action.daily, locationId: action.locationId}
case ActionTypes.FETCHDATASUCCESS:
return {...state, daily: action.daily, locationId: action.locationId}
case ActionTypes.FETCHDATAFAILED:
return {...state, daily: action.daily, locationId: action.locationId}
default:
return state
}
}
自定义中间件
自定义中间件时,需按如下格式进行:
function doNothyingMiddleware({dispatch, getState}) {
return function(next) {
return function(action) {
return next(action)
}
}
}
dispath & getState: store中的方法,用于发送action及获取state
next: function,执行next(action)将当前处理的action传递给下一个中间件
action: 系统中发送的action对象
可以看到函数嵌套了很多层 - Redux根据函数式编程的思想进行设计,其一个重要的点在于让每个函数的功能尽量的小,通过函数的嵌套组合实现复杂功能
由此,我们可自定义上节中的thunk中间件:当action类型为函数时,调用该函数,否则不做处理,将action直接向后传递
// customMiddlewares
let customThunkMiddleware = ({dispatch, getState}) => {
return function(next) {
return function(action) {
if(typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
}
}
export {customThunkMiddleware}
我们也可以定义多个中间件对action进行处理,其处理顺序会按照在Store.js中定义middlewares的顺序进行。
比如我们再定义一个打印action具体信息的中间件:
// customMiddlewares
...
let customLogMiddleware = ({dispatch, getState}) => {
return (next) => {
return (action) => {
console.log("action type is: " + action.type)
next(action)
}
}
}
export {customThunkMiddleware, customLogMiddleware}
并将其加入middlewares数组中:
import {createStore, compose, applyMiddleware} from 'redux'
import reducer from './Reducer.js'
import {customThunkMiddleware, customLogMiddleware} from './customThunkMiddleware'
const initValues = {
daily: undefined,
locationId: 0,
calenderId: 0
}
const middlewares = [customThunkMiddleware, customLogMiddleware]
const storeEnhancers = compose(applyMiddleware(...middlewares))
export default createStore(reducer, initValues, storeEnhancers)
运行后,通过断点可看到系统中每次发送的action都会先后经过customThunkMiddleware和 customLogMiddleware
最后,成果是: