umiJs_React学习笔记
UmiJs搭建react项目
1. 下载安装
// ^12.22.0 || ^14.17.0 || >=16.0.0"  支持版本  
yarn create @umijs/umi-app   // 第三版本
// 根目录 .umirc.ts 下配置
export default defineConfig({
  antd:{
    mobile:false
  }
});
- 安装antd-mobile v5 yarn add antd-mobile
 - 将TS文件改为js文件
 - 将.umirc.js文件中的routes注掉
 - 配置快速刷新.umirc.js文件中加上 fastRefresh: {}
 
2. 错误边界效果优化
import React, { Component } from 'react';
//  全局引入
export default class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = {
      flag: false
    };
  }
  static getDerivedStateFromError(error) {
    console.error(error)
    return {
      flag: true
    }
  }
  /* error: 抛出的错误
   * info: 带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息
   * 用这个函数作日志记录的操作,打印日志,因为返回信息比较全面
  */
  componentDidCatch(error, info) {
  }
  render() {
    return (
      <div>
        {this.state.flag ? <h1>发生错误,请稍后再试!</h1> : this.props.children}
      </div>
    )
  }
}
使用方式:
import ErrorBoundary from '@/components/ErrorBoundary';
  <ErrorBoundary>
     {props.children}
  </ErrorBoundary>
3. 使用think-react-store存储数据
yarn add think-react-store
新建stores文件夹
// index.js
export { default as user } from './user';
// user.js
export default {
  state: {
    id: 123,
    username: 'wzm'
  },
  reducers: {
    getUser(state, payload) {
      return {
        ...state,
        ...payload
      }
    }
  },
  effects: {
    async getUserAsync(dispatch, rootState, payload) {
      // const { id, username} = rootState.user;
      const detail = await Http({
        url: '/house/detail',
        body: payload
      });
      dispatch({
        type: 'getDetail',
        payload: detail
      });
    }
  }
};
使用方法:
- 父组件引入
 
// index.js
import React, { useState, useEffect } from 'react';
import { StoreProvider } from 'think-react-store';
import * as store from './stores';
import log from 'think-react-store/middlewares/log';
import User from './user';
export default function(props){
  const [state, setState] = useState()
  useEffect(() => {
  }, [])
  return (
    <StoreProvider store={store} middleware={[log]}>
      <User />
    </StoreProvider>
  )
}
- 子组件使用
 
// user.js
import React, { useState, useEffect } from 'react';
import { useStoreHook, useStateHook, useDispatchHook } from 'think-react-store';
export default function (props) {
  const [state, setState] = useState()
  const { user: { id, username,getUser, getUserAsync } } = useStoreHook();
  const states = useStateHook('user'); // 返回的state里的值
  // console.log(states)
  const dispatchs = useDispatchHook();
  const handleClick = () => {
    // getUserAsync({
    //   id: 20,
    //   username: 'admin2'
    // });
    dispatchs({
      key: 'user',
      type: 'getUserAsync',
      payload: {
        id: 20,
        username: 'admin2'
      }
    });
  };
  return (
    <div>
      user-id: {id}
      <br />
      username: {username}
      <br />
      <button onClick={handleClick}>修改</button>
    </div>
  )
}
4. 路由跳转
1. 基于标签
import { List } from 'antd-mobile';
import { Link } from 'umi';
<List>
     <List.Item>
       <Link to='/class/old'>old</Link>
       <NavLink to="路径" activeClassName="class名"></NavLink>
       
       <Link to="/路径/参数"></Link>
     </List.Item>
</List>
// 接收参数:
import { useParams } from 'umi'
const params = useParams();
2. JS 跳转
import { history } from 'umi';
// 跳转到指定路由
history.push('/list');
history.replace('/list');
// 跳转到上一个路由
history.goBack();
// 传递参数:
history.push({
    pathname: '/路径',
    query: {
        参数名: 参数值
    }
})
接收参数:
import { useLocation } from 'umi';
const location = useLocation();
console.log(location.pathname) // 当前路径
console.log(location.query)   // 参数
3. 动态路由
新建[id].js文件,则自动匹配动态路由
5. hook基本使用
import React, { useState, useEffect, useLayoutEffect, useCallback,memo , useRef} from 'react';
// function Header(props) {... } 缓存主键,优化性能
// export default memo(Header);
export default function (props) {
  const [count, setCount] = useState(0)
  const [text, setText] = useState('test-demo')
// ========================================
// useEffect 里写异步函数方式,
  async function demo() {
    console.log('demo')
  }
  // 在这里执行异步操作
  useEffect(() => {
    console.log('useEffect')
    demo()
  }, [count])
// ========================================
// 监听事件和取消监听
useEffect(() => {
   console.log("订阅一些事件");
   return () => {
     console.log("取消订阅事件")
   }
 }, []);
// ========================================
  //(很少用)dom渲染之前执行该方法,会堵塞dom更新,而useEffect会的dom渲染后执行,不会堵塞dom更新
  // 用法跟useEffect完全一样,
  useLayoutEffect(() => {
    console.log('useLayoutEffect')
  }, [])
// ========================================
  // 缓存函数,会返回一个记忆值,在依赖不发生变化的时候,返回的永远是同一个值。
  // 防止每次重新定义,定义函数时使用。一般在父组件向子组件传递函数的时候使用,在函数外加该功能。
const handleCount = useCallback(() => {
  console.log('count changed')
  setCount(count + 1)
}, [count])
// ========================================
  // Header ,缓存组件,只有props发生改变,才会重新调用,如果父组件已经阻止过了,子组件自动不会重新调用,所有函数式组件都可以包裹memo
const MemoHeader = memo(function Header() {
  console.log("Header被调用");
  return <h2>我是Header组件</h2>
})
// ========================================
// useRef返回一个对象,返回的对象在组件的整个生命周期保持不变
// 用法:1.引用DOM  2. 保存一个数据,这个对象在整个生命周期中可以保持不变
// 1.
const titleRef = useRef();
function changeDOM() {
  titleRef.current.innerHTML = "Hello World";
  console.log(testRef.current);
}
// 2. 
const [count, setCount] = useState(0);
const numRef = useRef(count); // 可以实现保留上一次的值
useEffect(() => {
  numRef.current = count;
}, [count])
// ========================================
  return (
    <div>
      <h2>numRef中的值: {numRef.current}</h2>
      <h2>count中的值: {count}</h2>
      <h2 ref={titleRef}>RefHookDemo01</h2>
      <h1 onClick={handleCount}>count: {count}</h1>
      {/* <h1>text: {noCacheText()}</h1> */}
      <h1>text: {memoText}</h1>
    </div>
  )
}
6. 自定义更改标题组件
新建hooks文件夹
// index.js
export { default as useTitleHook } from './useTitleHook';
// useTitleHook.js
import { useLayoutEffect, useState } from 'react';
import { history } from 'umi';
export default function useTitleHook(title) {
  const [state, setState] = useState()
  useLayoutEffect(() => {
    //console.error('useLayoutEffect');
    document.title = title;
    //history.push('/class/old');
    setState(title);
  }, [title])
  return state;
}
7. 自定义懒加载组件
- 新建文件夹LazyLoad
 - 其中新建文件 index.js(懒加载的实现) 和 error.js(组件懒加载错误的提示)
 
// index.js页面
import React, { Component, lazy, Suspense } from 'react';
export default class Index extends Component {
  constructor(props) {
    super(props);
    this.state = {
    };
  }
  _renderLazy = () => {
    let Lazy;
    const { component, delay, ...other } = this.props;
    if (!component || component.constructor.name !== 'Promise') {
      Lazy = lazy(() => import('./error'));
    } else {
      Lazy = lazy(() => {
        return new Promise(resolve => {
          setTimeout(() => {
            resolve(component);
          }, delay || 300);
        })
      });
    }
    return <Lazy {...other} />
  }
  render() {
    return (
      <div>
        <Suspense fallback={<div>loading...</div>}>
          {this._renderLazy()}
        </Suspense>
      </div>
    )
  }
}
// error.js页面
import React, { Component } from 'react';
export default class Error extends Component {
  constructor(props) {
    super(props);
    this.state = {
    };
  }
  render() {
    return (
      <div>
        组件引入错误!
      </div>
    )
  }
}
使用方式:
 <LazyLoad component={import('./lists')} delay={300} {...this.props}/> 
8. 新建节点(与root节点平级)
1.加入创建节点组件
// 新建CreatePortal文件夹,新建index.js文件
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
export default class CreatePortal extends Component {
  constructor(props) {
    super(props);
    this.body = document.querySelector('body');
    this.el = document.createElement('div');
  }
  componentDidMount() {
    this.el.setAttribute('id', 'portal-root');
    this.body.appendChild(this.el);
  }
  componentWillUnmount() {
    this.body.removeChild(this.el);
  }
  render() {
    return ReactDOM.createPortal(this.props.children, this.el)
  }
}
- 控制组件的显示与隐藏
 
// 新建Modal文件夹,新建index.js文件,引入CreatePortal
import React, { Component } from 'react';
import CreatePortal from '../CreatePortal';
import { Icon } from 'antd-mobile'; 
const Styles = {
  modal: {
    position: 'relative',
    top: '0',
    left: '0',
    zIndex: '999'
  },
  body: {
    backgroundColor: '#fff',
    position: 'fixed',
    height: '100%',
    width: '100%',
    top: '0',
    left: '0',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
  },
  close: {
    position: 'fixed',
    top: '10px',
    right: '10px'
  }
};
export default class Modal extends Component {
  constructor(props) {
    super(props);
    this.state = {
    };
  }
  handleClose = ()=>{
    const { onClose } = this.props;
    onClose && onClose();
  }
  render() {
    const { show } = this.props;
    return (
      <>
        {show ? <CreatePortal style={Styles.modal}>
          <div style={Styles.body}>
            {this.props.children}
            <Icon type='cross' size='lg' style={Styles.close} onClick={this.handleClose} />
          </div>
        </CreatePortal> : null}
      </>
    )
  }
}
- 自定义组件的样式
 
// 新建modal文件夹,新建index.js文件,引入Modal 
import React, { Component } from 'react';
import Modal from '@/Modal';
import { Button } from 'antd-mobile';
export default class index extends Component {
    constructor(props) {
        super(props);
        this.state = {
            show: false
        };
    }
    handleClose = () => {
        this.setState({
            show: false
        })
    }
    handleClick = () => {
        this.setState({
            show: true
        })
    }
    render() {
        return (
            <div>
                <Button type='primary' onClick={this.handleClick}>按钮  </Button>
                <Modal
                    show={this.state.show}
                    onClose={this.handleClose}>
                    弹窗
                </Modal>
            </div>
        )
    }
}
9. 数据mock
export default {
    'GET /api/getLists': {
        lists: ['a', 'b', 'c']
    },
    'GET /api/getListsAsync': (req, res) => {
        console.log('req ', req)
        setTimeout(() => {
            // res.json()该方法返回promise对象
            res.json({
                status: 200,
                data: {
        id: 100,
        username: 'admin'
      }
            })
        }, 1000);
    }
}
10. Storage二次封装
// storage.js
const config.namespace =  'manager'
export default {
    setItem(key, val) {
        let storage = this.getStroage();
        storage[key] = val;
        window.localStorage.setItem(config.namespace, JSON.stringify(storage));
    },
    getItem(key) {
        return this.getStroage()[key]
    },
    getStroage() {
        return JSON.parse(window.localStorage.getItem(config.namespace) || "{}");
    },
    clearItem(key) {
        let storage = this.getStroage()
        delete storage[key]
        window.localStorage.setItem(config.namespace, JSON.stringify(storage));
    },
    clearAll() {
        window.localStorage.clear()
    }
}
11. axios二次封装
默认配置
import axios from 'axios';
axios.defaults.baseURL = "https://httpbin.org";
axios.defaults.timeout = 5000;
axios.defaults.headers.common["token"] = "dafdafadfadfadfas";
// axios.defaults.headers.post["Content-Type"] = "application/text";
service/config.js
const devBaseURL = "https://httpbin.org";
const proBaseURL = "https://production.org";
export const BASE_URL = process.env.NODE_ENV === 'development' ? devBaseURL: proBaseURL;
export const TIMEOUT = 5000;
service/request.js
import axios from 'axios';
import { BASE_URL, TIMEOUT } from "./config";
const request= axios.create({
  baseURL: BASE_URL,
  timeout: TIMEOUT
});
// 请求拦截
request.interceptors.request.use(req=> {
  // 1.发送网络请求时, 在界面的中间位置显示Loading的组件
  // 2.某一些请求要求用户必须携带token, 如果没有携带, 那么直接跳转到登录页面
    const headers = req.headers;
    const { token = "" } = storage.getItem('userInfo') || {};
    if (!headers.Authorization){
     headers.Authorization = 'Bearer ' + token;
     }
  return req;
}, err => {
// 隐藏loading
console.log("加载超时");
return Promise.reject(err)
});
// 响应拦截
request.interceptors.response.use(res => {
  // 1.axios请求完成后,隐藏loading
  // 2. 处理请求
  const { code, data, msg } = res.data;
  if (code === 200) { // 成功
        return data;
    } else if (code === 50001) { // 如果没有登入,需要跳转到登入页面
        console.log("Token认证失败,请重新登录");
        setTimeout(() => {
            router.push('/login') //回到登入页 
        }, 1500)
        return Promise.reject("Token认证失败,请重新登录")
    } else {  // 单纯就是出错
    // 隐藏loading
    console.log("请求失败");
        console.log("网络请求异常,请稍后重试");
        return Promise.reject(msg || "网络请求异常,请稍后重试")
    }
    
}, err => {
  if (err && err.response) {
    switch (err.response.status) {
      case 400:
        console.log("请求错误");
        break;
      case 401:
        console.log("未授权访问");
        break;
      default:
        console.log("其他错误信息");
    }
  }
  return err;
});
export default request;
使用,之后在在外面封装成对应的函数
import request from './service/request';
// get 
request({
  url: "https://httpbin.org/get",
  params: {
    name: "why",
    age: 18
  }
})
// post
request({
  url: "https://httpbin.org/post",
  data: {
    name: "kobe",
    age: 40
  },
  method: "post"
})
// 例如
export function getHotRecommends(limit) {
  return request({
    url: "/personalized",
    params: {
      limit
    }
  })
}
12. 全局事件传递
import React, { PureComponent } from 'react';
import { EventEmitter } from 'events';
// 事件总线: event bus
const eventBus = new EventEmitter();
class Home extends PureComponent {
  componentDidMount() {
    eventBus.addListener("sayHello", this.handleSayHelloListener);
  }
  componentWillUnmount() {
    eventBus.removeListener("sayHello", this.handleSayHelloListener);
  }
  handleSayHelloListener(num, message) {
    console.log(num, message);
  }
  render() {
    return (
      <div>
        Home
      </div>
    )
  }
}
class Profile extends PureComponent {
  render() {
    return (
      <div>
        Profile
        <button onClick={e => this.emmitEvent()}>点击了profile按钮</button>
      </div>
    )
  }
  emmitEvent() {
    eventBus.emit("sayHello", 123, "Hello Home");
  }
}
export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Home/>
        <Profile/>
      </div>
    )
  }
}
13. 实现滚动加载自定义组件
IntersectionObserver
MDN上的定义是: IntersectionObserver接口 (Intersection Observer API)为开发者提供了一种可以异步监听目标元素与其祖先或者视窗(viewport)交叉状态的手段。祖先元素与视窗(viewport)被称为根(root)。
简单来说,IntersectionApi的功能就是用来判断:监听目标元素与其祖先或视窗交叉状态发生改变的手段
主要是用来检测 目标元素与root元素刚开始交叉和目标元素与root元素刚开始不交叉
图示如下:

IntersectionObserver API 是异步的, 不随着目标元素的滚动同步触发。即只有在线程空闲下来才会执行观察器。这意味着这个观察器的优先级非常的低,只有在其他的任务执行完,浏览器空闲了才会执行
基本使用
// callback 是当被监听元素的可见性变化时,触发的回调函数
// options是一个配置参数对象,可选的, 不进行配置时候存在对应的默认值
const observer = new IntersectionObserver(callback, options)
// IntersectionObserver接收的callback会在三种情况下被回调
// 1. 对应元素使用observe方法被添加到监听队列中
// 2. 对应元素和浏览器可视窗口刚开始产生交叉 --- 进入可视窗口
// 3. 对应元素和浏览器可视窗口由存在交叉转变为刚开始不交叉 --- 完全离开可视窗口
//  对元素target添加监听,当target元素变化时,就会触发回调
//  observe()的参数是一个DOM节点对象,如果要观察多个节点,就要多次调用这个方法
observer.observe(element);
// 移除一个监听,移除之后,target元素的交叉状态变化,将不再触发回调函数
observer.unobserve(element)
// 停止所有的监听
observer.disconnect();
IntersectionObserverEntry
IntersectionObserverEntry
// IntersectionObserverEntry对象提供目标元素的信息简化如下显示
{
  // time的值是一个时间戳,表示自观察器被实例化到被检测对象的交叉状态发生改变之间的时间戳
  // 例子:值为1000时,表示在IntersectionObserver实例化的1秒钟之后,目标元素的交叉状态发生改变了
  time: 78463997.025,
  
  // 根元素对应的矩形信息(即调用getBoundingClientRect()方法的返回值)
  // 如果没有根元素(即直接相对于视口滚动),则返回null
  rootBounds: null,
    
  // 目标元素的矩形信息
  boundingClientRect: DOMRectReadOnly { /* ... */ },
    
  // 目标元素与视口(或root根元素)的交叉区域的矩形信息
  intersectionRect: DOMRectReadOnly { /* ... */ },
    
  // 目标元素当前是否可见 Boolean值 可见为true
  isIntersecting: true,
    
  // 目标元素的可见比例 ---> [0, 1]
  intersectionRatio: 1,
    
  // 被监听的对象 --- 一个dom元素
  target: div#target.target
}
option选项
IntersectionObserver构造函数的第二参数是一个配置对象, 他可以设置以下属性:
threshold
threshold属性 决定了什么时候触发回调函,值为一个数组,每一个数组项代表着一个门槛值
当目标元素和根元素相交的面积占目标元素面积的百分比到达或跨过这些指定的临界值时就会触发回调函数
threshold的默认值是[0, 1],即只有在开始进入,或者是完全离开视图区域时,才会触发回调函数
rootMargin
用来扩大或者缩小视窗的大小, 使用css的定义方式, 10px 10px 10px 20px 表示top,right,bottom, left的值。
root
root属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点
简单例子
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
      height: 200vh;
      padding: 0 30px;
    }
    .target {
      width: 300px;
      height: 300px;
      margin-top: 2000px;
      background-color: red;
    }
  </style>
</head>
<body>
  <div id="target" class="target"></div>
  <script>
    // 创建IntersectionObserver实例对象 --- 参数是一个callback
    // entries是IntersectionObserverEntry对象数组
    // 监听了几个元素,数组长度就是多少
    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        console.log(entry.isIntersecting ? 'div在可视区域内' : 'div不在可视区域内')
      })
    })
    // 添加对某个元素的监听
    observer.observe(document.getElementById('target'))
  </script>
</body>
</html>
懒加载(lazy load)
我们希望某些静态资源(比如图片),只有用户向下滚动,它们进入视口时才加载,这样可以节省带宽,提高网页性能。也就是所谓的”惰性加载”。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>lazy load</title>
  <style>
    body {
      height: 200vh;
      padding: 0 30px;
    }
    .target {
      width: 300px;
      height: 300px;
      margin-top: 2000px;
    }
  </style>
</head>
<body>
  <img
    id="target"
    class="target"
    data-src="https://s6.jpg.cm/2021/10/29/I3Pa3D.jpg"
    src="https://img.alicdn.com/tps/i3/T1QYOyXqRaXXaY1rfd-32-32.gif"
  />
  <script>
    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          entry.target.src = entry.target.getAttribute('data-src')
          entry.target.removeAttribute('data-src')
          observer.unobserve(entry.target)
        }
      })
    })
    observer.observe(document.getElementById('target'))
  </script>
</body>
</html>
无限滚动时,最好在页面底部有一个页尾栏(又称sentinels)。
一旦页尾栏可见,就表示用户到达了页面底部,从而加载新的条目放在页尾栏前面。
这样做的好处是,IntersectionObserver只要调用一次observe方法监听一个对象即可完成相应的功能
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>infinite scroll</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div id="container" class="container">
    <div id="loading">
      数据加载中 。。。
    </div>
  </div>
  <script src="./index.js"></script>
</body>
</html>
let count = 0
const container = document.getElementById('container')
const observer = new IntersectionObserver(entries => {
  if (entries[0].isIntersecting) {
    const fragment = document.createDocumentFragment()
    for (let i = 0; i < 5; i++) {
      const dv = document.createElement('div')
      dv.className = 'unit'
      dv.innerHTML =  `第${count++ + 1}个元素`
      fragment.appendChild(dv)
    }
    container.insertBefore(fragment, entries[0].target)
  }
})
observer.observe(document.getElementById('loading'))
1. demo
* 1,页面初始化时候请求接口;
* 2,监听loading组件是否展示出来;
* 3,修改page,pageNum+1,再次重新请求接口;
* 4,拼装数据,然后page
import React, { useState, useEffect } from 'react';
import { history } from 'umi';
import { useObserverHook } from '@/hooks';
let observer;
export default function (props) {
  const [state, setState] = useState()
  // useObserverHook('#loading', (entries)=>{
  //   console.log(entries)
  // });
  const handleClick = () => {
    history.push('/');
  };
  useEffect(() => {
    console.log('进入页面')
    // 创建交叉观察对象
    observer = new IntersectionObserver(entries => {
      // intersectionRatio: 0  // 返回其中属性之一,0表示为显示,1表示完全显示,取值范围:0-1
      // isIntersecting: false // 返回其中属性之一,false 表示为显示 ,true表示显示
      console.log(entries)
    });
    // 绑定元素
    observer.observe(document.querySelector('#loading'));
    return () => {
      console.log('离开页面')
      if (observer) {
        // 解绑元素
        observer.unobserve(document.querySelector('#loading'));
        // 停止监听
        observer.disconnect();
      }
    }
  }, [])
  return (
    <div>
      observer
      <button onClick={handleClick}>首页</button>
      <div id='loading' style={{ width: '100px', height: '300px', background: '#f60', marginTop: '1000px' }}>
        loading
      </div>
    </div>
  )
}
2. 自定义滚动加载组件
IntersectionObserver的使用
useEffect使用
// useObserverHook.js
import { useEffect } from 'react';
let observer;
export default function useObserverHook(ele, callback, watch = []) {
  useEffect(() => {
    const node = document.querySelector(ele);
    if (node) {
      observer = new IntersectionObserver(entries => {
        callback && callback(entries);
      });
      observer.observe(node);
    }
    return () => {
      if (observer && node) {
        // 解绑元素
        observer.unobserve(node);
        // 停止监听
        observer.disconnect();
      }
    }
  }, watch);
}
具体使用
import React, { useState, useEffect } from 'react';
import { Tabs } from 'antd-mobile';
import Lists from './components/Lists';
import { useHttpHook, useObserverHook } from '@/hooks';
import { CommonEnum } from '@/enums';
import { Http } from '@/utils';
import { isEmpty } from 'project-libs';
import { ErrorBoundary } from '@/components';
import './index.less';
export default function (props) {
  const [page, setPage] = useState(CommonEnum.PAGE);
  const [orders, setOrders] = useState([]);
  const [showLoading, setShowLoading] = useState(true);
  const [type, setType] = useState(0);
  // const [orders] = useHttpHook({
  //   url: '/order/lists',
  //   body: {
  //     ...page
  //   }
  // });
  const invokeHttp = async (pageNum) => {
    const result = await Http({
      url: '/orders/lists',
      body: {
        ...page,
        pageNum,
        type
      }
    });
    return result;
  };
  const fetchOrder = async (pageNum) => {
    const result = await invokeHttp(pageNum);
    if (!isEmpty(result) && result.length <= page.pageSize) {
      setOrders(result);
      setShowLoading(true);
    } else {
      setShowLoading(false);
    }
  };
  const handleChange = (e) => {
    // console.log(e)
    setType(e.sub);
    setPage(CommonEnum.PAGE);
    setOrders([]);
    setShowLoading(true);
  };
  const tabs = [
    { title: '未支付', sub: 0 },
    { title: '已支付', sub: 1 }
  ];
  /**
   * 1,页面初始化时候请求接口;
   * 2,监听loading组件是否展示出来;
   * 3,修改page,pageNum+1,再次重新请求接口;
   * 4,拼装数据,然后page
   */
  useObserverHook('#' + CommonEnum.LOADING_ID, async (entries) => {
    console.log(entries)
    if (entries[0].isIntersecting) {
      const result = await invokeHttp(page.pageNum + 1);
      if (!isEmpty(orders) && !isEmpty(result) && result.length === page.pageSize) {
        setOrders([...orders, ...result]);
        setPage({
          ...page,
          pageNum: page.pageNum + 1
        });
        setShowLoading(true);
      } else {
        setShowLoading(false);
      }
    }
  }, null);
  useEffect(() => {
    fetchOrder(1);
  }, [type])
  return (
    <ErrorBoundary>
      <div className='order-page'>
        <Tabs
          tabs={tabs}
          onChange={handleChange}
        >
          <div className='tab'>
            <Lists orders={orders} type={0} showLoading={showLoading} />
          </div>
          <div className='tab'>
            <Lists orders={orders} type={1} showLoading={showLoading} />
          </div>
        </Tabs>
      </div>
    </ErrorBoundary>
  )
}
14. 项目搭建与配置
jsconfig.json
{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@/hooks": ["hooks/index"],
      "@/components": ["components/index"],
      "@/utils": ["utils/index"],
      "@/enums": ["enums/index"],
      "@/skeletons": ["skeletons/index"]
    }
  }
}
.umirc.js
import { defineConfig } from 'umi';
export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  routes: [
    {
      path: '/',
      component: '@/layouts/index',
      routes: [
        {
          path: '/',
          component: './home/index',
          title: '首页'
        },
        {
          path: '/order',
          component: './order/index',
          title: '订单',
          auth: true
        },
        {
          path: '/user',
          component: './user/index',
          title: '我的',
          auth: true
        },
        {
          path: '/login',
          component: './login',
          title: '登录'
        },
        {
          path: '/register',
          component: './register',
          title: '注册'
        },
      ]
    }
  ],
});
layouts/index.js
import styles from './index.css';
import { ErrorBoundary, MenuBar } from '@/components';
import { useLocation } from 'umi';
import { StoreProvider } from 'think-react-store';
import * as store from '../stores';
function BasicLayout(props) {
  const location = useLocation();
  const paths = ['/', '/order', '/user'];
  return (
    <StoreProvider store={store}>
      <MenuBar
        show={paths.includes(location.pathname)}
        pathname={location.pathname}
      />
      <ErrorBoundary>
        {props.children}
      </ErrorBoundary>
    </StoreProvider>
  );
}
export default BasicLayout;
新建 app.js
import { history } from 'umi';
// 未登入回到登入页面
export function onRouteChange(route){
  // console.log(route)
  const nowPath = route.routes[0].routes.filter(item => item.path === route.location.pathname);
  const isLogin = cookie.get('user');
  if(nowPath.length === 1 && nowPath[0].auth && !isLogin){
    history.push({
      pathname: '/login',
      query: {
        from: route.location.pathname
      }
    });
  }
}
部署发布
yarn add cross-env --dev
// cross-env NODE_ENV=development
// 构建产物默认生成到 ./dist 下
yarn build
本地验证
yarn global add serve
serve ./dist
15. 类组件使用与全局事件传递
import React, { Component , createRef} from 'react';
import TabControl from './TabControl';
export default class App extends Component {
  constructor(props) {
    super(props);
    this.titles = ['新款', '精选', '流行'];
this.titleRef = createRef();
    this.state = {
      currentTitle: "新款",
      currentIndex: 0
    }
  }
  render() {
    const {currentTitle} = this.state;
    return (
      <div  ref={this.titleRef}>
        <TabControl itemClick={index => this.itemClick(index)} titles={this.titles} />
        <h2>{currentTitle}</h2>
      </div>
    )
  }
  itemClick(index) {
    this.setState({
      currentTitle: this.titles[index]
    })
    this.titleRef.current.innerHTML = "Hello JavaScript";
  }
}
import React, { PureComponent } from 'react';
import { EventEmitter } from 'events';
// 事件总线: event bus
const eventBus = new EventEmitter();
class Home extends PureComponent {
  componentDidMount() {
    eventBus.addListener("sayHello", this.handleSayHelloListener);
  }
  componentWillUnmount() {
    eventBus.removeListener("sayHello", this.handleSayHelloListener);
  }
  componentDidUpdate() {
    console.log("执行了组件的componentDidUpdate方法");
  }
  handleSayHelloListener(num, message) {
    console.log(num, message);
  }
  render() {
    return (
      <div>
        Home
      </div>
    )
  }
}
class Profile extends PureComponent {
  render() {
    return (
      <div>
        Profile
        <button onClick={e => this.emmitEvent()}>点击了profile按钮</button>
      </div>
    )
  }
  emmitEvent() {
    eventBus.emit("sayHello", 123, "Hello Home");
  }
}
export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Home/>
        <Profile/>
      </div>
    )
  }
}
16. 自定义Hook与Redux
16.1 自定义Hook
- 自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性
 - 自定义hook函数名前必须加use
 
16.2 Redux
- redux要求我们通过action更新数据,所有数据的变化,必须通过派发(dispatch)action来更新
 - action是一个普通的js对象,用来描述这次更新的type和content
 - reducer是一个纯函数,用来将传入的state和action结合起来生成新的state
 - 唯一修改state的方式,一定是触发action,不要在其他地方修改
 - yarn add redux
 
const redux = require('redux');
// 数据初始化
const initialState = {
  counter: 0
}
// reducer
function reducer(state = initialState, action) { // 第一个参数为初始化值
  switch (action.type) {
    case "INCREMENT":
      return { ...state, counter: state.counter + 1 }
    case "DECREMENT":
      return { ...state, counter: state.counter - 1 }
    case "ADD_NUMBER":
      return { ...state, counter: state.counter + action.num }
    case "SUB_NUMBER":
      return { ...state, counter: state.counter - action.num }
    default:
      return state;
  }
}
// 创建store(创建的时候需要传入一个reducer)
const store = redux.createStore(reducer)
// 订阅store的修改,可以打印数据的变化
store.subscribe(() => {
  console.log("counter:", store.getState().counter);
})
// 定义actions
const action1 = { type: "INCREMENT" };
const action2 = { type: "DECREMENT" };
const action3 = { type: "ADD_NUMBER", num: 5 };
const action4 = { type: "SUB_NUMBER", num: 12 };
// 派发action
store.dispatch(action1);
store.dispatch(action2);
store.dispatch(action2);
store.dispatch(action3);
store.dispatch(action4);
ps:
// xxx.reduce(回调参数,初始化值) // 归纳
//  参数一:上一次回调函数的结果(第一次没有上一次函数的回调函数的结果,使用初始化值)
xxx.reduce((preValue,item,index,arr)=>{
return preValue + item.AAA // 计算的新结果
},0)
    const names = ["abc", "cba", "nba", "mba"];
    /**
     * 回调函数有三个参数:
     *  参数一: 执行时的对应元素
     *  参数二: 对应的下标值
     *  参数三: 完整的数组对象
     */
    const newNames = names.map((item, index, arr) => {
      return item + "000"
    })
    console.log(newNames);
    // const newNames1 = names.forEach((item) => {
    //   return item + "111";
    // })
    // console.log(newNames1);
    const nums = [110, 123, 50, 32, 55, 10, 8, 333];
    const newNums = nums.filter((item, index, arr) => {
      return item >= 50;
    })
    console.log(newNums);
    const newNums2 = nums.filter(item => {
      return item % 2 === 0;
    })
    console.log(newNums2);
    console.log(nums.slice(-2));
移动端
// base.css
html{
font-size:100px;
}
body {
font-size:.12rem;
}
 // npm install normalize.css
import 'normalize.css'
import 'base.css'
骨架屏
判断是否加载完成
import React, { useState, useEffect } from 'react';
import { ActivityIndicator } from 'antd-mobile';
import { isEmpty } from 'project-libs';
import OrderItem from '../Item';
import { ShowLoading } from '@/components';
import { OrderSkeletons } from '@/skeletons';
export default function (props) {
  const [state, setState] = useState(false)
  useEffect(() => {
    setTimeout(() => {
      if(isEmpty(props?.orders)){
        setState(true)
      }
    }, 1500);
  }, [])
  return (
    <div>
      {isEmpty(props?.orders) ?
        <>{state ? <ShowLoading showLoading={false}/> : <OrderSkeletons/>}</> :
        <div className='tab-lists'>
          {props.orders.map(item => (
            <OrderItem type={props.type} key={item.id} {...item}/>
          ))}
          <ShowLoading showLoading={props.showLoading}/>
        </div>
      }
    </div>
  )
}
import React, { useState, useEffect } from 'react';
import './index.less';
export default function (props) {
  const [state, setState] = useState(Array(3).fill(1));
  useEffect(() => {
  }, [])
  return (
    <div className='order-skeletons'>
      {state.map((item, index) => (
        <div className='order-item' key={index}>
          <div className={'skeletons left'}></div>
          <div className='center'>
            <div className={'skeletons title'}></div>
            <div className={'skeletons price'}></div>
            <div className={'skeletons time'}></div>
          </div>
          <div className={'skeletons pay'}>
          </div>
        </div>
      ))}
    </div>
  )
}
@import '../../assets/mixin.less';
.order-skeletons {
  .order-item {
    .flex(row, flex-start);
    margin-bottom: 12px;
    padding: 12px;
    width: 100%;
    box-sizing: border-box;
    .left {
      width: 120px;
      height: 80px;
    }
    .center {
      flex: 1;
      margin: 0 12px;
      .price {
        margin: 12px 0;
        width: 50px;
      }
      .time {
        width: 120px;
      }
    }
    .pay {
      margin-right: 4px;
      width: 70px;
      height: 30px;
    }
  }
}
自定义请求hook
import { Toast } from 'antd-mobile';
export default function Http({
  url,
  method = 'post',
  headers = {},
  body = {},
  setLoading,
  setResult,
}){
  setLoading && setLoading(true);
  const token = localStorage.getItem('token');
  let defaultHeader = {
    'Content-type': 'application/json'
  };
  defaultHeader = token ? {
    ...defaultHeader,
    token
  } : defaultHeader;
  let params;
  if(method.toUpperCase() === 'GET'){
    params = undefined;
  }else {
    params = {
      headers: {
        ...defaultHeader,
        ...headers
      },
      method,
      body: JSON.stringify(body)
    }
  }
  return new Promise((resolve, reject)=>{
    fetch('/api' + url, params)
      .then(res => res.json())
      .then(res => {
        if(res.status === 200){
          resolve(res.data);
          setResult && setResult(res.data);
        }else {
          if(res.status === 1001){
            location.href = '/login?from=' + location.pathname;
            localStorage.clear();
          }
          Toast.fail(res.errMsg);
          reject(res.errMsg);
        }
      })
      .catch(err => {
        //Toast.fail(err);
        //reject(err);
      })
      .finally(() => {
        setLoading && setLoading(false);
      })
  });
}