也許這是最優雅的前端請求方案

jarvan發表於2019-04-01

原諒我是個標題黨,不過仔細看完這篇文章,一定會有所收穫的!

本篇文章主要目的是把前端的請求方式做一個極度精簡和自動化,給我們繁重的搬磚生活,帶來一點幸福感^_^。

相信跟我一樣的前端狗早就寫煩了各種請求,寫煩了各種請求路徑,大家在專案中請求的方式都各不相同,但是大致的方式都是差不多的。來看看以下大家常用的請求方式弊端分析,看看是不是也有類似的痛點,以vue + axios專案為例。

# 通用的檔案結構
request
|-- config.js
|-- http.js
複製程式碼
// config.js,主要對專案中請求的異常捕捉,新增配置等
import Vue from "vue";
import axios from "axios";
import { Notification } from 'element-ui';
import store from "@/store";

// 配置 Content-Type
axios.defaults.headers.post["Content-Type"] = "aplication/json";

/**
 * 配置 axios
 */
// http request 攔截器
axios.interceptors.request.use(
    config => {
        return config;
    },
    err => {
        return Promise.reject(err);
    }
);

// http response 攔截器
axios.interceptors.response.use(
    response => {
        // 對某些錯誤程式碼進行判斷
        if(response.data.code == 2){ // identity failure
            store.dispatch("LogOut");
        }
        if (response.data.code !== 0 && response.data.msg !== -1) {
            Notification({
                title: '系統錯誤',
                message: response.data.msg,
                type: "error",
                offset: 35,
                duration: 2000
            });
        }
        return response;
    },
    error => {
        console.log(error);
    }
);

export default axios;
複製程式碼

這個檔案,在大家的專案裡應該都是存在的,主要用於請求中的公共配置和異常捕捉。

// http.js,對config完成的axios進行封裝
import axios from config
export function get(url, payload){
    return axios.get(url, {
        params: payload
    })
}

export function post(url, payload){
    return axios.post(url, {
        params: payload
    })
}

// export function delete...
複製程式碼

這個檔案主要對axios的常用方法做一些封裝,主要為了引數傳遞方式一致,呼叫時可以不用寫axios. 等,總之這樣就有了一個統一的入口,呼叫時更加方便。這樣有它的好處,但是有沒有更好的解決方案呢?如果還有 DELETE PUT這些請求方式呢?如果有10種請求方式呢?這裡標記為痛點一,我們在下面解決。

// test.vue
import * as http from '@/path/to/http'

export default {
    methods: {
        getData(){
            https.get('/v1/systemInfo').then(res => {
                // do something
            })
        }
    }
}
複製程式碼

這就是我們呼叫時候的樣子,看起來也很規範了。但是大家試想一下,如果後端對api做了批量的修改呢。如果每個介面呼叫都散落在每個元件檔案裡,我們是不是要渠道每個檔案裡取對它們逐一修改,維護起來就很繁瑣了。還有每次都要去檢視api文件(痛點二)要呼叫的介面路徑(痛點三)和請求方式(痛點四)是什麼,然後複製到業務頁面,每次我們在做這樣的事情的時候心裡是不是在默默罵娘,TMD怎麼這麼多介面要寫???

解決方案

上面說了一大堆廢話,說了那麼多問題。特麼要是沒拿出個好看的方案,勞資...,各位彆著急,聽..聽我慢慢說...

目錄結構

http
|--apiModules
|	|--user.js
|	|--system.js
|--parse
|   |--parse.js
|   |--api.json
|--fetch.js
|--config.js
複製程式碼

這就是我們的目錄結構,下面我就逐一介紹

如何解決痛點一?

為了避免繁瑣的封裝已有的請求方法,我們可以寫一個方法去實現,傳入什麼請求方式,就呼叫axios物件的什麼方法。引數的傳遞方式寫在判斷裡,這樣就避免了我們要用到什麼方式,就需要去封裝一個什麼請求方法。

import axios from './config' // config檔案還是跟上面的一樣,這裡不再說明
// fetch.js
export function fetch(method, url, payload){
    // 檢視axios的文件,我們知道除了get的傳參方式不一樣,其餘的都是直接傳遞,那麼我們只需要判斷get就可以
    if(method === 'get'){
        return axios['get'](url, {params: payload})
    } else {
        return axios[method](url, payload)
    }
}
複製程式碼

所以我們的業務頁面程式碼變成了這樣:

// test.vue
import fetch from '@/path/to/fetch'

export default {
    methods: {
        getData(){
            fetch('get','/v1/systemInfo', {...}).then(res => {
                // do something
            })
        }
    }
}
複製程式碼

這裡看起來其實沒什麼變化,僅僅改了方法名而已。但是又發現了一個小問題,每次新建一個頁面我都需要引用一次fetch嗎?麻煩啊!所以可以直接把fetch方法掛載到vue例項上,那麼在元件內部就可以直接呼叫了,又解決了一個小問題 ^ _ ^

// fetch.js
class Fetch {
    // 給Vue提供安裝介面
    install(vue) {
        Object.assign(vue.prototype, {
            $fetch: this.fetch
        });
    }

    fetch(method, url, payload) {
      if(method === 'get'){
            return axios['get'](url, {params: payload})
        } else {
            return axios[method](url, payload)
        }
    }
}
export default new Fetch();

// main.js
import Vue from 'vue'
import Fetch from '@/path/to/fetch'
Vue.use(Fetch)

// test.vue
export default {
    methods: {
        getData(){
            this.$fetch('get','/v1/systemInfo', {...}).then(res => {
                // code
            })
        }
    }
}
複製程式碼

如何優雅地解決痛點二三四?

我們再來回顧一下:

  • 請求方式的封裝 (痛點一)
  • 每次都要去檢視api文件(痛點二)
  • 要呼叫的介面路徑(痛點三)
  • 檢視請求方式(痛點四)

痛點一可能大家不一定都存在,那麼二三四應該是通病了,也是本文主要想解決的。為了不每次都要翻看文件的請求方式,請求路徑。作為一個標準的前端配置攻城獅我們可以把這些資訊統一配置起來,就避免了每次都去檢視的煩惱。我們關心的應該是返回的資料格式和傳入的引數等,設想一下我們每次這樣發請求該有多幸福啊!

this.$fetch('system.getVersion').then(res => {
    // code
})

/*
 *	大家的專案中,後端api肯定都是區別了某個模組的,可能每個模組做的人也不一樣
 *	在呼叫的時候指定一下模組名,介面名就可以
 *	不需要知道知道請求方式,請求路徑
 */
複製程式碼

要滿足以上的需求,我們肯定需要用配置檔案來記錄以上資訊,雖然我們不用關心,但是程式是需要關心的!./apiModules 就是用來存放這些資訊的。

// ./apiModules/system.js
export default {
    getVersion: {
        url: 'path/to/getVersion',
        method: 'get'
    },
    modVersion: {
        url: 'path/to/modVersion',
        method: 'post'
    }
}

// ./apiModules/user.js
export default {
    getInfo: {
        url: 'path/to/getInfo',
        method: 'get'
    }
}

// 當然,以上的配置欄位都可以根據需求自定義,比如同一apiName要根據使用者角色呼叫不同介面,只需要在fetch寫上相應的判斷就可以,非常方便!
複製程式碼

所以我們又要修改一下fetch檔案了

import axios from "./config";

// 根據 ./apiModules資料夾中 生成fetchCfg -- 實現方法 webpack-require.context()
// fetchCfg = {
//     system,
//     user
// };
const fetchCfg = {};
// 通過 require.context 可以讓webpack自動引用指定資料夾中的檔案
// 我們將它存到 fetchCfg 上以供 fetch 方法使用
const requireContext = require.context('./apiModules', false, /\.js$/)
requireContext.keys().forEach(path => {
    let module = path.replace(".js", "").replace("./", "")
    fetchCfg[module] = requireContext(path).default
})

/**
 * 解析引數
 * 這個函式主要負責解析傳入fetch的 module 和 apiName
 * @param {String} param
 */
const fetchParam = param => {
    var valid = /[a-z]+(\.[a-z])+/.test(param);
    if (!valid) {
        throw new Error(
            "[Error in fetch]: fetch 引數格式為 moduleName.apiName"
        );
    } else {
        return {
            moduleName: param.split(".")[0],
            apiName: param.split(".")[1]
        };
    }
};

class Fetch {
    // 給Vue提供安裝介面
    install(vue) {
        Object.assign(vue.prototype, {
            $fetch: this.fetch
        });
    }

    /**
     * 對axios封裝通用fetch方法
     * 會根據傳入的下列引數自動尋找 method 和路徑
     * @param {*} module 對應 fetch配置的名字
     * @param {*} apiName 該模組下的某個請求配置名
     */
    fetch(moduleInfo, payload) {
        let prefix = '/api'
        let moduleName = fetchParam(moduleInfo)["moduleName"];
        let apiName = fetchParam(moduleInfo)["apiName"];
        // 判斷沒有找到傳入模組
        if(!fetchCfg.hasOwnProperty(moduleName)){
            throw new Error(
                `[Error in fetch]: 在api配置檔案中未找到模組 -> ${moduleName}`
            );
        }
        // 判斷沒有找到對應介面
        if(!fetchCfg[moduleName].hasOwnProperty(apiName)){
            throw new Error(
                `[Error in fetch]: 在模組${moduleName}中未找到介面 -> ${apiName}`
            );
        }
        let fetchInfo = fetchCfg[moduleName][apiName];
        let method = fetchInfo["method"];
        let url = `${prefix}/${fetchInfo["url"]}`;

        if (method === "get") {
            return axios[method](url, {
                params: payload
            });
        } else {
            return axios[method](url, payload);
        }
    }
}

export default new Fetch();

複製程式碼

通過以上方法,優雅地解決了 二三四 三個痛點!

錦上添花的api配置檔案解析指令碼

最後來說說我們的parse資料夾,這是一個錦上添花的檔案,如果恰好你的後端用了類似 swaggerpostman 等可以匯出結構化的檔案給你,比如json,然後你通過簡單的node指令碼轉化就可以得到以上的api配置資訊,一旦後端修改了api,我們再run一遍指令碼就可以把所有的更改應用到專案,而不需要手動修改api檔案了,就算是需要手動修改,也不用在每個業務檔案中修改,方便快速~

以下是我讀取api層postman文件的指令碼,這裡也可以有很多自動化的方式。比如這個文件被託管在了git,每次api更新文件之後,我們可以預先寫一段shell指令碼,把git的更新同步到本地,然後啟動node指令碼(可以把命令放在package.json裡的script標籤中用npm呼叫)讀取/寫入文件。可能在第一次寫指令碼的時候有不會的地方,但是一旦寫好,與後端小夥伴做好約定,之後的工作是不是快了很多呢?

// parse.js
/**
 * README
 * 讀取中間層json檔案,生成api配置
 */

let fs = require("fs");
let path = require("path");
let dosJson = require("./api.json");

var jsFile = fs.createWriteStream(path.resolve(__dirname, "./api/ddos.js"), {
    encoding: "utf8"
});

function parsePostManJson(json) {
    Object.keys(json).map(key => {
        // 新增註釋
        if (key === "name") {
            jsFile.write(`// ${json[key]}`)
            console.log(`// ${json[key]}`);
        }
        if(key === "request"){
            let urlName = json[key].url.path[json[key].url.path.length - 1];
            let url = json[key].url.raw.replace("{{HOST}}", "");
            let method = json[key].method;
            let params = "";
            if(method === "GET"){
                params = `// ${url.split("?")[1] ? url.split("?")[1] : ""}`;
                url = url.split("?")[0];
            }
            // let content = `${method === 'GET' ? params : ""}`
            let content = `
                ${urlName}: {
                    url: "${url}",
                    method: "${method.toLowerCase()}",
                    custom: true
                },
            `
            console.log(content);
            jsFile.write(content)
        }
        if(key === "item" && json[key].constructor === Array){
            json[key].map(itemJson => {
                parsePostManJson(itemJson);
            })
        }
    });
}

jsFile.write(`export default {`)

parsePostManJson(dosJson);

jsFile.write(`}`)

jsFile.end();

jsFile.on('finish',function(){
    console.log('寫入完成');
})

jsFile.on('error',function(){
    console.log('寫入失敗');
})
複製程式碼

輸出的api檔案,還新增了一些註釋,如果有需要也可以直接把引數格式寫入,以後就不用去開啟線上文件檢視了,是不是很方便呢?

// ddos.js
export default {
    // 獲取ddos模式
    getDDosCfg: {
        url: "/getDDosCfg",
        method: "post",
        custom: true,
        napi: true
    },

    // DDos融入// 資料包表統計// 獲取機房概覽資訊
    statisticsInfo: {
        url: "/admin/Ddos/Statistic/statisticsInfo",
        method: "post",
        custom: true
    }
};

複製程式碼

總結一下

哈哈,能耐心看到這裡實屬不易,你真是一個小棒棒呢~ (⊙﹏⊙)

So,在開發中,我們儘量要去思考一個問題,就是怎麼讓繁瑣的事情變得簡單化,在遇到繁瑣重複性高的問題,要有去解決的想法。能用程式去完成的東西,我們就儘量不重複搬磚。這樣既可以在繁忙的開發中獲得一點幸福感,也可以讓我們的coding能力慢慢提升~

以上的解決方式僅僅是一種思路,具體的程式碼實現上可以根據專案的框架、實際引用的請求庫、業務需求來封裝。當然,如果恰好你跟我的業務需求差不多,以上的程式碼可以滿足業務,我把程式碼已經放到了github,歡迎大家參考使用。

相關文章