All Projects → alivebao → weather_app

alivebao / weather_app

Licence: MIT license
Study react and redux by a simple application

Programming Languages

javascript
184084 projects - #8 most used programming language
HTML
75241 projects
CSS
56736 projects

Projects that are alternatives of or similar to weather app

DeepLearningBenchmarks
Benchmarks across Deep Learning Frameworks in Julia and Python
Stars: ✭ 24 (-17.24%)
Mutual labels:  flux
mutable
State containers with dirty checking and more
Stars: ✭ 32 (+10.34%)
Mutual labels:  flux
continuous-analytics-examples
A collection of examples of continuous analytics.
Stars: ✭ 17 (-41.38%)
Mutual labels:  flux
Sunscreen
🌅 A macOS app that sets your wallpaper based on sunrise and sunset.
Stars: ✭ 52 (+79.31%)
Mutual labels:  flux
react-workshops
Online react workshops
Stars: ✭ 36 (+24.14%)
Mutual labels:  flux
NaiveNASflux.jl
Your local Flux surgeon
Stars: ✭ 20 (-31.03%)
Mutual labels:  flux
react-native-firebase-redux-authentication
Firebase Authentication using React Native, Redux, Router flux.
Stars: ✭ 45 (+55.17%)
Mutual labels:  flux
GeometricFlux.jl
Geometric Deep Learning for Flux
Stars: ✭ 288 (+893.1%)
Mutual labels:  flux
ReduxSharp
Unidirectional Data Flow in C# - Inspired by Redux
Stars: ✭ 18 (-37.93%)
Mutual labels:  flux
assembler
Functional, type-safe, stateless reactive Java API for efficient implementation of the API Composition Pattern for querying/merging data from multiple datasources/services, with a specific focus on solving the N + 1 query problem
Stars: ✭ 102 (+251.72%)
Mutual labels:  flux
temperjs
State management for React, made simple.
Stars: ✭ 15 (-48.28%)
Mutual labels:  flux
neutronics-workshop
A workshop covering a range of fusion relevant analysis and simulations with OpenMC, DAGMC, Paramak and other open source fusion neutronics tools
Stars: ✭ 29 (+0%)
Mutual labels:  flux
hermes-js
Universal action dispatcher for JavaScript apps
Stars: ✭ 15 (-48.28%)
Mutual labels:  flux
ONNX.jl
Read ONNX graphs in Julia
Stars: ✭ 112 (+286.21%)
Mutual labels:  flux
react-evoke
Straightforward action-driven state management for straightforward apps built with Suspense
Stars: ✭ 15 (-48.28%)
Mutual labels:  flux
mafuba
Simple state container for react apps.
Stars: ✭ 20 (-31.03%)
Mutual labels:  flux
k8s-gitops
Homelab GitOps repository. Cluster definition state via code.
Stars: ✭ 47 (+62.07%)
Mutual labels:  flux
relite
a redux-like library for managing state with simpler api
Stars: ✭ 60 (+106.9%)
Mutual labels:  flux
flux-action-class
Boilerplate free class-based action creator. Following flux-standard-action spec. Built with TypeScript. Works with redux and ngrx.
Stars: ✭ 22 (-24.14%)
Mutual labels:  flux
variant
Variant types in TypeScript
Stars: ✭ 147 (+406.9%)
Mutual labels:  flux

React实战 - weather_app

介绍

在读完程墨老师的《深入浅出React和Redux》后,结合自己的理解,构建一个显示天气的应用进行总结。
执行:

npm insatll
npm start

运行效果图:

目录

  1. React新的前端思维方式
  2. React基础
  3. 编写一个React实例
  4. 从Flux到Redux
  5. 中间件

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 是一些静态资源。 在 srcindex.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中最重要的地方有两点:

  1. 在return语句中,返回的DOM节点只能有一个根节点,也就是说顶层节点不能有两个(否则会报错):
// 错误示例
return (
  <div>Node 1</div>
  <div>Node 2</div>
)
  1. 需要注意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的主要区别在于:

  1. prop是由外部传入的,是组件无法修改的
  2. 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模拟获取温度的过程
这里要注意的地方:

  1. bind - JS中的this的坑,在组件中定义的方法都需要通过bind函数指定this(当然,也可以使用箭头函数)
  2. setState - 只有在构造函数初始化时能直接给state赋值,其他地方都要通过setState去操作。组件执行这个方法后才会刷新
  3. porps - 通过this.props.NAME 使用外界传递进来的属性(属性不可修改)
  4. 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;

这里存在两个问题:

  1. 如何对传递进来的props进行检测 - 也就是说组件如何预期传递捡来的属性的类型,以及需要的属性没穿进来的时候,组件该如何处理
  2. 组件间如何通信 - 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的检测放在开发环境里,在发布代码的时候就不要这么操作了(毕竟即增加了代码量,对用户又没什么意义)

关于组件间的通信,这里暂时通过父组件来完成。
父组件 WeatherAppWeatherSelecter 中传递一个回调函数,当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就方便多了

组件的生命周期

组件在生命周期中可能会经历三个过程:

  1. 装载(Mount),即组件第一次在DOM树种渲染的过程
  2. 更新(Update),即组件重新渲染的过程
  3. 卸载(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)  

那么各组件应该完成什么功能?分组件来看的话:

  1. WeatherHeader部分负责选择城市,当城市切换时,应用发起网络请求获取相应城市的天气信息
  2. WeatherSelectedStatus是一个纯负责展示的组件
  3. 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下面:

文件可以按角色或功能进行组织

  1. 按角色进行组织(MVC框架)
|
|--controllers/
|   |--...
|
|--models/
|   |--...
|
|--views/
|   |--...
|
|...
  1. 按功能进行组织
    书中提到,在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

之前的解决方案存在两个问题:

  1. 数据源可能不统一
  2. 过多层次的钩子函数传递
    当两个组件依赖同一个数据状态时,我们应该怎么做?是两组件各自维护一个数据状态吗?还是将状态抽至上一层组件?
    再看一下上一章的组件分解图:

    在上图的 WeatherSelectedStatusWeatherCalenderSelecter 中,两个组件都需要展示天气信息。
    如果两个组件分别维护各自的天气信息状态,那么也就是点击切换城市按钮时,让两个组件都去发更新天气状态的网络请求。
    这么做显然是不合适的,除去效率低不说,当某个组件请求出错,导致两个组件数据不一致时,该以哪个为准?
    因此,最好的方式是将天气信息抽象至上一层组件。在这里,我们做到了统一数据源,将天气信息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

  1. 首先定义一个Dispatcher,用于接收和处理其他组件发送来的信息:
import {Dispatcher} from 'flux'

export default new Dispatcher()
  1. 定义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的必要
3. 定义一个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()
      })
  }
})
  1. 修改需要监听消息的组件
    这里以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的一种实现,这里主要有两个地方需要注意:

  1. Redux只有一个store - 在上节Flux实现的实例中,虽然我们也只用了一个Store,但Flux中,应用可以拥有多个Store;Redux规定应用只能拥有一个store
  2. 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实例:

  1. npm install --save redux
  2. 修改action,不再通过Dispather派发,而是直接返回一个action对象
import * as ActionTypes from './ActionTypes'
import AppDispatcher from '../AppDispatcher'

export const updateLocation = (locationId) => { 
  return {
    type: ActionTypes.UPDATELOCATION, 
    locationId: locationId
  }
}
  1. 创建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
4. 修改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)
}
...
  1. 修改WeatherLocationSelecter里的方法locationIdUpdate,点击地址时由store发出action:
locationIdUpdate(locationId) {    
  store.dispatch(Actions.updateLocation(locationId))
}

由于只有一个Store,APPDispatcher也没有存在的必要了,可以直接删掉。
dispatch的方法合并到了store中,对action的处理由reducer进行定义

4.3 容器组件和傻瓜组件

在Redux框架下,React组件主要负责两个功能:

  1. 与Store进行交互,读取store的数据以及发送action
  2. 根据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;

写到这里的时候发现前面有部分写的不规范的地方...

  1. 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}
...
  1. 没有写明白容器组件和傻瓜组件 - 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渲染工作
    修改后的代码提交记录为:
Git log: 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;

这里需要注意两点:

  1. 由于使用了context,需要在构造函数中引入该参数
  2. 和Provider一样,需要定义contextTypes

4.5 react-redux

react-redux主要做了两件事:

  1. 提供了Provider - 直接从'react-redux'中导入{Provider}即可
  2. 之前的代码中有很多组件都存在大量的重复代码(subscribe/onChange/getOwnState),通过react-redux的connect可以避免这种方式
    各Wrapper组件的主要工作包括两点:
  3. 从store中获取state,将其传递给包裹的傻瓜组件
  4. 将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可接受两个参数:

  1. diapatch: 即store的dispath,当thunk处理完成后,可通过dispatch发送新的action
  2. 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都会先后经过 customThunkMiddlewarecustomLogMiddleware

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].