zfkt

148 阅读8分钟

ts+hooks 实现的小型项目笔记

项目依赖

  • gitee.com/zhufengpeix…
  • npm i react react-dom react-router-dom react-transition-group react-swipe qs antd @types/react @types/react-dom @types/react-router-dom @types/react-transition-group @types/react-swipe @types/qs -S
  • npm i webpack webpack-cli webpack-dev-server html-webpack-plugin -D
  • npm i typescript ts-loader source-map-loader style-loader css-loader less-loader less url-loader file-loader -D
  • npm i redux react-redux @types/react-redux redux-thunk redux-logger @types/redux-logger redux-promise @types/redux-promise -S
  • npm i connected-react-router -S

配置文件

  • tsc --init 创建 ts 配置文件
  • webpack.config.js
    • path.join join 连接,只是机械的相连接,不解析真实路径
    • path.resolve 要解析真实路径 永远是一个相对于盘符根目录的绝对路径
  • index.html 增加 refreshRem

rem

;(function () {
  function refreshRem() {
    // 如果屏幕的宽度是750px: 1rem = 100px 为了好计算  iphone6的话 1rem=50px
    // 如果是750的设计稿 div宽度120px 换算成rem就是 1.2rem
    document.documentElement.style.fontSize =
      (document.documentElement.clientWidth / 750) * 100 + 'px'
  }
  refreshRem()
  window.addEventListener('resize', refreshRem, false)
})()

ts 支持@

  • import history from '@/history';
  • tsconfig 增加配置
"baseUrl": ".", // 查找非相对路径的模块的起始位置
"paths": {
  "@/*" : [
    "src/*", // 将@/*映射成src/* 并且src是相对于baseUrl中配置的"."
  ]
}

rootReducer 类型的定义

方法 1: 根据定义的 recuder 对象推出 rootreducer 的类型 (有点像反向推)

const reducers = {
  home,
  mine,
  profile,
  router: connectRouter(history),
}
// 根据reducers推出rootState的类型
export type IRootState = {
  // 迭代reducers所有的key reducers[key]是reducer类型  ReturnType返回此函数类型的返回值类型
  [key in keyof typeof reducers]: ReturnType<typeof reducers[key]>
}

方法 2:正向一步一步定义

// 自定义IRootState类型
interface IRootState {
  home: HomeState
  mine: MineState
  profile: ProfileState
  router: RouterState
}

const reducers: ReducersMapObject<IRootState, AnyAction> = {
  home,
  mine,
  profile,
  router: connectRouter(history),
}

const rootReducer: Reducer<IRootState, AnyAction> =
  combineReducers<IRootState>(reducers)

删除 node_modules

  • npm install rimraf -g
  • rimraf node_modules

withRouter(Tabs)

  • 用 withRouter 包裹是因为 底部导航 Tabs 不是路由渲染的组件
  • 高阶组件中的 withRouter, 作用是将一个组件包裹进 Route 里面, 然后 react-router 的三个对象 history, location, match 就会被放进这个组件的 props 属性中.

怎么在代码中使用图片

  • 默认情况下不能导入一个图片,ts 里默认只能导入 ts、js 代码
  • 为了支持图片导入,需要在 src 目录下建立一个文件叫做 typings/images.d.ts

具体解释

  • 如果在 js 中引入本地静态图片时使用'import img from "./img/logo.png"'这种写法是没有问题的
  • 但是在 ts 中是无法识别非代码资源的,所以会报错 TS2307:cannot find module '.png'
  • 因此需要主动去声明这个 module
  • 新建一个 ts 声明文件如:images.d.ts 这种类型的文件,所以不需要手动去加载他们
  • 当然.d.ts 文件也不能随便放置在项目中 这类文件和 ts 文件一样需要被 typescript 编译,所以一样只能放在 tsconfig.json 中 include 属性所配置的文件夹下
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'

css

  • 可以在 less 文件中声明变量然后进行使用: @BG_COLOR: #2A2A2A;
  • antd 里图标样式的更改:span.anticon.anticon-bars

路由组件的属性类型定义

import React, { PropsWithChildren } from 'react'
import { RouteComponentProps } from 'react-router-dom'

// PropsWithChildren是一个联合类型 在原有属性的基础上加上children属性
// RouteComponentProps 路由组件属性
type Props = PropsWithChildren<RouteComponentProps<Params>>
// 这样定义的props包含children、路由参数location history match,还有本身的属性params
// 通过connect连接redux之后,属性上会多出新的属性和方法 类型就需要拓展
type StateProps = ReturnType<typeof mapStateToProps>
type DispatchProps = typeof actions
type Prop = PropsWithChildren<RouteComponentProps> & StateProps & DispatchProps
/*
function mapStateToProps(state:RootState):HomeState{
  return state.home;
}
*/

ts 类型

React.CSSProperties //CSS 的行内属性

如果定义了一个 interface,想获取到里面包含的所有的 key,作为参数的类型,可以使用 keyof

React.MouseEvent react 合成事件的类型

HTMLUListElement 原生的 dom 类型 就是 ul 元素的类型

ul dom 事件对象类型: event: React.MouseEvent<HTMLUListElement>

  • const target: HTMLUListElement = event.target as HTMLUListElement;//获取事件源对象
interface TransitionStyles {
  entering: React.CSSProperties,
  entered: React.CSSProperties,//CSS的行内属性
  exiting: React.CSSProperties,
  exited: React.CSSProperties,
}
keyof TransitionStyles => entering | entered | exiting | exited
动画库里的stage参数的类型就是keyof TransitionStyles

动画库

  • import { Transition } from 'react-transition-group';//react 官方动画库
// 动画库的使用
// 定义动画的持续时间
const duration = 1000;
// 定义默认样式
const defaultStyle = {//默认样式
  transition: `opacity ${duration}ms ease-in-out`,
  opacity: 0
}
interface TransitionStyles {
  entering: React.CSSProperties,
  entered: React.CSSProperties,//CSS的行内属性
  exiting: React.CSSProperties,
  exited: React.CSSProperties,
}
// 定义不同状态下的样式
const transitionStyles: TransitionStyles = {
  entering: { opacity: 1 }, // 进入中
  entered: { opacity: 1 }, // 进入后
  exiting: { opacity: 0 }, // 离开中
  exited: { opacity: 0 }, // 离开后
}
// 定义 in和timeout属性
<Transition
        in={isMenuVisible} // 显隐变量
        timeout={duration}
      >
// 组件里面是一个函数  参数表示不同的状态 stage
// 通过不同的状态值 增加行内样式来实现动画效果
style={
  {
    ...defaultStyle,
    ...transitionStyles[stage]
  }
}

/*
一开始 isMenuVisible=false  stage为exited
isMenuVisible切换为true
stage会显示 exited entering entered
isMenuVisible切换为false
stage会显示 entered exiting exited
每次切换 状态会变化三次
前两个状态是在切换时立马出现 第三个状态是延迟出现,1秒后出现

当要显示的时候,先设置两种状态
先变 exited {opacity: 0}
立刻变为 entering {opacity: 1}
因为设置了transition,所以会有渐变的效果,会在1s内有0变为1
1s后状态变为entered {opacity: 1} opacity从1到1没什么效果 状态固定下来为entered
要变成第三个状态的原因是可以在第三个状态上加一些额外的样式,如{opacity: 1, color: 'red'}
*/

组件

Home

  • 与 redux 库相连接

HomeHeader

  • 动画
  • 课程列表: 全部、react、vue 切换
    • dom 实现: ul=>li 在每个 li 上定义自定义属性data-category="all"/"react"/"vue"
    • 状态变更: 在 HomeState 中维护状态currentCategory表示当前选中的分类。当点击 li 的时候,将 li 上自定义属性的值存储到 currentCategory 这个状态上。
    • 用事件委托来实现,在 ul 上设置 onclick
    • 获取自定义属性:const category = target.dataset.category;//获取事件源对象对应的自定义属性的值 all react vue
    • 通过 classnames 动态设置类型 达到高亮选中的效果
    className={
      classnames({
        active: props.currentCategory === 'all'
      })
    }
    
  • props
    • 当前分类 currentCategory 跟仓库相关,通过 props 传入
    • 改变当前分类的方法 setCurrentCategory 跟仓库相关,通过 props 传入

Profile 个人中心

NavBar 公共组件 头部导航

import { withRouter, RouteComponentProps } from 'react-router-dom'
type Props = PropsWithChildren<RouteComponentProps>

export default withRouter(NavBar) // 使之能调用props.history.goBack()

身份验证逻辑

  • 在 Profile/index.tsx 这个路由组件中 在组件初始化的时候调接口验证身份 validate
  • redux-promise 可以让我们派发 promise redux-thunk 让我们可以派发函数
// 正常情况下派发的action是一个对象 {type: xxx, payload: xxx}
const actions = {
  validate(): AnyAction {
    //如果action的payload属性是一个promise的话,那么redux-promise中间件可以拦截,等待promise完成,完成后会重新派发
    //dispatch({type:types.VALIDATE,payload:变成原来的payload那个promise resolve出来的值);
    return {
      type: types.VALIDATE,
      payload: validate(),
    }
  },
}

useEffect(() => {
  props.validate().catch((error: AxiosError) => message.error(error.message))
}, [])

注册逻辑

  • action 为 function 时会经过 redux-thunk 的处理,如果里面有异步逻辑,可以包裹自执行函数来实现 async await 的书写
// actions
register(values:RegisterPayload){
  //派发本函数的时候,thunk中间件可以拦截到这个函数,然后让这个函数执行
  return function(dispatch:Function,getState:Function){//redux-thunk
    (async function(){
      let result:RegisterResult = await register<RegisterResult>(values);
      if(result.success){//如果注册成功了,路到登录页登录去
        dispatch(push('/login'));
      }else{
        message.error(result.message);
      }
    })();//自执行函数也会立刻执行
  }
},

axios 请求封装

import axios from 'axios'
axios.defaults.baseURL = 'http://localhost:8899'
// 设置post请求的content-type
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=utf8'
// 请求拦截器
//在每次请求服务器的时候,都要把sessionStorage 存放的token通过Authorization发给服务器
axios.interceptors.request.use(
  (config) => {
    let token = sessionStorage.getItem('token')
    config.headers = config.headers || {}
    config.headers['Authorization'] = `Bearer ${token}`
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

//响应拦截器
//response data headers
axios.interceptors.response.use(
  (response) => response.data, // 提取响应体向外暴露
  (error) => Promise.reject(error)
)
export default axios

首页轮播图的实现

  • let homeContainerRef = useRef(null) homeContainer 是中间部分所有内容的容器,包含轮播图、课程列表
  • 在 Home 路由组件中加入 div, ref=homeContainerRef
  • 在 div 容器里面包裹轮播图 HomeSliders 组件
  • HomeSliders 组件传入的属性:sliders 数组,获取 sliders 的方法
  • 不论是轮播图还是课程列表,都是在组件里面通过在 useeffect 里面判断有没有数据,如果没有,就调用获取数据的方法来加载数据
React.useEffect(() => {
  if (props.lessons.list.length === 0) {
    //如果传过来的课程列表为空
    props.getLessons() //请求加载一次
  }
}, [])

加载课程列表 实现上拉刷新

  • LessonList 课程列表组件也是放在 homeContainer 这个 dom 的里面
  • 在 redux-home 中存储 lessons 相关的状态
  • LessonList 组件传入的属性: lessons 数组,获取 lessons 的方法
  • 逻辑还需要用到 LessonList 的 dom 元素
export interface Lessons {
  loading: boolean; //是否处于加载中
  list: Lesson[]; //课程的数组
  hasMore: boolean; //是否有更多数据 是不是最后一页了
  offset: number; //取的时候偏移量
  limit: number; //每页的条数
}
export interface HomeState {
  title: string;
  currentCategory: string;
  sliders: Slider[];
  lessons: Lessons;
}
const initialState: HomeState = {
  title: '首页',
  currentCategory: 'all',
  sliders: [],
  lessons: {
    loading: false, //是否处于加载中
    list: [], //课程的数组
    hasMore: true, //是否有更多数据 是不是最后一页了
    offset: 0, //取的时候偏移量
    limit: 5, //每页的条数
  },
}

loadMore 方法

  • 给 homeContainer 绑定这个方法
  • 当滚动到底部的时候加载下一页
export function loadMore(element: HTMLDivElement, callback: Function) {
  function _loadMore() {
    let clientHeight = element.clientHeight //容器的高度
    let scrollTop = element.scrollTop //内容向上卷去的高度
    let scrollHeight = element.scrollHeight //内容的高度
    if (clientHeight + scrollTop + 10 >= scrollHeight) {
      // +10的目的是给一个偏移量,快到底的时候(也就是距离底部还剩10的时候)
      callback()
    }
  }
  element.addEventListener('scroll', debounce(_loadMore, 100))
}
//防抖
export function debounce(callback: Function, wait: number) {
  var timeout: any
  return function () {
    if (timeout) clearTimeout(timeout)
    timeout = setTimeout(callback, wait)
  }
}

// 在Home组件中:
React.useEffect(() => {
  //这个home-container这个div
  loadMore(homeContainerRef.current, props.getLessons)
}, [])
// 用于上拉加载的action
getLessons(){//获取下一页的课程数据
  return function(dispatch:Function,getState:Function){
  (async function(){
    let {currentCategory,lessons:{hasMore,offset,limit,loading}}=getState().home;
    //如果的确有更多的数据,并且不是处于加载中的话,就可以加载数据了
    if(hasMore && !loading){
      dispatch({type:types.SET_LESSONS_LOADING,payload:true});//加载状态改为true
      let result = await getLessons(currentCategory,offset,limit);
      dispatch({type:types.SET_LESSONS,payload:result.data});//设置新的课程数据
    }
  })();
  }
},

点击课程列表中的条目跳转到详情页

  • 在课程列表组件中通过 Link 进行跳转,在跳转的时候可以在 to 参数中传入数据
  • to 可以传入对象:pathname 表示跳转路径,state 表示跳转携带的数据
<Link
  to={{pathname:`/detail/${lesson.id}`,state:lesson}}
>
  • 在 Detail 组件中,可以拿到 state 里面传过来的数据
import {StaticContext} from 'react-router'; // 没啥用,只是占位
interface Params{//路径参数 /detail/:id = params={}
    id:string
}
type RouteProps = RouteComponentProps<Params,StaticContext,Lesson>;

// 在Detail组件中
let [lesson,setLesson]= useState<Lesson>({} as Lesson);
useEffect(()=>{
  (async function(){
    // 当能娶到lesson可以直接设置,否则请求接口来进行获取
    let lesson:Lesson = props.location.state; // 在RouteComponentProps中传入 StaticContext,Lesson 就是为了这里定义Lesson类型不报错
    if(!lesson){ // 如果用的是hash路由进行刷新的或者是直接访问的不是通过列表跳转过来的,需要请求数据
      let id = props.match.params.id;
      let result:LessonResult = await getLesson<LessonResult>(id);
      if(result.success){
        lesson = result.data;
      }
    }
    setLesson(lesson);
  })();
 },[]);

下拉刷新

// 用于下拉刷新的action 重新加载第一页的数据
refreshLessons(){//重新获取第一页的数据,读取最新的分类
   return function(dispatch:Function,getState:Function){
    (async function(){
      let {currentCategory,lessons:{limit,loading}}=getState().home;
      //如果的确有更多的数据,并且不是处于加载中的话,就可以加载数据了
      if(!loading){
        dispatch({type:types.SET_LESSONS_LOADING,payload:true});//加载状态改为true
        let result = await getLessons(currentCategory,0,limit);
        dispatch({type:types.REFRESH_LESSONS,payload:result.data});//设置新的课程数据
      }
    })();
   }
  }

为了实现下拉刷新,homeContainer div 需要绝对定位,固定高度

.home-container {
  position: fixed;
  top: 100px;
  left: 0;
  width: 100%;
  overflow-y: auto;
  height: calc(100vh - 222px);
  background-color: #fff;
}

在 touchstart 事件发生时才去绑定 touchmove 和 touchend 事件

/**
 * 下拉刷新
 * element 容器
 * callback刷新数据的函数
 */
export function downRefresh(element: HTMLDivElement, callback: Function) {
  let startY: number //变量,存放下拉时候起始纵坐标
  let distance: number //本次下拉的距离
  let originalTop = element.offsetTop //在初始状态下元素距离顶部的距离 初始top值
  let startTop: number
  let $timer: any
  //touchStart touchMove touchEnd
  element.addEventListener('touchstart', (event: TouchEvent) => {
    if (element.scrollTop === 0) {
      // 只有当没有滚动的时候才会走这个逻辑  如果滚动条不在顶部,就不触发
      startTop = element.offsetTop //元素初始top值
      startY = event.touches[0].pageY //开始按下的时候的纵坐标
      let touchMove = throttle(_touchMove, 30)
      element.addEventListener('touchmove', touchMove)
      element.addEventListener('touchend', touchEnd)
    }
    function _touchMove(event: TouchEvent) {
      let pageY = event.touches[0].pageY //移动过程中最新的纵坐标
      if (pageY > startY) {
        //说明是下拉
        distance = pageY - startY
        element.style.top = startTop + distance + 'px'
      } else {
        element.removeEventListener('touchmove', touchMove)
        element.removeEventListener('touchend', touchEnd)
      }
    }
    function touchEnd(event: TouchEvent) {
      element.removeEventListener('touchmove', touchMove)
      element.removeEventListener('touchend', touchEnd)
      if (distance > 50) {
        callback()
      }
      function _back() {
        let currentTop = element.offsetTop
        if (currentTop - originalTop >= 1) {
          element.style.top = currentTop - 1 + 'px' // 每次回弹1个像素
          requestAnimationFrame(_back) // 递归
        } else {
          element.style.top = originalTop + 'px'
        }
      }
      requestAnimationFrame(_back) // 每当渲染一帧开始的时候执行_back方法
    }
  })
}

export function throttle(func: Function, delay: number) {
  // 降低事件的执行频率
  let prev = Date.now()
  return function () {
    let context = this
    let args = arguments
    let now = Date.now() //每次执行节流函数的时候,取出最新的时间
    if (now - prev >= delay) {
      //减去上次执行的时候,如果已经超过delay,就再执行一次
      func.apply(context, args)
      prev = now
    }
  }
}

实现虚拟列表

  • 如果页面里的 dom 元素太多的话,会卡顿,可通过只渲染可视区域内的 dom 元素来进行优化
  • 需要对课程列表 LessonList 实现虚拟列表
  • 具体实现举例
    • 渲染容器内的内容,容器外的内容不渲染
    • 假设容器 width=100,height=100,每条数据高度 itemHeight=10
    • 容器的作用是根据卷曲的高度 scrollTop 来计算当前在容器中显示的数据条目对应的是哪几条
    • 在所有数据外面会包裹一个 div 作为内容容器
    • 内容容器相对定位,里面的每个元素是相对于内容容器绝对定位
    • 内容容器要设置 height 值,为数据条目乘以每一条数据的高度
// 要对LessonList组件传入ref,homeContainerRef
const homeContainerRef = React.useRef(null);//后面用来实现上拉加载,下拉刷新,虚拟列表
const lessonListRef = React.useRef(null);//用来实现虚拟列表
<div className="home-container" ref={homeContainerRef}>
  <HomeSliders sliders={props.sliders} getSliders={props.getSliders}/>
  <LessonList
      lessons = {props.lessons}
      getLessons = {props.getLessons}
      homeContainerRef={homeContainerRef}
      ref={lessonListRef}
  />
</div>

// 由于LessonList是函数组件,不能传递ref属性,需要用forwardRef包裹一下
function LessonList(props, PropsWithChildren<Props>, forwardedRef:any){

}
React.forwardRef(LessonList);
  • 在 Home 组件中,给 homeContainer dom 绑定滚动事件,当滚动事件触发时重新渲染 LessonList 组件
React.useEffect(() => {
  loadMore(homeContainerRef.current, props.getLessons) // 绑定上拉加载
  downRefresh(homeContainerRef.current, props.refreshLessons) // 绑定下拉刷新
  // 下面的两行代码都是触发LessonList刷新重新渲染
  lessonListRef.current() //ref.current可以存放任何东西,不一定是DOM
  // throttle(lessonListRef.current,16) 16ms是为了每一帧渲染执行一次
  homeContainerRef.current.addEventListener(
    'scroll',
    throttle(lessonListRef.current, 30)
  )
}, [])
  • 在 LessonList 组件中
function LessonList(props: PropsWithChildren<Props>, forwardedRef: any) {
  // 只是为了得到一个强制刷新函数组件的方法
  let [, forceUpdate] = React.useReducer((x) => x + 1, 0)
  lessonListRef.current = forceUpdate
}
.home-container{
    position: fixed;
    top:100px;
    left:0;
    width:100%;
    overflow-y: auto;
    height: calc(100vh - 222px);
    background-color: #FFF;
}
.lesson-list{
    h2{
        line-height: 100px;
        span{
            margin: 0 10px;
        }
    }
    .ant-card.ant-card-bordered.ant-card-hoverable{
        height:650px;
        overflow: hidden;
    }
}

// card的高度固定成了650px
// 将课程list的map渲染方法包裹在一个div里面,并且对这个div动态设置高度
<div style={{position:'relative',width:'100%',height:`${props.lessons.list.length*itemSize}px`}}>
 {
    visibleList.map()
 }
</div>
/*
let docElement = document.documentElement;//获取根元素
function setRemUnit(){//设置rem的单位px 默认是16px
    //docElement.clientWidth=375 docElement.style.fontSize=37.5px
    docElement.style.fontSize = docElement.clientWidth/10+'px';
}
*/
// itemSize就是想计算出卡片在不同尺寸下的像素高度
// 650是在750的设计稿下给卡片设置的高度  75是再设置宽度为750的时候设置的根元素的fontSize,也就是对应的1rem等于的像素值
// 所以650/75就是卡片占的高度rem值,再乘以remUnit得到像素高度值
let remUnit:number = parseFloat(document.documentElement.style.fontSize);//真实的rem值
let itemSize = (650/75)*remUnit;
// 屏幕的高度 - 头和底部导航栏的实际像素高度
// 得到显示内容的容器的高度 也就是homeContainer的高度
let screenHeight=window.innerHeight -(222/75)*remUnit;//屏幕的高度 header+footer总高度
if(homeContainer){
  const scrollTop =  homeContainer.scrollTop;//获取容器向上卷去的高度
  start = Math.floor(scrollTop /itemSize);//要显示的条目的起始索引
  end = start + Math.floor(screenHeight/itemSize);//10
  start -=2,end +=2;//缓存的条目  防止滚动太快显示白屏  所以设置缓存 向上向下都多显示两条数据
  // 越界判断
  start  = start<0?0:start;//如果少于0的就取0
  end = end > props.lessons.list.length?props.lessons.list.length:end;//如果已经 大于最后一条了,取最后一条
}
let visibleList = props.lessons.list.map((item:Lesson,index:number)=>({...item,index})).slice(start,end); // 可视条目列表
let cardStyle:React.CSSProperties = {position:'absolute',top:0,left:0,width:'100%',height:itemSize};
let bottomTop = props.lessons.list.length*itemSize;

return (
  <section className="lesson-list">
    <h2><MenuOutlined/>全部课程</h2>
    <Skeleton
        loading={props.lessons.list.length === 0 && props.lessons.loading}
        active
        paragraph={{rows:8}}
    >
    <div style={{position:'relative',width:'100%',height:`${props.lessons.list.length*itemSize}px`}}>
      {
        visibleList.map((lesson:VisibleLesson,index:number)=>(
            <Link
              key={lesson.id}
              to={{pathname:`/detail/${lesson.id}`,state:lesson}}
              style={{...cardStyle,top:`${itemSize*lesson.index}px`}}
              >
                <Card hoverable={true} style={{width:'100%'}}
                    cover={<img src={lesson.poster}/>}
                >
                    <Card.Meta title={lesson.title} description={`价格:¥${lesson.price}`}/>
                </Card>
            </Link>
        ))
      }
      {
          props.lessons.hasMore?(
              <Button style={{textAlign:'center',top:`${bottomTop}px`}} onClick={props.getLessons} loading={props.lessons.loading} type="primary" block>{props.lessons.loading?'':'加载更多'}</Button>
          ):(<Alert style={{textAlign:'center',top:`${bottomTop}px`}} message='我是有底线的' type='warning'/>)
      }
    </div>
    </Skeleton>
  </section>
)

购物车的实现 (redux-immer ,redux-persist)

redux-immer 在更新 state 的时候就不用先解构老的 state,再赋值

// 正常的写法
case actionTypes.GET_SLIDERS:
  return {...state, sliders: action.payload.data}

// 引入redux-immer之后的写法
case actionTypes.GET_SLIDERS:
  state.sliders = action.payload.data;
  break;
// import {combineReducers} from 'redux'
import { combindeReducers } from 'redux-immer'
import produce from 'immer'

// let rootReducer = combineReducers(reducers);
let rootReducer = combineReducers(produce, reducers)

redux-persist 实现刷新之后数据还在 redux 数据持久化

  • 持久化是说当改变 redux 仓库的时候,会把数据保存一份,可以保存在 localStorage、cookie、接口等,可以指定位置
  • npm install redux-persist
import { store, persistor } from './store'
import { PersistGate } from 'redux-persist/integration/react'
ReactDOM.render(
  <Provider store={store}>
    <PersistGate loading={<Spin />} persistor={persistor}></PersistGate>
  </Provider>
)
  • 在 store 文件中
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
const persistConfig = {
  key: 'root', // 如果把数据保存到localStorage里 setItem时对应的key
  storage, // 保存策略 默认是localStorage
  // whitelist: ['cart'], // 如果设置了白名单,只会持久化白名单里字段里的数据
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = applyMiddleware(
  routerMiddleware(history),
  thunk,
  promise,
  logger
)(createStore)(persistedReducer)
const persistor = persistStore(store)
export { store, persistor }
  • 购物车逻辑
// 加入购物车  动画效果的实现
const addCartItem = (lesson: Lesson) => {
  let video:HTMLVideoElement = document.querySelector('#lesson-video'); // video标签
  let cart:HTMLSpanElement = document.querySelector('.anticon.anticon-shopping-cart'); // 底部导航的购物车图标
  // 深克隆一份video标签
  let cloneVideo:HTMLVideoElement = video.cloneNode(true) as HTMLVideoElement;
  // video的宽高 购物车图标的宽高
  let videoWidth = video.offsetWidth;
  let videoHeight = video.offsetHeight;
  let cartWidth = cart.offsetWidth;
  let cartHeight = cart.offsetHeight;
  // 位置
  let videoLeft = video.getBoundingClientRect().left;
  let videoTop = video.getBoundingClientRect().top;
  let cartRight = cart.getBoundingClientRect().right;
  let cartBottom = cart.getBoundingClientRect().bottom;
  cloneVideo.style.cssText = `
    z-index:1000;
    opacity:0.8;
    position:fixed;
    width:${videoWidth}px;
    height:${videoHeight}px;
    top:${videoTop}px;
    left:${videoLeft}px;
    transition: all 2s ease-in-out
  `;
  document.body.appendChild(cloneVideo); // 克隆的元素添加到页面上,整个覆盖住原来的video元素
  setTimeout(() => {
    cloneVideo.style.left = (cartRight - (cartWidth/2)) + 'px';
    cloneVideo.style.top = (cartBottm - (cartHright/2)) + 'px';
    cloneVideo.style.width = '0px';
    cloneVideo.style.height = '0px';
    cloneVideo.style.opacity = '0.1';
  })
}