redux上手
npm install redux --D
redux
中的store
是管理数据的政委,具有全局唯一性,所有的数据都在这一个数据源里进行管理,但是本身redux
和react
并没有直接的联系。只有复杂的项目才需要redux
来管理数据,简单项目,state + props + content
足矣。
redux
特点:
- 需要一个
store
来存储数据 -
store
里的state
是放置数据的地方 - 通过
dispatch
一个action
来提交对数据的修改 - 请求提交到
reducer
函数里,根据传入的action
和state
,返回新的state
react-redux
npm install react-redux -D
提供两个api
1、Provider
顶级组件,提供数据
2、connect
高阶组件提供数据和方法
在调用组件的地方,增加如下代码
import {Provider} from 'react-redux';
import store from './store';
<Provider store={store}>
<ReduxText></ReduxText>
</Provider>
3、ReduxText.js做如下修改
import React from 'react'
// import store from '../store.js'
import {connect} from 'react-redux'
const mapStateToProps = state => ({num: state})
const mapDispatchToProps = {
add: () => ({type: 'add'}),
minus: () => ({type: 'minus'}),
}
function ReduxText({num,add,minus}){
return (
<div>
<p>{num}</p>
<div>
<button onClick={minus}>-</button>
<button onClick={add}>+</button>
</div>
</div>
)
}
export default connect(mapStateToProps,mapDispatchToProps)(ReduxText)
使用装饰器的写法
// 装饰器写法
@connect(mapStateToProps,mapDispatchToProps)
class ReduxText extends Component {
render(){
const {num,add,minus} = this.props;
return (
<div>
<p>{num}</p>
<div>
<button onClick={minus}>-</button>
<button onClick={add}>+</button>
</div>
</div>
)
}
}
export default ReduxText
异步
react默认只支持同步,实现异步任务,比如延迟,网络请求,需要中间件的支持,比如我们使用简单的redux-thunk
和redux-logger
npm install redux-thunk -D
npm install redux-logger -D
// store.js
import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'add':
return state + 1
case 'minus':
return state - 1
default:
return state
}
}
const store = createStore(counterReducer, applyMiddleware(logger, thunk))
export default store;
// ReduxTest.js
import React, {Component} from 'react'
import {connect} from 'react-redux'
const mapStateToProps = state => ({num: state})
const mapDispatchToProps = {
add: () => ({type: 'add'}),
minus: () => ({type: 'minus'}),
asyncAdd: () => dispatch => {
// 做异步操作
setTimeout(() => {
dispatch({type: 'add'})
},1500)
}
}
// 装饰器写法
@connect(mapStateToProps,mapDispatchToProps)
class ReduxText extends Component {
render(){
const {num,add,minus,asyncAdd} = this.props;
return (
<div>
<p>{num}</p>
<div>
<button onClick={minus}>-</button>
<button onClick={add}>+</button>
<button onClick={asyncAdd}>asyncAdd</button>
</div>
</div>
)
}
}
export default ReduxText
由图可以看出,先执行了异步操作,数据回来了后,才执行增加的操作
如何代码拆分优化:
1、新建一个store文件统一管理
2、store中的文件如下
// index.js
import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import { counterReducer } from './count.js'
const store = createStore(counterReducer, applyMiddleware(logger, thunk))
export default store;
// count.js
export const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'add':
return state + 1
case 'minus':
return state - 1
default:
return state
}
}
// action creator
export const add = () => ({ type: 'add' });
export const minus = () => ({ type: 'minus' });
export const asyncAdd = () => dispatch => {
// 做异步操作
setTimeout(() => {
dispatch({ type: 'add' })
}, 1500)
}
3、组件中使用
import {add,minus,asyncAdd} from '../store/count.js'
如何模块化 (combineReducers)
// store index.js
// 引入
import { createStore, applyMiddleware, combineReducers } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import { counterReducer, } from './count.js'
const store = createStore(
// 使用
combineReducers({ counter: counterReducer }),
applyMiddleware(logger, thunk)
)
export default store;
// 调用时也有部分修改,要使用命名空间的方式获取
const mapStateToProps = state => ({num: state.counter})
react-router
npm install react-router-dom -D
路由基本用法
// RouterSample.js
import React from 'react'
import {BrowserRouter,Link,Route} from 'react-router-dom'
function Home(){
return (
<div>
Home
</div>
)
}
function About(){
return (
<div>
About
</div>
)
}
export default function RouterSample(){
return (
<div>
<BrowserRouter>
<div>
{/* 导航链接 */}
<ul>
<li>
<Link to="/">首页</Link>
</li>
<li>
<Link to="/about">关于</Link>
</li>
</ul>
{/* 路由配置:路由即组件 */}
{/* 注意exact 路由匹配是包容性质 */}
<Route exact path="/" component={Home}></Route>
<Route path="/about" component={About}></Route>
</div>
</BrowserRouter>
</div>
)
}
路由传参(动态路由)
import React from 'react'
import {BrowserRouter,Link,Route} from 'react-router-dom'
function Home(){
return (
<div>
<h3>课程列表</h3>
<ul>
<li>
<Link to="/detail/web">web架构师</Link>
</li>
<li>
<Link to="/detail/python">python架构师</Link>
</li>
</ul>
</div>
)
}
function About(){
return (
<div>
About
</div>
)
}
// 传递进来路由器对象
function Detail(props){
// 1、history: 导航指令 后退等
// 2、match: 获取参数信息
// 3、location: 当前url信息
console.log(props)
return (
<div>
{/* 动态参:params, 查询参:query*/}
当前课程是:{props.match.params.course}
<button onClick={props.history.goBack}>后退</button>
</div>
)
}
export default function RouterSample(){
return (
<div>
<BrowserRouter>
<div>
{/* 导航链接 */}
<ul>
<li>
<Link to="/">首页</Link>
</li>
<li>
<Link to="/about">关于</Link>
</li>
</ul>
{/* 路由配置:路由即组件 */}
{/* 注意exact 路由匹配是包容性质 */}
<Route exact path="/" component={Home}></Route>
<Route path="/detail/:course" component={Detail}></Route>
<Route path="/about" component={About}></Route>
</div>
</BrowserRouter>
</div>
)
}
404问题,路由匹配不到
import {BrowserRouter,Link,Route,Switch} from 'react-router-dom'
function NoMatch({location}){
return (
<div>
404,{location.pathname}不存在
</div>
)
}
{/* 路由匹配是包容性质 */}
{/* Switch组件只会展示一个 */}
<Switch>
<Route exact path="/" component={Home}></Route>
<Route path="/detail/:course" component={Detail}></Route>
<Route path="/about" component={About}></Route>
{/* 404 没有path */}
<Route component={NoMatch}></Route>
</Switch>
路由嵌套
// 当前用户信息
function About(params){
return (
<div>
<h3>个人中心</h3>
<div>
<Link to="/about/me">个人信息</Link>
<Link to="/about/order">订单查询</Link>
</div>
<Switch>
<Route path="/about/me" component={() => (<div>Me</div>)}></Route>
<Route path="/about/order" component={() => (<div>order</div>)}></Route>
</Switch>
</div>
)
}
设置页面进来的默认页面
import {BrowserRouter,Link,Route,Switch,Redirect} from 'react-router-dom'
<Switch>
<Route path="/about/me" component={() => (<div>Me</div>)}></Route>
<Route path="/about/order" component={() => (<div>order</div>)}></Route>
{/* 访问/about时会重定向到/about/me */}
<Redirect path="/about/me"></Redirect>
</Switch>
路由的守卫
// store 新建 user.redux.js
const initial = {
isLogin: false,
loading: false
}
export const user = (state = initial, action) => {
switch (action.type) {
case 'requestLogin':
return {
isLogin: false,
loading: true
}
case 'login':
return {
isLogin: true,
loading: false
}
default:
return state
}
}
export const login = () => dispatch => {
dispatch({ type: 'requestLogin' })
// 做异步操作
setTimeout(() => {
dispatch({ type: 'login' })
}, 2000)
}
// store index.js 注册下
import { user } from './user.redux'
const store = createStore(
combineReducers({ counter: counterReducer, user })
)
// RouterSample.js
import React from 'react'
import {BrowserRouter,Link,Route,Switch,Redirect} from 'react-router-dom'
import {connect} from 'react-redux'
import {login} from '../store/user.redux'
function Home(params){
return (
<div>
<h3>课程列表</h3>
<ul>
<li>
<Link to="/detail/web">web架构师</Link>
</li>
<li>
<Link to="/detail/python">python架构师</Link>
</li>
</ul>
</div>
)
}
// 当前用户信息
function About(params){
return (
<div>
<h3>个人中心</h3>
<div>
<Link to="/about/me">个人信息</Link>
<Link to="/about/order">订单查询</Link>
</div>
<Switch>
<Route path="/about/me" component={() => (<div>Me</div>)}></Route>
<Route path="/about/order" component={() => (<div>order</div>)}></Route>
{/* 访问/about时会重定向到/about/me */}
<Redirect to="/about/me"></Redirect>
</Switch>
</div>
)
}
// 传递进来路由器对象
function Detail(props){
// 1、history: 导航指令 后退等
// 2、match: 获取参数信息
// 3、location: 当前url信息
console.log(props)
return (
<div>
{/* 动态参:params, 查询参:query*/}
当前课程是:{props.match.params.course}
<button onClick={props.history.goBack}>后退</button>
</div>
)
}
function NoMatch({location}){
return (
<div>
404,{location.pathname}不存在
</div>
)
}
// 路由守卫
// 希望用法:<PrivateRoute path="/xxx" component={} ... />
const PrivateRoute = connect( state => ({
isLogin: state.user.isLogin
})
)(
({component: Comp, isLogin, ...rest}) => {
// 做认证
// render 根据条件动态渲染组件
return (
<Route {...rest} render={
props => isLogin ? <Comp></Comp> : <Redirect to={{
pathname: '/login',
state: {
redirect: props.location.pathname
}
}}></Redirect>
}></Route>
)
}
)
// 登录组件的设置
const Login = connect(
state => ({
isLogin: state.user.isLogin,
loading: state.user.loading,
}),{login}
)(
function Login({location, isLogin, login, loading}){
const redirect = location.state.redirect || '/';
if(isLogin){
return <Redirect to={redirect}></Redirect>
}
return (
<div>
<p>用户登录</p>
<hr></hr>
<button onClick={login} disabled={loading}>{loading ? '登录中' : '登录'}</button>
</div>
)
}
)
export default function RouterSample(){
return (
<div>
<BrowserRouter>
<div>
{/* 导航链接 */}
<ul>
<li>
<Link to="/">首页</Link>
</li>
<li>
<Link to="/about">关于</Link>
</li>
</ul>
{/* 路由配置:路由即组件 */}
{/* 注意exact 路由匹配是包容性质 */}
{/* Switch组件只会展示一个 */}
<Switch>
<Route exact path="/" component={Home}></Route>
<Route path="/detail/:course" component={Detail}></Route>
<PrivateRoute path="/about" component={About}></PrivateRoute>
<Route path="/login" component={Login}></Route>
{/* 404 没有path */}
<Route component={NoMatch}></Route>
</Switch>
</div>
</BrowserRouter>
</div>
)
}
redux原理
import { compose } from "redux"
export function createStore(reducer, enhancer) {
// reducer 更新状态的函数
// enhancer强化器 又被称为高阶函数
if (enhancer) {
// 返回一个强化后的新函数
return enhancer(createStore)(reducer)
}
let currentState = {} // 当前状态
let currentListeners = [] // 当前监听器
// 暴露3个接口
function getState() {
return currentState
}
// 状态变更后,要执行的回调函数放到currentListeners数组中
function subscribe(listener) {
currentListeners.push(listener)
}
// 执行真正的更新操作
function dispatch(action) {
// 根据action指令做更新,返回全新的状态
currentState = reducer(currentState, action)
// 通知所有的listener做更新
currentListeners.forEach(v => v())
return action
}
dispatch({ type: '@IMOOC/WONIU-REDUX' })
return { getState, subscribe, dispatch }
}
export function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = store.dispatch
// 让中间件可以获取当前的状态和能够派发
const midApi = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// middlewareChain 称为中间件链
const middlewareChain = middlewares.map(middleware => middleware(midApi))
// 把一个数组按照一个顺序进行包裹复合,最终生成一个函数,把store.dispatch传进去进行强化返回
dispatch = compose(...middlewareChain)(store.dispatch)
return {
...store,
dispatch
}
}
}
export function compose(...funcs) {
if (funcs.length == 0) {
return arg => arg
}
if (funcs.length == 1) {
return funcs[0]
}
// 进行聚合操作 变成一层函数套另一层函数 从洋葱的心开始执行
return funcs.reduce((ret, item) => (...args) => ret(item(...args)))
}
react-redux原理
import React,{Component} from 'react'
import PropTypes from 'prop-types'
import { bindActionCreators } from 'redux'
export const connect = (mapStateToProps = state => state,mapDispatchToProps = {}) =>
(wrapComponent) => {
return class ConnectComponent extends Component{
// 上下文
static contextType = {
store: PropTypes.object
}
constructor(props,context){
super(props,context)
this.state = {
props: {}
}
}
componentDidMount(){
const {store} = this.context
// 订阅更新 如果有状态发生变化,执行更新函数
store.subscribe(() => this.update())
this.update()
}
update(){
const {store} = this.context
// 映射到组件的属性中
const stateProps = mapStateToProps(store.getState())
// 组合成一个新的对象
const dispatchProps = bindActionCreators(mapDispatchToProps,store.dispatch)
this.setState({
props: {
...this.state.props,
...stateProps,
...dispatchProps
}
})
}
render(){
return <wrapComponent {...this.state.props}></wrapComponent>
}
}
}
redux-thunk原理
const thunk = ({ dispatch, getState }) => next => action => {
if (typeof action == 'function') {
return action(dispatch, getState)
}
// 如果是普通对象,进入洋葱的下一层
return next(action)
}
export default thunk
redux-saga使用
redux-saga
使(数据获取、浏览器缓存获取)易于管理、执行、测试和失败处理。redux-saga
是基于generater实现的。
npm install redux-saga -D
- 1、在
store
文件夹下创建sagas.js
// call 用来调用异步函数
// put 通过异步函数拿到结果,通过put去通知状态更新
// takeEvery 负责全局监听action,拦截action,然后去执行异步的事情
import { call, put, takeEvery } from 'redux-saga/effects'
// 模拟登陆
const UserService = {
login(uname) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (uname === 'panjie') {
resolve({
id: 1,
name: 'panjie',
age: 20
})
} else {
reject('用户名或密码错误')
}
}, 1000)
})
}
}
function* login(action) {
try {
yield put({ type: 'requestLogin' })
const result = yield call(UserService.login, action.uname)
yield put({ type: 'loginSuccess', result })
} catch (error) {
yield put({ type: 'loginFailure', error })
}
}
// 作为redux的中间件,提前拦截action,不直接走reducer,而是走我们自己的逻辑先去处理
// 当事情处理完了再继续往下执行
function* mySaga() {
yield takeEvery('login', login)
}
export default mySaga
- 2、在
store
文件夹中的index.js
中注册上述步骤中导出的mySaga
import { createStore, applyMiddleware, combineReducers } from 'redux'
import logger from 'redux-logger'
import { user } from './user.redux'
import createSagaMiddleware from 'redux-saga'
import mySaga from './sagas'
// 1、创建saga中间件并注册
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
combineReducers({ user }),
applyMiddleware(logger, sagaMiddleware)
)
// 2、中间件运行saga
sagaMiddleware.run(mySaga)
export default store
- 3、修改
store
文件中的user-redux.js
const initial = {
isLogin: false,
loading: false,
error: ''
}
export const user = (state = initial, action) => {
switch (action.type) {
case 'requestLogin':
return {
isLogin: false,
loading: true,
error: ''
}
case 'loginSuccess':
return {
isLogin: true,
loading: false,
error: ''
}
case 'loginFailure':
return {
isLogin: false,
loading: false,
error: action.error
}
default:
return state
}
}
export function login(uname) {
return {
type: 'login',
uname
}
}
// export const login = () => dispatch => {
// dispatch({ type: 'requestLogin' })
// // 做异步操作
// setTimeout(() => {
// dispatch({ type: 'login' })
// }, 2000)
// }
- 4、完善页面
// RouterSample.js
// 登录组件的设置
const Login = connect(
state => ({
isLogin: state.user.isLogin,
loading: state.user.loading,
error: state.user.error // 登录错误信息
}),{login}
)(
function Login({location, isLogin, login, loading, error}){ // 登录错误信息
const redirect = location.state.redirect || '/';
const [uname, setUname] = useState('') // 用户名输入状态
if(isLogin){
return <Redirect to={redirect}></Redirect>
}
return (
<div>
<p>用户登录</p>
<hr></hr>
{/* 登录错误信息展示 */}
{error && <p>{error}</p>}
{/* 输入用户姓名 */}
<input type="text" onChange={e => setUname(e.target.value)} value={uname}></input>
<button onClick={() => login(uname)} disabled={loading}>{loading ? '登录中' : '登录'}</button>
</div>
)
}
)
企业级框架
umi
- 开箱即用,内置
reatc,react-router
等 - 类
next.js
且功能完备的路由约定,同时支持配置的路由方式 - 完善的插件系统,覆盖从源码到构建产物的每个生命周期
- 高性能,通过插件支持
PWA
,以路由为单元的code splitting
等 - 支持静态页面导出,适配各种环境,比如中台业务,无线业务,egg,支付宝钱包,云凤蝶等
- 开发启动快,支持一键开启
dll
和hard-source-webpack-plugin
等 - 一键兼容到IE9,基于
umi-plugin-polyfills
- 完善的TypeScript支持,包括
d.ts
定义和umi test
- 与
dva
数据流的深入融合,支持duck directory、model
的自动加载等等
dva+umi
的约定
1、src源码
- pages页面
- components组件
- layout布局
- model
2、config配置
3、mock数据模拟
4、test测试等
项目骨架
// 创建一个文件 umi-test
npm init
yarn global add umi
新建页面
umi g page index
umi g page users
起服务
umi dev
动态路由
以$
开头的文件或目录
// 创建users/$id.js
import React from 'react';
export default function (props) {
return (
<div>
{props.match.params.id}
</div>
);
}
嵌套路由
umi g page ./users/_layout
import styles from './_layout.css';
export default function(props) {
return (
<div className={styles.normal}>
<h1>Page _layout</h1>
<div>{props.children}</div>
</div>
);
}
页面跳转
// users/index.js
import styles from './index.css';
import Link from 'umi/link';
// router是个实例,全局只有一个
import router from 'umi/router'
export default function() {
// 模拟数据
const users = [
{
id: 1,
name: 'tom'
},
{
id: 2,
name: 'jerry'
}
]
return (
<div className={styles.normal}>
<h1>Page users</h1>
<ul>
{users.map(u => (
// 声明式
// <li key={u.id}>
// <Link to={`/users/${u.id}`}>{u.name}</Link>
// </li>
// 命令式
<li key={u.id} onClick={() => router.push(`/users/${u.id}`)}>
{u.name}
</li>
))}
</ul>
</div>
);
}
配置式路由
默认路由为声明式,根据pages
下面内容自动生成路由,业务复杂后仍需配置路由
// 根目录下新建config/config.js
export default {
routes: [{
path: '/about',
component: './about',
},
{
path: '/',
component: './index',
},
{
path: '/users',
component: './users/_layout',
routes: [{
path: '/users/',
component: './users/index',
},
{
path: '/users/:id',
component: './users/$id',
},
],
},
],
};
404页面
umi g page NotFound
// config.js中添加不带path的路由配置
{
component: './NotFound'
}
权限路由
- 通过配置路由的
Routes
属性来实现
{
path: '/about',
component: './about',
// 这里相对根目录,文件名后缀不能少
Routes: ['./routes/PrivateRoute.js']
}
- 创建./routes/PrivateRoute.js
import Redirect from 'umi/redirect'
export default props => {
// 50%概率需要去登录页面
if(Math.random() > 0.5){
return <Redirect to='/login'></Redirect>
}
return (
<div>
<div>PrivateRoute (routes/PrivateRoute.js)</div>
{props.children}
</div>
)
}
- 创建登录页面
umi g page login
引入antd
npm install antd -D
npm isntall umi-plugin-react -D
修改config/config.js
plugins: [
['umi-plugin-react', {
antd: true
}],
],
使用
import {Button} from 'antd'
export default () => {
return <div> <Button>登录</Button></div>
}
引入dva
配置dva
export default {
plugins: [
['umi-plugin-react', {
antd: true,
dva: true,
}], ],
// ...
}
创建model: 维护页面数据状态
src下新建models/goods.js
export default {
namespace: 'goods', // model的命名空间,区分多个model
state: [{ title: "web全栈" },{ title: "java架构师" }], // 初始状态 effects:{}, // 异步操作
reducers: { // 更新状态 }
}
使用状态
创建页面goods.js
umi g page goods
// pages/goods.js
import styles from './goods.css';
import { connect } from 'dva'
import { Card, Button } from 'antd'
import { useEffect } from 'react'
export default connect(state => ({
loading: state.loading, // dva可以通过loading空间获取加载状态
goodsList: state.goods // 获取指定命名空间的模型状态
}),{
addGood: title => ({
type: "goods/addGood", // action的type需要以命名空间为前缀+reducer名称
title
}),
getList: () => ({
type: "goods/getList"
})
})(function({goodsList,addGood,getList,loading}) {
useEffect(() => {
getList();
},[])
console.log(loading)
return (
<div className={styles.normal}>
<h1>Page goods</h1>
{/* 商品列表 */}
<div>
{/* 加载状态 */}
{loading.models.goods && <p>loading...</p> }
{goodsList.map(good => {
return (
<Card key={good.title}>
<div>{good.title}</div>
</Card>
)
})}
<div>
<Button onClick={() => addGood('商品' + new Date().getTime())}>添加商品</Button>
</div>
</div>
</div>
);
})
更新模型
// src/models/goods.js
import axios from 'axios'
// api 项目中写到service层
function getGoods() {
return axios.get('/api/goods')
}
export default {
namespace: 'goods', // model的命名空间,区分多个model
state: [], // 初始状态
effects: {
// 异步操作
* getList(action, { call, put }) {
const res = yield call(getGoods)
yield put({
type: 'initGoods',
payload: res.data.result
})
}
},
reducers: { // 同步操作
// 更新状态
addGood(state, action) {
return [...state, { title: action.title }]
},
initGoods(state, action) {
return action.payload
}
}
}
数据mock
: 模拟数据接口
// 根目录 新建mock/goods.js
const data = [{ title: "web全栈" }, { title: "java架构师" }];
export default {
'get /api/goods': function(req, res) {
setTimeout(() => {
res.json({ result: data })
}, 1250)
},
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。