通过Decorator让Taro支持本地化mock请求

1,227 阅读5分钟

前言

先说一下为什么要干这个事,笔者最近又开始开发小程序了,想起之前团队的小程序项目呢采取mini+server的模式,小程序中的请求经过node server(egg\express\koa)然后在node端去mock返回,某种程度上也增加项目复杂度,开发小程序的同时还要去维护另外的一个项目形成了一种依赖耦合。于是乎开始构思如果实现Taro的本地化mock,在Taro社区和文档查阅的相关资料都只有插件的情况下略感不满,遂有此文。通过Taro官方提供的cli初始化整个小程序项目的时候,taro的模板虽然提供ts/react/less等语法支持,但是对于mock的请求不支持。如果要支持mock,官方也有提供的插件@tarojs/plugin-mock 但是本人碍于强迫症,对于个人所维护的项目秉持着插件越少越好的原则,因为过多的插件不仅会带来很大的维护成本以及接手成本,下一个人接手的时候遇上没遇到过的插件无疑都要一一翻看文档,相比起代码支持Mock来说,代码的阅读性比起黑盒子插件可观的多了。况且mock本身属于业务层的范畴,如果使用了这个插件,配置文件还要引入业务的mock data,个人觉得是一种污染吧。本文部分代码使用了ts,建议可先阅读上篇文章效果更佳拥抱ts之后更优雅的异步请求处理;

初始化Taro

通过taro cli可以初始化一个如下模板项目 项目非常简单 起初只有pages和store模块。如果要让他支持mock 需要加以改造。

封装Taro请求

在src目录下新建一个模块helpers,在新建一个api.ts的文件干这个事,先封装一下Taro.request

function http(method: 'POST' | 'GET', url, data = {}) {
    return new Promise(function (resolve, reject) {
        Taro.request({
            url: `${SERVERURL}${url}`,
            method,
            data,
            dataType: 'json',
            header: {
                'Content-Type': 'application/json; charset=utf-8',
                'token': token
            },
            success: function (res) {

                if (res.statusCode === 200) {

                    if (res.data.code === 400) {
                        console.log('400,参数错误')
                        reject(res)
                    }
                    if (res.data.code === 401) {
                        console.log('401,登录过期')
                        Taro.showToast({
                            title: '登录过期,重新登录',
                            icon: 'none',
                            duration: 2000
                        })
                        reject(res)
                    }

                    if (res.data.code === 404) {
                        console.log('404,接口错误')
                        reject(res)
                    }

                    if (res.data.code === 500) {
                        console.log('500,后台程序错误')
                        reject(res)
                    } else {
                        resolve(res)
                        console.log(res)
                    }
                } else {
                    reject(res)
                    console.log(res)
                }
            },
            fail: function (err) {
                console.log(err)
                reject(err)
            }
        })
    })
}

管理mock数据

src目录下新增mock目录,单独管理mock数据,并且可以按照业务需要按照业务模块去划分mock。 intex 负责收集所有的mock,封装成promise进行输出。

// index.ts
import login from './login';
import isFunction from 'lodash/isFunction';
import isObject from 'lodash/isObject';

export const promiseMockData = (res) => {
    return Promise.resolve([
        {
            data: res,
            error_code: 0,
            message: "ok",
        },
        undefined
    ]);
}

function pack(...mocks) {
    const allMocks = mocks.reduce((totalMock, mock) => {
        let mockAPIs;

        if (isFunction(mock)) {
            mockAPIs = mock();
        } else if (isObject(mock)) {
            mockAPIs = mock;
        } else {
            throw new Error('mock file require both Function or Object');
        }
        return {
            ...totalMock,
            ...mockAPIs
        }
    });
    Object.keys(allMocks).forEach(key => {

        // 定制化
        if (isFunction(allMocks[key])) {
            allMocks[key] = allMocks[key]();
        } else {
            allMocks[key] = promiseMockData(allMocks[key])
        }
    });
    return allMocks;
}

export const allMockData = pack(
    login
)

login就是普通的业务了,可以按照api对mock进行书写,非常简单易用,因为内部封装了status等信息,对外部mock的部分只暴露了data部分,如果需要定制化返回的api对应的也可以采用函数



const mock = {
    '/api/minilogin': {
        name: 'lemon',
        age: 26,
        phone: 15602281234,
        level: 1,
        token: '123213'
    }
}

export default mock;

拦截请求进行mock映射

回到我们的helpers/api.ts的模块,其实要拦截一个请求可以把这个问题看成拦截一个函数一个方法,实现的方式有很多种,笔者能想到的办法有如下几种:

  • 设置钩子,仿照axios-mock-adaptor的设计,这个工具将axios传入,并且在请求的时候通过拦截onGet/onPost钩子,把mock数据返回。
  • 中间件,可以仿照redux的dispatch的chunk-middleware设计,将Mock数据返回前置于请求
  • Decorator装饰器模式,拦截方法的结果,用Mock数据替代并返回。

三种模式都有各自的好处,本文采用的是装饰器模式,话不多说可以先看看这个装饰器做了啥

import { allMockData } from '../mock';
function mockDecorator(target, key, descriptor) {

    const oldValue = descriptor.value;

    // 修改 descriptor属性 注入mock
    descriptor.value = function (p: { url: string, data?: any, form?: boolean }) {
		
        // 判断环境变量 返回mock
        if (process.env.NODE_ENV === 'development') {
            return allMockData[p.url];
        } else {
            return oldValue.apply(this, arguments);
        }
    }
    return descriptor;
}

import了mock/index输出得mock data集合,判断环境环境更改原来方法的输出,但是因为装饰器只能用于class类,所以我们需要对我们之前设计的http的方法进行改造。

// 返回一个结果固定的promise
function packPromise<T = any>(p: Promise<any>): [T, any] {
    return p.then(res => [res, undefined]).catch(err => [undefined, err]) as unknown as [T, any];
}

class Api {
    @mockDecorator
    async Post<T = any>(p: { url: string, data?: any, form?: boolean }): Promise<[FetchRes<T>, any]> {
        const { url, data = {}, form = false } = p;
        const [res, err] = await packPromise<FetchRes<T>>(http('POST', url, form ? assign(data, { form }) : data));
        return [res, err]
    }

    @mockDecorator
    async Get<T = any>(p: { url: string, params?: any }): Promise<[FetchRes<T>, any]> {
        const { url, params = {} } = p;
        const [res, err] = await packPromise<FetchRes<T>>(http('GET', url, params));
        return [res, err]
    }
}

const api = new Api();

export const { Get, Post } = api;

检验mock效果。

在pages页面写下如下业务代码

// @ts-nocheck
import React, { Component } from 'react'
import { View } from '@tarojs/components'

import './index.less'

export interface UserData {
  name: string
  code: string
  date: number
  avator: string
  level: number
  token: string
}

class Index extends Component {

  componentDidMount() {
    this.login()
  }
  
  login = async () => {
		const [res, err] = await Get<UserData>({
			url: '/api/minilogin'
		});

		switch (true) {
			case !!err:
				console.log('login err');
				break;
			case !!res:
                                console.log('login success');
                                console.log(res);
				break;
			default:
				console.log('login not res');
		}
	}

  render () {
    return (
      <View className='index'>
        login
      </View>
    )
  }
}

export default Index

控制台输出如下

总结

一切do work之后,本地化mock实在太舒适了,不需要依赖其他的项目,等到后台可以联调的时候更改全局变量SERVERURL和环境变量即可去掉mock。