Архитектура внешнего интерфейса API, возможно, вы сделали недостаточно

внешний интерфейс JavaScript
Архитектура внешнего интерфейса API, возможно, вы сделали недостаточно

Доброе утро, сегодня, поделиться с людьми для передней для всехAPIНебольшой опыт и взгляд на многоуровневую архитектуру. Архитектурный дизайн — это бесконечная дорога, лучшего нет, есть только лучше. Этот принцип применим к различным сценариям проектирования программного обеспечения,APIДизайн слоя не является исключением.Если вы чувствуете, что при вызове интерфейса еще много слотов, это означает, что ваша архитектура слоя интерфейса нуждается в оптимизации. Сегодня я беруvue + axiosНапример, чтобы разобраться с некоторыми из моих опытов и идей для вас.

боль каменного века

позвонить напрямуюaxios, Это действительно болезненно, каждое место, куда вы звоните, должно оценивать статус ответа, и слишком много избыточных кодов.

import axios from "axios"

axios.get('/usercenter/user/page?pageNo=1&pageSize=10').then(res => {
    const data = res.data
    // 判断请求状态,success字段为true代表成功,视前后端约束而定
    if (data.success) {
        // 结果成功后的业务代码
    } else {
        // 结果失败后的业务代码
    }
})

Это кажется действительно неудобным, каждый раз, когда вызывается интерфейс, столько повторяющейся работы!

Бронзовый век, вполне удовлетворительно

Для решения прямого вызоваaxiosболевые точки, мы обычно используемPromiseправильноaxiosВторичная инкапсуляция, централизованное суждение о статусе ответа интерфейса и внешнем воздействииget, post, put, deleteЖдатьhttpметод.

вторичная упаковка аксиос

import axios from "axios"
import router from "@/router"
import { BASE_URL } from "@/router/base-url"
import { errorMsg } from "@/utils/msg";
import { stringify } from "@/utils/helper";
// 创建axios实例
const v3api = axios.create({
    baseURL: process.env.BASE_API,
    timeout: 10000
});
// axios实例默认配置
v3api.defaults.headers.common['Content-Type'] = 'application/x-www-form-urlencoded';
v3api.defaults.transformRequest = data => {
    return stringify(data)
}
// 返回状态拦截,进行状态的集中判断
v3api.interceptors.response.use(
    response => {
        const res = response.data;
        if (res.success) {
            return Promise.resolve(res)
        } else {
            // 内部错误码处理
            if (res.code === 1401) {
                errorMsg(res.message || '登录已过期,请重新登录!')
                router.replace({ path: `${BASE_URL}/login` })
            } else {
                // 默认的错误提示
                errorMsg(res.message || '网络异常,请稍后重试!')
            }
            return Promise.reject(res);
        }
    },
    error => {
        if (/timeout\sof\s\d+ms\sexceeded/.test(error.message)) {
            // 超时
            errorMsg('网络出了点问题,请稍后重试!')
        }
        if (error.response) {
            // http状态码判断
            switch (error.response.status) {
                // http status handler
                case 404:
                    errorMsg('请求的资源不存在!')
                    break
                case 500:
                    errorMsg('内部错误,请稍后重试!')
                    break
                case 503:
                    errorMsg('服务器正在维护,请稍等!')
                    break
            }
        }
        return Promise.reject(error.response)
    }
)

// 处理get请求
const get = (url, params, config = {}) => v3api.get(url, { ...config, params })
// 处理delete请求,为了防止和关键词delete冲突,方法名定义为deletes
const deletes = (url, params, config = {}) => v3api.delete(url, { ...config, params })
// 处理post请求
const post = (url, params, config = {}) => v3api.post(url, params, config)
// 处理put请求
const put = (url, params, config = {}) => v3api.put(url, params, config)
export default {
    get,
    deletes,
    post,
    put
}

Вызывающий больше не определяет статус запроса

import api from "@/api";

methods: {
    getUserPageData() {
        api.get('/usercenter/user/page?pageNo=1&pageSize=10').then(res => {
            // 状态已经集中判断了,这里直接写成功的逻辑
            // 业务代码......
            const result = res.result;
        }).catch(res => {
            // 失败的情况写在catch中
        })
    }
}

асинхронный/ожидающий дооснащения

Использование семантических асинхронных функций

methods: {
    async getUserPageData() {
        try {
           const res = await api.get('/usercenter/user/page?pageNo=1&pageSize=10') 
           // 业务代码......
           const { result } = res;
        } catch(error) {
            // 失败的情况写在catch中
        }
    }
}

существующие проблемы

  • Степень семантики ограничена, и вызов интерфейса по-прежнему требует запроса интерфейса.url
  • внешний интерфейсapiСлой сложно поддерживать, если меняется внутренний интерфейс, внешний интерфейс нужно сильно менять.
  • еслиUIСуществуют различия между моделью данных компонента и структурой данных, необходимой для внутреннего интерфейса.Обработка данных должна выполняться перед вызовом каждого интерфейса, чтобы сгладить различия, такие как[1,2,3]Перемена1,2,3Это (конечно, это только самый простой пример). Таким образом, если данные обрабатываются небрежно, у вызывающего абонента высока вероятность ошибок!
  • Трудно выполнить специальные сценарии, например, сценарий запроса, внутренние требования, если введено ключевое слово поиска.keyword, надо позвонить/user/searchИнтерфейс, если ключевое слово не введено, его можно только вызвать/user/pageинтерфейс. Если каждый вызывающий абонент должен оценить, было ли введено ключевое слово, а затем решить, какой интерфейс вызывать, какова, по вашему мнению, вероятность ошибки и раздражает ли его использование?
  • В продукте сказано, что эти сценарии нужно оптимизировать, и по умолчанию стоит сортировка по времени создания в порядке убывания. стирать и менять по одному?
  • ......

Итак, как решить эти проблемы? Пожалуйста, наберитесь терпения и смотрите...  

Железный век, это круто

Решение, которое я имею в виду, состоит в том, чтобы добавить еще один слой между базовой инкапсуляцией и вызывающей стороной.APIУровень адаптации (уровень адаптации, принимающий специальное значение), уровень адаптации унифицирует обработку, включая параметры обработки, обработку заголовка запроса, специализированную обработку, для извлечения более семантического способа, позволяющего вызывающему абоненту звонить в стиле «Дурак», а не находить интерфейсurlи беспокоясь о повторяющейся работе со структурами данных, поместитеViewModelМодель данных, привязанная к слою, передается непосредственно на уровень адаптации для унифицированной обработки.

Согласуйте микросервисную архитектуру

Во-первых, чтобы согласовать внутреннюю микросервисную архитектуру, во внешнемAPIЗвонок разделен на три модуля.

├─api
    index.js axios底层封装
    ├─base  负责调用基础服务,basecenter
    ├─iot  负责调用物联网服务,iotcenter
    └─user  负责调用用户相关服务,usercenter

Под каждым модулем определяется единое пространство имен микросервисов, например/src/api/user/index.js:

export const namespace = 'usercenter';

функциональный модуль

Каждая функция имеет независимыйjsМодуль, взяв в качестве примера интерфейс, связанный с управлением ролями, модуль/src/api/user/role.js

import api from '../index'
import { paramsFilter } from "@/utils/helper";
import { namespace } from "./index"
const feature = 'role'

// 添加角色
export const addRole = params => api.post(`/${namespace}/${feature}/add`, paramsFilter(params));
// 删除角色
export const deleteRole = id => api.deletes(`/${namespace}/${feature}/delete`, { id });
// 更新角色
export const updateRole = params => api.put(`/${namespace}/${feature}/update`, paramsFilter(params));
// 条件查询角色
export const findRoles = params => api.get(`/${namespace}/${feature}/find`, paramsFilter(params));
// 查询所有角色,不传参调用find接口代表查询所有角色
export const getAllRoles = () => findRoles();
// 获取角色详情
export const getRoleDetail = id => api.get(`/${namespace}/${feature}/detail`, { id });
// 分页查询角色
export const getRolePage = params => api.get(`/${namespace}/${feature}/page`, paramsFilter(params));
// 搜索角色
export const searchRole = params => params.keyword ? api.get(`/${namespace}/${feature}/search`, paramsFilter(params)) : getRolePage(params);
  • Каждый интерфейс основан наRESTfulстиль, вызывая приращение(api.post)удалять(api.deletes)изменять(api.put)чек об оплате(api.get) базового метода и внешнего вывода семантического метода.

  • называетсяurlОн состоит из трех частей в формате:/微服务命名空间/特性命名空间/方法

  • Соглашение об именовании функций уровня адаптации интерфейса:

    • Добавлен:addXXX
    • удалять:deleteXXX
    • обновить:updateXXX
    • Запросить записи по ID:getXXXDetail
    • Условный запрос для записи:findOneXXX
    • Условный запрос:findXXXs
    • Запросить все записи:getAllXXXs
    • Пейджинговый запрос:getXXXPage
    • поиск:searchXXX
    • Остальные интерфейсы персонализации названы в соответствии с семантикой

Решать проблему

  • Высшая степень семантики, сотрудничествоvscodeФункция подсказки кода , не будьте слишком круты для использования!

  • Быстрая реакция на изменения интерфейса, унифицированная обработка адаптационным слоем

  • Централизованная обработка данных (для обработки общедоступных данных мы используемparamsFilterРешите для особых случаев, а затем разберитесь отдельно), звонящий может чувствовать себя непринужденно при ведении бизнеса.

  • Для удовлетворения специальных сценариев, буддийская система имеет дело с друзьями и продуктами

    • Для сценария запроса по ключевому слову, упомянутого в предыдущем разделе, мы оцениваем, существует лиkeywordполе, решите позвонитьsearchещеpageинтерфейс. Нам просто нужно разоблачитьsearchRoleметод, вызывающей стороне нужно только вызватьsearchRoleметод, никаких других соображений не требуется.
    export const searchRole = params => params.keyword ? api.get(`/${namespace}/${feature}/search`, paramsFilter(params)) : getRolePage(params);
    
    • Для заказа продукта при внезапном увеличении спроса мы можем работать с параметрами по умолчанию, чтобы сделать слой адаптации.

    Во-первых, мы создаем новый, предназначенный для управления параметрами по умолчанию.js,Такие какsrc/api/default-options.js

    // 默认按创建时间降序的参数对象
    export const SORT_BY_CREATETIME_OPTIONS = {
        sortField: 'createTime',
        // desc代表降序,asc是升序
        sortType: 'desc'
    }
    

    Далее делаем централизованную обработку на уровне адаптации интерфейса

    import api from '../index'
    import { SORT_BY_CREATETIME_OPTIONS } from "../default-options"
    import { paramsFilter } from "@/utils/helper";
    import { namespace } from "./index"
    const feature = 'role'
    
    export const getRolePage = params => api.get(`/${namespace}/${feature}/page`, paramsFilter({ ...SORT_BY_CREATETIME_OPTIONS, ...params }));
    

    SORT_BY_CREATETIME_OPTIONSОно помещается впереди, чтобы удовлетворить другие требования сортировки, а поле сортировки, переданное вызывающей стороной, может переопределить параметры по умолчанию.

издеваться над первым

идеально подходитAPIДизайн слоев определенно неотделимmockиз. Прежде чем серверная часть предоставит интерфейс, внешний интерфейс должен быть разработан параллельно с использованием смоделированных данных, иначе прогресс не может быть гарантирован. Так как же разработать интерфейс, максимально совместимый с реальным интерфейсом?mockЧто насчет системы? Я просто поделюсь здесь.

  • Сначала создайтеmockпреданныйaxiosПример

мы вsrcновый каталогmockкаталог и вsrc/mock/index.jsпростой пакетaxiosПример

// 仅限模拟数据使用
import axios from "axios"
const mock = axios.create({
    baseURL: ''
});
// 返回状态拦截
mock.interceptors.response.use(
    response => {
        return Promise.resolve(response.data)
    },
    error => {
        return Promise.reject(error.response)
    }
)

export default mock
  • mockОн также разделен на модули дляusercenterУправление ролями в микросервисахmockИнтерфейс как пример
├─mock
    index.js mock底层axios封装
    ├─user  负责调用基础服务,usercenter
        ├─role
            ├─index.js

мы вsrc/mock/user/role/index.jsПростое моделирование интерфейса для получения всех ролейgetAllRoles

import mock from "@/mock";

export const getAllRoles = () => mock.get('/static/mock/user/role/getAllRoles.json')

Как видите, мыmockполучено из интерфейсаstatic/mockв каталогеjsonданные. Поэтому нам нужно подготовиться в соответствии с интерфейсным документом или согласованной структурой данных.getAllRoles.jsonданные

{
    "success": true,
    "result": {
        "pageNo": 1,
        "pageSize": 10,
        "total": 2,
        "list": [
            {
                "id": 1,
                "createTime": "2019-11-19 12:53:05",
                "updateTime": "2019-12-03 09:53:41",
                "name": "管理员",
                "code": "管理员",
                "description": "一个拥有部分权限的管理员角色",
                "sort": 1,
                "menuIds": "789,2,55,983,54",
                "menuNames": "数据字典, 后台, 账户信息, 修改密码, 账户中心"
            },
            {
                "id": 2,
                "createTime": "2019-11-27 17:18:54",
                "updateTime": "2019-12-01 19:14:30",
                "name": "前台测试",
                "code": "前台测试",
                "description": "一个拥有部分权限的前台测试角色",
                "sort": 2,
                "menuIds": "15,4,1",
                "menuNames": "油耗统计, 车联网, 物联网监管系统"
            }
        ]
    },
    "message": "请求成功",
    "code": 0
}
  • Давайте посмотримmockКак ты сделал это?

Давайте сначала посмотрим на вызывающий метод реального интерфейса.

import { getAllRoles } from "@/api/user/role";

created() {
    this.getAllRolesData()
},
methods: {
    async getAllRolesData() {
        const res = await getAllRoles()
        console.log(res)
    }
}

Такmockкак это сделать? очень просто, просто положиmockзаменить метод, указанный вapiпредоставленный метод.

// import { getAllRoles } from "@/api/user/role";
import { getAllRoles } from "@/mock/user/role";

Видно, что этоmockСоответствие между методом и вызовом реального интерфейса довольно высокое, когда интерфейс официально отлажен, вам нужно только настроить закомментированный код, и переход очень плавный!

  • Обратите внимание, что в производственной среде для предотвращения упаковкиstatic/mockсодержимое каталогаcopyприбытьdistкаталог, нам нужно настроитьCopyWebpackPluginvue-cli@2Например, мы модифицируемwebpack.base.conf.jsВот и все.
const devMode = process.env.NODE_ENV === 'development';

new CopyWebpackPlugin([
    {
        from: path.resolve(__dirname, '../static'),
        to: devMode ? config.dev.assetsSubDirectory : config.build.assetsSubDirectory,
        ignore: devMode ? '' : 'mock/**/*'
    }
])

Паровой век, действительно ароматный

Далее представьте, используйте type-safetypescript, пусть передний конецAPIУровень действительно реализует программирование интерфейса документа, стандартизируя входные параметры, выходные параметры, дополнительные параметры и т. д., что повышает удобство сопровождения и значительно снижает вероятность ошибок на этапе кодирования. Хотя он все еще находится в стадии рефакторинга, я хотел бы сказать, чтоtypescriptОн действительно ароматный, я вдруг скучаю по немуAngularПрошло два года, ждуvue3.0способенtypescriptСочетается более идеально ...

Электрический век, больше воображения

Будущее открывает безграничные возможности. Перед лицом все более сложных и разнообразных бизнес-сценариев мы будем извлекать лучшие архитектурные и дизайнерские шаблоны. В настоящее время существует незрелая идея, может ли он быть более стандартизированным в дизайне интерфейса, и в то же время внутренние документы интерфейса вывода извлекатьAPI jsonструктуры данных, как? внешний интерфейсAPI json,пройти черезnodejsВозможность файлового программирования, автоматическая генерация кода внешнего интерфейса, свободные руки.

Эпилог

Конечно, все вышеизложенное — лишь часть моего опыта и предположений. Это то, что я могу придумать в пределах своих возможностей. Надеюсь, это поможет некоторым нуждающимся студентам. Если у больших парней больше опыта, вы можете подсказать.


Прошлые основные моменты:

яTusi, небольшой фронтенд-лидер начинающей компании, все еще беспокоящийся о ежедневном написании бесконечных бизнес-кодов, накоплении технологий на пути к полировке продуктов и изучении путей роста. Если вы похожи на меня и думаете о собственном технологическом росте и ценности, добро пожаловать, добавьте меня в WeChat для общения и обсуждения, WeChat IDice_lloly. я буду в публичном аккаунтеБольшой салон фронтенд техникии апплетБлог ТусиСинхронизируйте содержимое блога, приходите и дразните меня!

欢迎关注