Skip to content

121React 全家桶入门教程 02

统计信息:字数 26205 阅读53分钟

第六章 react-ui

常用的开源组件库:material-UI ant-design

教程版本时 4.8.2,项目中使用 2.x 版本,文档和实际代码可能有差异;注意更新。

95 antd样式的按需引入

我们开始引入全部的CSS文件,实际上体积比较大(60kb),性能不好,最好使用按需引入

在 create-react-app 脚手架中,可以进行下面的配置

npm install react-app-rewired customize-cra babel-plugin-import

改写脚本命令

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

增加单独的配置文件 config-overrides.js (需要 customize-cra 库,处理自定义配置)

module.exports = function override(config, env) {
  // 这里可以改写默认的 webpack 配置文件
  return config;
}

实际上改成这样

const { override, fixBabelImports } = require('customize-cra');

module.exports = override(
    fixBabelImports('import', {
    libraryName: 'antd',
    libraryDirectory: 'es',
    style: 'css',
  })
);

更改后,就不需要在组件内部单独引入对应的样式文件了

96 antd 自定义主题

Ant-design 使用 less 作为样式文件,所以自定义样式,需要覆盖原来的样式文件。

npm install less less-loader

更改配置文件

const { override, fixBabelImports, addLessLoader } = require('customize-cra');

module.exports = override(
    fixBabelImports('import', {
    libraryName: 'antd',
    libraryDirectory: 'es',
    style: true,
  }),
  // 注意:这里官网配置会报错,因为 lessLoader 版本更新了,参数更改了
  addLessLoader({
    lessOptions: {
      javascriptEnabled: true,
        modifyVars: { '@primary-color': '#fff' },
    }
  }),
);

第七章 redux

97 redux 基本

  • 定义:redux 是专门状态管理的第三方库,和 react 没有直接联系(不是Facebook 团队出品)集中式管理 react 中多个组件共享的状态
  • 优点:解决了多个组件共享状态的问题,适应于不同组件通信比较复杂的情况
  • 缺点:学习和使用成本较高(简单项目不用,复杂项目使用)
  • 全部 state 都使用 redux? 可以把项目中共享的 state 使用 redux 管理,组件私有的 state 维护在内部。

个人想法:如果多个组件需要共享状态,最好状态和组件是对应的,分析组件拆分的逻辑是否合理,优先考虑状态提升,次要考虑 eventBus,最后考虑 redux。

98 Redux 基本概念和工作流程

个人理解的工作流程

0、初始化 Store 调用 Reducer 并设置初始状态

1、React 组件想要更新状态,发出一个 dispatch(Action), {type, data}

2、Store 接收到 Action,转发给 Reducer 处理(switch 处理多个 type)

3、Reducer 根据原状态 prevState 和 Action,计算出新的状态 newState ,并返回到 Store

4、Store 中状态更新,重新渲染整个组件,组件 getState 获取数据,界面刷新(需要提前订阅 Store 更新的事件)

说明:初始 Reducer 时,prevState 是 undefined, type 是随机字符串

基本概念

  • action 动作对象(type 唯一,data 可选参数)
  • reducer 用于初始化数据,或者更新数据(根据旧 state 和 action 产生新的 state 的纯函数)
  • store 连接 state action reducer 的核心,主要使用
import { createStore } form 'redux';
import demo_reducer from './demo_reducer';

const store = createStore(demo_reducer);

export default store;

基本 API

import store from './store';

store.getState();
store.dispatch(action);
store.subscribe(callback);

99 使用 react 实现的求和案例

实现一个简单的加减法计算器

import React from 'react';

export default class Count extends React.Component {

  state = {
    result: 0,
  }

  onAdd = () => {
    this.setState({
      result: this.state.result + this.selectRef.value * 1,
    });
  }

  onDec = () => {
    this.setState({
      result: this.state.result - this.selectRef.value * 1,
    });
  }

  render() {
    return (
      <div>结果是 {this.state.result} </div>
      <select ref={node => this.selectRef = node}>
        <option value='1'>1</option>
        <option value='2'>2</option>
      </select>
      <button onClick={this.onAdd}>加</button>
      <button onClick={this.onDec}>减</button>
    );
  }
}

100 实现基本计算器

基本实现:入口文件 index 组件 Count store reducer

Store 整个应用维护一个 store 管理全部的 reducer

import { createStore } from 'redux';
import countReducer from './count_reducer';

const store = createStore(countReducer);

export default store;

Reducer 本质是一个函数,Store 会调用这个函数,每一个 reducer 对应一个组件

const initState = 0;

// 首次 Store 初始化时,preState 是 undefined,action.type 是乱码,所以使用初始值,直接返回
function countReducer(preState = initState, action) {
  const { type, data } = action;
  switch (type) {
    case 'add':
      return preState + data;
    case 'reduce':
      return preState - data;
    default:
      return preState;
  }
}

export default countReducer;

组件中获取 state,发出 action

import store from './store';

// 组件内部的 state 不变,和 redux 不冲突
state = {
  name: 'Mike'
};

// 可以在组件内部监听状态变化(最好在全局顶层组件监听状态变化,触发回调函数)
componentDidMount() {
  // 参数是一个回调函数(状态变化后,强制更新这个组件)
  store.subscribe(() => {
    this.forceUpdate();
  })
}

onAdd = () => {
  let action = new Action({ type: 'xxx', data: 1 });
  store.dispatch(action);
}

最好的办法是入口函数监听 state 变化,并触发全局重新渲染(因为 diff 算法,确保实际性能可以)

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './redux/store';

ReactDOM.render(<App/>, document.getElementById('root'));

// 更新 state 后,渲染全部组件
store.subscribe(() => {
  ReactDOM.render(<App/>, document.getElementById('root'));
});

小结:使用 redux 步骤

1、检查哪些 state 是公共部分,去掉组件原来的 state

2、src 下面建立 redux 目录,内部写入 store reducer 作为单独的文件目录

3、store 中,引入 createStore(reducer) 然后暴露这个 store 对象

4、reducer 中,处理初始化状态和更新状态(纯函数)

5、index 中监听 store 状态改变,改变后重新渲染组件。redux 只处理状态管理,状态更新后渲染页面的步骤需要根据需求完成

101 redux 完整版

在前一节课程基础上,把 count_action 单独拿出来做成一个文件,专门为 Count 生成 Action

export const addAction = (data) => { type: 'add', data };
export const decAction = (data) => { type: 'dec', data };

PS 如果箭头函数的返回值是对象,那么需要用括号包起来

export const test = para => ({ type: 'add', data });

这样写,可能产生魔法字符串,生产中如果 Action 比较多,最好使用常量文件

export const INCREASE = 'increase';
export const DECREASE = 'decrease';

然后在 action.js 和 reducer 中引入 constant 常量,即可解决魔法字符串问题。

102 求和案例-异步action版

实现异步有两种办法:UI 层通过 setTimeout 实现定时执行

setTimeout(() => {
  store.dispatch(createIncreasementAction(value));
}, 1000);

或者在 redux 中实现异步(需要加入 middleware)

store.dispatch(createIncrementAsyncAction(value))
export const craeteIncrementAsyncAction = (data, time) => {
  return (dispatch) => {
    // action 的值是一个函数,时间过去后,执行这个函数
    setTimeout((time) => {
      dispatch(createIncrementAction(data));
    }, time);
  }
}

store

import { createStore, applyMiddleware } from 'redux';
import countReducer from './count-reducer';
// 支持异步 action
import thunk from 'redux-thunk';

export default createStore(countReducer, applyMiddleware(thunk));

异步 action,实际使用较少(官方文档和代码不一定对应,这是很大的问题,可能依赖的第三方库变化了,造成官方文档传参无效)

103 react-redux

设计理念:UI 组件外层加一个容器组件;容器组件和 redux 进行状态交互(使用 redux 的 API)UI 组件和容器组件,使用 props 传参,不直接使用 redux 的 API。

也就是说,UI 组件和 redux 中间加了一层容器组件,处理 redux 的状态更新。

104 UI 组件和容器组件传参

容器组件

import CountUI from '../../components/Count';
import store from '../../redux/store';
// connext 用于连接 UI 组件和容器组件
import { connect } from 'react-redux';

// 现在这样写,把容器组件和 UI 组件连接了,但是还没有和 redux 链接
export default connect()(CountUI);

渲染容器组件

import React from 'react';
import Count from './container/Count';
import store from './redux/store';

export default class App extends React.Component {
  render() {
    return (
        <Count/>
    );
  }
}

105 数据交互

需要在根组件中,订阅数据变化,然后更新状态(下面是过程代码,不是最后实现代码)

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './redux/store';

ReactDOM.render(<App />, document.getElementById('root'));

// 当状态变化时,重新渲染全部的组件(把新的状态传下去)
store.subscribe(() => {
  ReactDOM.render(<App />, document.getElementById('root'));
});

更改后的容器组件(容器组件和 redux 交互)

import CountUI from '../../components/Count';

import { connect } from 'react-redux';

// 以 props 的形式返回,state先存一个数值(正常应该存一个对象)
function a(state) {
  return { count: state };
}

function b() {
  return {
    add: (number) => {
      console.log(1);
      // store.dispatch({ type: 'increase', data: number });
      dispatch(createIncrementAction(number));
    }
  }
}

export default connect(a, b)(CountUI);

继续更改

import CountUI from '../../components/Count';
import { createIncrementAction } from '../../redux/count_action';
import { connect } from 'react-redux';

// 返回对象,作为 props 传递给 UI 组件,用于传递状态
function mapStateToProps(state) {
  return { count: state };
}

// 返回对象,通过 props 传递给子组件,用于操作状态的方法(也可以简化成对象)
function mapDispatchToProps(dispatch) {
  return {
    add: number => dispatch(createIncrementAction(number)),
    del: number => dispatch(createDecreaseAction(number)),
  }
}

// 使用 connect()() 创建并暴露 Count 的容器组件
export default connect(mapStateToProps, mapDispatchToProps)(CountUI);

小结

UI 组件:不能使用任何 redux 的 api, 只负责界面的交互和呈现

容器组件:负责和 redux 通信,将结果交给 UI 组件

怎样创建容器组件?——connect 函数

容器中的 store 是依靠 props 传入的,并不是直接 import

106 优化1-简写mapDispatch

export default connect(
    state => ({ count: state }),
  dispatch => (
    add: num => dispatch(createIncrementAction(number)),
    del: number => dispatch(createDecreaseAction(number)),
  ),
)(CountUI);

107 优化2-Provider组件的使用

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

ReactDOM.render(
    <Provider store={store}><App/></Provider>,
  document.getElementById('root')
);

子组件

import { createIncrementAction, createDeleteAction, } from '../../redux/count_action';

export default connect(
    state => ({ count: state }),
  dispatch => (
    add: createIncrementAction,
    del: createDeleteAction,
  ),
)(CountUI);

108 优化3-整合UI组件与容器组件

  1. 因为容器组件代码比较少,把容器组件和 UI 组件组合在一个文件中
  2. 不需要给容器传递 props,给 APP 包裹一个 Provider store={store} 即可
  3. 使用 react-redux 不需要自己检测 redux 的状态变化,容器组件自动完成
  4. mapDispatchToProps 简写成一个对象
  5. connect 把 UI 组件包裹起来 export default connext(mapStateToProps, mapDispatchToProps)(UI 组件), UI 组件通过 props 获取状态
import { connect } from 'react-redux';

class Count extends Component {
  render() {
    return (
        <span onClick={() => {this.props.add(1)}}>{this.props.value}</span>
    );
  }
}

export default connect(
    state => ({ value: state }),
  {add: createAddAction},
)(Count);

109 数据共享-编写Person组件

store 函数

// 这个文件暴露一个 store 对象(整个应用一个 store 即可)
import { createStore, applyMiddleware } from 'redux';
import counterReducer from './reducer/count';
import thunk from 'redux-thunk';

export default createStore(countReducer, applyMiddleware(thunk));

UI 组件

export default class Person extends React.Component {

  add = () => {
    this.props.add(1);
  }

  render() {
    return (
      <>
            <span onClick={this.add}></span>
            </>
    );
  }
}

110 数据共享-编写Person组件的reducer

reducer

import { ADD } from '../constant';

const initState = [
  {id: '012', name: 'Tom', age: 20},
];

export default function personReducer(preState = initState, action) {
  const { type, data } = action;
  switch (type) {
    case ADD:
      return [data, ...perState],
    default:
      return preState;  
  }
}

111 数据共享-完成数据共享

  1. 定义Person组件,和 Count 通过 redux 共享数据
  2. 为 Person 组件编写,reducer, action,
  3. PersonReducer 和 countReducer 需要合并 combineReducer 成一个对象
  4. 把合并后的 reducer 交给 store,最后在 UI 组件中取出状态

112 纯函数

reducer 是一个纯函数(不会改变已有状态,会生成新的对象 ——不使用 splice 使用 [date, ...preState] 这样的语法生成新的数组)

纯函数(reducer):同样的参数,那么函数返回值相同。不能改变参数数据,不能产生副作用(网络请求,输入输出),不能调用随机函数(随机数,随机时间戳)

高阶函数(connect):参数或者返回值是函数

113 redux开发者工具

浏览器插件:redux dev tools,在代码中需要加入

import { createStore, applyMiddleware, combineReducer } from 'redux';
import countReducer from './reducers/count';
import personReducer from './resucer/person';
import thunk from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';

const allReducer = combineReducers({
  sum: countReducer,
  peopleLen: personReducer,
});

export default createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)));

重启项目,打开控制台,可以看到 redux 调试面板

可以看到 state 变化,actions 序列,方便直接执行 action 或者回退到某个 action

114 最终版

index.js 入口组件

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './redux/store';
import {Provider} from 'react-redux';

// 使用 Provider 包裹顶层组件,让所有后代容器组件能接收到 store
ReactDOM.render(
    <Provider store={store}>
    <App/>
  </Provider>,
    document.getElementBuId('root')
);

/redux/reducers/count.js 创建为 Count 服务的 reducer 纯函数

import {ADD, DEL} from '../constant';

const initState = 0;

export default function countReducer(preState = initState, action) {
  const {type, action} = action;
  switch (type) {
    case ADD:
      return preState + data,  
    case DEL:
      return preState - data,
    default:
        return preState,
  }
}

/redux/reducers/person.js

import {ADD_PERSON} from '../constant';

const initState = [
  {
    id: '01',
    name: 'Amber',
    age: 18
  },
  {
    id: '02',
    name: 'Noella',
    age: 14
  }
];

export default function personReducer(preState = initState, action) {
  const { type, data } = action;
  switch (type) {
    case ADD_PERSON:
      return [data, ...preState],  
    default:
        return preState,
  }
}

redux/reducer.js 用于汇总所有 reducer 到一个总对象

import { combineReducers } from 'redux';
import countReducer from './reducers/count';
import personReducer from './reducers/person';

export default combineReducers({
  count: countReducer,
  persons: personReducer,
});

redux/actions/count.js 为 Count 组件生成 action 对象

import { ADD, DEL } from '../constant';

// 同步 action,action 是 object
export const increment = data => ({ type: ADD, data });
export const decrement = data => ({ type: DEL, data });

// 异步 action, action 是 function
export const incrementAsync = (data, time) => {
  return (dispatch) => {
    setTimeout(() => {
      dispatch(increment(data))
    }, time)
  }
}

containers/Person.js

class Person extends Component {

}

export default connect(
  // 映射状态
    state => ({
    persons: state.persons,
    count: state.count,
  }),
  // 映射操作状态的方法
  {
    add: createAddPersonAction,
  }
)(Person)

containers/count.js

import {
  increment,
  decrement,
  incrementAsync,
} from '../../redux/actions/count';
import { connect } from 'react-redux';

class Count extends Component {

}

export default connect(
  // 映射状态
    state => ({
    count: state.count,
    personCount: state.persons.length,
  }),
  // 映射操作状态的方法
  {
    increment,
    decrement,
    incrementAsync,
  }
)(Count)

app.js

import { Count, Person } from './containers/';

export default class App extends Component {
  render() {
    return (
        <>
        <Count/>
        <Person/>
      </>
    );
  }
}

redux/store.js

// 创建核心 store 对象 
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducers';
import thunk from 'redux-thunk';
import {composeWithDevTools} from 'redux-devtools-extension';

export default createStore(reducer, composeWithDevTools(applyMiddleware(thunk)));

115 本地打包测试

npm install serve -G
serve -s build

第八章 扩展

116 setState 的参数

setState 第一个参数是必选的,可以是对象或者函数(函数返回一个对象);第二个参数是可选的,状态更新后的回调函数。

this.setState({
  num: 1
}, callback);

this.setState((state, props) => {
  return {
    num: state + this.props.num,
  };
}, callback);

// 这两种写法效果一样
this.setState({ num: this.state.num + this.props.num });

两个写法的效果一致,对象是函数的语法糖(简写)。

使用原则:如果新状态不依赖原始的 state 和 props,优先使用对象。如果新状态依赖原始的状态,可以使用函数。

个人想法:优先使用对象(简单可读);如果其他同事使用函数作为参数,也应该熟悉。

117 lazyload 和 suspense

问题:如果界面中组件很多,首次加载太消耗性能,那么可以首次加载一部分组件,后续组件懒加载。

例如:一个组件中有20个路由,用户可能只看5个,那么不需要首次加载全部的组件,可以只加载5个,用户点击其他组件,通过网络请求再加载其他的组件。

import { React, lazy, Suspense } from 'react';
import { NavLink, Route } from 'react-router-dom';
import Loading from './loading';

// 把需要懒加载的组件,使用 lazy 函数包裹(参数是函数,返回值是组件)
const Login = lazy(() => import('./Login'));
const Forget = lazy(() => import('./Forget'));
const Main = lazy(() => import('./Main'));

render() {
  // 需要懒加载的组件,使用 Suspense 包裹一层,设置 fallback 为加载中的动画效果
  return (
    <Suspense fallback={<Loading/>}>
      <Route path="/login" component={Login}/>
      <Route path="/forget" component={Forget}/>
      <Route path="/main" component={Main}/>
    </Suspense>
  );
}

个人想法:如果全部使用懒加载,那么最后 webpack 打包结果是否有很多文件,是否便于维护?如果多项目协同开发,是否合适?

118 hook 概述

hook 可以在函数组件中使用类组件的API,但是有局限

常用 hook

React.useState();
React.useEffect();
React.useRef();

state hook: 让函数组件有 state,并可以操作更新 state。参数是 state 的初始值。

const [ name, setName ] = React.useState('Mike');
const [ age, setAge ] = React.useState(0);

119 EffectHook

Effect hook:让函数组件有生命周期函数,是三个生命周期函数的组合(componentDidMout, ComponentDidUpdate, ComponentWillUNmount)

const [ count, setCount ] = React.useState(0);

useEffect(() => {
  // componentDidMount
  let timer = setTimeOut(() => {
    setCount(count => count + 1);
  });
  return => {
    // componentWillUnmount
    clearTimeout(timer);
  }
}, []); // 如果是空数组,表示 DidMount,如果有数据,表示 DidMount + DidUpdate

120 REF hook

可以在函数组件中获取 DOM

const refDom = useRef();

return (
    <div ref={refDom}></div>
);

121 扩展6-Fragment

Fragment 只有两个属性,key 和 children。其他属性不能加。如果没有 key 可以直接去掉Fragment,写一对尖括号即可。react 不会渲染任何节点。

122 扩展7-Context 上下文

context 主要用于多级组件传变量(例如全局的变量)比逐层传递 Props 更容易

实际上,当前项目使用全局变量形式传参,没有直接使用 context 传参(封装组件使用 context)

一个组件中,默认有 props, refs, state, context 这些属性

const testContext = React.createContext();
const lang = 'en';

// 根组件中
<testContext.Provider value={lang}>
  <Children/>
</testContext.Provider>

// 子组件中
static contextType = testContext;
console.log(this.context);

// 子组件时函数组件时
<testContext.Consumer>
    {value => (
    <span>{value}</span>
   )
  }  
<testContext.Consumer/>

123 扩展8-PureComponent

Component 的性能问题:只要 setState 执行,即使不改变状态数据,组件也会重新渲染。如果当前组件渲染,那么会自动重新渲染子组件(效率较低)。shouldComponentUpdate 默认返回 true,所以组件会重新渲染。

解决思路:当 props 或者 state 变化时,才重新渲染

解决方法

1、简单组件(props 和 state 是简单数据类型),直接使用 PureComponent ,在组件 state 或者 props 中变化后才重新更新组件。注意:如果 props 是对象,这里实现浅对比判断是否渲染。如果对象引用没变化,只改变了值,可能不会引起组件重新渲染。

2、复杂组件(props 可能是复杂数据类型)需要在 shouldComponentUpdate 中,判断每一个变量,然后判断组件是否更新。

shouldComponentUpdate(nextProps, nextState) {
  console.log(this.state, nextState);
  console.log(this.props, nextProps);
}

在 setState 阶段,最好直接更改对象,避免只更改引用

// standard
this.setState({ name: 10 });

// bad
let state = this.state;
state.name = 10;
this.setState(state);

如果 state 中是数组,最好实现深复制,避免 push 操作

state = {
  list: [1, 2, 3],
};

// bad
let list = this.state.list;
list.push(4);
this.setState(list); // 这样 list 指针没有变化,pureComponent 可能出错

// good
let list = this.state.list;
this.setState({ list: [...list, 4] });

124 renderProps

这里指的是子组件直接渲染父组件传递的 children,主要用于 UI 组件套一层样式(例如对话框和移动端等)不适应于底层功能组件,这样可以大大减少代码量,同时确保多个组件的样式和交互一致性

实际上项目中使用的不多,也造成了不同组件代码重复率很高,而且样式不一致的问题

class A extends Component {
  render() {
    return (
        <div className="parent">
        <B render={(name) => <C name={name} />}>
      </div>
    );
  }
}

class B extends Component {
  state = {name: 'tom'};
  render() {
    // 这里渲染了父组件的 props,并且传参了 name
    return(
        <div>
        {this.props.render(this.state.name)}
      </div>
    );
  }
}

class C extends Component {
  render() {
    return (
        <div>{this.props.name}</div>
    );
  }
}

上面的案例,实际使用不多,毕竟这样不好维护

children props 通过组件标签体传入结构

render props 通过属性标签传入结构(通常是render)

(在 VUE 中这是 slot 插槽技术)

125 错误边界

可以捕获后代组件中渲染出现的错误,并渲染出备用界面

  • 后代组件,不包括当前组件

  • 捕获生命周期的错误,并不能捕获合成事件,定时器中的错误

state = {
  hasError: false,
};

static getDerivedStateFromError(error) {
  console.log(error);
  return {
    hasError: true,
  }
}

componentDidCatch(error, info) {
  console.log(error, info);
  // 这里可以用于数据统计(记录出错的问题并发送给服务器)
}

render() {
  return (
    {this.state.hasError ? <span>出错了!!!</span>: <Child/>}
  );
}

126 组件间通信方式总结

组件间关系:父子组件;兄弟组件;跨级组件(多级嵌套)

父子组件 props;兄弟组件 eventBus; 跨级组件 context 全局变量

状态管理 redux react-redux


Last update: November 9, 2024