最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 基于hooks api手写dva----useModel

    正文概述 掘金(佟舟)   2021-06-03   1533

    0、为什么不使用react-redux

    学习react最早使用react-redux,但是写法太麻烦,后来接触蚂蚁金服umi dva这一套体系,开始dva,简化了redux的写法,同时有了model的概念。 后来学习hooks,感觉hooks那种纯函数的写法更加优雅,一直使用hooks一直爽,后面学习到hooks的useContext、useReducer相关api,这些api配合context完全可以代替react-redux。

    还有一个原因,有次在b站看教程React Hooks教学教程 | 案例详解 - useReducer,里面看到了这段话,感觉也很有道理:

    基于hooks api手写dva----useModel

    于是有了以下代码:

    1、雏形 useFeature

    我有一段时间使用以下这种写法,虽然脱离了react-redux,但是缺点是不支持异步:

    import React, { useReducer, useContext } from 'react';
    
    /**
     * context
     */
    const IndexContext = React.createContext();
    
    /**
     * reducer
     */
    const initialState = {
      lookingRecord: null,
    };
    
    const initStateFunc = () => {
      return initialState;
    };
    
    const reducer = (state, action) => {
      const { type, ...restState } = action;
    
      if (type == 'SAVE') {
        return {
          ...state,
          ...restState,
        };
      } 
      
      return state;
    };
    
    const ContextProvider = (props) => {
      const [state, dispatch] = useReducer(reducer, initialState, initStateFunc);
    
      return (
        <IndexContext.Provider value={{ state, dispatch }}>{props.children}</IndexContext.Provider>
      );
    };
    
    /**
     * hooks
     */
    const useFeature = () => {
      const { state, dispatch } = useContext(IndexContext);
    
      return {
        state,
        dispatch,
      };
    };
    
    export { ContextProvider, useFeature };
    
    

    最终暴露出来的的是一个provider和一个userFeatrue,其实就是React.createContext创建了一个context,去包裹我想共享数据的页面或者组件,然后被包裹的组件里去使用useFeature就能读取到state也能拿到dispatch去修改state

    使用

    import { ContextProvider, useFeature } from './features/index';
    
    const IndexWrap = (props) => (
      <ContextProvider>
        <App {...props} />
      </ContextProvider>
    );
    
    export default IndexWrap;
    

    组件中:

    基于hooks api手写dva----useModel

    缺点

    不支持异步,只能请求之后再去dispatch修改state

    2、目前的版本:基于context、useContext、useReducer的手写dva--useModel

    看过其他人写的帖子分析redux-thunk,好像就是判断action是function还是对象,如果是function就把dispatch传过去,于是仿造这种写法,在原基础上支持异步,写到最后竟然发现其实这不就是dva么。。。

    相关目录

    基于hooks api手写dva----useModel

    index.js:入口文件

    core.js: 核心方法

    global.js和login.js是不同的模块,根据业务可以在index.js中添加

    我们先看入口文件:

    index.js

    import React from "react";
    import { generateLoadingModel, generateProvider, generateUseModel } from "@/models/core.js";
    import global from "./global";
    import login from "./login";
    
    const allModel = {
        loading: generateLoadingModel(),
        global,
        login
    };
    
    /**
     * redux
     */
    const IndexContext = React.createContext();
    
    // Provider
    const ContextProvider = generateProvider({
        context: IndexContext,
        allModel
    });
    
    /**
     * @returns {{
     * 	state:object;
     * 	dispatch:function;
     *      getLoading:function;
     * }}
     */
    const useModel = generateUseModel({
        context: IndexContext,
        allModel
    });
    
    export { ContextProvider, useModel };
    
    

    最终暴露出去的就是一个provider和一个useModel,这里provider和useModel的使用方式和原来的useFeature是一样的,provider最外层包裹,内部使用useModel可以拿到state和dispatch

    看几个关键的方法:

    generateUseModel : 生成一个useModel,关键代码:里面的thunkDispatch方法

    /**
     * 生成一个useModel
     * @param {object} options
     * @param {*} options.context
     * @param {object} options.allModel
     * @param {function} [options.dealExport]
     */
    export function generateUseModel({ context, allModel, dealExport }) {
        /**
         * @returns {{
         *  state,
         *  dispatch,
         *  getLoading
         * }}
         */
        return function () {
            const { state, dispatch } = useContext(context);
    
            const stateRef = useRef(state);
    
            useEffect(() => {
                stateRef.current = state;
            }, [state]);
    
            function getState() {
                return { ...stateRef.current };
            }
    
            // loading
            function setLoading(type, flag) {
                dispatch({
                    modelName: "loading",
                    methodName: "setLoading",
                    payload: {
                            type,
                            loading: flag
                    },
                    dispatch: thunkDispatch
                });
            }
    
            function getLoading(type) {
                return state.loading[type] || false;
            }
    
            /**
             * 最终暴露出去的,外面用到的dispatch 可以处理异步
             * @param {string} type // 'global/toggle'
             * @param {*} payload // 自定义携带参数
             */
            function thunkDispatch(type, payload) {
                const modelName = type.split("/")[0];
                const methodName = type.split("/")[1];
    
                const modelAction = allModel[modelName].actions?.[methodName];
    
                if (modelAction) {
                    // 异步
                    setLoading(type, true);
                    modelAction({
                        getState,
                        payload,
                        dispatch: thunkDispatch
                    }).finally(() => {
                            setLoading(type, false);
                    });
                } else {
                    // 同步
                    dispatch({
                        modelName,
                        methodName,
                        payload,
                        dispatch: thunkDispatch
                    });
                }
            }
    
            // 暴露出去
            const defaultExport = {
                state,
                dispatch: thunkDispatch,
                getLoading
            };
    
            if (dealExport) {
                return dealExport(defaultExport);
            }
            return defaultExport;
        };
    }
    

    这里和雏形版useFeature的区别最大的一点是 我们暴露出去的dispatch并不是useReducer()出来的原生的dispatch,是thunkDispatch,这里先看一下model的结构和dispatch(指thunkDispatch)在组件中的使用方法:

    model的结构:

    例:globalModel

    function toggleCollapsedAjax(flag) {
        return new Promise((resolve) => {
            setTimeout(() => {
                resolve(!flag);
            }, 1000);
        });
    }
    
    const initialState = {
        isCollapsed: false
    };
    
    const model = {
        name: "global",
        state: initialState,
        actions: {
            async toggleCollapsedFunc({ dispatch, getState, payload }) {
                await toggleCollapsedAjax();
                const state = getState().global;
                dispatch("global/save", {
                    isCollapsed: !state.isCollapsed
                });
            }
        },
        reducers: {
            save({ state, payload }) {
                return {
                    ...state,
                    ...payload
                };
            }
        }
    };
    
    export default model;
    

    这里actions是异步的方法,reducers是同步的方法

    看下组件中dispatch的调用:

    基于hooks api手写dva----useModel

    thunkDispatch方法接收两个参数,type是这种格式:'global/toggleCollapsedFunc'

    "/"之前是模块名字,后面是调用的方法,可以是action也可以是reducer,我们在thunkDispatch方法中会去判断, 如果是action就先调用action同时把dispatch传进去,如果不是action那就走原生的dispatch,它会触发原生的reducer:

    /**
     * 生成reducer
     * @param {object} options
     * @param {object} options.allModel
     * @returns
     */
    export function generateReducer({ allModel }) {
        /**
         * reducer 原生的dispatch触发的 就是这个
         * @param {*} allState
         * @param {object} options
         * @returns
         */
        return function (allState, options) {
            const { modelName, methodName, payload, dispatch } = options;
    
            const modelReducer = allModel[modelName].reducers?.[methodName];
    
            const oldModelState = allState[modelName];
            // 调用model里的reducer
            const newModelState = modelReducer({
                state: oldModelState,
                payload,
                dispatch
            });
    
            return lodash.cloneDeep({
                ...allState,
                [modelName]: newModelState
            });
        };
    }
    

    调用对应model里的reducer方法去生成一个新的state,然后根据modelName去修改整个state

    这样就是实现了redux的异步调用:)

    3、实现dva的loading

    dva框架有个方便的地方,就是当你调用一个异步请求的时候,它会自动生成一个loading的状态,看一下我是如何实现的:

    allModel中总会有一个loadingModel,它是通过generateLoadingModel这个方法生成的:

    /**
     * 生成一个loading model
     * @returns model
     */
    export function generateLoadingModel() {
        return {
            name: "loading",
            state: {},
            reducers: {
                setLoading({ state, payload, dispatch }) {
                    const { type, loading } = payload;
    
                    const newState = { ...state };
    
                    if (loading) {
                            newState[type] = true;
                    } else {
                            delete newState[type];
                    }
    
                    return newState;
                }
            }
        };
    }
    

    因为每个model中的action都是promise,可以使用.finally的写法

    在thunkDispatch中,如果调用的是action,就在调用前setLoading为true,finally的时候就setLoading为false,注意,在loadingModel的reducer中的setLoading方法,如果set为false的时候,其实是删除掉这个属性,并不是真的把loading的值变成false,因为一个应用的请求一般很多,防止loadingModel这个state属性过于多

    使用方法

    在页面中通过getLoading的方法获取:

    基于hooks api手写dva----useModel

    总结

    基本原理就是使用useContext创建全局状态,useReducer创建dispatch去更新state,主要对dispatch做了封装

    组件中哪里需要就引入useModel,随时可以拿到dispatch和state

    通过以上的写法,现在项目中基本不需要react-redux了

    另外也有局部useModel的写法,详细可以看github :)

    如果发现有不对的地方或者建议 欢迎讨论

    github项目地址:github.com/jiqishoubi/…


    起源地下载网 » 基于hooks api手写dva----useModel

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元