react的完整的操作步骤记录

636 阅读7分钟

安装以ts为基础的react项目

create-react-app my-react-ts-app --template typescript

写一个hello world的小栗子

App.tsx 中添加Hello组件

import Hello from './components/Hello'

<Hello message='hello world' />

Hello.tsx的组件

import React from 'react'

interface IhelloProps {
  message?: string
}
//FC 是FunctionComponent的类型别名
const Hello: React.FC<IhelloProps> = (props) => {
  console.log(props.children)
  return <h2>{props.message}</h2>
  // return <h2>{props.children}</h2>
}
// React.FC 可以使用defaultProps
Hello.defaultProps = {
  message: '222222222222'
}

export default Hello;

什么是react hook

react16.8版本带来的全新特性,即将替代class组件的写法

react有两种写法 一个是class一个是函数式(不能使用state和组件的生命周期的钩子函数, hook的推出就是为了打破这种壁垒)

图片.png

react hook没有破坏性改动

完全可选

百分之百向后兼容

没有计划从react移除class

hook为了解决什么?

组件很难复用状态逻辑

图片.png

复杂组件难以理解 尤其是生命周期函数

图片.png

react组件一直是函数,使用hook完全是拥抱函数

自定义hook

将组件逻辑提取到可重用的函数中(高阶组件也可以)

案例 使用自定义hook抽象鼠标跟踪器

// 定义hook
import { useState, useEffect } from 'react';

// 自定义hook 必须用use开头,每次使用自定义hook的时候 里面的state都是隔离的
// useEffect 添加第二个参数 [] 表示 只会在初始化的执行一次 其他的时候只会更新数据
const useMousePosition = () => {
  const [positions, setPositions] = useState({ x: 0, y: 0 })
  useEffect(() => {
    const updateMouse = (e: MouseEvent) => {
      setPositions({ x: e.clientX, y: e.clientY })
    }
    document.addEventListener('mousemove', updateMouse)
    return () => {
      document.removeEventListener('mousemove', updateMouse)
    }
  }, [])
  return positions
}

export default useMousePosition

使用hook -- 在需要的组件中

import React from 'react';
import logo from './logo.svg';
import './App.css';
import Hello from './components/Hello'
**使用hook -- 在需要的组件中**
import useMousePosition from './hooks/useMousePosition'
function App () {
// 调用自定义hook这个方法positions 或者这个方法返回的值
  const positions = useMousePosition()
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        // 开始 根据hook函数返回的值 进行展示数据
        <p>x: {positions.x}, y: {positions.y}</p>
         // 结束
        <Hello />
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

使用HOC的劣势 Higher order component

在异步请求时有个需求显示和隐藏loading的状态,发送请求的时候显示loading的状态,结束之后会隐藏loading,如果一直有这个需要,就需要把这个逻辑提取出来,在hook没有出现之前,使用HOC的使用方法--高阶组件

高级组件就是一个函数,接受一个组件作为参数,返回一个新的组件。

比如loading的实现和隐藏时一段逻辑 但是要分开写 并且添加了多了的节点

自定义hook的写法 完成loading的显示与隐藏 useSatet useEffect

把逻辑提取到一个函数中,通过调用函数来实现公共逻辑部分的实现

import axios from 'axios'
import { useState, useEffect } from 'react'

//any[] = [] 设置deps数组中可以时任意值 并且设置了默认值为空数组
const useURLLoading = (url: string, deps: any[] = []) => {
  // data 设置默认null的时候会出错,使用泛型 添加<any>
  const [data, setdata] = useState<any>(null)
  const [loading, setLoading] = useState(false)
  useEffect(() => {
    setLoading(true)
    axios.get(url).then(result => {
      setdata(result.data)
      setLoading(false)
    })
  }, deps)
  return [data, loading]
}

export default useURLLoading;

使用:

import useURLLoading from './hooks/useURLLoading'

  const positions = useMousePosition()
  const [data, loading] = useURLLoading('https://dog.ceo/api/breeds/image/random')
  const resultDate = data as IsShow;
  ...
  {loading ? <p>读取中</p>
      : <img alt='' src={resultDate && resultDate.message} />
    }

遇到的问题:

 Line 23:6:  React Hook useEffect was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependencies  react-hooks/exhaustive-depseps  
 
Line 23:6:  React Hook useEffect has a missing dependency: 'url'. Either include it or remove the dependency array                                                                react-hooks/exhaustive-deps

问题: 自定义的hook给useEffect传入依赖时,eslint会自动产生警告,这是为什么呢

解决:当你的 effect 有任何外部依赖的时候,但是没有放入依赖数组的时候,就会出现这些警告,出现这些警告要结合实际情况,在这里我们可以警告提示我们需要把 fetchSuggestions 放入,添加这个依赖就可以了,在后面的内容中,我们是会处理这个依赖问题的

useRef 解决多次渲染中值是最新的问题

useRef的值改变不会渲染组件

const likeRef = useRef(0)
...
// 延迟3s打印出来的是最新的数据
function handleAlertClick () {
    setTimeout(() => {
      alert('you clicked on ' + likeRef.current)
    }, 3000)
  }

获取dom节点做一些操作

import React, { useState, useEffect, useRef } from 'react';
...

const domRef = useRef<HTMLInputElement>(null)
useEffect(() => {
    if (domRef && domRef.current) {
      domRef.current.focus()
    } else {
      console.log('1111111')
    }
  })
...

<input type="text" ref={domRef} />

useContext解决多层传递属性的林丹妙药

interface IThemeProps {
  [key: string]: { color: string, background: string }
}
const themes: IThemeProps = {
  'light': {
    'color': '#000',
    'background': '#eee'
  },
  'dark': {
    'color': '#fff',
    'background': '#222'
  }
}
export const ThemeContext = React.createContext(themes.light)
// 将需要值得节点全部包括
// 开始
 <ThemeContext.Provider value={themes.dark}>
 
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.tsx</code> and save to reload.
          </p>
          <input type="text" ref={domRef} />
          <p> x: {positions.x}, y: {positions.y}</p>
          {loading ? <p>读取中</p>
            : <img alt='' src={resultDate && resultDate.message} />
          }
          <Hello />
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
        // 结束
      </ThemeContext.Provider>

// 在组件中使用,引用app中导出得值
import React, { useContext } from 'react'
import { ThemeContext } from '../App'

const Hello: React.FC<IhelloProps> = (props) => {
// 通过useContext使用导入得值
const theme = useContext(ThemeContext)
// 获取值之后 给需要得参数传递参数
const style = {
  color: theme.color,
  background: theme.background
}
// 使用
  return <h2 style={style}>{props.message}</h2>
}
Hello.defaultProps = {
  message: '111111111111'
}
export default Hello;

Hook规则

只在最顶层使用hook

只在react函数中使用hook

其他得hook

useRducer

useCallback

完成一个组件库需要考虑得问题

代码结构

样式解决方案

组件需求分析和编码

组件测试用例分析和编码

代码打包输出和发布

CI/CD,文档生成等等

从简单入手,在需求中慢慢复杂

图片.png

图片.png

图片.png

图片.png

图片.png zhongguose.com/#pinlan

图片.png

图片.png

图片.png

import React, { FC } from 'react'
import classNames from 'classnames'

// export type ButtonSize = 'lg' | 'sm'
// export type ButtonType = 'primary' | 'default' | 'danger' | 'link'

// 或者使用enum 枚举
export enum ButtonSize {
  Large = 'lg',
  Small = 'sm'
}

export enum ButtonType {
  Primary = 'primary',
  Default = 'default',
  Danger = 'danger',
  Link = 'link',
}

interface BaseButtonProps {
  className?: string,
  disabled?: boolean,
  size?: ButtonSize,
  btnType?: ButtonType,
  children: React.ReactNode,
  href?: string
}

const Button: FC<BaseButtonProps> = (props) => {
  const { className, disabled, size, btnType, children, href } = props
  // btn, btn-lg, btn-primary

  const classes = classNames('btn', {
    [`btn-${btnType}`]: btnType,
    [`btn-${size}`]: size,
    'disabled': (btnType === ButtonType.Link) && disabled
  })
  if (btnType === ButtonType.Link && href) {
    return (
      <a href={href} className={classes}>{children}</a>
    )
  } else {
    return (
      <button disabled={disabled} className={classes}>{children}</button>
    )
  }
}
Button.defaultProps = {
  disabled: false,
  btnType: ButtonType.Primary
}
export default Button;

图片.png

图片.png

图片.png

/*
 * @Author: your name
 * @Date: 2021-05-14 16:09:22
 * @LastEditTime: 2021-05-17 10:58:11
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: \my-react-ts-app\src\components\menu\menu.tsx
 */

import React, {useState,createContext} from 'react'
import classNames from 'classnames'
import {MentItemProps} from './menuItem'

export type MenuMode = 'horizontal' | 'vertical'
export type SelectCallBack = (selectIndex: number) => void

export interface MenuProps {
  defaultIndex?: number;
  className?: string;
  mode?: MenuMode;
  style?: React.CSSProperties;
  onSelect?: SelectCallBack
}
interface IMenuContext {
  index : number;
  onSelect?: SelectCallBack
}
export const MenuContext = createContext<IMenuContext>({index: 0})

const Menu: React.FC<MenuProps> = (props) => {
  const { defaultIndex, className, mode, style, onSelect, children } = props;
  const [current, setIndex] = useState(defaultIndex)
  const classnames = classNames('menu', className, {
    'menu-vertical': mode === 'vertical'
  })
  const handleClick = (index:number) => {
    setIndex(index)
    if(onSelect){
      onSelect(index)
    }
  }
  const passedContext:IMenuContext ={
    index: current ? current : 0,
    onSelect: handleClick
  }
  //children是一个不透明得数据,react提供了用于处理this.props.children 不透明数据结构得实用方法
  // 如果出现不符合规则得类型,map完美得跳过这些类型
  // React.Children.map(children, function[(thisArg)])
  const renderChildren = () => {
    return React.Children.map(children, (child, index) => {
      const childElement = child as React.FunctionComponentElement<MentItemProps>
      const {displayName} = childElement.type
      if(displayName === 'MenuItem'){
        return React.cloneElement(childElement, {
          index
        })
      }else{
        console.error('warning22222222222222222');
        
      }
    })
  }
  return (
    <ul className={classnames} style={style}>
      <MenuContext.Provider value={passedContext}>
        {renderChildren()}
        </MenuContext.Provider>
      
    </ul>
  )

}
Menu.defaultProps = {
  defaultIndex: 0,
  mode: 'horizontal'
}

export default Menu

// 子组件menuItem
import React, {useContext} from 'react'
import classNames from 'classnames'
import {MenuContext} from './menu'

export interface MentItemProps {
  index?: number;
  disabled?: boolean;
  className?: string;
  style?: React.CSSProperties;
}

const MenuItem: React.FC<MentItemProps> = (props) => {
  const {index, disabled, className, style, children} = props;
  const context = useContext(MenuContext)
  const classnames = classNames('menu-item', className, {
    'is-disabled': disabled,
    'is-actived': context.index === index
  })
  const handleClick = () => {
    if(context.onSelect && !disabled && (typeof index ==='number')){
      context.onSelect(index)
    }
  }
  return (
    <li className={classnames} style={style} onClick={handleClick}>
      {children}
    </li>
  )
}
MenuItem.displayName = 'MenuItem'
export default MenuItem

//  调用
<Menu defaultIndex = {0} onSelect={(index) => {alert(index)}}>
    <MenuItem>2222222222</MenuItem>
    <MenuItem>2222222222</MenuItem>
    <MenuItem>2222222222</MenuItem>
    <MenuItem>2222222222</MenuItem>
</Menu>


//添加submenu 菜单 可点击二层菜单
import React, {useContext, useState} from 'react'
import classNames from 'classnames'
import {MenuContext} from './menu'
import {MentItemProps} from './menuItem'

export interface SubMenu{
  index ?: number;
  title: string;
  className?: string
}

const SubMenu: React.FC<SubMenu> = ({index, title, className, children}) => {
  const context = useContext(MenuContext)
  const [menuOpen, setOpen] = useState(false)
  const classnames = classNames('menu-item sub-menu', className, {
    'is-active': context.index === index
  })
  const handleClick = (e:React.MouseEvent) => {
    e.preventDefault()
    setOpen(!menuOpen)
  }
   let timer: any
  const handleMouse = (e:React.MouseEvent, toggle: boolean) => {
    clearTimeout(timer)
    e.preventDefault()
    timer = setTimeout(() => {
      setOpen(toggle)
    }, 300);
  }
  const clickEvents = context.mode === 'vertical' ? {
    onClick: handleClick
  } : {}
  const clickMouses = context.mode !== 'vertical' ? {
    onMouseEnter: (e:React.MouseEvent) => {handleMouse(e, true)},
    onMouseLeave: (e:React.MouseEvent) => {handleMouse(e, false)}
  }: {}
  const renderChildren = () => {
    const subMenuClasses = classNames('viking-submenu', {
      'menu-open': menuOpen
    })
    const childComponet = React.Children.map(children, (child, index) => {
      const childElemet = child as React.FunctionComponentElement<MentItemProps>
      if(childElemet.type.displayName === 'MenuItem'){
        return childElemet
      }else{
        console.error('22222222222222');
      }
    })
    return (
      <ul className={subMenuClasses}>
        {childComponet}
      </ul>
    )
  }
  return (
    <li key={index} className={classnames} {...clickMouses}>
      <div className="submenu-title" {...clickEvents}>
        {title}
      </div>
    {renderChildren()}
    </li>
  )
}
SubMenu.displayName = 'SubMenu'
export default SubMenu

react中图标得二次封装

import React from 'react'
import classNames from "classnames";

import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome'

export type ThemeProps = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger'

export interface IconProps extends FontAwesomeIconProps {
  theme?: ThemeProps
  className?: string
}

const Icon: React.FC<IconProps> = (props) => {
  const { className, theme, ...restProps } = props
  const classes = classNames('viking-icon', className, {
    [`icon-${theme}`]: theme
  })
  return (
    <FontAwesomeIcon className={classes} {...restProps} />
  )
}
export default Icon

// 使用
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
library.add(fas)
import Icon from './components/Icon/icon'

...
 <Icon icon="acorn" theme="danger" size="10x"></Icon>

图片.png

$theme-colors: 
(
  "primary":    $primary,
  "secondary":  $secondary,
  "success":    $success,
  "info":       $info,
  "warning":    $warning,
  "danger":     $danger,
  "light":      $light,
  "dark":       $dark
);

@each $key,
$val in $theme-colors {
  .icon-#{$key} {
    color: $val;
  }
}

<Icon icon="coffee" theme="primary" size="10x"></Icon>

图片.png

图片.png