part4-m2

  • 本阶段将带你学习前端圈子中口碑极佳的 React 框架以及它的一些高阶用法、组件库封装、数据流方案、服务端渲染(SSR)、静态站点生成(SSG),同时深入 React 框架内部,剖析 React 部分核心源码和实现,最后还会结合 TypeScript 和蚂蚁金服的 Ant Design 库做出实战。

模块二 React 数据流方案专题(Redux、MobX

  • 随着一个应用规模的增长,应用的数据状态一定会变得越来越复杂,慢慢的数据流管理成为最主要话题。本模块中我们重点学习 React 数据流方案,其中会包括两个具体的库,分别是 Redux、MobX。掌握这些内容,轻松应对大型应用中的数据状态管理。

任务一:Redux

  1. 【课程资料】课程资料购物车代码shooping_serve

  2. 【课程资料】ReduxTookit资料

  3. 【课程资料】课程资料

  4. Redux专题内容介绍

  • Redux 核心
  • React + Redux
  • Redux 中间件
  • 开发 Redux 中间件
  • Redux 综合案例
  1. Redux简介

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MOSSEAYI-1629715365861)(./img/1629439983089.jpg)]

  1. Redux核心概念及工作流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ELOUVXsc-1629715365864)(./img/1629440227491.jpg)]

  1. Redux计数器案例
<button id="plus">+</button>
<span id="count">0</span>
<button id="minus">-</button>
<script src="redux.min.js"></script>
// 3. 存储默认状态
var initialState = {
  count: 0
}
// 2. 创建 reducer 函数
function reducer (state = initialState, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1}
    default:
      return state;
  }
}
// 1. 创建 store 对象
var store = Redux.createStore(reducer);
// 4. 定义 action
var increment = { type: 'increment' };
var decrement = { type: 'decrement' };
// 5. 获取按钮 给按钮添加点击事件
document.getElementById('plus').onclick = function () {
  // 6. 触发action
  store.dispatch(increment);
}
document.getElementById('minus').onclick = function () {
  // 6. 触发action
  store.dispatch(decrement);
}
// 7. 订阅 store
store.subscribe(() => {
  // 获取store对象中存储的状态
  // console.log(store.getState());
  document.getElementById('count').innerHTML = store.getState().count;
})
  1. Redux核心API总结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YpfJVCjQ-1629715365867)(./img/1629441565703.jpg)]

  1. 在React中使用Redux解决的问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8gamF7Pc-1629715365870)(./img/1629441677942.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dKTX6M6I-1629715365871)(./img/1629441839579.jpg)]

  1. React 计数器
  • 下载 Redux
    • npm install redux react-redux (第二个文件让react和redux更好的结合在一起)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2mxNhPqs-1629715365872)(./img/1629442093262.jpg)]

  • npm install create-react-app -g
  • create-react-app react-redux-guide
  • npm install redux react-redux
  1. Provider组件与connect方法

  2. 使用connect方法的第二个参数简化组件视图

  3. bindActionsCreators方法的使用

  4. 代码重构

  5. Action传递参数

  6. redux实现弹出框案例

  7. 拆分合并reducer

  8. 中间件概念介绍

  • 什么是中间件
  • 中间件允许我们扩展redux应用程序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t7lGk2Au-1629715365874)(./img/1629450060895.jpg)]

  1. 开发Redux中间件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pICy4FiG-1629715365875)(./img/1629450474242.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mtrg0k0F-1629715365875)(./img/1629450696478.jpg)]

  1. Redux中间件开发实例thunk
export default ({dispatch}) => next => action => {
  // 1. 当前这个中间件函数不关心你想执行什么样的异步操作 只关心你执行的是不是异步操作
  // 2. 如果你执行的是异步操作 你在触发action的时候 给我传递一个函数 如果执行的是同步操作 就传递action对象
  // 3. 异步操作代码要写在你传递进来的函数中
  // 4. 当前这个中间件函数在调用你传递进来的函数时 要将dispatch方法传递过去
  if (typeof action === 'function') {
    return action(dispatch)
  }
  next(action)
}
  1. Redux-thunk中间件的使用
  • redux-thunk 允许在redux的工作流程中加入异步操作
  • npm install redux-thunk
  • import thunk from ‘redux-thunk’

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lw4mBmMF-1629715365876)(./img/1629452193396.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XxbpWbgs-1629715365877)(./img/1629452273211.jpg)]

  1. Redux-saga中间件的使用
  • redux-saga 可以将异步操作从 Action Creator 文件中抽离出来,放在一个独立的文件中
  • npm install redux-saga

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jFW84swG-1629715365878)(./img/1629452640707.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uaj1njMu-1629715365879)(./img/1629452698170.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kXRzrvAe-1629715365880)(./img/1629452769603.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Xv02iyw-1629715365881)(./img/1629452903005.jpg)]

  1. Redux-saga中的action传参

  2. saga文件的拆分与合并

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

import { Provider } from 'react-redux';
import { store } from './store';

ReactDOM.render(
  // 通过provider组件 将 store 放在了全局的组件可以够的到的地方
  <Provider store={store}><App/></Provider>,
  document.getElementById('root')
);

/*
  react-redux
    Provider
    connect
*/

// src/App.js
import React from 'react';
import Counter from './components/Counter';

function App() {
  return <div>
    <Counter/>
  </div>
}

export default App;

// src/components/Counter.js

import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as couterActions from '../store/actions/counter.actions';

function Counter ({count, increment, decrement, increment_async}) {
  return <div>
    <button onClick={() => increment_async(20)}>+</button>
    <span>{count}</span>
    <button onClick={() => decrement(5)}>-</button>
  </div>
}

// 1. connect 方法会帮助我们订阅store 当store中的状态发生更改的时候 会帮助我们重新渲染组件
// 2. connect 方法可以让我们获取store中的状态 将状态通过组件的props属性映射给组件
// 3. connect 方法可以让我们获取 dispatch 方法

const mapStateToProps = state => ({
  count: state.counter.count
});

const mapDispatchToProps = dispatch => bindActionCreators(couterActions, dispatch)

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

// src/store------------------------------------------------

// index.js
import { createStore, applyMiddleware } from "redux";
import RootReducer from "./reducers/root.reducer";
// import thunk from 'redux-thunk';
// import logger from "./middleware/logger";
// import test from "./middleware/test";
// import thunk from './middleware/thunk';
import createSagaMidddleware from 'redux-saga';
import rootSaga from './sagas/root.saga';

// 创建 sagaMiddleware
const sagaMiddleware = createSagaMidddleware();

export const store = createStore(RootReducer, applyMiddleware(sagaMiddleware));

// 启动 counterSaga
sagaMiddleware.run(rootSaga)

// actions/counter.actions.js
import { INCREMENT, DECREMENT, INCREMENT_ASYNC } from "../const/counter.const";

export const increment = payload => ({type: INCREMENT, payload});
export const decrement = payload => ({type: DECREMENT, payload});

export const increment_async = payload => ({type: INCREMENT_ASYNC, payload});

// const/counter.const.js
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
export const INCREMENT_ASYNC = 'increment_async'

// redecers/counter.redecers.js

import { INCREMENT, DECREMENT } from "../const/counter.const";

const initialState = {
  count: 0
}

export default (state = initialState, action) => {
  switch(action.type) {
    case INCREMENT:
      return {
        ...state,
        count: state.count + action.payload
      }
    case DECREMENT:
      return {
        ...state,
        count: state.count - action.payload
      }
    default: 
      return state;
  }
}

// redecers/root.reducer.js
import CounterReducer from './counter.reducer';

// { counter: { count: 0 }, model: { show: false } }
export default combineReducers({
  counter: CounterReducer
})

// saga/counter.saga.js
import { takeEvery, put, delay } from 'redux-saga/effects';
import { increment } from '../actions/counter.actions';
import { INCREMENT_ASYNC } from '../const/counter.const';

// takeEvery 接收 action
// put 触发 action

function* increment_async_fn (action) {
  yield delay(2000);
  yield put(increment(action.payload))
}

export default function* counterSaga () {
  // 接收action
  yield takeEvery(INCREMENT_ASYNC, increment_async_fn)
}

// saga/root.saga.js
import { all } from 'redux-saga/effects';
import counterSaga from './counter.saga';
import modalSaga from './modal.saga';

export default function* rootSaga () {
  yield all([
    counterSaga(),
    modalSaga()
  ])
}
  1. redux-actions中间件的使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hSPGDUNM-1629715365883)(./img/1629465789291.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V3TZ1EVJ-1629715365884)(./img/1629465970510.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2A7U8YG1-1629715365885)(./img/1629466130515.jpg)]

  • npm install redux-actions
  1. shopping项目初始化
  • create-react-app shoping
  • 资料 文件夹下
  • shoppingCartService 文件夹下启动服务端
  1. shopping项目搭建redux工作流
  • npm install redux react-redux redux-saga redux-actions
  1. 实现商品列表数据展示

  2. 将商品加入到购物车中

  3. 购物车列表数据展示

  4. 从购物车中删除商品

  5. 更改购物车中商品的数量

  6. 更正视图图片显示错误问题

  7. 计算商品总价

  8. Redux源码实现:核心逻辑

  9. Redux源码实现:参数类型约束

  10. Redux源码实现:Enhancer

  • 可以对Redux进行增强,中间件
  1. Redux源码实现:applyMiddleware
  • 可以合并多个中间件
  1. Redux源码实现:bindActionCreators
  • 更加便捷创建 action
  1. Redux源码实现:combineReducers
  • 合并多个 reducer
function createStore (reducer, preloadedState, enhancer) {
  // reducer 类型判断 
  if (typeof reducer !== 'function') throw new Error('redcuer必须是函数');

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('enhancer必须是函数')
    }
    return enhancer(createStore)(reducer, preloadedState);
  }
  // 状态
  var currentState = preloadedState;
  // 订阅者
  var currentListeners = [];
  // 获取状态
  function getState () {
    return currentState;
  }
  // 用于触发action的方法
  function dispatch (action) {
    // 判断action是否是一个对象
    if (!isPlainObject(action)) throw new Error('action必须是一个对象');
    // 判断action中的type属性是否存在
    if (typeof action.type === 'undefined') throw new Error('action对象中必须有type属性');
    // 调用reducer函数 处理状态
    currentState = reducer(currentState, action);
    // 调用订阅者 通知订阅者状态发生了改变
    for (var i = 0; i < currentListeners.length; i++) {
      var listener = currentListeners[i];
      listener();
    }
  }
  // 订阅状态的改变
  function subscribe (listener) {
    currentListeners.push(listener);
  }

  // 默认调用一次dispatch方法 存储初始状态(通过reducer函数传递的默认状态)
  dispatch({type: 'initAction'})

  return {
    getState,
    dispatch,
    subscribe
  }
}

// 判断参数是否是对象类型
// 判断对象的当前原型对象是否和顶层原型对象相同
function isPlainObject (obj) {
  if (typeof obj !== 'object' || obj === null) return false;
  var proto = obj;
  while (Object.getPrototypeOf(proto) != null) {
    proto = Object.getPrototypeOf(proto)
  }
  return Object.getPrototypeOf(obj) === proto;
}

function applyMiddleware (...middlewares) {
  return function (createStore) {
    return function (reducer, preloadedState) {
      // 创建 store
      var store = createStore(reducer, preloadedState);
      // 阉割版的 store
      var middlewareAPI = {
        getState: store.getState,
        dispatch: store.dispatch
      }
      // 调用中间件的第一层函数 传递阉割版的store对象
      var chain = middlewares.map(middleware => middleware(middlewareAPI));
      var dispatch = compose(...chain)(store.dispatch);
      return {
        ...store,
        dispatch
      }
    }
  }
}

function compose () {
  var funcs = [...arguments];
  return function (dispatch) {
    for (var i = funcs.length - 1; i >= 0; i--) {
      dispatch = funcs[i](dispatch);
    }
    return dispatch;
  }
}

function bindActionCreators (actionCreators, dispatch) {
  var boundActionCreators = {};
  for (var key in actionCreators) {
    (function (key) {
      boundActionCreators[key] = function () {
        dispatch(actionCreators[key]())
      }
    })(key)
  }
  return boundActionCreators;
}

function combineReducers (reducers) {
  // 1. 检查reducer类型 它必须是函数
  var reducerKeys = Object.keys(reducers);
  for (var i = 0; i < reducerKeys.length; i++) {
    var key = reducerKeys[i];
    if (typeof reducers[key] !== 'function') throw new Error('reducer必须是函数');
  }
  // 2. 调用一个一个的小的reducer 将每一个小的reducer中返回的状态存储在一个新的大的对象中
  return function (state, action) {
    var nextState = {};
    for (var i = 0; i < reducerKeys.length; i++) {
      var key = reducerKeys[i];
      var reducer = reducers[key];
      var previousStateForKey = state[key];
      nextState[key] = reducer(previousStateForKey, action)
    }
    return nextState;
  }
}
function counterReducer(state, action) {
  switch (action.type) {
    case "increment":
      return state + 1;
    case "decrement":
      return state - 1;
    default:
      return state;
  }
}
function enhancer (createStore) {
  return function (reducer, preloadedState) {
    var store = createStore(reducer, preloadedState);
    var dispatch = store.dispatch;
    function _dispatch (action) {
      if (typeof action === 'function') {
        return action(dispatch)
      }
      dispatch(action);
    }
    return {
      ...store,
      dispatch: _dispatch
    }
  }
}
var rootReducer = combineReducers({counter: counterReducer})
var store = createStore(rootReducer, {counter: 100}, applyMiddleware(logger, thunk));
store.subscribe(function () {
  document.getElementById("box").innerHTML = store.getState().counter;
});
var actions = bindActionCreators({increment, decrement}, store.dispatch);
function increment () {
  return {type: "increment"}
}
function decrement () {
  return {type: "decrement"};
}
document.getElementById("increment").onclick = function () {
  // logger -> thunk -> reducer
  // store.dispatch({ type: "increment" });
  actions.increment()
};
document.getElementById("decrement").onclick = function () {
  // store.dispatch({ type: "decrement" });
  actions.decrement()
};
  1. Redux Toolkit概述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CRMhLcUt-1629715365886)(./img/1629555824268.jpg)]

  1. 创建状态切片
  • 对于状态切片,我们可以认为它就是原本Redux中的那一个个的小的Reducer函数
  • 在Redux中,原本Reducer函数和Actioon对象需要分别创建,现在通过状态切片替代,它会返回Reducer函数和Action对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XGvIhGo1-1629715365887)(./img/1629556405933.jpg)]

  1. 创建Store

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mR42t8IK-1629715365888)(./img/1629556971428.jpg)]

  1. 配置Provider触发Action

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ewalY2u1-1629715365889)(./img/1629557148174.jpg)]

  1. Action预处理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rNp5V5OQ-1629715365890)(./img/1629557466887.jpg)]

  1. 执行异步操作方式一

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pbpxudhp-1629715365891)(./img/1629557785998.jpg)]

  • npm run json-server
  1. 执行异步操作的第二种方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qxVSFY2r-1629715365892)(./img/1629558145075.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ESJ1sUke-1629715365892)(./img/1629558201770.jpg)]

  1. 配置中间件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0O01fdOw-1629715365893)(./img/1629558145075.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8PAZbl92-1629715365895)(./img/1629558442532.jpg)]

  1. 实体适配器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fjtcWN1f-1629715365896)(./img/1629558666600.jpg)]

  1. 简化实体适配器代码
  • 实体适配器可以自动检测传入的是不是action,如果是,则会找action下payload属性
  1. 将实体唯一标识从id更改为其他字段

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Qg5KyHk-1629715365897)(./img/1629559190561.jpg)]

  1. 状态选择器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AsDTt43I-1629715365897)(./img/1629559393996.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BAtKuuT4-1629715365899)(./img/1629559526029.jpg)]

任务二:Mobx6

  • 2.01-mobx概述

1. 概述

MobX 是一个简单的可扩展的状态管理库,无样板代码风格简约。

目前最新版本为 6,版本 4 和版本 5 已不再支持。

在 MobX 6 中不推荐使用装饰器语法,因为它不是 ES 标准,并且标准化过程要花费很长时间,但是通过配置仍然可以启用装饰器语法。

MobX 可以运行在任何支持 ES5 的环境中,包含浏览器和 Node。

MobX 通常和 React 配合使用,但是在 AngularVue 中也可以使用 MobX。

  • 3.02-mobx中的核心概念

2. 核心概念

  1. observable:被 MobX 跟踪的状态。
  2. action:允许修改状态的方法,在严格模式下只有 action 方法被允许修改状态。
  3. computed:根据现有状态衍生出来的状态。
  4. flow:执行副作用,它是 generator 函数。可以更改状态值。
  • 4.03-mobx工作流程

3. 工作流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o4XrOQhH-1629715365900)(./img/mobx6.png)]

4. 下载

  • mobx:MobX 核心库
  • mobx-react-lite:仅支持函数组件
  • mobx-react:既支持函数组件也支持类组件

yarn add mobx@6.3.1 mobx-react-lite@3.2.0

5. 案例驱动之计数器

在组件中显示数值状态,单击[+1]按钮使数值加一,单击[-1]按钮使数值减一。

  1. 创建用于存储状态的 Store

    export default class CounterStore {
      constructor() {
        this.count = 0
      }
    }
    
  2. 创建用于修改状态的方法

    export default class CounterStore {
      constructor() {
        this.count = 0
      }
      increment() {
        this.count += 1
      }
      decrement() {
        this.count -= 1
      }
    }
    
  3. 让 MobX 可以追踪状态的变化

    1. 通过 observable 标识状态,使状态可观察
    2. 通过 action 标识修改状态的方法,状态只有通过 action 方法修改后才会通知视图更新
    import { action, makeObservable, observable } from "mobx"
    
    export default class CounterStore {
      constructor() {
        this.count = 0
        makeObservable(this, {
          count: observable,
          increment: action,
          decrement: action
        })
      }
      increment() {
        this.count += 1
      }
      decrement() {
        this.count -= 1
      }
    }
    
  4. 创建 Store 类的实例对象并将实例对象传递给组件

    // App.js
    import Counter from "./Counter"
    import CounterStore from "../store/Counter"
    
    const counterStore = new CounterStore()
    
    function App() {
      return <Counter counterStore={counterStore} />
    }
    
    export default App
    
  5. 在组件中通过 Store 实例对象获取状态以及操作状态的方法

    function Counter({ counterStore }) {
      return (
        <Container>
          <Button onClick={() => counterStore.increment()}>
            INCREMENT
          </Button>
          <Button>{counterStore.count}</Button>
          <Button onClick={() => counterStore.decrement()}>
            DECREMENT
          </Button>
        </Container>
      )
    }
    
    export default Counter
    
  6. 当组件中使用到的 MobX 管理的状态发生变化后,使视图更新。通过 observer 方法包裹组件实现目的

    import { observer } from "mobx-react-lite"
    
    function Counter() { }
    
    export default observer(Counter)
    
  7. 简化组件代码

    function Counter({ counterStore }) {
      const { count, increment, decrement } = counterStore
      return (
        <Container>
          <Button border="left" onClick={increment}>
            INCREMENT
          </Button>
          <Button>{count}</Button>
          <Button border="right" onClick={decrement}>
            DECREMENT
          </Button>
        </Container>
      )
    }
    
  8. 当代码简化后,修改状态的方法中的 this 指向出现了问题,通过 action.bound 强制绑定 this,使 this 指向 Store 实例对象

    import { action, makeObservable, observable } from "mobx"
    
    export default class CounterStore {
      constructor() {
        this.count = 0
        makeObservable(this, {
          count: observable,
          increment: action.bound,
          decrement: action.bound
        })
      }
      increment() {
        this.count += 1
      }
      decrement() {
        this.count -= 1
      }
    }
    
  9. 总结:状态变化更新视图的必要条件

    1. 状态必须被标记为 observable
    2. 更改状态的方法必须被标记为 action
    3. 组件必须通过 observer 方法包裹
  10. 创建 RootStore

    在应用中可存在多个 Store,多个 Store 最终要通过 RootStore 管理,在每个组件都需要获取到 RootStore。

    // store/index.js
    import { createContext, useContext } from "react"
    import CounterStore from "./Counter"
    
    class RootStore {
      constructor() {
        this.counterStore = new CounterStore()
      }
    }
    const rootStore = new RootStore()
    const RootStoreContext = createContext()
    
    export const RootStoreProvider = ({ children }) => {
      return (
        <RootStoreContext.Provider value={rootStore}>
          {children}
        </RootStoreContext.Provider>
      )
    }
    
    export const useRootStore = () => {
      return useContext(RootStoreContext)
    }
    
    // App.js
    import { RootStoreProvider } from "../store"
    import Counter from "./Counter"
    
    function App() {
      return (
        <RootStoreProvider>
          <Counter />
        </RootStoreProvider>
      )
    }
    
    export default App
    
    import { observer } from "mobx-react-lite"
    import { useRootStore } from "../store"
    
    function Counter() {
      const { counterStore } = useRootStore()
      const { count, increment, decrement } = counterStore
      return (
        <Container>
          <Button onClick={increment}>
            INCREMENT
          </Button>
          <Button>{count}</Button>
          <Button onClick={decrement}>
            DECREMENT
          </Button>
        </Container>
      )
    }
    
    export default observer(Counter)
    

6. 案例驱动之 Todo

6.1 创建 Store
  1. 创建用于管理 Todo 任务的 Store

    import { makeObservable, observable } from "mobx"
    
    export default class Todo {
      constructor(todo) {
        this.id = todo.id
        this.title = todo.title
        this.isCompleted = todo.isCompleted || false
        this.isEditing = false
        makeObservable(this, {
          title: observable,
          isCompleted: observable,
          isEditing: observable
        })
      }
    }
    
  2. 创建用于管理 Todo 任务列表的 Store

    import { makeObservable, observable } from "mobx"
    
    export default class TodoStore {
      constructor() {
        this.todos = []
        makeObservable(this, {
          todos: observable
        })
      }
    }
    
6.2 添加任务
  1. 创建向 todo 任务列表中添加 todo 任务的方法

    import { action, makeObservable, observable } from "mobx"
    import Todo from "./Todo"
    
    export default class TodoStore {
      constructor() {
        this.todos = []
        makeObservable(this, {
          todos: observable,
          addTodo: action.bound
        })
      }
      addTodo(title) {
        this.todos.push(new Todo({ title, id: this.generateTodoId() }))
      }
      generateTodoId() {
        if (!this.todos.length) return 1
        return this.todos.reduce((id, todo) => (id < todo.id ? todo.id : id), 0) + 1
      }
    }
    
  2. 在组件中实现添加任务的逻辑

    import { useState } from "react"
    import { useRootStore } from "../../store"
    
    function Header() {
      const [title, setTitle] = useState("")
      const { todoStore } = useRootStore()
      const { addTodo } = todoStore
      return (
        <header className="header">
          <input
            value={title}
            onChange={e => setTitle(e.target.value)}
            onKeyUp={e => {
              if (e.key !== "Enter") return
              addTodo(title)
              setTitle("")
            }}
          />
        </header>
      )
    }
    
    export default Header
    
6.3 显示任务列表
import { observer } from "mobx-react-lite"
import { useRootStore } from "../../store"
import Todo from "./Todo"

function Main() {
  const { todoStore } = useRootStore()
  const { todos } = todoStore
  return (
    <section className="main">
      <ul className="todo-list">
        {todos.map(todo => (
          <Todo key={todo.id} todo={todo} />
        ))}
      </ul>
    </section>
  )
}

export default observer(Main)
function Todo({ todo }) {
  return (
    <li>
      <div className="view">
        <input className="toggle" type="checkbox" />
        <label>{todo.title}</label>
        <button className="destroy" />
      </div>
      <input className="edit" />
    </li>
  )
}

export default Todo
6.4 加载远端任务
  1. 下载 json-server:yarn add json-server@0.16.3

  2. 创建 db.json

    {
      "todos": [
        {
          "id": 1,
          "title": "吃饭",
          "isCompleted": false
        },
        {
          "id": 2,
          "title": "睡觉",
          "isCompleted": false
        },
        {
          "id": 3,
          "title": "打豆豆",
          "isCompleted": false
        }
      ]
    }
    
  3. 在 package.json 文件中添加启动命令

    "scripts": {
        "json-server": "json-server --watch ./db.json --port 3001"
      }
    
  4. 启动 json-server:npm run json-server

  5. 在 todoStore 中添加加载任务列表的方法

    import axios from "axios"
    import { flow, makeObservable, observable } from "mobx"
    import Todo from "./Todo"
    
    export default class TodoStore {
      constructor() {
        this.todos = []
        makeObservable(this, {
          todos: observable,
          loadTodos: flow
        })
        this.loadTodos()
      }
      *loadTodos() {
        let response = yield axios.get("http://localhost:3001/todos")
        response.data.forEach(todo => this.todos.push(new Todo(todo)))
      }
    }
    
6.5 更改任务状态
  1. 在 Todo 类中添加修改任务是否已经完成的方法

    export default class Todo {
      constructor() {
        makeObservable(this, {
          modifyTodoIsCompleted: action.bound
        })
      }
      modifyTodoIsCompleted() {
        this.isCompleted = !this.isCompleted
      }
    }
    
  2. 创建 TodoCompleted 组件实现逻辑

    import { observer } from "mobx-react-lite"
    
    function TodoCompleted({ todo }) {
      const { isCompleted, modifyTodoIsCompleted } = todo
      return (
        <input
          className="toggle"
          type="checkbox"
          checked={isCompleted}
          onChange={modifyTodoIsCompleted}
        />
      )
    }
    
    export default observer(TodoCompleted)
    
  3. Todo 组件中引用TodoCompleted 组件并根据条件决定是否为 li 添加 completed 类名

    import { observer } from "mobx-react-lite"
    import TodoCompleted from "./TodoCompleted"
    
    function Todo({ todo }) {
      return (
        <li className={todo.isCompleted ? "completed" : ""}>
          <div className="view">
            <TodoCompleted todo={todo} />
          </div>
        </li>
      )
    }
    
    export default observer(Todo)
    
6.6 删除任务
  1. todoStore 中添加实现删除任务的方法

    import axios from "axios"
    import { action, makeObservable,  } from "mobx"
    
    export default class TodoStore {
      constructor() {
        makeObservable(this, {
          removeTodo: action.bound
        })
      }
      removeTodo(id) {
        this.todos = this.todos.filter(todo => todo.id !== id)
      }
    }
    
  2. 创建 TodoDelete 组件实现删除 todo 任务逻辑

    import { useRootStore } from "../../store"
    
    function TodoDelete({ id }) {
      const { todoStore } = useRootStore()
      const { removeTodo } = todoStore
      return <button className="destroy" onClick={removeTodo.bind(null, id)} />
    }
    
    export default TodoDelete
    
  3. Todo 组件调用 TodoDelete 组件并传入 todo ID

    import { observer } from "mobx-react-lite"
    import TodoDelete from "./TodoDelete"
    
    function Todo({ todo }) {
      return (
        <li>
          <div className="view">
            <TodoDelete id={todo.id} />
          </div>
        </li>
      )
    }
    
    export default observer(Todo)
    
6.7 编辑任务
  1. 在 todoStore 中添加更改任务是否处于编辑状态的方法

    import { action, makeObservable } from "mobx"
    
    export default class Todo {
      constructor(todo) {
        makeObservable(this, {
          modifyTodoIsEditing: action.bound,
        })
      }
      modifyTodoIsEditing() {
        this.isEditing = !this.isEditing
      }
    }
    
  2. 添加 TodoTitle 组件展示任务标题并为其添加双击事件,当事件发生时将任务更改为可编辑状态

    function TodoTitle({ todo }) {
      const { title, modifyTodoIsEditing } = todo
      return <label onDoubleClick={modifyTodoIsEditing}>{title}</label>
    }
    
    export default TodoTitle
    
  3. Todo 组件中调用 TodoTitle 组件,并为 li 添加 editing 类名

    import { observer } from "mobx-react-lite"
    import TodoTitle from "./TodoTitle"
    import classnames from "classnames"
    
    function Todo({ todo }) {
      return (
        <li className={classnames({ completed: todo.isCompleted, editing: todo.isEditing })} >
          <div className="view">
            <TodoTitle todo={todo} />
          </div>
        </li>
      )
    }
    
    export default observer(Todo)
    
  4. 创建 TodoEditing 组件实现编辑 todo 任务标题

    import { useRef, useEffect } from "react"
    
    function TodoEditing({ todo }) {
      const { title, modifyTodoTitle, isEditing } = todo
      const ref = useRef(null)
      useEffect(() => {
        if (isEditing) ref.current.focus()
      }, [isEditing])
      return (
        <input
          ref={ref}
          className="edit"
          defaultValue={title}
          onBlur={e => modifyTodoTitle(e.target.value)}
        />
      )
    }
    
    export default TodoEditing
    
  5. Todo 组件中调用 TodoEditing 组件并传递 todo 任务

    import { observer } from "mobx-react-lite"
    import TodoTitle from "./TodoTitle"
    import classnames from "classnames"
    import TodoEditing from "./TodoEditing"
    
    function Todo({ todo }) {
      return (
        <li className={classnames({ completed: todo.isCompleted, editing: todo.isEditing })} >
          <div className="view">
            <TodoTitle todo={todo} />
          </div>
          <TodoEditing todo={todo} />
        </li>
      )
    }
    
    export default observer(Todo)
    
6.8 计算未完成任务数量
  1. 在 todoStore 中添加获取未完成任务数量的派生状态

    import axios from "axios"
    import { makeObservable, computed } from "mobx"
    
    export default class TodoStore {
      constructor() {
        makeObservable(this, {
          unCompletedTodoCount: computed
        })
      }
      get unCompletedTodoCount() {
        return this.todos.filter(todo => !todo.isCompleted).length
      }
    }
    
  2. 创建 UnCompletedTodoCount 组件实现逻辑

    import { observer } from "mobx-react-lite"
    import { useRootStore } from "../../store"
    
    function UnCompletedTodoCount() {
      const { todoStore } = useRootStore()
      const { unCompletedTodoCount } = todoStore
      return (
        <span className="todo-count">
          <strong>{unCompletedTodoCount}</strong> item left
        </span>
      )
    }
    
    export default observer(UnCompletedTodoCount)
    
  3. Footer 组件中调用 UnCompletedTodoCount 组件

    import UnCompletedTodoCount from "./UnCompletedTodoCount"
    
    function Footer() {
      return (
        <footer className="footer">
          <UnCompletedTodoCount />
        </footer>
      )
    }
    
    export default Footer
    
6.9 任务过滤
  1. todoStore 中添加存储过滤条件的属性以及更改过滤条件的方法

    import axios from "axios"
    import { action, makeObservable, observable, } from "mobx"
    
    export default class TodoStore {
      constructor() {
        this.filterCondition = "All"
        makeObservable(this, {
          modifyFilterCondition: action.bound,
          filterCondition: observable,
        })
      }
      modifyFilterCondition(filterCondition) {
        this.filterCondition = filterCondition
      }
    }
    
  2. 创建 TodoFilter 组件,为过滤按钮添加事件以更改过滤条件,根据过滤条件为按钮添加 selected 类名

    import classNames from "classnames"
    import { observer } from "mobx-react-lite"
    import { useRootStore } from "../../store"
    
    function TodoFilter() {
      const { todoStore } = useRootStore()
      const { filterCondition, modifyFilterCondition } = todoStore
      return (
        <ul className="filters">
          <li>
            <button
              onClick={() => modifyFilterCondition("All")}
              className={classNames({ selected: filterCondition === "All" })}
            >
              All
            </button>
          </li>
          <li>
            <button
              onClick={() => modifyFilterCondition("Active")}
              className={classNames({ selected: filterCondition === "Active" })}
            >
              Active
            </button>
          </li>
          <li>
            <button
              onClick={() => modifyFilterCondition("Completed")}
              className={classNames({ selected: filterCondition === "Completed" })}
            >
              Completed
            </button>
          </li>
        </ul>
      )
    }
    
    export default observer(TodoFilter)
    
  3. Footer 组件中调用 TodoFilter 组件

    import TodoFilter from "./TodoFilter"
    
    function Footer() {
      return (
        <footer className="footer">
          <TodoFilter />
        </footer>
      )
    }
    
    export default Footer
    
  4. TodoStore 中添加派生状态,根据条件获取过滤后的 todo 列表

    import axios from "axios"
    import { action, flow, makeObservable, observable, computed } from "mobx"
    import Todo from "./Todo"
    
    export default class TodoStore {
      constructor() {
        makeObservable(this, {
          filterTodos: computed
        })
      }
      get filterTodos() {
        switch (this.filterCondition) {
          case "Active":
            return this.todos.filter(todo => !todo.isCompleted)
          case "Completed":
            return this.todos.filter(todo => todo.isCompleted)
          default:
            return this.todos
        }
      }
    }
    
  5. 在 Main 组件获取 filterTodos 派生状态

    import { observer } from "mobx-react-lite"
    import { useRootStore } from "../../store"
    import Todo from "./Todo"
    
    function Main() {
      const { todoStore } = useRootStore()
      const { filterTodos } = todoStore
      return (
        <section className="main">
          <ul className="todo-list">
            {filterTodos.map(todo => (
              <Todo key={todo.id} todo={todo} />
            ))}
          </ul>
        </section>
      )
    }
    
    export default observer(Main)
    
6.10 清除已完成任务
  1. TodoStore 中添加清除已完成任务的方法

    import axios from "axios"
    import { action, makeObservable, } from "mobx"
    
    export default class TodoStore {
      constructor() {
        makeObservable(this, {
          clearCompleted: action.bound
        })
      }
      clearCompleted() {
        this.todos = this.todos.filter(todo => !todo.isCompleted)
      }
    }
    
  2. 创建 ClearCompleted 组件实现清除已完成任务功能

    import { useRootStore } from "../../store"
    
    function ClearCompleted() {
      const { todoStore } = useRootStore()
      const { clearCompleted } = todoStore
      return (
        <button className="clear-completed" onClick={clearCompleted}>
          Clear completed
        </button>
      )
    }
    
    export default ClearCompleted
    
  3. Footer 组件中调用 ClearCompleted 组件

    import ClearCompleted from "./ClearCompleted"
    
    function Footer() {
      return (
        <footer className="footer">
          <ClearCompleted />
        </footer>
      )
    }
    
    export default Footer
    
    • _rfce 快速生成组件

任务三:MobX-5[更新之前的Mobx]

  1. 【课程资料】课程资料

  2. MobX专题内容介绍

  • Mobx 简介
  • 开发钱准备
  • MobX + React
  • Mobx 异步
  • Mobx 数据监测
  • 综合案例
  1. MobX简介

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OAzqawu8-1629715365902)(./img/1629640218804.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1tFQEXLd-1629715365903)(./img/1629640406592.jpg)]

  1. 开发前的准备工作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5AvhLVap-1629715365904)(./img/1629640501896.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uRMRkyHx-1629715365904)(./img/1629640557681.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YCtkqBUT-1629715365905)(./img/1629640608972.jpg)]

启用装饰器语法支持

方式一:

  1. npm run eject
  2. npm install @babel/plugin-proposal-decorators
  3. package.json

    "babel": {
        "plugins": [
            [
                "@babel/plugin-proposal-decorators",
                {
                    "legacy": true
                }
            ]
        ]
    }

方式二:

  1. npm install react-app-rewired @babel/plugin-proposal-decorators customize-cra

  2. 在项目根目录下创建 config-overrides.js

      const { override, addDecoratorsLegacy } = require("customize-cra");

      module.exports = override(addDecoratorsLegacy());
  
  3. package.json

      "scripts": {
          "start": "react-app-rewired start",
          "build": "react-app-rewired build",
          "test": "react-app-rewired test",
      }

解决vscode编辑器关于装饰器语法的警告

"javascript.implicitProjectConfig.experimentalDecorators": true
  1. mobx使用(一)
  • npm install mobx mobx-react

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BVhFrTQM-1629715365908)(./img/1629641135058.jpg)]

  1. mobx使用(二)

  2. 更正类中的普通函数的this指向

  • 使用箭头函数或者 .bound的方式
  • @action.bound fnname () {}
  1. 异步更新状态方式一
  • 如果代码里有异步,需要通过 runInAction 更新状态
  1. 异步更新状态方式二
  • 通过 flow 函数
  1. 数据监测computed
  • 计算值是可以根据现有的状态或其它计算值衍生出的值
  • 什么时候使用计算值
    • 将复杂的业务逻辑从模版中进行抽离

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9j8PNvGQ-1629715365910)(./img/1629643419713.jpg)]

  1. 禁止普通函数更改程序状态并引入action装饰器

  2. 数据监测autorun

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X55ekjJw-1629715365911)(./img/1629643780288.jpg)]

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'mobx-react';
import App from './App';
import counter from './stores/counterStore';

ReactDOM.render(
  <Provider counter={counter}><App /></Provider>,
  document.getElementById('root')
);
import React, { Component } from "react";
import { inject, observer } from "mobx-react";

@inject("counter")
@observer
class App extends Component {
  componentDidMount() {
    // const { getData } = this.props.counter;
    // getData();
  }
  render() {
    const { counter } = this.props;
    return (
      <div>
        <button onClick={counter.increment}>+</button>
        <span>{counter.count}</span>
        <button onClick={counter.decrement}>-</button>
        <span>{counter.getResult}</span>
        <div>
          <input type="text" value={counter.username} onChange={e => counter.changeUsername(e.target.value)}/>
        </div>
        {/* <div>
          {counter.users.map((user) => (
            <div>
              <span>{user.id}</span>
              <span>{user.login}</span>
            </div>
          ))}
        </div> */}
      </div>
    );
  }
}

export default App;
// stores/counterStore.js
// 1. 创建store对象 存储默认状态0
// 2. 将store对象放在一个全局的 组件可以够的到的地方
// 3. 让组件获取store对象中的状态 并将状态显示在组件中
import { observable, configure, action, runInAction, flow, computed, autorun } from 'mobx';
import axios from 'axios';

// 通过配置强制程序使用action函数更改应用程序中的状态
configure({enforceActions: 'observed'});

class CounterStore {

  constructor () {
    autorun (() => {
      try {
        uniqueUsername(this.username)
        console.log('用户名可用')
      }catch (e) {
        console.log(e.message)
      }
    }, {
      delay: 2000
    })
  }

  @observable count = 0;
  @observable users = [];
  @observable username = '';

  // @action increment = () => {
  //   this.count = this.count + 1;
  // }

  // @action decrement = () => {
  //   this.count = this.count - 1;
  // }

  @action.bound increment () {
    this.count = this.count + 1;
  }

  @action.bound decrement () {
    this.count = this.count - 1;
  }

  // @action.bound async getData () {
  //   let { data } = await axios.get('https://api.github.com/users');
  //   runInAction(() => this.users = data);
  // }

  // getData = flow(function* () {
  //   let { data } = yield axios.get('https://api.github.com/users');
  //   this.users = data
  // }).bind(this)

  @computed get getResult () {
    return this.count * 10
  }

  @action.bound changeUsername (username) {
    this.username = username;
  }
}

const counter = new CounterStore();

function uniqueUsername (username) {
  return new Promise((resolve, reject) => {
    if (username === 'admin') {
      reject('用户名已经存在')
    }else {
      resolve()
    }
  })
}

export default counter;
  1. todo案例构建项目组件

  2. 构建mobx工作流

  3. 实现添加任务功能

  4. 实现任务列表数据展示功能

  5. 实现任务删除功能

  6. 更改任务的是否完成状态

  7. 计算未完成任务的数量

  8. 实现任务筛选功能

任务四:Mobx 版本更新,已更更新完成详见任务二

  1. 【课程资料】MobX6

  2. Mobx概述

  • 如果是多个人,多个团队开发项目,倾向选择Redux,因为Redux有严格的工作流程,有很多样板代码,Redux通过这些样板代码去约束每个人的代码风格,Redux在多个团队之间做状态共享也是很容易的
  • 但是如果前端项目就自己负责,不倾向选择Redux,因为Redux开发效率没有那么高,因为Redux要写很多样板代码,这个时候更倾向选择Mobx,很简单,很直接
  1. Mobx入门
  • React 官方推荐使用函数组件
  1. Mobx 入门案例
  • 在React17中已经不要在组件中引用 inport React from ‘react’ 了,编译过程中会自动引入
  1. makeAutoObservable方法详解
  • 在Mobx中,代码不符合它的要求,只会报警告,不会报错(比如在非action方法中修改了状态state)
  1. todo添加任务
  • makeAutoObservable 把该对象的所有属性和方法都变成 Observable 和 actions
  • makeObservable 指定特定的属性和方法变成 Observable 和 actions
  1. 通过上下文暴露TodoListStore

  2. 更改任务状态

  3. 更正this指向

  4. 通过computed实现待办事项数量计算

  • 有缓存的概念,多次调用也只会执行一次
  1. runlnAction
  • 解决异步调用更新 Observable 属性的问题,可以是给api调用,或者直接给点击事件用。本质是在不能更新Observable属性的地方更新Observable
  1. 创建RootStore

  2. autorun

  • useEffect(() => { autorun(() => {}) }) 要在useEffect使用autorun,因为useEffect只在组件初始化中执行一次,防止autorun多次执行
  • 初始执行一次,后续在Observable或者计算属性改变时执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bTayfXPO-1629715365912)(./img/1629651234864.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y7uAtxUm-1629715365913)(./img/1629651307280.jpg)]

  1. reaction

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7oGA45Ul-1629715365913)(./img/1629651101679.jpg)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值