组件通信
props
适用于父子组件通信
父组件->子组件
父组件将需要传递的参数通过key={xxx}方式传递至子组件,子组件通过this.props.key获取参数
import React from 'react'
import Son from './son'
class Father extends React.Component {
constructor(props) {
super(props)
}
state = {
info: '父组件',
}
handleChange = (e) => {
this.setState({
info: e.target.value,
})
}
render() {
return (
<div>
<input type='text' value={this.state.info} onChange={this.handleChange} />
<Son info={this.state.info} />
</div>
)
}
}
export default Father
// 子组件
import React from 'react'
interface IProps {
info?: string
}
class Son extends React.Component<IProps> {
constructor(props) {
super(props)
}
render() {
return (
<div>
<p>{this.props.info}</p>
</div>
)
}
}
export default Son
子组件->父组件
利用 props callback 通信,父组件传递一个 callback 到子组件,当事件触发时将参数放置到 callback带回给父组件
// 父组件
import React from 'react'
import Son from './son'
class Father extends React.Component {
constructor(props) {
super(props)
}
state = {
info: '',
}
callback = (value) => {
// 此处的value便是子组件带回
this.setState({
info: value,
})
}
render() {
return (
<div>
<p>{this.state.info}</p>
<Son callback={this.callback} />
</div>
)
}
}
export default Father
// 子组件
import React from 'react'
interface IProps {
callback: (string) => void
}
class Son extends React.Component<IProps> {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
}
handleChange = (e) => {
// 在此处将参数带回
this.props.callback(e.target.value)
}
render() {
return (
<div>
<input type='text' onChange={this.handleChange} />
</div>
)
}
}
export default Son
Context
适用于跨层级组件之间通信
// context.js
import React from 'react'
const { Consumer, Provider } = React.createContext(null) //创建 context 并暴露Consumer和Provide
export { Consumer, Provider }
// 父组件
import React from 'react'
import Son from './son'
import { Provider } from './context'
class Father extends React.Component {
constructor(props) {
super(props)
}
state = {
info: 'info from father',
}
render() {
return (
<Provider value={this.state.info}>
<div>
<p>{this.state.info}</p>
<Son />
</div>
</Provider>
)
}
}
export default Father
// 子组件
import React from 'react'
import GrandSon from './grandson'
import { Consumer } from './context'
class Son extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<Consumer>
{(info) => (
// 通过Consumer直接获取父组件的值
<div>
<p>父组件的值:{info}</p>
<GrandSon />
</div>
)}
</Consumer>
)
}
}
export default Son
// 孙子组件
import React from 'react'
import { Consumer } from './context'
class GrandSon extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<Consumer>
{(info) => (
// 通过 Consumer 中可以直接获取组父组件的值
<div>
<p>组父组件的值:{info}</p>
</div>
)}
</Consumer>
)
}
}
export default GrandSon
特别注意
如果需要消费多个 Context,则 React 需要使每一个 consumer 组件的 context 在组件树中成为一个单独的节点。
// provider
...
<Context1.Provider value={this.state.info}>
<Context2.Provider value={this.state.theme}>
<div>
<p>{this.state.info}</p>
<p>{this.state.theme}</p>
<Son />
</div>
</Context2.Provider>
</Context1.Provider>
// consumer
...
<Context1.Consumer>
{(info: string) => (
// 通过Consumer直接获取父组件的值
<Context2.Consumer>
{(theme: string) => (
<div>
<p>父组件info的值:{info}</p>
<p>父组件theme的值:{theme}</p>
</div>
)}
</Context2.Consumer>
)}
</Context1.Consumer>
很多优秀的 React 组件的核心功能都通过 Context 来实现的,比如 react-redux 和 react-router 等,所以掌握 Context 是必须的。
OnRef
OnRef 的原理很简单,本质上就是通过props将子组件的组件实例(也是我们常说的 this)当作参数,通过回调传到父组件,然后在父组件就可以拿到子组件的实例,拿到了它的实例就可以调用它的方法(为所欲为)了。
// 父组件
import React from 'react'
import Son from './son'
import { Button } from 'antd'
class Father extends React.Component {
child: any
constructor(props) {
super(props)
}
sonRef = (ref) => {
this.child = ref // 在这里拿到子组件的实例
}
clearSonInput = () => {
this.child.clearInput()
}
render() {
return (
<div>
<Son onRef={this.sonRef} />
<Button type='primary' onClick={this.clearSonInput}>
清空子组件的input
</Button>
</div>
)
}
}
export default Father
// 子组件
import React from 'react'
import { Button } from 'antd'
interface IProps {
onRef: any
}
class Son extends React.Component<IProps> {
constructor(props) {
super(props)
}
componentDidMount() {
this.props.onRef(this) // 在这将子组件的实例传递给父组件this.props.onRef()方法
}
state = {
info: 'son',
}
handleChange = (e) => {
this.setState({
info: e.target.value,
})
}
clearInput = () => {
this.setState({
info: '',
})
}
render() {
return (
<div>
<div>{this.state.info}</div>
<input type='text' onChange={this.handleChange} />
</div>
)
}
}
export default Son
ref
ref是react提供给我们的一个属性,通过它,我们可以访问 DOM 节点或者组件
// 父组件
import React from 'react'
import Son from './son'
import { Button } from 'antd'
class Father extends React.Component {
son: any
constructor(props) {
super(props)
this.son = React.createRef() // 在此处创建ref
}
clearSonInput = () => {
const { current } = this.son // 注意,这里必须通过 this.son.current拿到子组件的实例
current.clearInput()
}
render() {
return (
<div>
<Son ref={this.son} />
<Button type='primary' onClick={this.clearSonInput}>
清空子组件的input
</Button>
</div>
)
}
}
export default Father
// 子组件
import React from 'react'
import { Button } from 'antd'
class Son extends React.Component {
constructor(props) {
super(props)
}
state = {
info: 'son',
}
handleChange = (e) => {
this.setState({
info: e.target.value,
})
}
clearInput = () => {
this.setState({
info: '',
})
}
render() {
return (
<div>
<div>{this.state.info}</div>
<input type='text' onChange={this.handleChange} />
</div>
)
}
}
export default Son
值得注意的是,我们必须通过 this.childRef.current才能拿到子组件的实例。
使用 ref 常见的场景有管理焦点,文本选择或媒体播放、触发强制动画、集成第三方 DOM 库等。
第三方工具
events(发布订阅)
这种方式适用于没有任何嵌套关系的组件通信。其原理就是使用事件订阅。即是一个发布者,一个或者多个订阅者。 使用之前需要先安装:
yarn add events
// event.ts
import { EventEmitter } from 'events'
export default new EventEmitter()
// 发布者 通过emit事件触发方法,发布订阅消息给订阅者
import React from 'react'
import Son1 from './son1'
import Son2 from './son2'
import { Button } from 'antd'
import emitter from './event'
class Father extends React.Component {
son: any
constructor(props) {
super(props)
}
handleClick = () => {
//emit事件触发方法,通过事件名称找对应的事件处理函数info,将事件处理函数作为参数传入
emitter.emit('info', '我是来自father的 info')
}
render() {
return (
<div>
<Button type='primary' onClick={this.handleClick}>
点击按钮发布事件
</Button>
<Son1 />
<Son2 />
</div>
)
}
}
export default Father
// 订阅者1
// 通过emitter.addListener(事件名称,函数名)方法,进行事件监听(订阅)。
// 通过emitter.removeListener(事件名称,函数名)方法 ,进行事件销毁(取消订阅)
import React from 'react'
import { Button } from 'antd'
import emitter from './event'
class Son1 extends React.Component {
constructor(props) {
super(props)
}
state = {
info: '',
}
componentDidMount() {
// 在组件挂载完成后开始监听
emitter.addListener('info', (info) => {
this.setState({
info: 'Son1收到消息--' + info,
})
})
}
componentWillUnmount() {
// 组件销毁前移除事件监听
emitter.removeListener('info', (info) => {
this.setState({
info: 'Son1即将移除事件监听--' + info,
})
})
}
render() {
return (
<div>
<div>{this.state.info}</div>
</div>
)
}
}
export default Son1
// 订阅者2
import React from 'react'
import { Button } from 'antd'
import emitter from './event'
class Son2 extends React.Component {
constructor(props) {
super(props)
}
state = {
info: '',
}
componentDidMount() {
// 在组件挂载完成后开始监听
emitter.addListener('info', (info) => {
this.setState({
info: 'Son2收到消息--' + info,
})
})
}
componentWillUnmount() {
// 组件销毁前移除事件监听
emitter.removeListener('info', (info) => {
this.setState({
info: 'Son2即将移除事件监听--' + info,
})
})
}
render() {
return (
<div>
<div>{this.state.info}</div>
</div>
)
}
}
export default Son2
路由
随着前端工程的复杂度越来越高,所以路由管理在现在的前端工程中,也是一个值得注意的点,vue提供了vue-router来管理路由。相似的,react则提供了react-router来管理路由。
react-router
react-router 包含 3 个,分别为react-router、react-router-dom 和 react-router-native。
react-router提供最基本的路由功能,但是实际使用的时候我们不会直接安装 react-router,而是根据应用运行的环境来选择安装react-router-dom和react-router-native。因为react-router-dom 和 react-router-native 都依赖 react-router,所以在安装时,react-router也会⾃自动安装。
其中react-router-dom在浏览器器中使⽤,而react-router-native在react-native 中使用。
在 react-router里面,一共有 3 种基础组件,他们分别是
- 路由组件(
router components) 比如<BrowserRouter> 和 <HashRouter> - 路由匹配组件(
route matchers components) 比如<Route> 和 <Switch> - 导航组件(
navigation components) 比如<Link>, <NavLink>, 和 <Redirect>
路由组件
对于 web 项目,react-roruter-dom提供了 <BrowserRouter> 和 <HashRouter>两个路由组件
BrowserRouter:浏览器的路由方式,也就是使用HTML5提供的history API ( pushState , replaceState 和 popstate 事件)来保持UI 和 url的同步。这种方式在react开发中是经常使用的路由方式,但是在打包后,打开会发现访问不了页面,所以需要通过配置nginx解决或者后台配置代理。HashRouter:在路径前加入#号成为一个哈希值,Hash模式的好处是,再也不会因为我们刷新而找不到我们的对应路径,但是链接上面会有#/。在vue开发中,经常使用这种方式。
要使用路由组件,我们只需要确保它是在根组件使用,我们应该将<App />包裹在路由组件下面:
import { BrowserRouter } from 'react-router-dom';
...
<BrowserRouter>
<App />
</BrowserRouter>
...
匹配组件
有两种路由匹配组件:<Route> 和 <Switch>
这两个路由匹配组件通常在一起使用,在<Switch>里面包裹多个<Route>,然后它会逐步去比对每个<Route>的path属性 和浏览器当前location的pathname是否一致,如果一致则返回内容,否则返回null。
<Switch>
<Route exact path='/' component={Home} />
{/* 如果当前的URL是`/about`,即 location = { pathname: '/about' } ,那么About组件就应该被渲染,其余的Route就会被忽略 */
<Route path='/about' component={About} />
<Route path='/contact' component={Contact} />
</Switch>
值得注意 ⚠️ 的是: <Route path={xxx}> 只会匹配 URL的开头,而不是整个 URL。简单的来说就是它不是精确匹配 ,例如<Route path ='/'> 和<Route path ='/about'> 它永远都只能匹配到<Route path ='/'>,他们开头都有'/'。
在这里我们有两种解决方法:
- 将此
<Route path='/'>放在<Switch>的最后一个位置 - 另一种解决方案是添加
'exact'实现精确匹配:<Route exact='/'>
所以<Switch>组件只会render 第一个匹配到的路由,像上面我们说的,如果没有设置path,则一定会匹配,我们可以用来实现 404的功能:
<Switch>
<Route exact path='/' component={Home} />
<Route path='/about' component={About} />
<Route path='/contact' component={Contact} />
{/* 当上面的组件都没有匹配到的时候, 404页面 就会被 render */}
<Route render={() => <div> 404页面 </div>} />
</Switch>
导航组件
导航组件有<Link>, <NavLink>, 和 <Redirect>。
当我们使用<Link>的时候,在 html 页面会被渲染为一个a标签:
<Link to='/'>Home</Link>
// <a href='/'>Home</a>
<NavLink>是一种特殊的<Link> ,当<NavLink>中的地址和浏览器地址匹配成功的时候,会添加一个style样式,如下:
<NavLink to='/about' activeClassName='active'>
About
</NavLink>
在 html 页面当中,它会被渲染为:
<a href='/about' className='active'>
About
</a>
但是有时你可能想要强制跳转到某个页面,比如未登录不能进入首页,这个时候你可以使用<Redirect>
<Redirect to='/login' />
状态管理
前端工程的复杂度越来越高,状态管理也是一个很重要的点。在react生态中,现在最火的状态管理方案就是redux。
redux
我们都知道,react 是单向的数据流,数据几乎都是通过props 依次从上往下传
一个组件的状态有两种方式改变:
- 来自父组件的
props改变了,那么这个组件也会重新渲染 - 自身有
state,自身的state可以通过this.setstate方法改变
现在假如我们组件树的层级比较深,有很多子组件需要共享状态,那么我们只能通过状态提升来改变状态,将状态提升到顶级父组件改变,当顶级父组件的状态改变了,那么旗下的所有子节点都会重新渲染
当出现这种情况当时候,你就该使用redux了。那么使用redux之后,就会变成这样
Store
在redux 里面,只有一个Store,整个应用需要管理的数据都在这个Store里面。这个Store我们不能直接去改变,我们只能通过返回一个新的Store去更改它。redux提供了一个createStore来创建state
import { createStore } from 'redux'
const store = createStore(reducer)
action
这个action 指的是视图层发起的一个操作,告诉Store我们需要改变。比如用户点击了按钮,我们就要去请求列表,列表的数据就会变更。每个action 必须有一个 type 属性,这表示 action 的名称,然后还可以有一个 payload属性,这个属性可以带一些参数,用作Store 变更:
const action = {
type: 'ADD_ITEM',
payload: 'new item', // 可选属性
}
上面这个例子就定义了一个名为ADD_ITEM的Action,它还携带了一个payload的参数。 Redux 可以用 Action Creator批量来生成一些 Action。
reducer
在上面我们定义了一个Action,但是Action不会自己主动发出变更操作到Store,所以这里我们需要一个叫dispatch的东西,它专门用来发出action,不过还好,这个dispatch不需要我们自己定义和实现,redux已经帮我们写好了,在redux里面,store.dispatch()是 View发出 Action 的唯一方法。
store.dispatch({
type: 'ADD_ITEM',
payload: 'new item', // 可选属性
})
当dispatch发起了一个 action之后,会到达reducer,那么这个reducer用来干什么呢?顾名思义,这个reducer就是用来计算新的store的,reducer接收两个参数:当前的state和接收到的action,然后它经过计算,会返回一个新的state。(前面我们已经说过了,不能直接更改state,必须通过返回一个新的state来进行变更)
const reducer = function(prevState, action) {
...
return newState;
};
这个reducer是一个纯函数。纯函数的意思是说,对于相同的输入,只会有相同的输出,不会影响外部的值,也不会被外部的值所影响。
可以看到,我们在创建store的时候,我们在createStore里面传入了一个reducer参数,在这里,我们就是为了,每次store.dispatch发送一个新的action,redux都会自动调用reducer,返回新的state。
那么当项目特别大特别复杂的时候,state 肯定是非常大的一个对象,所以我们需要写很多个 reducer,那么在这里,我们就需要把reducer进行拆分。每个 reducer只负责管理 state 的一部分数据。那么我们如何统一对这些reducer进行管理呢?redux给我们提供了combineReducers 方法,顾名思义,就是将所有的子reducer 合成一个 reducer,方便我们管理。
import { combineReducers } from 'redux'
import listReducer from './listReducer/reducers'
import detailReducer from './detailReducer/reducers'
import aboutReducer from './aboutReducer/reducers'
const rootReducer = combineReducers({
listReducer,
detailReducer,
aboutReducer,
})
export default rootReducer
中间件
熟悉koa的朋友们,应该知道中间件的概念。中间件的意思简单理解就是,在某两个操作之间,我们需要进行某些操作。那么在 redux,我们为什么要引入中间件呢?到目前为止,我们来捋一下我们刚刚已经进行的步骤:
- 创建 store
import { createStore } from 'redux'
const store = createStore(reducer)
- 发出 action
store.dispatch({
type: 'ADD_ITEM',
payload: 'new item', // 可选属性
})
- reducer 计算返回新的 state
const reducer = function(prevState, action) {
...
return newState;
};
我们发现,我们这次发起的变更,都是同步操作,那么问题来了。假如我们state里面有一个列表:list,用户根据在view上面点击了一些筛选条件,发起请求,然后变更state里面list的值。在这里,有异步请求,但是我们变更redux的过程都是同步的,显然是不支持异步的,所以这里就用到中间件了。那么我们应该将异步请求放在以上哪个步骤去执行呢?显然第 1 步和第 3 步不可能,其中第 1 步只是在创建 store,第 3 步reducer 是纯函数,根本不可能加入异步操作。所以我们很自然的想到,就是在store.dispatch的之后,到达reducer之前进行异步操作:
store.dispatch = function(prevAction) async{
console.log("发请求啦");
// 异步操作执行完成之后才派发action
const list = await getList();
// 把 list 放到action里面
const newAction = {
type: prevAction.type,
payload:list
}
store.dispatch(newAction);
};
就是给store.dispatch再包裹一层,这就是中间件的原理。
redux 常见的中间件有redux-thunx、redux-promise、redux-saga。相关的详细用法在这里不再赘述(下面会介绍dva-core的用法)。
redux应用中间件的方法:
import { applyMiddleware, createStore } from 'redux'
import myMiddleware from './myMiddleware'
const store = createStore(reducer, applyMiddleware(myMiddleware))
通知变更
那么到这一步了,我们变更了state,下一步是将变更通知给 view 了。在redux里面,提供了store.subscribe(listener)这个方法,这个方法传入一个listener,比如在 react里面,listener可以是this.setState(xxx),每当redux 里面的state改变了,通过store.subscribe(listener)我们的页面也会重新渲染。意思是我们每个页面都得手动去store.subscribe(listener),这也太麻烦了吧,对吧。
react-redux 和 redux
为了解决上述的痛点问题,更好的将redux 和 react结合,官方给我们提供了react-redux这个包(可能你到现在脑子有点乱了,我刚开始也是)。那么现在,我们需要明确一个概念:redux 和 react是两个八竿子不着的人。redux 只是一个状态管理框架,react 只是一个前端应用框架。redux可以用于前端任何框架,例如vue,甚至纯javaScript 都可以。后来 react-redux出现了,他把redux 和 react撮合在一起了,于是他们两强强联合,风云合璧,所向披靡,好了不扯了。说了这么多就是想说明react-redux 这个包的作用。
在详细说明react-redux的作用之前,我们先介绍以下知识点: react-redux将react 组件划分为容器组件和展示组件,其中
- 展示组件:只是负责展示
UI,不涉及到逻辑的处理,数据来自父组件的props - 容器组件:负责逻辑、数据交互,将
state里面的数据传递给展示组件进行UI呈现
容器组件是react-redux提供的,也就是说,我们只需要负责展示组件,react-redux负责状态管理。
我们知道,redux提供了一个大的state。这里我们需要面对两个问题,第一个问题,如何让我们react项目里面的所有组件都能够拿到state?;第二个,每当state变更之后,组件如何收到变更信息?
Provider
针对第一个问题,react-redux提供了Provider组件。用这个Provider包裹根组件,将redux导出的state,作为参数往下面传
import React from "react";
import { Provider } from "react-redux";
import App from "./App";
import { store } from "./store"; // 这个store由redux导出
···
<Provider store={store}>
<App />
</Provider>;
···
return
这样所有的组件都能拿到state了。这个 Provider 组件原理就是通过react的Context来实现的,我们可以看看源码:
....
const Context = context || ReactReduxContext;
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
....
这里的contextValue就包裹了我们传入的store,很明显,它创建了 Context,通过<Context.Provider value={contextValue}>{children}</Context.Provider>这种方式将我们传入的store提供给了react所有组件。
connect
在上面我们知道怎么将redux 暴露出来的 state 提供给react组件的,那么接下来,我们在某个子组件里面如何收到 state的变更呢?react-redux给我们提供了connect方法。这个方法可以传入两个可选参数:mapStateToProps和mapDispatchToProps,然后会返回一个容器组件,这个组件可以自动监听到 state 的变更,将 state 的值映射为组件的 props 参数,之后我们可以直接通过 this.props 取到 state里面的值。
const mapStateToProps = (state) => ({
goodsList: state.goodsList,
totalCount: state.totalCount,
});
export default connect(
mapStateToProps, // 可选
// mapDispatchToProps, // 可选
(GoodsList);
mapStateToProps就是将state 的值映射为组件的props,mapDispatchToProps就是将store.dispatch映射为props。如果我们不传mapDispatchToProps的话,connect会自动将dispatch注入到 props 里面,我们在组件里可以直接通过this.props.dispatch发起一个action给reducer。
connected-react-router 和 redux
当我们在项目中同时用了react-router 和 redux的时候,我们可以把他们两个深度整合。我们想要在store里面拿到router,甚至可以操作router,还可以记录router的改变。例如我们把用户是否登录的状态存在redux里面,在登录之后会进行页面的跳转。正常的操作是我们在发起请求之后,得到一个状态,此时我们需要dispatch一个action去改变redux的状态,同时我们需要进行路由的跳转,类似于这样:store.dispatch(replace('/home'))。想要实现这种深度整合,我们需要用到 connected-react-router和history两个库。
首先需要history生成history对象,结合connected-react-router的connectRouter生成routerReducer,同时利用connected-react-router的routerMiddleware实现dispatch action导航,也就是我们刚刚说的store.dispatch(replace('/home')):
// APP.tsx
const createHistory = require('history').createBrowserHistory
export const history = createHistory()
// reducer/index.ts
const routerReducer = connectRouter(history)
const routerMiddlewareForDispatch = routerMiddleware(history)
const middleware = [routerMiddlewareForDispatch]
接着利用redux的combineReducers将我们自己的reducer和routerReducer合并起来,组成rootReducer,然后利用 createStore创建store并暴露出去:
// reducer/index.ts
export default function geneGrateSotore(history: any) {
const routerReducer = connectRouter(history)
const routerMiddlewareForDispatch = routerMiddleware(history)
const middleware = [routerMiddlewareForDispatch]
//合并routerReducer
const rootRuder = combineReducers({
info: infoRuder,
router: routerReducer,
})
const store = createStore(rootRuder, applyMiddleware(...middleware))
return store
}
最后我们在App.tsx导入刚刚我们创建的这个方法,生成store,接着将我们创建的history对象传入connected-react-router的ConnectedRouter组件作为props,并用它包裹我们的Router组件:
// App.tsx
import React from 'react'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
import geneGrateSotore from './store'
import Router from './router'
import './App.css'
const createHistory = require('history').createBrowserHistory
const history = createHistory()
const store = geneGrateSotore(history)
const f: React.FC = () => {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<Router></Router>
</ConnectedRouter>
</Provider>
)
}
export default f
这样我们就将connected-react-router 和 redux整合起来了。现在当我们在View利用Link进行路由跳转的时候,会通过react-router-dom进行路由跳转,并且也会通过connected-react-router发起一个action去更新redux state里面的router对象,以记录路由的变化。同时现在我们在状态管理的时候,也可以直接通过connected-react-router提供的push、replace等方法了,他们是从redux出发,也就是说先发起一个action,然后再进行路由跳转。
小结一下
看了上面的这些东西,是不是感觉脑子贼乱,什么react、redux、react-redux、react-router、react-router-dom、connected-react-router,这些概念是真的多,我刚开始接触的时候直接看懵逼。。所以react的可组合性比vue高很多,所以说 vue 真的是自动挡、react 是手动挡。但是我们只需记住,前端的一切概念,都是纸老虎而已。静下心来捋一捋,很快就理解了。好了,现在来看看这个图:

dva
通过上面的这些工具,我们已经可以搭建一个很棒的基于react和redux的工程。如果单纯使用redux,当项目越来越大的时候,我们的redux的操作也会变得繁杂,代码和文件的组织会变得臃肿,我们就必须把action、reducer、createActions、actionType等等文件放在不同的目录下面,当我们需要使用的时候,就要打开文件目录去翻半天,我们需要在这些文件里面不停切换,简直抓狂。所以我们不禁会想:要是所有的操作都放在一个文件该多好!没错,它来了。它就是dva。
dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva
还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
因为我们前面已经自己组织了react-router,所以我们只使用dva-core,有了这个,我们就可以将 reducers, effects 等等组织在一个model文件里面了。以前我们组织代码的方式是createAction.ts、actionType.ts、reducer/xxx.ts,但是通过dva,我们就可以把这些都写在一个文件里面。接下来我们来看看如何配置dva-core:
首先我们需要通过dva create暴露一个dva app:
// dva/index.tsx
import React from 'react'
import { create } from 'dva-core'
import { Provider } from 'react-redux'
export default function(options: any) {
const app = create(options)
options.models.forEach((model: any) => app.model(model))
app.start()
const store = app._store
app.start = (container: any) => {
return () => <Provider store={store}>{container}</Provider>
}
app.getStore = () => store
return app
}
上面的 options即是我们的model 文件。然后我们需要利用这个app重新配置我们的 store:
/// store/index.ts
import { connectRouter, routerMiddleware } from 'connected-react-router'
import dva from './dva'
import models from '../models'
export default function geneGrateSotore(history: any) {
const routerReducer = connectRouter(history)
const routerMiddlewareForDispatch = routerMiddleware(history)
const app = dva({
models,
initState: {},
extraReducers: { router: routerReducer },
onAction: [routerMiddlewareForDispatch],
})
return app
}
然后一样的,我们在App.tsx使用它即可:
import React from 'react'
import { ConnectedRouter } from 'connected-react-router'
import geneGrateSotore from './store'
import Router from './router'
import './App.css'
const createHistory = require('history').createBrowserHistory
const history = createHistory()
const app = geneGrateSotore(history)
const f: React.FC = app.start(
<ConnectedRouter history={history}>
<Router />
</ConnectedRouter>
)
export default f
这和之前配置 redux 对比起来,我们已经省了很多的文件了,我们的 store 文件清净多了。下面我们要来编写model文件了。例如有个模块叫model1:
// model/model1.ts
export const namespace = 'model1'
interface IDvaState {
info1: string
}
const state: IDvaState = {
info1: 'init info1',
}
const model1 = {
namespace,
state,
effects: {
*changeInfo1({ payload }, { put }) {
try {
const { text } = payload
yield put({
type: 'setState',
payload: { info1: text },
})
// eslint-disable-next-line no-empty
} catch (error) {}
},
},
reducers: {
setState(state: IDvaState, { payload }) {
return { ...state, ...payload }
},
},
}
export default [model1]
这个文件可能有的小伙伴一下子看不明白,接下来我们来一起捋一捋。
namespace: 命名空间(我们组件可以通过这个namespace拿到我们的model,从而取值和进行操作)state:这个就是我们需要管理的状态(存放状态的地方)effects:一些操作,我们可以把同步操作和异步操作都放到effects里面,简单来说就是我们的业务逻辑。这里会详细介绍,咱们先明白它是做什么的reducers:reducer这个概念就是跟redux的reducer是同一个意思(返回一个newState,更新state)
好了有了上面这些概念,接下来我们来看看如何把组件和model联系起来。
// home.tsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Button } from 'antd'
import { namespace } from '../models/model1'
interface Iprops {
info1: string
dispatch: any
}
export class Home extends Component<Iprops> {
changeFormHome = () => {
const { dispatch } = this.props
dispatch({
type: `${namespace}/changeInfo1`,
payload: {
text: '从Home改变的info1',
},
})
}
render() {
console.log(this.props.info1)
return (
<div>
<p>我是home页</p>
<p>{this.props.info1}</p>
<Button onClick={this.changeFormHome}> home点击更改redux</Button>
</div>
)
}
}
const mapStateToProps = (model: any) => ({
info1: model[namespace].info1,
})
export default connect(mapStateToProps)(Home)
看了上面的代码,你是不是瞬间理解了,咱们还是通过react-redux的connect来获取model的,只不过我们通过namespace指定了具体是哪个model。到目前为止,咱们已经知道,组件如何从model里面取值。那么我们在组件里面如何改变 model里面的 state呢?
现在我们的home.tsx页面上有一个按钮,点击这个按钮会dispatch一个action:
...
dispatch({
type: `${namespace}/changeInfo1`,
payload: {
text: '从Home改变的info1',
},
})
...
可以看到,这个 action 的 type 为我们的model/effects/changeInfo1。看到这里相信你已经理解得差不多了,对的,就是通过dispatch 一个和 effects里面的一个同名方法,即可找到effects对应的方法。
接下来到effect对应的changeInfo1看看是如何改变 state。
...
*changeInfo1({ payload }, { put }) {
try {
const { text } = payload
yield put({
type: 'setState',
payload: { info1: text },
})
// eslint-disable-next-line no-empty
} catch (error) {}
}
...
我们通过put ,发起了一个action,到最终的reducer,其中的payload来自我们传入的text,然后state被修改了,同时页面也刷新了
除了put,还有call(用于发起异步请求)、select(用于取出 state 里面的值),都是redux-saga提供的,这里不过多叙述,具体使用的方法请查阅 redux-saga 文档。
我这里整理了两个图,对比redux和dva的流程操作:


简单总结一下:dva 把 action -> reducer拆分成了,action -> model(reducer, effect)我们在写业务的时候,再也不用到处去找我们的reducer、action了。dva真的给我们提供了极大的便利。
本文详细介绍了React中的组件通信方法,包括props的父子组件双向数据流、利用props回调进行通信、Context的跨层级通信,以及OnRef和ref的DOM节点管理。同时涵盖了状态管理的最佳实践,如Redux的基础概念、状态提升和使用dva简化开发。
5577





