feat: 增加代码

This commit is contained in:
pangdezhen 2023-06-19 09:19:48 +08:00
commit 9284f8029b
39 changed files with 6317 additions and 0 deletions

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
git-static.js

3
.eslintrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
extends: ['@mistjs'],
}

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.idea
.vscode
*.log
node_modules
.DS_Store
dist
git-static.js

4
.husky/commit-msg Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit $1

4
.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install lint-staged

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
registry=http://10.0.59.229:5055/
//10.0.59.229:5055/:_authToken="zkijreGTZRgAkrch3Qza8pwSpWNeg0tWTBHhnM32rsY="

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# @crami/http@2.x
## 开发
```shell
# 安装依赖
pnpm i
# 版本发布
pnpm release
```
### 版本发布注意事项
本项目使用自动化版本发布脚本执行release以后选择对应的版本号会自动tag一个版本号。查看版本自动发布流程前往[droneCI](http://10.0.59.229:8119/),找到对应的项目即可查看。

12
build.config.ts Normal file
View File

@ -0,0 +1,12 @@
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
'src/index',
],
rollup: {
emitCJS: true,
},
externals: ['axios', 'vue'],
declaration: true,
})

6
commitlint.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', ['feat', 'fix', 'style', 'refactor', 'perf', 'test', 'build', 'chore', 'revert', 'release']],
},
}

58
package.json Normal file
View File

@ -0,0 +1,58 @@
{
"name": "@crami/http",
"version": "2.0.10",
"packageManager": "pnpm@7.1.7",
"description": "this is crami http",
"keywords": [],
"license": "ISC",
"author": "crami-original",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"scripts": {
"playground": "vite dev playground --port 7788 --open",
"prepare": "husky install",
"preinstall": "npx only-allow pnpm",
"build": "unbuild",
"stub": "unbuild --stub",
"release": "bumpp package.json --commit \"release: v\" --push --tag",
"prepublish": "pnpm build",
"pb": "pnpm publish --registry=http://10.0.59.229:5055/ --no-git-checks",
"pb:b": "pnpm publish --registry=http://10.0.59.229:5055/ --no-git-checks --tag=beta"
},
"files": [
"dist"
],
"dependencies": {
"axios": "^0.27.2",
"lodash": "^4.17.21",
"qs": "^6.10.5",
"vue": "^3.2.36"
},
"devDependencies": {
"@commitlint/cli": "^17.0.2",
"@unocss/reset": "^0.37.4",
"@commitlint/config-conventional": "^17.0.2",
"@iconify-json/carbon": "^1.1.5",
"@mistjs/eslint-config": "^0.0.2",
"@types/lodash": "^4.14.182",
"@types/node": "^17.0.40",
"@types/qs": "^6.9.7",
"@vitejs/plugin-vue": "^2.3.3",
"@vue/tsconfig": "^0.1.3",
"bumpp": "^7.1.1",
"eslint": "^8.17.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.0",
"npm-run-all": "^4.1.5",
"typescript": "^4.7.3",
"unbuild": "^0.7.4",
"unocss": "^0.37.4",
"vite": "^2.9.9",
"vitest": "^0.14.0",
"vue-tsc": "^0.35.2"
},
"lint-staged": {
"*.{ts,tsx,js,vue}": "eslint src --fix"
}
}

28
playground/.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

40
playground/README.md Normal file
View File

@ -0,0 +1,40 @@
# playground
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
pnpm install
```
### Compile and Hot-Reload for Development
```sh
pnpm dev
```
### Type-Check, Compile and Minify for Production
```sh
pnpm build
```

1
playground/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

17
playground/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Crami Http</title>
<style>
html{
background: #121212;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

13
playground/src/App.vue Normal file
View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import GetComp from './components/get.vue'
import PostComp from './components/post.vue'
import Upload from './components/upload.vue'
</script>
<template>
<div>
<get-comp />
<post-comp />
<Upload />
</div>
</template>

View File

@ -0,0 +1,10 @@
export interface RequestParams{
username?: string
password?: string
}
export interface ResponseData{
data?: any
success?: boolean
code: number
msg: string
}

View File

@ -0,0 +1,74 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
position: relative;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition: color 0.5s, background-color 0.5s;
line-height: 1.6;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@ -0,0 +1,49 @@
<script lang="ts" setup>
import { Http, useGet, useHttp } from '@crami/http'
import type { ResponseData } from '~/api'
const handleGet = async() => {
const data = await Http.get<ResponseData>({
url: '/api/table/columns',
})
// eslint-disable-next-line no-console
console.log(data)
}
const handleUseHttp = async() => {
const data = await useHttp<ResponseData>({
url: '/api/table/columns',
params: {
aaa: 'SAdasdas',
nnn: 'dasdasd',
},
})
// eslint-disable-next-line no-console
console.log(data)
}
const useGetData = async() => {
const data = await useGet('/api/table/columns')
// eslint-disable-next-line no-console
console.log(data)
}
</script>
<template>
<div p-3>
<div text-white mb-2>
Get类型请求测试
</div>
<div flex="~ wrap gap-2">
<button btn @click="handleGet">
Http测试请求
</button>
<button btn @click="handleUseHttp">
useHttp请求测试
</button>
<button btn @click="useGetData">
useGet请求测试
</button>
</div>
</div>
</template>

View File

@ -0,0 +1,54 @@
<script lang="ts" setup>
import { Http, useHttp, usePost } from '@crami/http'
import type { RequestParams, ResponseData } from '~/api'
const handleGet = async() => {
const data = await Http.post<ResponseData>({
url: '/api/date/table/data',
})
// eslint-disable-next-line no-console
console.log(data)
}
const handleUseHttp = async() => {
const data = await useHttp<ResponseData>({
url: '/api/date/table/data',
method: 'POST',
data: [
'1', '2',
],
})
// eslint-disable-next-line no-console
console.log(data)
}
const useGetData = async() => {
const data = await usePost<ResponseData, RequestParams>(
'/api/date/table/data',
{
username: '',
password: '',
})
// eslint-disable-next-line no-console
console.log(data)
}
</script>
<template>
<div p-3>
<div text-white mb-2>
Post类型请求测试
</div>
<div flex="~ wrap gap-2">
<button btn @click="handleGet">
Http测试请求
</button>
<button btn @click="handleUseHttp">
useHttp请求测试
</button>
<button btn @click="useGetData">
usePost请求测试
</button>
</div>
</div>
</template>

View File

@ -0,0 +1,31 @@
<script lang="ts" setup>
import { Http } from '@crami/http'
import { ref } from 'vue'
const file = ref<File>()
const handleChange = (e: any) => {
// console.log(e.target.files)
file.value = e.target.files[0]
}
const handleGet = () => {
Http.uploadFile({
url: '/api/table/columns',
}, {
filename: 'test.txt',
file: file.value,
})
}
</script>
<template>
<div p-3>
<div text-white mb-2>
文件上传测试
</div>
<div flex="~ wrap gap-2">
<input type="file" @change="handleChange">
<button btn @click="handleGet">
文件上传测试
</button>
</div>
</div>
</template>

34
playground/src/main.ts Normal file
View File

@ -0,0 +1,34 @@
import { createApp } from 'vue'
import { createAxios, createHttp, icpxTransform, icpxTransformConfig } from '@crami/http'
import App from './App.vue'
import '@unocss/reset/tailwind.css'
import 'uno.css'
const axios = createAxios({
baseURL: '/',
})
axios.interceptors.request.use((config) => {
return config
})
axios.interceptors.response.use((response) => {
return response.data
})
// axios.interceptors.request = function() {
//
// }
const http = createHttp(axios, {
responseTransform: icpxTransform,
requestTransform: icpxTransformConfig,
headers: () => ({
token: 'this is my token',
}),
})
const app = createApp(App)
app.use(http)
app.mount('#app')

View File

@ -0,0 +1,38 @@
import {
defineConfig,
presetAttributify,
presetIcons,
presetTypography,
presetUno,
presetWebFonts,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
export default defineConfig({
shortcuts: [
['btn', 'px-4 py-1 rounded inline-block bg-teal-700 text-white cursor-pointer hover:bg-teal-800 disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50'],
['icon-btn', 'inline-block cursor-pointer select-none opacity-75 transition duration-200 ease-in-out hover:opacity-100 hover:text-teal-600'],
],
presets: [
presetUno(),
presetAttributify(),
presetIcons({
scale: 1.2,
warn: true,
}),
presetTypography(),
presetWebFonts({
fonts: {
sans: 'DM Sans',
serif: 'DM Serif Display',
mono: 'DM Mono',
},
}),
],
transformers: [
transformerDirectives(),
transformerVariantGroup(),
],
safelist: 'prose prose-sm m-auto text-left'.split(' '),
})

30
playground/vite.config.ts Normal file
View File

@ -0,0 +1,30 @@
import { URL, fileURLToPath } from 'url'
import Unocss from 'unocss/vite'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
reactivityTransform: true,
}),
Unocss(),
],
resolve: {
alias: {
'@crami/http': fileURLToPath(new URL('../src', import.meta.url)),
'~/': fileURLToPath(new URL('./src/', import.meta.url)),
},
},
server: {
proxy: {
'/api': {
target: 'http://10.0.59.229:7300/mock/622ff143ec91f63c580c0560',
rewrite: (path) => {
return path.replace(/^\/api/, '')
},
},
},
},
})

5212
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

64
src/composables/http.ts Normal file
View File

@ -0,0 +1,64 @@
import type { BaseApi } from '../typing'
import { Http } from '../constant'
type RequestBaseApiType = Omit<BaseApi, 'url'|'params'|'method'|'data'>
function useHttp <R=any, D=any>(api: BaseApi<D>): Promise<R> {
return Http.request<R, D>(api)
}
function useRequest<R, D = any>(url: string, options?: Omit<BaseApi<D>, 'url'>): Promise<R> {
return useHttp<R, D>({
...(options || {}),
url,
})
}
function useGet<R, D = any>(url: string, params?: D, options?: RequestBaseApiType): Promise<R> {
return Http.get<R, D>({
...(options || {}),
url,
params,
})
}
function usePost<R, D = any>(url: string, data?: D, options?: RequestBaseApiType): Promise<R> {
return Http.post<R, D>({
...(options || {}),
url,
data,
})
}
function usePut<R, D = any>(url: string, data?: D, options?: RequestBaseApiType): Promise<R> {
return Http.put<R, D>({
...(options || {}),
url,
data,
})
}
function useDelete<R, D=any>(url: string, data?: D, options?: RequestBaseApiType): Promise<R> {
return Http.delete<R, D>({
...(options || {}),
url,
data,
})
}
function useUpload<R>(url: string, data?: FormData, options?: RequestBaseApiType): Promise<R> {
return Http.upload<R>({
...(options || {}),
url,
data,
})
}
export {
useRequest,
useGet,
usePost,
usePut,
useDelete,
useUpload,
}
export default useHttp

62
src/constant.ts Normal file
View File

@ -0,0 +1,62 @@
import type { InitHttpCallback } from './init-http'
import type { CramiHttpOptions } from './typing'
export interface HttpObjType extends Partial<InitHttpCallback>{
__isMerging?: boolean
[key: string]: any
}
// 去除vue中的白名单
const ALLOW_WHITE_LIST = [
'__isVue',
'__v_isRef',
'__v_isReadonly',
'__v_isReactive',
]
const ERROR_LIST = [
'request',
'get',
'post',
'put',
'delete',
'option',
]
const httpObj: HttpObjType = {
__isMerging: false,
}
export const Http: InitHttpCallback = new Proxy(
httpObj, {
get: (target, key) => {
const isExist = Reflect.has(target, key)
// 如果我们想要的属性不存在,就给他报错
if (!isExist && !target.__isMerging && !ALLOW_WHITE_LIST.includes(String(key))) {
if (ERROR_LIST.includes(String(key)))
console.error(`Http.${String(key)} is not exist,maybe you forgot to import {createHttp} from "@crami/http" to init it`)
return undefined
}
return Reflect.get(target, key)
},
set(target, key, value) {
return Reflect.set(target, key, value)
},
},
) as InitHttpCallback
interface AppOptions{
hooks: Omit<CramiHttpOptions, 'headers'>
}
export const app: AppOptions = {
hooks: {},
}
export enum ContentTypeEnum {
// json
JSON = 'application/json;charset=UTF-8',
// form-data qs
FORM = 'application/x-www-form-urlencoded;charset=UTF-8',
// form-data upload
UPLOAD = 'multipart/form-data;charset=UTF-8',
}

7
src/create-axios.ts Normal file
View File

@ -0,0 +1,7 @@
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
export const createAxios = (config: AxiosRequestConfig): AxiosInstance => {
// TODO
return axios.create(config)
}

27
src/create-http.ts Normal file
View File

@ -0,0 +1,27 @@
import type { AxiosInstance } from 'axios'
import type { App, Plugin } from 'vue'
import { merge } from 'lodash'
import type { CramiHttpOptions } from './typing'
import initOptions from './init-options'
import type { InitHttpCallback } from './init-http'
import initHttp from './init-http'
import { Http } from './constant'
export const createHttp = (instance: AxiosInstance, options?: CramiHttpOptions): Plugin &{ version: string } => {
// 初始化配置
initOptions(instance, options)
// 创建axios实例
const InitHttp = initHttp(instance) as InitHttpCallback
// 合并实例
(Http as any).__isMerging = true
merge(Http as any, InitHttp);
(Http as any).__isMerging = false
delete (Http as any).__isMerging
// 返回需要实例化的对象的信息
return {
version: '2.0.0',
install: (app: App) => {
app.config.globalProperties.$http = Http
},
}
}

17
src/index.ts Normal file
View File

@ -0,0 +1,17 @@
import { createAxios } from './create-axios'
import { createHttp } from './create-http'
import { ContentTypeEnum, Http } from './constant'
import useHttp from './composables/http'
export * from './composables/http'
export * from './transformer'
export type { BaseApi, CramiHttpOptions, HttpHeaderType } from './typing'
export type { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosResponseHeaders, AxiosResponseTransformer, AxiosRequestTransformer, AxiosRequestHeaders } from 'axios'
export {
createAxios,
createHttp,
Http,
useHttp,
ContentTypeEnum,
}
export default createHttp

182
src/init-http.ts Normal file
View File

@ -0,0 +1,182 @@
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
import qs from 'qs'
import { ContentTypeEnum, app } from './constant'
import { defaultTransform, defaultTransformConfig } from './transformer'
import type { BaseApi } from './typing'
import { checkContentType, checkIsGet, checkParams, isString, runFunction } from './utils'
type BaseApiNoMethod<D> = Omit<BaseApi<D>, 'method'>
interface UploadFileParams<D>{
data?: D
name?: string
file: File | Blob
filename?: string
[key: string]: any
}
export interface InitHttpCallback {
request <R=any, D=any>(config: BaseApi<D>): Promise<R>
get <R=any, D=any>(config: BaseApiNoMethod<D>): Promise<R>
post <R=any, D=any>(config: BaseApiNoMethod<D>): Promise<R>
put <R=any, D=any>(config: BaseApiNoMethod<D>): Promise<R>
form <R=any, D=any>(config: BaseApiNoMethod<D>): Promise<R>
upload <R=any>(config: BaseApiNoMethod<FormData>): Promise<R>
delete <R=any, D=any>(config: BaseApiNoMethod<D>): Promise<R>
uploadFile <R=any, D=any>(config: Omit<BaseApiNoMethod<any>, 'params'|'data'>, params: UploadFileParams<D>): Promise<R>
}
export default function initHttp(instance: AxiosInstance): InitHttpCallback {
// 请求request
async function request<R = any, D=any>(config: BaseApi<D>): Promise<R> {
const { beforeRequest, afterRequest, beforeRequestConfig, beforeRequestQuery, onError, responseTransform, urlParser, requestTransform, dataType, ...restConfig } = config
let newConfig
// 处理请求类型
if (dataType) {
switch (dataType) {
case 'form':
restConfig.headers = {
...restConfig.headers,
'Content-type': ContentTypeEnum.FORM,
}
break
case 'upload':
restConfig.headers = {
...restConfig.headers,
'Content-type': ContentTypeEnum.UPLOAD,
}
break
case 'json':
restConfig.headers = {
...restConfig.headers,
'Content-type': ContentTypeEnum.JSON,
}
break
default:
if (!checkContentType(restConfig.headers as any)) {
restConfig.headers = {
...restConfig.headers,
'Content-type': ContentTypeEnum.JSON,
}
}
}
}
const requestFun = requestTransform || app.hooks?.requestTransform || defaultTransformConfig
newConfig = checkParams(requestFun(restConfig), restConfig)
newConfig = checkParams(await runFunction<AxiosRequestConfig<D>>(app.hooks?.beforeRequestConfig, newConfig), newConfig)
newConfig = checkParams(await runFunction<AxiosRequestConfig<D>>(beforeRequestConfig, newConfig), newConfig)
// 拦截url的处理钩子
newConfig.url = checkParams(await runFunction(app.hooks?.urlParser, newConfig.url), newConfig.url)
newConfig.url = checkParams(await runFunction(urlParser, newConfig.url), newConfig.url)
// GET 请求钩子特殊处理
if (checkIsGet(newConfig?.method)) {
// 全局的钩子
newConfig.params = checkParams(await runFunction(app.hooks?.beforeRequest, newConfig.params), newConfig.params)
// 单个请求的钩子
newConfig.params = checkParams(await runFunction(beforeRequest, newConfig.params), newConfig.params)
}
else {
// 管他存不存在的都要走这些钩子
newConfig.data = checkParams(await runFunction(app.hooks?.beforeRequest, newConfig.data), newConfig.data)
newConfig.data = checkParams(await runFunction(beforeRequest, newConfig.data), newConfig.data)
// 如果存在query的参数就直接使用
newConfig.params = checkParams(await runFunction(app.hooks?.beforeRequestQuery, newConfig.params), newConfig.params)
newConfig.params = checkParams(await runFunction(beforeRequestQuery, newConfig.params), newConfig.params)
}
try {
let response = await instance.request<R>(newConfig)
// 请求后的钩子函数
response = await checkParams(await runFunction(afterRequest, response), response)
// transform优先级从左到右由大到小 接口自定义 -> 全局自定义 -> 默认
const responseFun = responseTransform || app.hooks?.responseTransform || defaultTransform
// 这里还可以加处理
return responseFun(response)
}
catch (e) {
app.hooks?.onError?.(e)
// TODO
onError?.(e)
throw e
}
}
// get请求
async function get<R=any, D=any>(config: BaseApiNoMethod<D>): Promise<R> {
return request<R, D>({ ...config, method: 'GET' })
}
// POST请求
async function post<R=any, D=any>(config: BaseApiNoMethod<D>): Promise<R> {
return request<R, D>({ ...config, method: 'POST' })
}
// Put请求
async function put<R=any, D=any>(config: BaseApiNoMethod<D>): Promise<R> {
return request<R, D>({ ...config, method: 'PUT' })
}
// delete请求
async function deleteFun<R=any, D=any>(config: BaseApiNoMethod<D>): Promise<R> {
return request<R, D>({ ...config, method: 'DELETE' })
}
// upload请求
async function upload<R=any>(config: BaseApiNoMethod<FormData>): Promise<R> {
return request<R>({
...config,
method: 'POST',
headers: {
...config.headers,
'Content-Type': ContentTypeEnum.UPLOAD,
},
})
}
async function uploadFile<R=any, D = any>(config: Omit<BaseApiNoMethod<any>, 'params'|'data'>, params: UploadFileParams<D>): Promise<R> {
// 处理上传文件类型
const formData = new FormData()
if (params.data) {
for (const pKey in params.data) {
const value = params.data[pKey]
if (Array.isArray(value)) {
value.forEach((item) => {
formData.append(`${pKey}[]`, item)
})
}
else {
formData.append(pKey, (value as any))
}
}
}
formData.append(params.name || 'file', params.file, params.filename)
if (params.filename)
formData.append('fileName', params.filename)
return upload<R>(
{
...config,
data: formData,
},
)
}
// form请求
async function form<R=any, D=any>(config: BaseApiNoMethod<D>): Promise<R> {
return request<R, string>({
...config,
method: 'POST',
headers: {
...config.headers,
'Content-Type': ContentTypeEnum.FORM,
},
data: isString(config.data) ? config.data : qs.stringify(config.data),
})
}
return {
request,
get,
delete: deleteFun,
post,
upload,
form,
put,
uploadFile,
}
}

25
src/init-options.ts Normal file
View File

@ -0,0 +1,25 @@
import type { AxiosInstance } from 'axios'
import { merge, omit } from 'lodash'
import { ContentTypeEnum, app } from './constant'
import type { CramiHttpOptions, HttpHeaderType } from './typing'
import { runFunction } from './utils'
export default function(instance: AxiosInstance, options?: CramiHttpOptions) {
if (!instance) {
// 不存在instance
console.error('[crami] createHttp: instance is required, maybe you forgot to import {createAxios} from "@crami/http"')
return
}
merge(app.hooks, omit(options, ['headers']))
// 处理headers
if (options?.headers) {
// 存在判断是不是一个函数
const headers = runFunction<HttpHeaderType>(options?.headers || {})
headers.then((res) => {
// 设置headers
merge(instance.defaults.headers, {
'Content-Type': ContentTypeEnum.JSON,
}, res)
})
}
}

View File

@ -0,0 +1,12 @@
import type { AxiosResponse } from 'axios'
import type { BaseApi } from '../typing'
export const defaultTransform = <R=any>(response: AxiosResponse<R>): R => {
if (response.config && response.data && response.status && response.headers && response.statusText)
return response.data
return response as any
}
export const defaultTransformConfig = (config: BaseApi): BaseApi => {
return config
}

View File

@ -0,0 +1,71 @@
import type { AxiosResponse } from 'axios'
import type { BaseApi } from '../typing'
import { checkIsGet, isObject } from '../utils'
export type ICPXResponseData<D> = {
data: D
code?: number
msg?: string
success?: boolean
} & Record<string, any>
export const icpxTransform = <D>(response: AxiosResponse<D>): any => {
if (
response.config
&& response.data
&& response.status
&& response.headers
&& response.statusText
) {
const data: any = response.data
const status = response.status
if (status === 200) {
if (!data?.code) {
return {
code: 200,
msg: 'ok',
success: true,
data,
}
}
}
return data
}
else {
if (!(response as any)?.code) {
return {
code: 200,
msg: 'ok',
success: true,
data: response,
}
}
return response
}
}
export const icpxTransformConfig = (config: BaseApi): BaseApi => {
// 判断请求方式
if (config.method && !checkIsGet(config.method)) {
if (isObject(config.params)) {
config.data = {
...(config.data || {}),
...(config.params || {}),
}
}
else {
if (!config.data)
config.data = config.params
}
config.params = undefined
}
else {
// 如果是get请求是不是需要增加一个请求参数,防止缓存请求
const _t = Date.now()
config.params = {
_t,
...(config.params),
}
}
return config
}

2
src/transformer/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './default-transform'
export * from './icpx-transform'

42
src/typing.ts Normal file
View File

@ -0,0 +1,42 @@
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
export type HttpHeaderType = Record<string, any>
export interface BaseApi<D = any> extends AxiosRequestConfig<D>{
// 请求前的钩子函数
beforeRequest?: (params: any) => any
// 请求前的参数处理
beforeRequestConfig?: (config: AxiosRequestConfig) => AxiosRequestConfig
// 请求前置的query中的参数
beforeRequestQuery?: (query: any) => any
// 请求失败的钩子
onError?: (e: any) => void
// 请求接口格式化处理
urlParser?: (url: string) => string
// 请求数据格式化 同步方法不支持Promise
responseTransform?: <R, D>(response: AxiosResponse<D>) => R
// 请求前的数据格式化 同步方法不支持Promise
requestTransform?: <D>(config: BaseApi<D>) => BaseApi<D>
// 请求类型
dataType?: 'form'|'upload'|'json'
// 请求后的钩子函数
afterRequest?: (response: AxiosResponse) => AxiosResponse | undefined
}
export interface CramiHttpOptions{
headers?: (() => HttpHeaderType)|HttpHeaderType
// 请求前的钩子函数
beforeRequest?: (params: any) => any
// 请求前的参数处理
beforeRequestConfig?: (config: AxiosRequestConfig) => AxiosRequestConfig
// 请求前置的query中的参数
beforeRequestQuery?: (query: any) => any
// 请求失败的钩子
onError?: (e: any) => void
// 请求接口格式化处理
urlParser?: (url: string) => string
// 请求数据格式化 同步方法不支持Promise
responseTransform?: <R = any, D = any>(response: AxiosResponse<D>) => R
// 请求前的数据格式化 同步方法不支持Promise
requestTransform?: <D>(config: BaseApi<D>) => BaseApi<D>
}

36
src/utils.ts Normal file
View File

@ -0,0 +1,36 @@
import type { Method } from 'axios'
export const isPromise = (value: any): value is Promise<any> => {
return value && typeof value.then === 'function' && typeof value.catch === 'function'
}
export const isObject = (value: unknown): value is object => Object.prototype.toString.call(value) === '[Object Object]'
export const isArray = (value: unknown): value is any[] => Array.isArray(value)
export const isString = (value: unknown): value is string => typeof value === 'string'
export const isFunction = (value: any): value is Function => typeof value === 'function'
export const runFunction = async<T>(fn: any, ...args: any[]): Promise<T> => {
if (!isFunction(fn))
return fn
// TODO
const result = fn(...args)
if (isPromise(result))
return await result
return result
}
export const checkParams = <T>(checkerParams: T, defaultParams: T): T => checkerParams ?? defaultParams
export const checkIsGet = (method?: Method | string|any): boolean => method === 'get' || method === 'GET' || method === undefined || method === null
export const checkContentType = (headers: Record<string, any>): boolean => {
if (Reflect.get(headers || {}, 'content-type'))
return true
else if (Reflect.get(headers || {}, 'Content-Type'))
return true
return false
}

8
tsconfig.config.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["./playground/vite.config.*", "vitest.config.*", "cypress.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["./playground/src/*", "./playground/src/env.d.ts", "./playground/src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@crami/http":["./src/index.ts"],
"~/*": ["./playground/src/*"],
}
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}