项目初始化,封装Axios请求

This commit is contained in:
zhangyiming 2020-11-17 19:33:36 +08:00
commit cc6157e12b
28 changed files with 17861 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/node_modules

34
README.md Normal file
View File

@ -0,0 +1,34 @@
# vue-ts
## Vue3.0中文文档地址
https://vue3js.cn/docs/zh/
## Project setup
```
npm install
```
## Compiles and hot-reloads for development
```
npm run serve
```
## Compiles and minifies for production
```
npm run build
```
## Run your unit tests
```
npm run test:unit
```
## Run your end-to-end tests
```
npm run test:e2e
```
## Lints and fixes files
```
npm run lint
```

3
cypress.json Normal file
View File

@ -0,0 +1,3 @@
{
"pluginsFile": "tests/e2e/plugins/index.js"
}

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

6
jest.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
preset: '@vue/cli-plugin-unit-jest/presets/typescript',
transform: {
'^.+\\.vue$': 'vue-jest'
}
}

17161
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "vue-ts",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vite",
"build": "vite build",
"test:unit": "vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e",
"lint": "vue-cli-service lint"
},
"dependencies": {
"await-to-js": "^2.1.1",
"axios": "^0.21.0",
"vue": "^3.0.0",
"vue-class-component": "^8.0.0-0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@types/jest": "^24.0.19",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-e2e-cypress": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-unit-jest": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/eslint-config-typescript": "^5.0.2",
"@vue/test-utils": "^2.0.0-0",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^7.0.0-0",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"typescript": "~3.9.3",
"vite": "^1.0.0-rc.9",
"vue-jest": "^5.0.0-0"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

3
src/App.vue Normal file
View File

@ -0,0 +1,3 @@
<template>
<router-view/>
</template>

3
src/api/index.ts Normal file
View File

@ -0,0 +1,3 @@
import EnclosureHttp from "./utils/core";
export default EnclosureHttp;

BIN
src/api/utils/.DS_Store vendored Normal file

Binary file not shown.

29
src/api/utils/config.ts Normal file
View File

@ -0,0 +1,29 @@
import { AxiosRequestConfig } from "axios";
import { excludeProps } from "./utils";
/**
*
*/
export const defaultConfig: AxiosRequestConfig = {
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
}
};
export function genConfig(config?: AxiosRequestConfig): AxiosRequestConfig {
if (!config) {
return defaultConfig;
}
const { headers } = config;
if (headers && typeof headers === "object") {
defaultConfig.headers = {
...defaultConfig.headers,
...headers
};
}
return { ...excludeProps(config!, "headers"), ...defaultConfig };
}
export const METHODS = ["post", "get", "put", "delete", "option", "patch"];

267
src/api/utils/core.ts Normal file
View File

@ -0,0 +1,267 @@
import Axios, {
AxiosRequestConfig,
CancelTokenStatic,
AxiosInstance,
Canceler
} from "axios";
import { genConfig } from "./config";
import { transformConfigByMethod } from "./utils";
import {
cancelTokenType,
RequestMethods,
EnclosureHttpRequestConfig,
EnclosureHttpResoponse,
EnclosureHttpError
} from "./types.d";
class EnclosureHttp {
// 初始化配置对象
private static initConfig: EnclosureHttpRequestConfig = {};
// 保存当前Axios实例对象
private static axiosInstance: AxiosInstance;
// 保存 EnclosureHttp实例
private static EnclosureHttpInstance: EnclosureHttp;
// axios取消对象
private CancelToken: CancelTokenStatic = Axios.CancelToken;
// 取消的凭证数组
private sourceTokenList: Array<cancelTokenType> = [];
// 记录当前这一次cancelToken的key
private currentCancelTokenKey = "";
private beforeRequestCallback: EnclosureHttpRequestConfig["beforeRequestCallback"] = undefined;
private beforeResponseCallback: EnclosureHttpRequestConfig["beforeResponseCallback"] = undefined;
public get cancelTokenList(): Array<cancelTokenType> {
return this.sourceTokenList;
}
// eslint-disable-next-line class-methods-use-this
public set cancelTokenList(value) {
throw new Error("cancelTokenList不允许赋值");
}
/**
* @description
* @returns void 0
*/
// constructor() {}
/**
* @description key
* @param config axios配置
* @returns string
*/
// eslint-disable-next-line class-methods-use-this
private genUniqueKey(config: EnclosureHttpRequestConfig): string {
return `${config.url}--${JSON.stringify(config.data)}`;
}
/**
* @description
* @returns void 0
*/
private cancelRepeatRequest(): void {
const temp: { [key: string]: boolean } = {};
this.sourceTokenList = this.sourceTokenList.reduce<Array<cancelTokenType>>(
(res: Array<cancelTokenType>, cancelToken: cancelTokenType) => {
const { cancelKey, cancelExecutor } = cancelToken;
if (!temp[cancelKey]) {
temp[cancelKey] = true;
res.push(cancelToken);
} else {
cancelExecutor();
}
return res;
},
[]
);
}
/**
* @description CancelToken
* @returns void 0
*/
private deleteCancelTokenByCancelKey(cancelKey: string): void {
this.sourceTokenList =
this.sourceTokenList.length < 1
? this.sourceTokenList.filter(
cancelToken => cancelToken.cancelKey !== cancelKey
)
: [];
}
/**
* @description
* @returns void 0
*/
private httpInterceptorsRequest(): void {
EnclosureHttp.axiosInstance.interceptors.request.use(
(config: EnclosureHttpRequestConfig) => {
const $config = config;
const cancelKey = this.genUniqueKey($config);
$config.cancelToken = new this.CancelToken((cancelExecutor: (cancel: any) => void) => {
this.sourceTokenList.push({ cancelKey, cancelExecutor });
});
this.cancelRepeatRequest();
this.currentCancelTokenKey = cancelKey;
// 优先判断post/get等方法是否传入回掉否则执行初始化设置等回掉
if (typeof this.beforeRequestCallback === "function") {
this.beforeRequestCallback($config);
this.beforeRequestCallback = undefined;
return $config;
}
if (EnclosureHttp.initConfig.beforeRequestCallback) {
EnclosureHttp.initConfig.beforeRequestCallback($config);
return $config;
}
return $config;
},
error => {
return Promise.reject(error);
}
);
}
/**
* @description cancelTokenList
* @returns void 0
*/
public clearCancelTokenList(): void {
this.sourceTokenList.length = 0;
}
/**
* @description
* @returns void 0
*/
private httpInterceptorsResponse(): void {
const instance = EnclosureHttp.axiosInstance;
instance.interceptors.response.use(
(response: EnclosureHttpResoponse) => {
// 请求每次成功一次就删除当前canceltoken标记
const cancelKey = this.genUniqueKey(response.config);
this.deleteCancelTokenByCancelKey(cancelKey);
// 优先判断post/get等方法是否传入回掉否则执行初始化设置等回掉
if (typeof this.beforeResponseCallback === "function") {
this.beforeResponseCallback(response);
this.beforeResponseCallback = undefined;
return response.data;
}
if (EnclosureHttp.initConfig.beforeResponseCallback) {
EnclosureHttp.initConfig.beforeResponseCallback(response);
return response.data;
}
return response.data;
},
(error: EnclosureHttpError) => {
const $error = error;
// 判断当前的请求中是否在 取消token数组理存在如果存在则移除单次请求流程
if (this.currentCancelTokenKey) {
const haskey = this.sourceTokenList.filter(
cancelToken => cancelToken.cancelKey === this.currentCancelTokenKey
).length;
if (haskey) {
this.sourceTokenList = this.sourceTokenList.filter(
cancelToken =>
cancelToken.cancelKey !== this.currentCancelTokenKey
);
this.currentCancelTokenKey = "";
}
}
$error.isCancelRequest = Axios.isCancel($error);
// 所有的响应异常 区分来源为取消请求/非取消请求
return Promise.reject($error);
}
);
}
/**
* @description
* @returns EnclosureHttp实例
*/
static getInstance(): EnclosureHttp {
if (!this.EnclosureHttpInstance) {
this.axiosInstance = Axios.create(this.initConfig);
this.EnclosureHttpInstance = new EnclosureHttp();
this.EnclosureHttpInstance.httpInterceptorsRequest();
this.EnclosureHttpInstance.httpInterceptorsResponse();
}
return this.EnclosureHttpInstance;
}
// eslint-disable-next-line class-methods-use-this
public request<T>(
method: RequestMethods,
url: string,
param?: AxiosRequestConfig,
axiosConfig?: EnclosureHttpRequestConfig
): Promise<T> {
const config = transformConfigByMethod(param, {
method,
url,
...axiosConfig
} as EnclosureHttpRequestConfig);
// 单独处理自定义请求/响应回掉
if (axiosConfig?.beforeRequestCallback) {
this.beforeRequestCallback = axiosConfig.beforeRequestCallback;
}
if (axiosConfig?.beforeResponseCallback) {
this.beforeResponseCallback = axiosConfig.beforeResponseCallback;
}
return new Promise((resolve, reject) => {
Axios
.request(config)
.then((response: EnclosureHttpResoponse) => {
resolve(response.data);
})
.catch((error: any) => {
reject(error);
});
});
}
public post<T>(
url: string,
params?: T,
config?: EnclosureHttpRequestConfig
): Promise<T> {
return this.request<T>("post", url, params, config);
}
public get<T>(
url: string,
params?: T,
config?: EnclosureHttpRequestConfig
): Promise<T> {
return this.request<T>("get", url, params, config);
}
/**
*
* @description
* @param config axios配置
* @returns void 0
*/
static install(config?: EnclosureHttpRequestConfig): void {
this.initConfig = genConfig(config);
}
}
export default EnclosureHttp;

50
src/api/utils/types.d.ts vendored Normal file
View File

@ -0,0 +1,50 @@
import Axios, {
AxiosRequestConfig,
Canceler,
AxiosResponse,
Method,
AxiosError
} from "axios";
import { METHODS } from './config'
export type cancelTokenType = { cancelKey: string, cancelExecutor: Canceler }
export type RequestMethods = Extract<Method, 'get' | 'post' | 'put' | 'delete' | 'patch' |
'option' | 'head'>
export interface EnclosureHttpRequestConfig extends AxiosRequestConfig {
beforeRequestCallback?: (request: EnclosureHttpRequestConfig) => void // 请求发送之前
beforeResponseCallback?: (response: EnclosureHttpResoponse) => void // 相应返回之前
}
export interface EnclosureHttpResoponse extends AxiosResponse {
config: EnclosureHttpRequestConfig
}
export interface EnclosureHttpError extends AxiosError {
isCancelRequest?: boolean
}
export default class EnclosureHttp {
cancelTokenList: Array<cancelTokenType>
clearCancelTokenList(): void
request<T>(
method: RequestMethods,
url: string,
param?: AxiosRequestConfig,
axiosConfig?: EnclosureHttpRequestConfig
): Promise<T>
post<T>(
url: string,
params?: T,
config?: EnclosureHttpRequestConfig
): Promise<T>
get<T>(
url: string,
params?: T,
config?: EnclosureHttpRequestConfig
): Promise<T>
static install(config?: EnclosureHttpRequestConfig): void
static getInstance(): EnclosureHttp
}

29
src/api/utils/utils.ts Normal file
View File

@ -0,0 +1,29 @@
import { EnclosureHttpRequestConfig } from "./types.d";
export function excludeProps<T extends { [key: string]: any }>(
origin: T,
prop: string
): { [key: string]: T } {
return Object.keys(origin)
.filter(key => !prop.includes(key))
.reduce((res, key) => {
res[key] = origin[key];
return res;
}, {} as { [key: string]: T });
}
export function transformConfigByMethod(
params: any,
config: EnclosureHttpRequestConfig
): EnclosureHttpRequestConfig {
const { method } = config;
const props = ["delete", "get", "head", "options"].includes(
method!.toLocaleLowerCase()
)
? "params"
: "data";
return {
...config,
[props]: params
};
}

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

12
src/main.ts Normal file
View File

@ -0,0 +1,12 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import EnclosureHttp from "./api/utils/core"
const app = createApp(App)
// 全局注册Axios
app.config.globalProperties.$http = new EnclosureHttp()
app.use(store).use(router).mount('#app')

25
src/router/index.ts Normal file
View File

@ -0,0 +1,25 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/home.vue'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: Home
},
// {
// path: '/about',
// name: 'About',
// // route level code-splitting
// // this generates a separate chunk (about.[hash].js) for this route
// // which is lazy-loaded when the route is visited.
// component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
// }
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router

5
src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

12
src/store/index.ts Normal file
View File

@ -0,0 +1,12 @@
import { createStore } from 'vuex'
export default createStore({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})

18
src/views/home.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<div>{{ text }}</div>
</template>
<script lang="ts">
import { ref, onMounted, nextTick } from "vue"
export default {
mounted() {
this.$http.request("get", "https://api.github.com/users")
},
setup() {
const text = ref("vue-ts")
return {
text,
}
},
}
</script>

12
tests/e2e/.eslintrc.js Normal file
View File

@ -0,0 +1,12 @@
module.exports = {
plugins: [
'cypress'
],
env: {
mocha: true,
'cypress/globals': true
},
rules: {
strict: 'off'
}
}

View File

@ -0,0 +1,25 @@
/* eslint-disable arrow-body-style */
// https://docs.cypress.io/guides/guides/plugins-guide.html
// if you need a custom webpack configuration you can uncomment the following import
// and then use the `file:preprocessor` event
// as explained in the cypress docs
// https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples
// /* eslint-disable import/no-extraneous-dependencies, global-require */
// const webpack = require('@cypress/webpack-preprocessor')
module.exports = (on, config) => {
// on('file:preprocessor', webpack({
// webpackOptions: require('@vue/cli-service/webpack.config'),
// watchOptions: {}
// }))
return Object.assign({}, config, {
fixturesFolder: 'tests/e2e/fixtures',
integrationFolder: 'tests/e2e/specs',
screenshotsFolder: 'tests/e2e/screenshots',
videosFolder: 'tests/e2e/videos',
supportFile: 'tests/e2e/support/index.js'
})
}

8
tests/e2e/specs/test.js Normal file
View File

@ -0,0 +1,8 @@
// https://docs.cypress.io/api/introduction/api.html
describe('My First Test', () => {
it('Visits the app root url', () => {
cy.visit('/')
cy.contains('h1', 'Welcome to Your Vue.js + TypeScript App')
})
})

View File

@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -0,0 +1,12 @@
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
props: { msg }
})
expect(wrapper.text()).toMatch(msg)
})
})

41
tsconfig.json Normal file
View File

@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"jest"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}