使用单例设计掌握TypeScript,并将Axios与Vue + Element-UI集成

12,483 阅读4分钟

简介

本文将实现在 Vue 框架中使用 Typescript + Element-ui 以单例模式个性化封装 Axios, 满足我们项目的所需,留个赞再走吧

Typescript

什么是Typescript?

typescript 是 JavaScript 的强类型版本。然后在编译期去掉类型和特有语法,生成纯粹的 JavaScript 代码。由于最终在浏览器中运行的仍然是 JavaScript,所以 TypeScript 并不依赖于浏览器的支持,也并不会带来兼容性问题。

TypeScript 是 JavaScript 的超集,这意味着他支持所有的 JavaScript 语法。并在此之上对 JavaScript 添加了一些扩展,如 class / interface / module 等。这样会大大提升代码的可阅读性。

与此同时,TypeScript 也是 JavaScript ES6 的超集,Google 的 Angular 2.0 也宣布采用 TypeScript 进行开发。这更是充分说明了这是一门面向未来并且脚踏实地的语言。

为什么要学习 Typescript?

下面我们列出了原因,为什么我们应该拥抱TypeScript:

  1. 完全的面向对象,类和对象。基于此,TypeScript将成为提高开发人员开发效率的利器,它很容易理解和接受。
  2. 在编写代码的阶段,TypeScript就能够找到大部分的错误,而JavaScript在这方面就没那么友好了。要知道,运行时错误越少,你的程序的bug就越少
  3. 相比JavaScript,TypeScript的重构也更容易

强类型语言的优势在于静态类型检查。概括来说主要包括以下几点:

  • 静态类型检查
  • IDE 智能提示
  • 代码重构
  • 可读性
  • 静态类型检查可以避免很多不必要的错误, 不用在调试的时候才发现问题

Axios

什么是 axios?

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

特性

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

来看看 Axios 官方的例子

GET 的请求方式

// 为给定 ID 的 user 创建请求
axios.get('/user?ID=12345')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

// 上面的请求也可以这样做
axios.get('/user', {
    params: {
      ID: 12345
    }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

POST

axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

请求方法的别名

为方便起见,为所有支持的请求方法提供了别名

  • axios.request(config)
  • axios.get(url[, config])
  • axios.delete(url[, config])
  • axios.head(url[, config])
  • axios.options(url[, config])
  • axios.post(url[, data[, config]])
  • axios.put(url[, data[, config]])
  • axios.patch(url[, data[, config]])

我们大致了解了 Axios 以及一些常用的方法, 那接下来我们就开始进入正题吧

起手式项目

创建项目

$ vue create my-vue-typescript

上下键选择,空格键确定

接下来是一些常规选项

下面是询问要不要记录这次配置以便后面直接使用,我们选择y

创建 Utils 文件夹以及 我们今天的主角 request.ts 文件

安装所需包

Element-ui

$ npm i element-ui -S

qs

$ npm i qs -S

qs 是一个增加了一些安全性的查询字符串解析和序列化字符串的库

Axios

$ npm i axios -S

在此我不会为大家讲解太多 Ts 的知识,但在开始之前想让大家明白 Typescript 中的几个点,不然没法继续下去

小知识讲堂

类型注解

TypeScript里的类型注解是一种轻量级的为函数或变量添加约束的方式

# 我们指定了 hello 这个变量必须是 string 类型
const hello: string = 'Hello World'

# 我们指定了 greeter 传入的 person 参数必须是 string 类型
function greeter(person: string) {
    return "Hello, " + person;
}

接口

在TypeScript里,只在两个类型内部的结构兼容那么这两个类型就是兼容的。 这就允许我们在实现接口时候只要保证包含了接口要求的结构就可以,而不必明确地使用 implements语句

interface IFamilyData {
    father: string
    mom: string
    son: string
}

function getFamily(family: IFamilyData) {
    return `爸爸${family.father},妈妈${family.mom},儿子${family.son}`
}

const family = { father: 'Jack', mom: 'Ruth', son: 'Bieber' }

document.body.innerHTML = getFamily(family)

TypeScript支持JavaScript的新特性,比如支持基于类的面向对象编程

class Person{
    // 增加两个属性
    name:string
    age:number
    // 增加可以传参的构造方法
    constructor(name:string,age:number){
        this.name = name
        this.age = age
    }
    // 增加一个自定义的普通的打印函数
    print(){
        return this.name + ':'' + this.age
    }
    
    // 使用上面创建的类
    // var p = new Person() // 这里在使用上面的类时没有传递参数是会报错的,因为上面定义的 constructor 构造方法中存在参数,所以这里也一定要传递参数
    var p = new Person('xiaochuan',22)
    alert(p.print())
}

单例模式

最早接触单例模式是在学 PHP 的时候,那个时候在还没有使用框架 PHP 引入 Mysql 的时候,我都会把 Mysql 封装为一个单例模式的类

单例模式(Singleton),也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理

优点

  • 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
  • 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性
  • 提供了对唯一实例的受控访问
  • 由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能
  • 允许可变数目的实例
  • 避免对共享资源的多重占用

缺点

  • 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态
  • 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失

适用场景

单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:

  1. 需要频繁实例化然后销毁的对象。
  2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  3. 有状态的工具类对象。
  4. 频繁访问数据库或文件的对象

实现思路:

一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名 称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们 还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例

开始

撸基础结构


# public 公开的
# protected 受保护的
# private 私有的


import http from 'http'
import https from 'https'
import axios, { AxiosResponse, AxiosRequestConfig, CancelTokenStatic } from 'axios'
import { Message, MessageBox } from 'element-ui'
import qs from 'qs'
import { UserModule } from '@/store/modules/user'

// 类名
class Request {
  // 属性
  protected baseURL: any = process.env.VUE_APP_BASE_API 
  protected service: any
  protected pending: Array<{
    url: string,
    cancel: Function
  }> = []
  protected CancelToken: CancelTokenStatic = axios.CancelToken
  protected axiosRequestConfig: AxiosRequestConfig = {}
  protected successCode: Array<Number> = [200, 201, 204]
  private static _instance: Request;

  // 构造函数 初始化工作
  private constructor() {
   
  }
 
  // 唯一实例
  public static getInstance() : Request {}

  protected requestConfig(): void {}

  protected interceptorsRequest() {}

  protected interceptorsResponse(): void {}

  protected removePending(config: any): void {}

  public async post(url: string, data: any = {}, config: object = {}) {}

  public async delete(url: string, config: object = {}) {}

  public async put(url: string, data: any = {}, config: object = {}) {}

  public async get(url: string, params: any = {}, config: object = {}) {}

  protected requestLog(request: any): void {}

  protected responseLog(response: any): void {}
}

export default Request.getInstance()

自定义实例默认值 requestConfig

从名字上我们就看的出来这是一个关于配置的方法 小提示: void 表示没有返回值

protected requestConfig(): void {
    this.axiosRequestConfig = {
      // baseURL`将自动加在 `url` 前面,除非 `url` 是一个绝对 URL
      baseURL: this.baseURL,  
      // `headers` 是即将被发送的自定义请求头
      headers: {
        timestamp: new Date().getTime(),
        'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
      },
      // transformRequest` 允许在向服务器发送前,修改请求数据
      transformRequest: [function (data: any) {
        //对data进行任意转换处理
        return data;
      }],
      // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
      transformResponse: [function(data: AxiosResponse) {
        return data
      }],
      // `paramsSerializer` 是一个负责 `params` 序列化的函数
      paramsSerializer: function(params: any) {
        return qs.stringify(params, { arrayFormat: 'brackets' })
      },
      // `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
      // 如果请求话费了超过 `timeout` 的时间,请求将被中断
      timeout: 30000,
      // `withCredentials` 表示跨域请求时是否需要使用凭证
      withCredentials: false,
      // `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
      responseType: 'json',
      // `xsrfCookieName` 是用作 xsrf token 的值的cookie的名称
      xsrfCookieName: 'XSRF-TOKEN',
      // `xsrfHeaderName` 是承载 xsrf token 的值的 HTTP 头的名称
      xsrfHeaderName: 'X-XSRF-TOKEN',
      // `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目
      maxRedirects: 5,
      // `maxContentLength` 定义允许的响应内容的最大尺寸
      maxContentLength: 2000,
      // `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject  promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
      validateStatus: function(status: number) {
        return status >= 200 && status < 300
      },
      // `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。允许像这样配置选项:
      // `keepAlive` 默认没有启用
      httpAgent: new http.Agent({ keepAlive: true }),
      httpsAgent: new https.Agent({ keepAlive: true })
    }
  }

请求拦截器 interceptorsRequest

protected interceptorsRequest() {
    this.service.interceptors.request.use(
      (config: any) => {
        if (UserModule.token) {
          config.headers['authorization'] = UserModule.token
        }
        return config
      },
      (error: any) => {
        return Promise.reject(error)
      }
    )
  }

响应拦截器 `interceptorsResponse

protected interceptorsResponse(): void {
    this.service.interceptors.response.use(
      (response: any) => {
        if (this.successCode.indexOf(response.status) === -1) {
          Message({
            message: response.data.message || 'Error',
            type: 'error',
            duration: 5 * 1000
          })
          if (response.data.code === 401) {
            MessageBox.confirm(
              '你已被登出,可以取消继续留在该页面,或者重新登录',
              '确定登出',
              {
                confirmButtonText: '重新登录',
                cancelButtonText: '取消',
                type: 'warning'
              }
            ).then(() => {
              UserModule.ResetToken()
              location.reload()
            })
          }
          return Promise.reject(new Error(response.message || 'Error'))
        } else {
          return response.data
        }
      },
      (error: any) => {
        Message({
          message: error.message,
          type: 'error',
          duration: 5 * 1000
        })
        return Promise.reject(error)
      }
    )
  }

重复点击取消上一次请求 removePending

protected removePending(config: any): void {
    for (let p in this.pending) {
      let item: any = p
      let list: any = this.pending[p]
      if (list.url === `${config.url}/${JSON.stringify(config.data)}&request_type=${config.method}`) {
        list.cancel()
        this.pending.splice(item, 1)
      }
    }
  }

响应 logs responseLog

protected responseLog(response: any): void {
    if (process.env.NODE_ENV === 'development') {
      const randomColor = `rgba(${Math.round(Math.random() * 255)},${Math.round(
        Math.random() * 255
      )},${Math.round(Math.random() * 255)})`
      console.log(
        '%c┍------------------------------------------------------------------┑',
        `color:${randomColor};`
      )
      console.log('| 请求地址:', response.config.url)
      console.log('| 请求参数:', qs.parse(response.config.data))
      console.log('| 返回数据:', response.data)
      console.log(
        '%c┕------------------------------------------------------------------┙',
        `color:${randomColor};`
      )
    }
  }

请求方式 POST GET PUT DELETE

public async post(url: string, data: any = {}, config: object = {}) {
    try {
      const result = await this.service.post(url, qs.stringify(data), config)
      return result.data
    } catch (error) {
      console.error(error)
    }
  }

  public async delete(url: string, config: object = {}) {
    try {
      await this.service.delete(url, config)
    } catch (error) {
      console.error(error)
    }
  }
  
...

整合代码

import http from 'http'
import https from 'https'
import axios, { AxiosResponse, AxiosRequestConfig, CancelTokenStatic } from 'axios'
import { Message, MessageBox } from 'element-ui'
import qs from 'qs'
import { UserModule } from '@/store/modules/user'

class Request {
  protected baseURL: any = process.env.VUE_APP_BASE_API
  protected service: any = axios
  protected pending: Array<{
    url: string,
    cancel: Function
  }> = []
  protected CancelToken: CancelTokenStatic = axios.CancelToken
  protected axiosRequestConfig: AxiosRequestConfig = {}
  protected successCode: Array<Number> = [200, 201, 204]
  private static _instance: Request;

  constructor() {
    this.requestConfig()
    this.service = axios.create(this.axiosRequestConfig)
    this.interceptorsRequest()
    this.interceptorsResponse()
  }

  public static getInstance() : Request {
    // 如果 instance 是一个实例 直接返回,  如果不是 实例化后返回
    this._instance || (this._instance = new Request())
    return this._instance
  }

  protected requestConfig(): void {
    this.axiosRequestConfig = {
      baseURL: this.baseURL,
      headers: {
        timestamp: new Date().getTime(),
        'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
      },
      transformRequest: [obj => qs.stringify(obj)],
      transformResponse: [function(data: AxiosResponse) {
        return data
      }],
      paramsSerializer: function(params: any) {
        return qs.stringify(params, { arrayFormat: 'brackets' })
      },
      timeout: 30000,
      withCredentials: false,
      responseType: 'json',
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxRedirects: 5,
      maxContentLength: 2000,
      validateStatus: function(status: number) {
        return status >= 200 && status < 500
      },
      httpAgent: new http.Agent({ keepAlive: true }),
      httpsAgent: new https.Agent({ keepAlive: true })
    }
  }

  protected interceptorsRequest() {
    this.service.interceptors.request.use(
      (config: any) => {
        this.removePending(config)
        config.CancelToken = new this.CancelToken((c: any) => {
          this.pending.push({ url: `${config.url}/${JSON.stringify(config.data)}&request_type=${config.method}`, cancel: c })
        })
        if (UserModule.token) {
          config.headers['authorization'] = UserModule.token
        }
        this.requestLog(config)
        return config
      },
      (error: any) => {
        return Promise.reject(error)
      }
    )
  }

  protected interceptorsResponse(): void {
    this.service.interceptors.response.use(
      (response: any) => {
        this.responseLog(response)
        this.removePending(response.config)
        if (this.successCode.indexOf(response.status) === -1) {
          Message({
            message: response.data.message || 'Error',
            type: 'error',
            duration: 5 * 1000
          })
          if (response.data.code === 401) {
            MessageBox.confirm(
              '你已被登出,可以取消继续留在该页面,或者重新登录',
              '确定登出',
              {
                confirmButtonText: '重新登录',
                cancelButtonText: '取消',
                type: 'warning'
              }
            ).then(() => {
              UserModule.ResetToken()
              location.reload()
            })
          }
          return Promise.reject(new Error(response.message || 'Error'))
        } else {
          return response.data
        }
      },
      (error: any) => {
        Message({
          message: error.message,
          type: 'error',
          duration: 5 * 1000
        })
        return Promise.reject(error)
      }
    )
  }

  protected removePending(config: any): void {
    for (let p in this.pending) {
      let item: any = p
      let list: any = this.pending[p]
      if (list.url === `${config.url}/${JSON.stringify(config.data)}&request_type=${config.method}`) {
        list.cancel()
        console.log('=====', this.pending)
        this.pending.splice(item, 1)
        console.log('+++++', this.pending)
      }
    }
  }

   public async post(url: string, data: any = {}, config: object = {}) {
    try {
      const result = await this.service.post(url, qs.stringify(data), config)
      return result.data
    } catch (error) {
      console.error(error)
    }
  }

  public async delete(url: string, config: object = {}) {
    try {
      await this.service.delete(url, config)
    } catch (error) {
      console.error(error)
    }
  }

  public async put(url: string, data: any = {}, config: object = {}) {
    try {
      await this.service.put(url, qs.stringify(data), config)
    } catch (error) {
      console.error(error)
    }
  }

 public async get(url: string, parmas: any = {}, config: object = {}) {
    try {
      await this.service.get(url, parmas, config)
    } catch (error) {
      console.error(error)
    }
  }
  
  protected requestLog(request: any): void {
  }

  protected responseLog(response: any): void {
    if (process.env.NODE_ENV === 'development') {
      const randomColor = `rgba(${Math.round(Math.random() * 255)},${Math.round(
        Math.random() * 255
      )},${Math.round(Math.random() * 255)})`
      console.log(
        '%c┍------------------------------------------------------------------┑',
        `color:${randomColor};`
      )
      console.log('| 请求地址:', response.config.url)
      console.log('| 请求参数:', qs.parse(response.config.data))
      console.log('| 返回数据:', response.data)
      console.log(
        '%c┕------------------------------------------------------------------┙',
        `color:${randomColor};`
      )
    }
  }
}

export default Request.getInstance()

使用方法

import Request from '@/utils/request'
import { ADMIN_LOGIN_API, ADMIN_USER_INFO_API } from '@/api/interface'

interface ILoginData {
  username: string
  password: string
}

export const login = (params: ILoginData) => Request.post(ADMIN_LOGIN_API, params)
export const getUserInfo = () => Request.get(ADMIN_USER_INFO_API)

结尾

各位大哥大姐留个赞吧 O(∩_∩)O哈哈~ 到此就结束了,我也是第一次学习 ts 并且 封装 axios 写的不好,下方留言指出,谢谢。