學習Typescript 並使用單例模式 組合Vue + Element-ui 封裝 Axios

praise發表於2019-12-03

簡介

本文將實現在 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
複製程式碼

學習Typescript 並使用單例模式 組合Vue + Element-ui 封裝 Axios

上下鍵選擇,空格鍵確定

學習Typescript 並使用單例模式 組合Vue + Element-ui 封裝 Axios

接下來是一些常規選項

學習Typescript 並使用單例模式 組合Vue + Element-ui 封裝 Axios

下面是詢問要不要記錄這次配置以便後面直接使用,我們選擇y

學習Typescript 並使用單例模式 組合Vue + Element-ui 封裝 Axios

建立 Utils 資料夾以及 我們今天的主角 request.ts 檔案

學習Typescript 並使用單例模式 組合Vue + Element-ui 封裝 Axios

安裝所需包

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};`
      )
    }
  }
複製程式碼

學習Typescript 並使用單例模式 組合Vue + Element-ui 封裝 Axios

請求方式 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)
複製程式碼

學習Typescript 並使用單例模式 組合Vue + Element-ui 封裝 Axios

結尾

各位大哥大姐留個贊吧 O(∩_∩)O哈哈~ 到此就結束了,我也是第一次學習 ts 並且 封裝 axios 寫的不好,下方留言指出,謝謝。

相關文章