create-vite-vue 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +179 -179
- package/package.json +2 -2
- package/template/axios-js/src/utils/request.js +35 -3
- package/template/axios-js/src/utils/requestCache.js +41 -0
- package/template/axios-ts/src/utils/request.ts +35 -3
- package/template/axios-ts/src/utils/requestCache.ts +41 -0
- package/template/base-js/types/request.d.ts +148 -0
- package/template/base-ts/types/request.d.ts +148 -0
package/bin/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
import { execSync } from 'child_process'
|
|
3
3
|
import fs from 'fs'
|
|
4
4
|
import path from 'path'
|
|
@@ -8,204 +8,204 @@ import { fileURLToPath } from 'url'
|
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url)
|
|
9
9
|
const __dirname = path.dirname(__filename)
|
|
10
10
|
|
|
11
|
-
async
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
; (async () => {
|
|
12
|
+
// 1️⃣ 输入项目名
|
|
13
|
+
let projectName
|
|
14
|
+
while (true) {
|
|
15
|
+
const res = await prompts({
|
|
16
|
+
type: 'text',
|
|
17
|
+
name: 'projectName',
|
|
18
|
+
message: '📦 项目名称',
|
|
19
|
+
validate: v => v ? true : '项目名不能为空'
|
|
20
|
+
})
|
|
21
|
+
projectName = res.projectName
|
|
22
|
+
if (!projectName) process.exit(1)
|
|
23
|
+
|
|
24
|
+
const targetDir = path.resolve(process.cwd(), projectName)
|
|
25
|
+
if (fs.existsSync(targetDir)) {
|
|
26
|
+
console.log('❌ 目录已存在,请重新输入')
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
break
|
|
28
30
|
}
|
|
29
|
-
break
|
|
30
|
-
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
const targetDir = path.resolve(process.cwd(), projectName)
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
const { language } = await prompts({
|
|
35
|
+
type: 'select',
|
|
36
|
+
name: 'language',
|
|
37
|
+
message: '请选择项目语言',
|
|
38
|
+
choices: [
|
|
39
|
+
{ title: 'JavaScript', value: 'js' },
|
|
40
|
+
{ title: 'TypeScript', value: 'ts' }
|
|
41
|
+
]
|
|
42
|
+
})
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
// 2️⃣ 功能选择
|
|
46
|
+
const features = await prompts([
|
|
47
|
+
{
|
|
48
|
+
type: 'select',
|
|
49
|
+
name: 'router',
|
|
50
|
+
message: '是否使用 vue-router?',
|
|
51
|
+
choices: [{ title: 'Yes', value: true }, { title: 'No', value: false }]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'select',
|
|
55
|
+
name: 'pinia',
|
|
56
|
+
message: '是否使用 Pinia(含持久化)?',
|
|
57
|
+
choices: [{ title: 'Yes', value: true }, { title: 'No', value: false }]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: 'select',
|
|
61
|
+
name: 'axios',
|
|
62
|
+
message: '是否使用 Axios?',
|
|
63
|
+
choices: [{ title: 'Yes', value: true }, { title: 'No', value: false }]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
type: 'select',
|
|
67
|
+
name: 'ui',
|
|
68
|
+
message: '请选择 UI 框架',
|
|
69
|
+
choices: [
|
|
70
|
+
{ title: 'Element Plus(PC)', value: 'element' },
|
|
71
|
+
{ title: 'Vant(Mobile)', value: 'vant' },
|
|
72
|
+
{ title: '不使用 UI 框架', value: 'none' }
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
// 3️⃣ 是否立即运行 dev
|
|
78
|
+
const { runDev } = await prompts({
|
|
48
79
|
type: 'select',
|
|
49
|
-
name: '
|
|
50
|
-
message: '
|
|
80
|
+
name: 'runDev',
|
|
81
|
+
message: '是否立即运行 npm run dev?',
|
|
51
82
|
choices: [{ title: 'Yes', value: true }, { title: 'No', value: false }]
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
type: 'select',
|
|
61
|
-
name: 'axios',
|
|
62
|
-
message: '是否使用 Axios?',
|
|
63
|
-
choices: [{ title: 'Yes', value: true }, { title: 'No', value: false }]
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
type: 'select',
|
|
67
|
-
name: 'ui',
|
|
68
|
-
message: '请选择 UI 框架',
|
|
69
|
-
choices: [
|
|
70
|
-
{ title: 'Element Plus(PC)', value: 'element' },
|
|
71
|
-
{ title: 'Vant(Mobile)', value: 'vant' },
|
|
72
|
-
{ title: '不使用 UI 框架', value: 'none' }
|
|
73
|
-
]
|
|
74
|
-
}
|
|
75
|
-
])
|
|
76
|
-
|
|
77
|
-
// 3️⃣ 是否立即运行 dev
|
|
78
|
-
const { runDev } = await prompts({
|
|
79
|
-
type: 'select',
|
|
80
|
-
name: 'runDev',
|
|
81
|
-
message: '是否立即运行 npm run dev?',
|
|
82
|
-
choices: [{ title: 'Yes', value: true }, { title: 'No', value: false }]
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
// 4️⃣ 拷贝 base 模板
|
|
86
|
-
const baseTemplate = language === 'ts' ? 'base-ts' : 'base-js'
|
|
87
|
-
fs.cpSync(
|
|
88
|
-
path.resolve(__dirname, `../template/${baseTemplate}`),
|
|
89
|
-
targetDir,
|
|
90
|
-
{ recursive: true }
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
// 4️⃣-1️⃣ 替换 index.html 的 title
|
|
94
|
-
const indexPath = path.join(targetDir, 'index.html')
|
|
95
|
-
if (fs.existsSync(indexPath)) {
|
|
96
|
-
const indexContent = fs.readFileSync(indexPath, 'utf-8')
|
|
97
|
-
fs.writeFileSync(
|
|
98
|
-
indexPath,
|
|
99
|
-
indexContent.replace(/<title>.*<\/title>/, `<title>${projectName}</title>`)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// 4️⃣ 拷贝 base 模板
|
|
86
|
+
const baseTemplate = language === 'ts' ? 'base-ts' : 'base-js'
|
|
87
|
+
fs.cpSync(
|
|
88
|
+
path.resolve(__dirname, `../template/${baseTemplate}`),
|
|
89
|
+
targetDir,
|
|
90
|
+
{ recursive: true }
|
|
100
91
|
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
'
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
'
|
|
122
|
-
|
|
92
|
+
|
|
93
|
+
// 4️⃣-1️⃣ 替换 index.html 的 title
|
|
94
|
+
const indexPath = path.join(targetDir, 'index.html')
|
|
95
|
+
if (fs.existsSync(indexPath)) {
|
|
96
|
+
const indexContent = fs.readFileSync(indexPath, 'utf-8')
|
|
97
|
+
fs.writeFileSync(
|
|
98
|
+
indexPath,
|
|
99
|
+
indexContent.replace(/<title>.*<\/title>/, `<title>${projectName}</title>`)
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 5️⃣ 拷贝可选模板
|
|
104
|
+
const copy = name => {
|
|
105
|
+
fs.cpSync(path.resolve(__dirname, `../template/${name}`), targetDir, { recursive: true })
|
|
106
|
+
}
|
|
107
|
+
features.router && copy(language === 'ts' ? 'router-ts' : 'router-js')
|
|
108
|
+
features.pinia && copy(language === 'ts' ? 'pinia-ts' : 'pinia-js')
|
|
109
|
+
features.axios && copy(language === 'ts' ? 'axios-ts' : 'axios-js')
|
|
110
|
+
|
|
111
|
+
// 6️⃣ 生成 main.js
|
|
112
|
+
const mainFile = language === 'ts' ? 'main.ts' : 'main.js'
|
|
113
|
+
const mainTplPath = path.join(targetDir, `src/${mainFile}.tpl`)
|
|
114
|
+
let main = fs.readFileSync(mainTplPath, 'utf-8')
|
|
115
|
+
|
|
116
|
+
const replacements = {
|
|
117
|
+
'/* __ROUTER_IMPORT__ */': features.router ? "import router from './router'" : '',
|
|
118
|
+
'/* __PINIA_IMPORT__ */': features.pinia
|
|
119
|
+
? "import { createPinia } from 'pinia'\nimport persistedstate from 'pinia-plugin-persistedstate'"
|
|
120
|
+
: '',
|
|
121
|
+
'/* __ELEMENT_IMPORT__ */': features.ui === 'element'
|
|
122
|
+
? `import ElementPlus from 'element-plus'
|
|
123
123
|
import 'element-plus/dist/index.css'
|
|
124
124
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'`
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
: '',
|
|
126
|
+
'/* __VANT_IMPORT__ */': features.ui === 'vant'
|
|
127
|
+
? `import Vant from 'vant'
|
|
128
128
|
import 'vant/lib/index.css'`
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
: '',
|
|
130
|
+
'/* __ROUTER_USE__ */': features.router ? 'app.use(router)' : '',
|
|
131
|
+
'/* __PINIA_USE__ */': features.pinia
|
|
132
|
+
? 'app.use(createPinia().use(persistedstate))'
|
|
133
|
+
: '',
|
|
134
|
+
'/* __ELEMENT_USE__ */': features.ui === 'element'
|
|
135
|
+
? `app.use(ElementPlus)
|
|
136
136
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
|
137
137
|
app.component(key, component)
|
|
138
138
|
}`
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function escapeRegExp (str) {
|
|
146
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
for (const [placeholder, content] of Object.entries(replacements)) {
|
|
150
|
-
if (content) {
|
|
151
|
-
main = main.replace(placeholder, content)
|
|
152
|
-
} else {
|
|
153
|
-
const re = new RegExp(`^\\s*${escapeRegExp(placeholder)}\\s*$\\n?`, 'gm')
|
|
154
|
-
main = main.replace(re, '')
|
|
139
|
+
: '',
|
|
140
|
+
'/* __VANT_USE__ */': features.ui === 'vant'
|
|
141
|
+
? 'app.use(Vant)'
|
|
142
|
+
: ''
|
|
155
143
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
path.join(targetDir, `src/${mainFile}`),
|
|
160
|
-
main
|
|
161
|
-
)
|
|
162
|
-
fs.unlinkSync(mainTplPath)
|
|
163
|
-
|
|
164
|
-
// 7️⃣ 生成 package.json
|
|
165
|
-
const pkgTpl = path.join(targetDir, 'package.json.tpl')
|
|
166
|
-
if (fs.existsSync(pkgTpl)) {
|
|
167
|
-
let pkg = fs.readFileSync(pkgTpl, 'utf-8')
|
|
168
|
-
|
|
169
|
-
const optionalDeps = {}
|
|
170
|
-
if (features.router) optionalDeps['vue-router'] = '^4.4.0'
|
|
171
|
-
if (features.pinia) {
|
|
172
|
-
optionalDeps['pinia'] = '^2.2.2'
|
|
173
|
-
optionalDeps['pinia-plugin-persistedstate'] = '^3.2.1'
|
|
174
|
-
}
|
|
175
|
-
if (features.axios) optionalDeps['axios'] = '^1.7.7'
|
|
176
|
-
if (features.ui === 'element') {
|
|
177
|
-
optionalDeps['element-plus'] = '^2.8.8'
|
|
178
|
-
optionalDeps['@element-plus/icons-vue'] = '^2.3.1'
|
|
179
|
-
}
|
|
180
|
-
if (features.ui === 'vant') {
|
|
181
|
-
optionalDeps['vant'] = '^4.9.22'
|
|
144
|
+
|
|
145
|
+
function escapeRegExp (str) {
|
|
146
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
182
147
|
}
|
|
183
148
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
149
|
+
for (const [placeholder, content] of Object.entries(replacements)) {
|
|
150
|
+
if (content) {
|
|
151
|
+
main = main.replace(placeholder, content)
|
|
152
|
+
} else {
|
|
153
|
+
const re = new RegExp(`^\\s*${escapeRegExp(placeholder)}\\s*$\\n?`, 'gm')
|
|
154
|
+
main = main.replace(re, '')
|
|
155
|
+
}
|
|
188
156
|
}
|
|
189
157
|
|
|
190
|
-
|
|
191
|
-
.
|
|
192
|
-
|
|
158
|
+
fs.writeFileSync(
|
|
159
|
+
path.join(targetDir, `src/${mainFile}`),
|
|
160
|
+
main
|
|
161
|
+
)
|
|
162
|
+
fs.unlinkSync(mainTplPath)
|
|
163
|
+
|
|
164
|
+
// 7️⃣ 生成 package.json
|
|
165
|
+
const pkgTpl = path.join(targetDir, 'package.json.tpl')
|
|
166
|
+
if (fs.existsSync(pkgTpl)) {
|
|
167
|
+
let pkg = fs.readFileSync(pkgTpl, 'utf-8')
|
|
168
|
+
|
|
169
|
+
const optionalDeps = {}
|
|
170
|
+
if (features.router) optionalDeps['vue-router'] = '^4.4.0'
|
|
171
|
+
if (features.pinia) {
|
|
172
|
+
optionalDeps['pinia'] = '^2.2.2'
|
|
173
|
+
optionalDeps['pinia-plugin-persistedstate'] = '^3.2.1'
|
|
174
|
+
}
|
|
175
|
+
if (features.axios) optionalDeps['axios'] = '^1.7.7'
|
|
176
|
+
if (features.ui === 'element') {
|
|
177
|
+
optionalDeps['element-plus'] = '^2.8.8'
|
|
178
|
+
optionalDeps['@element-plus/icons-vue'] = '^2.3.1'
|
|
179
|
+
}
|
|
180
|
+
if (features.ui === 'vant') {
|
|
181
|
+
optionalDeps['vant'] = '^4.9.22'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let depsStr = ''
|
|
185
|
+
const keys = Object.keys(optionalDeps)
|
|
186
|
+
if (keys.length > 0) {
|
|
187
|
+
depsStr = ',\n' + keys.map(k => ` "${k}": "${optionalDeps[k]}"`).join(',\n')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
pkg = pkg
|
|
191
|
+
.replace('__PROJECT_NAME__', projectName)
|
|
192
|
+
.replace('__OPTIONAL_DEP__', depsStr)
|
|
193
|
+
|
|
194
|
+
fs.writeFileSync(path.join(targetDir, 'package.json'), pkg)
|
|
195
|
+
fs.unlinkSync(pkgTpl)
|
|
196
|
+
}
|
|
193
197
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
198
|
+
// 8️⃣ 安装依赖
|
|
199
|
+
console.log('📦 安装依赖中...')
|
|
200
|
+
execSync('npm install', { cwd: targetDir, stdio: 'inherit' })
|
|
197
201
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
// 9️⃣ 运行 dev
|
|
203
|
+
if (runDev) {
|
|
204
|
+
console.log('🚀 启动开发服务器...')
|
|
205
|
+
execSync('npm run dev', { cwd: targetDir, stdio: 'inherit' })
|
|
206
|
+
} else {
|
|
207
|
+
console.log(`\n✅ 项目创建完成\n👉 cd ${projectName}\n👉 npm run dev\n`)
|
|
208
|
+
}
|
|
209
|
+
})()
|
|
201
210
|
|
|
202
|
-
// 9️⃣ 运行 dev
|
|
203
|
-
if (runDev) {
|
|
204
|
-
console.log('🚀 启动开发服务器...')
|
|
205
|
-
execSync('npm run dev', { cwd: targetDir, stdio: 'inherit' })
|
|
206
|
-
} else {
|
|
207
|
-
console.log(`\n✅ 项目创建完成\n👉 cd ${projectName}\n👉 npm run dev\n`)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
211
|
|
|
211
|
-
createProject()
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import axios from 'axios'
|
|
2
|
+
import { addPendingRequest, removePendingRequest } from './requestCache'
|
|
2
3
|
|
|
3
4
|
const service = axios.create({
|
|
4
5
|
baseURL: '/api',
|
|
@@ -6,13 +7,44 @@ const service = axios.create({
|
|
|
6
7
|
})
|
|
7
8
|
|
|
8
9
|
service.interceptors.request.use(
|
|
9
|
-
config =>
|
|
10
|
+
config => {
|
|
11
|
+
// 在发送请求之前做些什么
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
addPendingRequest(config)
|
|
15
|
+
} catch (error) {
|
|
16
|
+
// 如果是取消请求,直接reject
|
|
17
|
+
if (error.isCancel) {
|
|
18
|
+
return Promise.reject({ canceled: true, message: '请求取消' })
|
|
19
|
+
}
|
|
20
|
+
throw error
|
|
21
|
+
}
|
|
22
|
+
return config
|
|
23
|
+
},
|
|
24
|
+
// 对请求错误做些什么
|
|
10
25
|
error => Promise.reject(error)
|
|
11
26
|
)
|
|
12
27
|
|
|
13
28
|
service.interceptors.response.use(
|
|
14
|
-
response =>
|
|
15
|
-
|
|
29
|
+
response => {
|
|
30
|
+
removePendingRequest(response.config)
|
|
31
|
+
|
|
32
|
+
// 对响应数据做点什么
|
|
33
|
+
return response.data
|
|
34
|
+
},
|
|
35
|
+
error => {
|
|
36
|
+
// 即使是错误(包括网络错误)也要清理
|
|
37
|
+
if (error.config) {
|
|
38
|
+
removePendingRequest(error.config)
|
|
39
|
+
}
|
|
40
|
+
// 判断是否是重复请求被取消
|
|
41
|
+
if (error.canceled) {
|
|
42
|
+
return Promise.reject(error)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 对响应错误做点什么
|
|
46
|
+
return Promise.reject(error)
|
|
47
|
+
}
|
|
16
48
|
)
|
|
17
49
|
|
|
18
50
|
export default service
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const pendingRequests = new Map()
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 生成请求唯一kaey
|
|
5
|
+
* @param {RequestConfig} config 请求配置对象
|
|
6
|
+
* 注意:只适用于可序列化的 params 和 data(如普通对象)
|
|
7
|
+
*/
|
|
8
|
+
function generateReqKey (config) {
|
|
9
|
+
const { method, url, params, data } = config
|
|
10
|
+
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 添加pending请求,并自动取消请求
|
|
15
|
+
* @param {RequestConfig} config 请求配置对象
|
|
16
|
+
*/
|
|
17
|
+
export function addPendingRequest (config) {
|
|
18
|
+
// 获取请求key
|
|
19
|
+
const requestKey = generateReqKey(config)
|
|
20
|
+
// 如果存在相同请求,取消当前请求
|
|
21
|
+
if (pendingRequests.has(requestKey)) {
|
|
22
|
+
const error = new Error('重复请求')
|
|
23
|
+
error.isCancel = true
|
|
24
|
+
throw error
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//否则为本次请求创建新的AbortController
|
|
28
|
+
const controller = new AbortController()
|
|
29
|
+
// 绑定 signal 到 axios 配置
|
|
30
|
+
config.signal = controller.signal
|
|
31
|
+
pendingRequests.set(requestKey, controller)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 移除已完成/失败/取消的请求
|
|
36
|
+
* @param {RequestConfig} config 请求配置对象
|
|
37
|
+
*/
|
|
38
|
+
export function removePendingRequest (config) {
|
|
39
|
+
const requestKey = generateReqKey(config)
|
|
40
|
+
pendingRequests.delete(requestKey)
|
|
41
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import axios from 'axios'
|
|
2
|
+
import { addPendingRequest, removePendingRequest } from './requestCache'
|
|
2
3
|
|
|
3
4
|
const service = axios.create({
|
|
4
5
|
baseURL: '/api',
|
|
@@ -6,13 +7,44 @@ const service = axios.create({
|
|
|
6
7
|
})
|
|
7
8
|
|
|
8
9
|
service.interceptors.request.use(
|
|
9
|
-
config =>
|
|
10
|
+
config => {
|
|
11
|
+
// 在发送请求之前做些什么
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
addPendingRequest(config)
|
|
15
|
+
} catch (error) {
|
|
16
|
+
// 如果是取消请求,直接reject
|
|
17
|
+
if (error.isCancel) {
|
|
18
|
+
return Promise.reject({ canceled: true, message: '请求取消' })
|
|
19
|
+
}
|
|
20
|
+
throw error
|
|
21
|
+
}
|
|
22
|
+
return config
|
|
23
|
+
},
|
|
24
|
+
// 对请求错误做些什么
|
|
10
25
|
error => Promise.reject(error)
|
|
11
26
|
)
|
|
12
27
|
|
|
13
28
|
service.interceptors.response.use(
|
|
14
|
-
response =>
|
|
15
|
-
|
|
29
|
+
response => {
|
|
30
|
+
removePendingRequest(response.config)
|
|
31
|
+
|
|
32
|
+
// 对响应数据做点什么
|
|
33
|
+
return response.data
|
|
34
|
+
},
|
|
35
|
+
error => {
|
|
36
|
+
// 即使是错误(包括网络错误)也要清理
|
|
37
|
+
if (error.config) {
|
|
38
|
+
removePendingRequest(error.config)
|
|
39
|
+
}
|
|
40
|
+
// 判断是否是重复请求被取消
|
|
41
|
+
if (error.canceled) {
|
|
42
|
+
return Promise.reject(error)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 对响应错误做点什么
|
|
46
|
+
return Promise.reject(error)
|
|
47
|
+
}
|
|
16
48
|
)
|
|
17
49
|
|
|
18
50
|
export default service
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const pendingRequests = new Map()
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 生成请求唯一kaey
|
|
5
|
+
* @param {RequestConfig} config 请求配置对象
|
|
6
|
+
* 注意:只适用于可序列化的 params 和 data(如普通对象)
|
|
7
|
+
*/
|
|
8
|
+
function generateReqKey (config) {
|
|
9
|
+
const { method, url, params, data } = config
|
|
10
|
+
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 添加pending请求,并自动取消请求
|
|
15
|
+
* @param {RequestConfig} config 请求配置对象
|
|
16
|
+
*/
|
|
17
|
+
export function addPendingRequest (config) {
|
|
18
|
+
// 获取请求key
|
|
19
|
+
const requestKey = generateReqKey(config)
|
|
20
|
+
// 如果存在相同请求,取消当前请求
|
|
21
|
+
if (pendingRequests.has(requestKey)) {
|
|
22
|
+
const error = new Error('重复请求') as Error & { isCancel: boolean }
|
|
23
|
+
error.isCancel = true
|
|
24
|
+
throw error
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//否则为本次请求创建新的AbortController
|
|
28
|
+
const controller = new AbortController()
|
|
29
|
+
// 绑定 signal 到 axios 配置
|
|
30
|
+
config.signal = controller.signal
|
|
31
|
+
pendingRequests.set(requestKey, controller)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 移除已完成/失败/取消的请求
|
|
36
|
+
* @param {RequestConfig} config 请求配置对象
|
|
37
|
+
*/
|
|
38
|
+
export function removePendingRequest (config) {
|
|
39
|
+
const requestKey = generateReqKey(config)
|
|
40
|
+
pendingRequests.delete(requestKey)
|
|
41
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// request.d.ts
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
/**
|
|
5
|
+
* HTTP 请求方法类型
|
|
6
|
+
*/
|
|
7
|
+
type HttpMethod =
|
|
8
|
+
| 'GET'
|
|
9
|
+
| 'POST'
|
|
10
|
+
| 'PUT'
|
|
11
|
+
| 'DELETE'
|
|
12
|
+
| 'PATCH'
|
|
13
|
+
| 'HEAD'
|
|
14
|
+
| 'OPTIONS'
|
|
15
|
+
| 'CONNECT'
|
|
16
|
+
| 'TRACE'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 请求配置对象(Axios 风格)
|
|
20
|
+
*/
|
|
21
|
+
interface RequestConfig<T = any> {
|
|
22
|
+
/** 请求方法,默认 GET */
|
|
23
|
+
method?: HttpMethod
|
|
24
|
+
/** 请求 URL */
|
|
25
|
+
url: string
|
|
26
|
+
/** 基础 URL,会自动拼接到 url 前面 */
|
|
27
|
+
baseURL?: string
|
|
28
|
+
/** URL 查询参数(GET 请求的参数) */
|
|
29
|
+
params?: Record<string, any>
|
|
30
|
+
/** 请求体数据(POST、PUT、PATCH 等请求的参数) */
|
|
31
|
+
data?: T
|
|
32
|
+
/** 请求超时时间(毫秒),0 表示不超时 */
|
|
33
|
+
timeout?: number
|
|
34
|
+
/** 超时错误消息 */
|
|
35
|
+
timeoutErrorMessage?: string
|
|
36
|
+
/** 是否携带跨域凭证(cookies) */
|
|
37
|
+
withCredentials?: boolean
|
|
38
|
+
/** 响应数据类型 */
|
|
39
|
+
responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
|
|
40
|
+
/** 响应编码 */
|
|
41
|
+
responseEncoding?: string
|
|
42
|
+
/** 自定义请求头 */
|
|
43
|
+
headers?: Record<string, string>
|
|
44
|
+
/** HTTP 基础认证 */
|
|
45
|
+
auth?: {
|
|
46
|
+
username: string
|
|
47
|
+
password: string
|
|
48
|
+
}
|
|
49
|
+
/** 代理配置 */
|
|
50
|
+
proxy?: {
|
|
51
|
+
host: string
|
|
52
|
+
port: number
|
|
53
|
+
protocol?: string
|
|
54
|
+
auth?: string
|
|
55
|
+
}
|
|
56
|
+
/** 响应内容最大长度 */
|
|
57
|
+
maxContentLength?: number
|
|
58
|
+
/** 请求体最大长度 */
|
|
59
|
+
maxBodyLength?: number
|
|
60
|
+
/** 最大重定向次数 */
|
|
61
|
+
maxRedirects?: number
|
|
62
|
+
/** Unix Socket 路径 */
|
|
63
|
+
socketPath?: string
|
|
64
|
+
/** Node.js HTTP Agent */
|
|
65
|
+
httpAgent?: any
|
|
66
|
+
/** Node.js HTTPS Agent */
|
|
67
|
+
httpsAgent?: any
|
|
68
|
+
/** 是否自动解压响应 */
|
|
69
|
+
decompress?: boolean
|
|
70
|
+
/** 自定义状态码验证函数 */
|
|
71
|
+
validateStatus?: (status: number) => boolean
|
|
72
|
+
/** 参数序列化函数 */
|
|
73
|
+
paramsSerializer?: (params: any) => string
|
|
74
|
+
/** 请求数据转换函数 */
|
|
75
|
+
transformRequest?: ((data: any, headers: any) => any) | Array<(data: any, headers: any) => any>
|
|
76
|
+
/** 响应数据转换函数 */
|
|
77
|
+
transformResponse?: ((data: any, headers: any, status: number) => any) | Array<(data: any, headers: any, status: number) => any>
|
|
78
|
+
/** 上传进度事件回调 */
|
|
79
|
+
onUploadProgress?: (progressEvent: any) => void
|
|
80
|
+
/** 下载进度事件回调 */
|
|
81
|
+
onDownloadProgress?: (progressEvent: any) => void
|
|
82
|
+
/** 取消令牌 */
|
|
83
|
+
cancelToken?: any
|
|
84
|
+
/** CSRF token cookie 名称 */
|
|
85
|
+
xsrfCookieName?: string
|
|
86
|
+
/** CSRF token header 名称 */
|
|
87
|
+
xsrfHeaderName?: string
|
|
88
|
+
/** 请求适配器 */
|
|
89
|
+
adapter?: (config: RequestConfig) => any
|
|
90
|
+
/** AbortSignal(用于取消请求) */
|
|
91
|
+
signal?: AbortSignal
|
|
92
|
+
/** 是否使用不安全的 HTTP 解析器 */
|
|
93
|
+
insecureHTTPParser?: boolean
|
|
94
|
+
/** 过渡性配置 */
|
|
95
|
+
transitional?: {
|
|
96
|
+
/** 是否静默 JSON 解析错误 */
|
|
97
|
+
silentJSONParsing?: boolean
|
|
98
|
+
/** 是否强制 JSON 解析 */
|
|
99
|
+
forcedJSONParsing?: boolean
|
|
100
|
+
/** 是否澄清超时错误 */
|
|
101
|
+
clarifyTimeoutError?: boolean
|
|
102
|
+
}
|
|
103
|
+
/** 代理连接回调 */
|
|
104
|
+
onProxyConnect?: (proxyReq: any, req: any, res: any) => void
|
|
105
|
+
/** 代理错误回调 */
|
|
106
|
+
onProxyError?: (err: Error, req: any, res: any) => void
|
|
107
|
+
/** 代理响应回调 */
|
|
108
|
+
onProxyResponse?: (proxyRes: any, req: any, res: any) => void
|
|
109
|
+
/** 方法名称(用于拦截器) */
|
|
110
|
+
methodName?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Axios 响应对象
|
|
115
|
+
*/
|
|
116
|
+
interface AxiosResponse<T = any> {
|
|
117
|
+
/** 响应数据 */
|
|
118
|
+
data: T
|
|
119
|
+
/** HTTP 状态码 */
|
|
120
|
+
status: number
|
|
121
|
+
/** HTTP 状态文本 */
|
|
122
|
+
statusText: string
|
|
123
|
+
/** 响应头 */
|
|
124
|
+
headers: Record<string, string>
|
|
125
|
+
/** 请求配置 */
|
|
126
|
+
config: RequestConfig<T>
|
|
127
|
+
/** 原始请求对象 */
|
|
128
|
+
request?: XMLHttpRequest
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Axios 错误对象
|
|
133
|
+
*/
|
|
134
|
+
interface AxiosError<T = any> extends Error {
|
|
135
|
+
/** 错误代码 */
|
|
136
|
+
code?: string
|
|
137
|
+
/** 响应对象(如果收到响应) */
|
|
138
|
+
response?: AxiosResponse<T>
|
|
139
|
+
/** 请求配置 */
|
|
140
|
+
config?: RequestConfig<T>
|
|
141
|
+
/** 原始请求对象 */
|
|
142
|
+
request?: XMLHttpRequest
|
|
143
|
+
/** 是否为 Axios 错误 */
|
|
144
|
+
isAxiosError: boolean
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export { }
|
|
148
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// request.d.ts
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
/**
|
|
5
|
+
* HTTP 请求方法类型
|
|
6
|
+
*/
|
|
7
|
+
type HttpMethod =
|
|
8
|
+
| 'GET'
|
|
9
|
+
| 'POST'
|
|
10
|
+
| 'PUT'
|
|
11
|
+
| 'DELETE'
|
|
12
|
+
| 'PATCH'
|
|
13
|
+
| 'HEAD'
|
|
14
|
+
| 'OPTIONS'
|
|
15
|
+
| 'CONNECT'
|
|
16
|
+
| 'TRACE'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 请求配置对象(Axios 风格)
|
|
20
|
+
*/
|
|
21
|
+
interface RequestConfig<T = any> {
|
|
22
|
+
/** 请求方法,默认 GET */
|
|
23
|
+
method?: HttpMethod
|
|
24
|
+
/** 请求 URL */
|
|
25
|
+
url: string
|
|
26
|
+
/** 基础 URL,会自动拼接到 url 前面 */
|
|
27
|
+
baseURL?: string
|
|
28
|
+
/** URL 查询参数(GET 请求的参数) */
|
|
29
|
+
params?: Record<string, any>
|
|
30
|
+
/** 请求体数据(POST、PUT、PATCH 等请求的参数) */
|
|
31
|
+
data?: T
|
|
32
|
+
/** 请求超时时间(毫秒),0 表示不超时 */
|
|
33
|
+
timeout?: number
|
|
34
|
+
/** 超时错误消息 */
|
|
35
|
+
timeoutErrorMessage?: string
|
|
36
|
+
/** 是否携带跨域凭证(cookies) */
|
|
37
|
+
withCredentials?: boolean
|
|
38
|
+
/** 响应数据类型 */
|
|
39
|
+
responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
|
|
40
|
+
/** 响应编码 */
|
|
41
|
+
responseEncoding?: string
|
|
42
|
+
/** 自定义请求头 */
|
|
43
|
+
headers?: Record<string, string>
|
|
44
|
+
/** HTTP 基础认证 */
|
|
45
|
+
auth?: {
|
|
46
|
+
username: string
|
|
47
|
+
password: string
|
|
48
|
+
}
|
|
49
|
+
/** 代理配置 */
|
|
50
|
+
proxy?: {
|
|
51
|
+
host: string
|
|
52
|
+
port: number
|
|
53
|
+
protocol?: string
|
|
54
|
+
auth?: string
|
|
55
|
+
}
|
|
56
|
+
/** 响应内容最大长度 */
|
|
57
|
+
maxContentLength?: number
|
|
58
|
+
/** 请求体最大长度 */
|
|
59
|
+
maxBodyLength?: number
|
|
60
|
+
/** 最大重定向次数 */
|
|
61
|
+
maxRedirects?: number
|
|
62
|
+
/** Unix Socket 路径 */
|
|
63
|
+
socketPath?: string
|
|
64
|
+
/** Node.js HTTP Agent */
|
|
65
|
+
httpAgent?: any
|
|
66
|
+
/** Node.js HTTPS Agent */
|
|
67
|
+
httpsAgent?: any
|
|
68
|
+
/** 是否自动解压响应 */
|
|
69
|
+
decompress?: boolean
|
|
70
|
+
/** 自定义状态码验证函数 */
|
|
71
|
+
validateStatus?: (status: number) => boolean
|
|
72
|
+
/** 参数序列化函数 */
|
|
73
|
+
paramsSerializer?: (params: any) => string
|
|
74
|
+
/** 请求数据转换函数 */
|
|
75
|
+
transformRequest?: ((data: any, headers: any) => any) | Array<(data: any, headers: any) => any>
|
|
76
|
+
/** 响应数据转换函数 */
|
|
77
|
+
transformResponse?: ((data: any, headers: any, status: number) => any) | Array<(data: any, headers: any, status: number) => any>
|
|
78
|
+
/** 上传进度事件回调 */
|
|
79
|
+
onUploadProgress?: (progressEvent: any) => void
|
|
80
|
+
/** 下载进度事件回调 */
|
|
81
|
+
onDownloadProgress?: (progressEvent: any) => void
|
|
82
|
+
/** 取消令牌 */
|
|
83
|
+
cancelToken?: any
|
|
84
|
+
/** CSRF token cookie 名称 */
|
|
85
|
+
xsrfCookieName?: string
|
|
86
|
+
/** CSRF token header 名称 */
|
|
87
|
+
xsrfHeaderName?: string
|
|
88
|
+
/** 请求适配器 */
|
|
89
|
+
adapter?: (config: RequestConfig) => any
|
|
90
|
+
/** AbortSignal(用于取消请求) */
|
|
91
|
+
signal?: AbortSignal
|
|
92
|
+
/** 是否使用不安全的 HTTP 解析器 */
|
|
93
|
+
insecureHTTPParser?: boolean
|
|
94
|
+
/** 过渡性配置 */
|
|
95
|
+
transitional?: {
|
|
96
|
+
/** 是否静默 JSON 解析错误 */
|
|
97
|
+
silentJSONParsing?: boolean
|
|
98
|
+
/** 是否强制 JSON 解析 */
|
|
99
|
+
forcedJSONParsing?: boolean
|
|
100
|
+
/** 是否澄清超时错误 */
|
|
101
|
+
clarifyTimeoutError?: boolean
|
|
102
|
+
}
|
|
103
|
+
/** 代理连接回调 */
|
|
104
|
+
onProxyConnect?: (proxyReq: any, req: any, res: any) => void
|
|
105
|
+
/** 代理错误回调 */
|
|
106
|
+
onProxyError?: (err: Error, req: any, res: any) => void
|
|
107
|
+
/** 代理响应回调 */
|
|
108
|
+
onProxyResponse?: (proxyRes: any, req: any, res: any) => void
|
|
109
|
+
/** 方法名称(用于拦截器) */
|
|
110
|
+
methodName?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Axios 响应对象
|
|
115
|
+
*/
|
|
116
|
+
interface AxiosResponse<T = any> {
|
|
117
|
+
/** 响应数据 */
|
|
118
|
+
data: T
|
|
119
|
+
/** HTTP 状态码 */
|
|
120
|
+
status: number
|
|
121
|
+
/** HTTP 状态文本 */
|
|
122
|
+
statusText: string
|
|
123
|
+
/** 响应头 */
|
|
124
|
+
headers: Record<string, string>
|
|
125
|
+
/** 请求配置 */
|
|
126
|
+
config: RequestConfig<T>
|
|
127
|
+
/** 原始请求对象 */
|
|
128
|
+
request?: XMLHttpRequest
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Axios 错误对象
|
|
133
|
+
*/
|
|
134
|
+
interface AxiosError<T = any> extends Error {
|
|
135
|
+
/** 错误代码 */
|
|
136
|
+
code?: string
|
|
137
|
+
/** 响应对象(如果收到响应) */
|
|
138
|
+
response?: AxiosResponse<T>
|
|
139
|
+
/** 请求配置 */
|
|
140
|
+
config?: RequestConfig<T>
|
|
141
|
+
/** 原始请求对象 */
|
|
142
|
+
request?: XMLHttpRequest
|
|
143
|
+
/** 是否为 Axios 错误 */
|
|
144
|
+
isAxiosError: boolean
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export { }
|
|
148
|
+
|