typescript + react + redux + axios+mock+ + hooks 开发模式探索

redux

types.ts

首先根据typescript的特性,需要对redux中的接口和数据类型进行interfacetype定义

// 数据类型
export interface todoItem{
    id:string,
    content:string,
    done:boolean
}
export interface todos{
    [index:number]:todoItem
}

// action.type
export const add_Todo="addTodo";
export const set_Done="setDone";

// action 接口类型
interface addTodoAction{
    type:typeof add_Todo
    content:string
}
interface setDoneAction{
    type:typeof set_Done
    id:string
    done:boolean
}

// 联合类型表明可能的action
export type TodoActionTypes=addTodoAction|setDoneAction

action.ts

依据types类型定义,需要对action进行函数定义,使得后面可以在组件中直接注入reducer

import {add_Todo,set_Done,TodoActionTypes} from './types'

// action 定义
export function addTodo(content:string) :TodoActionTypes{
    return{
        type:add_Todo,
        content
    }
}

export function setDone(id:string,done:boolean):TodoActionTypes{
    return{
        type:set_Done,
        id,
        done
    }
}

reducers.ts

依据types类型与接口定义,编写reducer函数,实现redux功能

import {todoItem,TodoActionTypes,delete_Todo,add_Todo,set_Done,set_Content,toggle_Done} from './types'

function guid(){
    var s4=()=>{
        return (((1+Math.random())*0x10000|0).toString(16).substring(1))
    };

    //return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`
    return s4()
}

const newTodo=(content:string)=>({
    done:false,
    id:guid(),
    content:(content||"").trim()
})

const initialState:todoItem[]=JSON.parse(localStorage.getItem("todos")||"[]");

export function TodoReducer(
    state=initialState,
    action:TodoActionTypes
) :todoItem[]{
    switch(action.type){
        case delete_Todo:
            return state.filter(todo=>todo.id!==action.id);
        case add_Todo:
            return [...state,newTodo(action.content)];
        case set_Done:
            return state.map(todo=>todo.id===action.id?{...todo,done:action.done}:todo);
        case set_Content:
            return state.map(todo=>todo.id===action.id?{...todo,content:action.content}:todo);
        case toggle_Done:
            return state.map(todo=>todo.id===action.id?{...todo,done:!todo.done}:todo);
        default:
            return state
    }
}

middleware.ts

根据业务需求,编写中间件,在每次reducer执行前后完成特定操作

import {TodoActionTypes} from './types'
import { todoItem } from './types';
import {postTodo} from '../../axios/index'

export type MiddlewareFunction = (store: any) => (next: any) => (action: TodoActionTypes) => any;

const setLocal=(todos:todoItem[])=>{
    localStorage.setItem('todos',JSON.stringify(todos));
}

const post=async (todos:[])=>{
    const res= await postTodo(todos);
    console.log(res)
}

export const localSet :MiddlewareFunction= store => next => action => {
    let result = next(action)
    setLocal(store.getState().TodoReducer)
    return result
}

export const postTodos:MiddlewareFunction=store=>next=>action=>{
    console.log(store.getState().TodoReducer)
    let result = next(action);
    post(store.getState().TodoReducer)
    return result
}

index.ts

进行reducers合并,并且暴露store
并且,为了后面能够将数据state注入到组件中,需要暴露Reducer的typescript类型

import {combineReducers,createStore,applyMiddleware} from 'redux'
import {localSet,postTodos} from './todos/middleware'
import {TodoReducer} from './todos/reducers'

// 合并reducers
const rootReducer=combineReducers({
    TodoReducer
})

export const store=createStore(rootReducer,
	// 中间件注入 
    applyMiddleware(localSet));				
    
export type AppState=ReturnType<typeof rootReducer>

结合react使用

首先,需要在react主入口中注入store,使得在整体环境中可以获取redux

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

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

接着,就是在组件中使用,理论上,现在可以通过props.store.dipatch()以及props.store.getState()使用redux了,但是对于复杂的业务场景,需要把state和reducer直接注入到组件中进行使用,如下:

import { connect } from 'react-redux'
import { bindActionCreators, Dispatch } from 'redux';
import {AppState} from '../../reducers/index';

import {todoItem} from '../../reducers/todos/types';
import {deleteTodo,addTodo,setDone,setContent,toggleDone} from '../../reducers/todos/actions'
import * as actions from '../../reducers/todos/actions'
// 将数据state注入到组件中
const mapStateToProps=(state:AppState)=>({
  todos:state.TodoReducer
})
// 根据actions定义将dispatch方法注入到组件中
const mapDispatchToProps=(dispatch:Dispatch)=>({
  todosAction:bindActionCreators(actions,dispatch)
})
// 定义组件props类型
interface Props{
  todosAction:{
    deleteTodo:typeof deleteTodo,
    addTodo:typeof addTodo,
    setDone:typeof setDone,
    setContent:typeof setContent,
    toggleDone:typeof toggleDone,
  }
  todos:todoItem[]
}

const TodoList:React.FC<Props> = (props)=>{
	// 从props中获取state和dispatch方法
    let reduxTodos=props.todos;
    let {addTodo,deleteTodo,setDone,setContent,toggleDone}=props.todosAction;
}

// 实现注入 
export default connect(mapStateToProps,mapDispatchToProps)(TodoList)

axios

axios实现对服务端的请求

import axios from 'axios'
import {AxiosInstance} from 'axios'
import { todoItem } from '../reducers/todos/types';

const axiosTodo:AxiosInstance=axios.create({
    baseURL:"http://localhost:3001"
});

axiosTodo.interceptors.request.use(req=>{
    console.log('http request  '+req.method+'  '+req.url);
    return req
})


export function getTodo(params:object={}){
    const url='/todo';
    return axiosTodo({
        method:'GET',
        url,
        params
    }).then(res=>{
        return res.data?.todos||[]
    }).catch(err=>{
        console.log(err);
        return err
    })
}

export function postTodo(data:todoItem[],params:object={}){
    const url='/todo';
    return axiosTodo({
        method:'POST',
        url,
        params,
        data:data
    }).then(res=>{
        return res.data
    }).catch(err=>{
        console.log(err);
        return err
    })
}

axios实现对mock数据的请求

import axios from 'axios'

// 环境判断后,进行mock的引入
if(process.env.NODE_ENV=='development'){
    require('../mock/product');
}

const axiosMock=axios.create({
    baseURL:'/localtest',
})

axiosMock.interceptors.request.use((req)=>{
    console.log('catch data from mock :'+req.method+'  '+ req.url);
    return req
})

export function getproduct(count=16){
    const url='/products';
    return new Promise((resolve,reject)=>{
        return axiosMock.get(url).then(res=>{
            resolve(res.data)
        }).catch(err=>{
            reject(err)
        })
    }) 
}

mock的定义形式如下:

import Mock from 'mockjs'

// 设置延时
 Mock.setup({
     timeout:500
})

// 请求形式与返回数据的定义
export default Mock.mock('/localtest/products','get',{
    "arr|12":[
        {
            "id|+1":1,
            "name|1-2":'商品名',
            "brand|+1":['Apple','Beats','Sonos','B&O','Bose','DJI'],
            "color|+1":['白色','金色','红色','蓝色','黑色'],
            "sales|+1":588,
            "cost|+1":1288,
            "stock|+1":2
        }
    ]
})

hooks

对于一些常用的hooks,给出一些使用样例:

useEffect

对于异步请求,可以将useEffect看作是mountedunmounted

import React, {useCallback,useMemo,useEffect} from 'react'

let [todos,setTodos]=useState([])
useEffect(() => {
      const get=async ()=>{
        const todos= await getTodo();
        console.log(todos)
        setTodos(todos)
      };
      get();
    },[setTodos])

如果对于useState([])中的数据需要进行定义,使得后面业务代码可以正确判断类型,可以进行封装:

import {useState} from 'react'
import {todoItem} from '../reducers/todos/types';

export default function useTodoState(defaultValue:todoItem[]){
    const [todos,setTodos]=useState(defaultValue);

    return [todos,setTodos] as const
}

useMemo

对于一些依赖一些数据进行计算的数据,可以使用useMemo,指定依赖的数据项

const visibleTodos = useMemo(
        () =>
          filter
            ? todos.filter(todo =>
                filter === "active" ? !todo.done : todo.done
              )
            : todos,
        [todos, filter]
      );

    const ifAnyDone=useMemo(()=>todos.some(todo=>todo.done),[todos]);
    const allDone=useMemo(()=>visibleTodos.every(todo=>todo.done),[visibleTodos]);

useCallback

对于需要注入到元素中的方法,可以通过useCallback进行封装,依赖一些数据项决定是否重设;也可以对一些逻辑进行分离

const onToggleAll=useCallback(()=>{
      visibleTodos.forEach(todo=>setDone(todo.id,!allDone))
    },[visibleTodos,allDone]);

    const onClearCompleted=useCallback(()=>{
      todos.forEach(todo=>{
        if(todo.done){
          deleteTodo(todo.id)
        }
      })
    },[todos]);

封装hooks

在业务开发中,可以对一些hooks进行二次封装,以完成我们对逻辑的分离,减少冗余代码,暴露特定的接口

比如,对input输入的onChange,对应到useState设置的值,可以进行简单的封装,暴露onChange接口

import {useCallback,useState} from "react"

export default function useInput(deafultValue:string=""){
    const [value,setValue]=useState(deafultValue||"");

    const onChange=useCallback(value=>{
        setValue(value)
    },[]);

    return [value,onChange,setValue] as const
}

对于需要对输入的特定情况进行监听,对应到useCallback需要返回一个函数,可以进行判定的封装:

import { useCallback } from 'react';

export interface callback{
    (e:object):void
}

export default function useOnEnter(callback:callback,inputs:string[]|object[]){
    return useCallback(event=>{
        if(event.key==="Enter"){
            event.preventDefault();
            callback(event);
        }
    },inputs);
}

使用:

const onAddTodo=useOnEnter(()=>{
      if(newValue){
        addTodo(newValue);
        setValue('')
      }
    },[newValue])

特定逻辑封装

根据业务逻辑,对于一些非hooks情景,依据闭包的形式进行封装:

interface onClick{
    (event:object,...rest:[]):void
}

interface onDoubleClick{
    (event:object,...rest:[]):void
}

export default function useDoubleClick(onClick:onClick|null,onDoubleClick:onDoubleClick|null) {
    let clicks:number[]=[];
    let timeout:number;

    return (event:object,...rest:[])=>{
        clicks.push(new Date().getTime());

        clearTimeout(timeout);
        timeout=window.setTimeout(()=>{
            if(clicks.length>1&&(clicks[clicks.length-1]-clicks[clicks.length-2]<250)){
                if(onDoubleClick){
                    onDoubleClick(event,...rest);
                }
            }else if(onClick){
                onClick(event,...rest);
            }
            clicks=[];
        },250)
    }
}

结语

react生态强大,以上只是简单的探索,作为个人记录使用

你可能感兴趣的:(前端知识记录)