Vue如何优雅的绑定请求状态?

187 阅读2分钟

你是否还在使用以下代码绑定请求的loading状态?

    try {
        this.loading = true;
        let res = await fetch("https://api.com/xxxx")
        console.log(res)
        this.loading = false;
    } catch (err) {
        this.loading = false;
    }

总感觉这样写有点麻烦,而且,不优雅。所以得想办法弄个 mixin 来包装一下请求,自动绑定请求的状态,而且还应该能够绑定深层的状态。比如 loginForm.loading

先创建一个 mixin 吧。

reqStateMixin.js

export const LoadingMixin = {
    methods: {}
};

因为核心功能就是将请求的状态设置到 data 中,所以第一步先搞定 setData 函数,同时支持深层数据。

如果要支持深层的数据,key 就可能是 loginForm.state.loading 这样的,那么我们就可以通过 path.split(".") 进行分割,然后一层一层取 Object,再在 Object 中设置 loading 状态。

有了思路就开始写代码,在 mixin 中新增一个函数

reqStateMixin.js

export const LoadingMixin = {
    methods: {
        _setDeepData(dataKey, data) {
            let dPath = dataKey.split(".");
            let rootObj = this.$data;
            dPath.forEach((key, index) => {
                if (index === dPath.length - 1) {
                    // 如果这是最后一层了,则将 data 设置到 object 中
                    this.$set(rootObj, key, data)
                } else {
                    // 如果当前这一层不是最后一层,则表示这是一个 Object,取出来存好
                    rootObj = rootObj[key];
                    // TODO 不过也有可能取出来的是空,这里需要抛出一个错误
                }
            });
        }
    }
};

这样子就可以绑定到 data 中任意深度的数据了。

接下来再增加一个函数来包装请求,并返回一个包装后的请求。

export const LoadingMixin = {
    methods: {
        // _setDeepData(dataKey, data){...}
        wrapPromising(promise, key = "loading") {
            this._setDeepData(key, true);
            return promise.finally(() => {
                this._setDeepData(key, false);
            });
        },
    }
}

这样子在其他地方调用的时候就可以很优雅了,像这样

new Vue({
    mixins: [ LoadingMixin ],
    template: `<div>loading: {{loading}}</div> <div>deepLoading: {{a.b.c}}</div>`,
    data() {
        return {
            loading: false,
            a: {
                b: {
                    c: false
                }
            }
        }
    },
    methods: {
        loadData() {
            this.wrapPromising(fetch("/api/xxxx"), "loading")
                .then(res => console.log(res))
            this.wrapPromising(fetch("/api/abc"), "a.b.c")
                .then(res => console.log(res))
        }
    }
})

当然,都到了这一步了,如果想绑定 Promise 的整个状态都是可以的。只需要再添加一点细节就好了。像这样:

export const PmStateMap = {
    idle: "idle",
    pending: "pending",
    resolve: "resolve",
    reject: "reject"
};

/// promise状态绑定混合类
export const PmStateMixin = {
    methods: {
        /**
         * 包装 promise,将 promise 状态绑定到 data 中
         * @param promise promise对象
         * @param key 要绑定的 data key。与 vue watch 类似,支持深度绑定,如 "form.loading"
         * @returns Promise
         */
        wrapPromiseState(promise, key) {
            console.assert(key !== "" && key != null, "key的值不能为空");
            this._setDeepData(key, PmStateMap.pending);
            return promise.then((res) => {
                this._setDeepData(key, PmStateMap.resolve);
                return res;
            }).catch(error => {
                console.error(error);
                this._setDeepData(key, PmStateMap.reject);
                return Promise.reject(error);
            });
        },
        /**
         * 包装 promise,将进行中状态绑定到 data 中
         * @param promise
         * @param key 要绑定的 data key。与 vue watch 类似,支持深度绑定,如 "form.loading"
         * @returns Promise
         */
        wrapPromising(promise, key = "loading") {
            this._setDeepData(key, true);
            return promise.finally(() => {
                this._setDeepData(key, false);
            });
        },
        _setDeepData(dataKey, data) {
            let dPath = dataKey.split(".");
            let rootObj = this.$data;
            const notUndefined = (obj, index) => {
                if (obj === undefined) {
                    const joined = dPath.slice(0, index).join(".");
                    const msg = `vm.$data.${dataKey} 中的 ${joined} 不存在 ${key}`;
                    throw new Error(msg);
                }
            };
            const beObject = (obj, index) => {
                if (typeof rootObj !== "object") {
                    const joined = dPath.slice(0, index + 1).join(".");
                    const msg = `vm.$data.${dataKey} 中的 ${joined} 不为 Object`
                    throw new Error(msg);
                }
            };

            dPath.forEach((subKey, index) => {
                if (index === dPath.length - 1) {
                    this.$set(rootObj, subKey, data);
                } else {
                    rootObj = rootObj[subKey];
                    notUndefined(rootObj, index);
                    beObject(rootObj, index)
                }
            });
        }
    },
    computed: {
        pmStateMap() {
            return PmStateMap;
        }
    }
};