• Day 5 登录页及路由 (三) 基于axios的API调用


    系列文章目录

    本系列记录一下通过Abp搭建后端,Vue+Element UI Plus搭建前端,实现一个小型项目的过程。



    前言

    作为前后端分离的系统,请求后端API是一个必不可少的工作,本文就介绍了Vue中调用后端API的基础内容。


    一、axios

    Vue官方推荐使用axios来进行网络请求,于是先到axios的官网上看了一遍文档,其介绍是这样的:

    Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。

    大致看了一下官方文档,发现功能已经很齐全了,唯一需要处理的就是针对特定系统的配置性处理。程序员之魂开始燃烧,封印术已经按捺不住了。

    二、封装还是不封装,这是个问题

    1. 封装的目的

    老套路,先看看别人怎么做的(程序员的事情,怎么能叫抄呢 🤣)。搜索一下,有做简单封装的(比如 这个 ),有做复杂封装的(比如这个这个),同时评论区也有人疑惑为何要封装,是不是和axios背道而驰。

    首先,封装的目的,总结下来大致有这么几个:

    1.  统一请求头。大部分项目对应的后端,需要统一请求头,比如app,version,token之类的。既然是统一,那天然的就应该是放在封装层里面。
    2. 错误处理。根据API返回结果,或者更底层一点,根据HttpStatus来进行通用的错误处理。啊哈,通用,懂了把。
    3. 重试及取消操作。在网络世界,不通是常见的“偶发”问题。或者是连接失败,或者是服务器错误,或者是太阳黑子影响,总之,反应到页面上就是无法访问。然后刷新一下就神奇的好了。另外,操作响应时间不固定,性急的人往往会连击,而响应不一定是按请求顺序返回,或者直接不返回。不做处理的话,容易造成数据混乱,页面卡死之类的。这个时候取消一下前面的请求,让页面只对最后一个请求结果进行处理,会简化处理逻辑。同样,这个“应该”也是一个统一处理。
    4. 开发过程中的配置。设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分。我的理解这是一个伪需求, axios本身的baseUrl就是干这个的。
    5. 觉得axios是第三方库,对项目而言需要隔离一下,以备将来换成别的库。这个就见仁见智了,解耦到这个程度,还是需要一些勇气的。另外,你确定不是在重复设计axios?
    6. 就是单纯不封装不舒服,不服来战

    2. 怎么封装

    具体封装的手段大致有两个:

    1. 依赖拦截器,无论是请求拦截器还是响应拦截器,都是axios提供的扩展点。
    2. 封装常用代码为方法。即把get、post等常用请求方法,再封装一次。这个我以为仅限于项目中对这些方法有特定需要的。因为axios本身就有别名方法,够满足大部分需要了。

    3. 到底要不要封装

    看了一圈下来,结合官方文档,我感觉二次封装有必要,但过度封装就没有必要了,搭建一个架子,把基本目标实现了就可以了,其它的后续再根据需求调整,渐进式开发嘛。

    其实,从后端开发的角度来看,封装axios就和封装WebRequest,HttpClient之类的一样,根据实际需要来就行。要克制,过度设计有害健康。

    另外,大部分文章都提到了将实际api的调用封装起来,放到单独的文件中去(比如api\xxxManage.ts)。这个我赞成,和后端服务层或数据访问层的作用一样。这么看,axios就和DBDriver一样,属于偏底层的库(当然,它的底层是XmlHttpRquest 和 node.js http 模块)。

    基础研究到此为止,开始重复发明轮子。

    三、实际操作

    1. 代码结构

    创建 src/api 文件夹,下面创建 index.ts 文件,axios的封装代码就写在这里了。然后针对不同的领域,建立不同的api封装文件。比如 login.ts 就封装登录相关api,提供给LoginView使用。

    2. 调用端代码

    参照TDD的方式,先写最顶层调用端代码

    src/view/LoginView.vue

    1. async function fakeLogin(event: Event) {
    2. //currentUser.setToken("faked login token")
    3. const loginRequest: Login.LoginRequest = { username: "test", password: "pw" }
    4. const loginResponse: Login.LoginResponse = await login(loginRequest)
    5. currentUser.setToken(loginResponse.access_token)
    6. currentUser.setUserInfo({ name: loginResponse.name })
    7. }

    伪代码一路到底,这里利用了语法糖, await还是很甜的。

    然后定义login api 的封装接口,这里没有想好login方法是否要放入Login命名空间,暂时先这样吧。

    src/api/login.ts

    1. // 登录模块
    2. export namespace Login {
    3. export interface LoginRequest {
    4. username: string
    5. password: string
    6. }
    7. export interface LoginResponse {
    8. access_token: string
    9. name:string
    10. }
    11. }
    12. export const login = async (params: Login.LoginRequest) => {
    13. console.log("call async login.")
    14. const response:LoginResponse = { access_token:"token", name:"test user"}
    15. return response
    16. }

    测试一下,看看效果,点击Fake Login按钮后:

    3. API 接入实现

    先做基础配置工作,src/api/index.ts

    1. import axios, { AxiosError } from 'axios'
    2. import type { InternalAxiosRequestConfig } from 'axios'
    3. import { BASE_URL } from '@/config'
    4. import { useCurrentUserStore } from '@/stores/currentUser'
    5. const instance = axios.create({
    6. baseURL: BASE_URL,
    7. timeout: 3000,
    8. })
    9. instance.interceptors.request.use(
    10. (config: InternalAxiosRequestConfig) => {
    11. // 当前用户已登录的情况下,添加Authorization请求头
    12. const userStore = useCurrentUserStore()
    13. if (userStore.token != '') {
    14. if (config.headers && typeof config.headers.set === 'function') {
    15. config.headers.set('Authorization', userStore.token)
    16. }
    17. }
    18. return config
    19. },
    20. (error: AxiosError) => {
    21. return Promise.reject(error)
    22. }
    23. )
    24. export default instance

    到这个阶段,暂时只是添加token到请求头里,其实这个都算早的,只是为了使用下拦截器,而且确实也是后面需要的功能,所以先写下来。后面错误处理还没有弄,这个需要结合页面表现再看。

    另外就是BASE_URL直接使用了config的配置,关于环境的切换,发布时直接更改这个也不是很麻烦,暂时就先这样了。

    重写 src/api/login.ts

    1. import http from '@/api'
    2. import type { Login, ResultDTO } from './interfaces'
    3. export const login = async (
    4. params: Login.LoginRequest
    5. ): Promise<ResultDTO<Login.LoginResponse>> => {
    6. const { data } = await http.post('/api/account/login', params)
    7. return data
    8. }

    把请求和响应移到 interfaces文件中,统一管理了。这个只是一个习惯,看将来大概会从什么角度来阅读代码。所有请求放到一个文件,方便上手就能看到整体。放到接近使用的地方,比如login.ts中,则可以在使用的地方就知道具体含义,不过IDE的Go to definition功能也是很好用的。

    interfaces里面还包括了后端响应基类,没有采用REST的推荐,自定义了Code,具体代码如下:

    src/api/interface.ts

    1. /** API响应 */
    2. export interface ResultBase {
    3. /** 响应代码 */
    4. code: string
    5. /** 消息 */
    6. message: string
    7. /** 是否成功 */
    8. success: boolean
    9. }
    10. /** API响应 */
    11. export interface ResultDTO extends ResultBase {
    12. /** 数据 */
    13. data: T
    14. }
    15. // 登录模块
    16. export namespace Login {
    17. /** 登录请求 */
    18. export interface LoginRequest {
    19. /** 用户名 */
    20. username: string
    21. /** 密码 */
    22. password: string
    23. }
    24. /** 登录响应账号信息 */
    25. export interface LoginResponse {
    26. /** Token */
    27. token: string
    28. /** 用户昵称 */
    29. nickname: string
    30. }
    31. }

    四、总结

    前面搞了一大堆,实际代码没有多少,另外错误处理也还没有弄,但是理清了思路。这里有一个点,就是src/api/login.ts 这个文件是否有必要,因为就是一行代码,封装成函数似乎有点重复且繁复。暂时留在这里也是为了错误处理,还没有思考具体错误处理是留在这里,还是放到view层,或者直接在 axios 封装代码里,通过响应拦截器处理。各有各的优缺点。等先把页面样式写好,再看看写到哪里比较合适。

  • 相关阅读:
    docker save 命令 docker load 命令 快速复制容器
    Linux:进程概念的引入和理解
    stm32之串口/蓝牙控制led灯
    【Jmeter】安装配置:Jmeter 下载 MySQL JDBC 驱动
    设计模式——装饰器模式
    python爬虫——爬取豆瓣top250电影数据(适合初学者)
    【C语言 数据结构】顺序表的使用
    复变函数在软件开发中的应用
    【面试题精讲】Mysql如何实现乐观锁
    MySQL数据库用户管理
  • 原文地址:https://blog.csdn.net/lee_leefox/article/details/134151759