和服务端交互
整理了大概学习的几大问题:
- 基于axios的二次封装
- ts: 关于接口的类型问题
- 多环境baseURL
- 跨域处理
- 数据mock
基于axios的二次封装
基于axios封装请求模块
- 安装axios:
npm install axios
- 封装模块
在utils里创建request.ts,封装模板
import axios from 'axios'
// 创建一个实例,操作实例,不会影响axios本身
const request = axios.create({
baseURL: 'https://shop.fed.lagou.com/api/admin'
})
// 请求拦截器interceptor
request.interceptors.request.use(function (config) {
// 统一设置用户身份 token
return config
}, function (error) {
// Do something with request error
return Promise.reject(error)
})
// // 响应拦截器 处理服务端异常
request.interceptors.response.use(function (response) {
// 统一处理接口响应错误 比如 token过期无效、服务端异常等
return response
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error)
})
export default request
请求拦截器和响应拦截器在axios的github说明中的interceptors里就有。
使用思路:
- 把所有的接口封装成请求方法,然后放到模块里,放置在api的目录下进行组织。
- 比如创建一个用户相关的接口,
user.ts
/**
* 用户相关请求模块
*/
比如要登录,创建一个common.ts
公共基础接口封装。
/**
* 公共基础接口封装
*/
import request from '@/untils/request'
export const getLoginInfo = () => {
return request({
method: 'GET',
url: '/login/info' // 接口路径
})
// 第二种写法:调用request的API,get: return request.get('/login/info')
}
这就封装好了,然后找到login.vue
<script lang="ts" setup>
// 想在挂载完毕后,调用接口
import { getLoginInfo } from '@/api/common'
import { onMounted } from 'vue'
onMounted(() => {
getLoginInfo().then(res => { // res:axios 包装的响应对象,data、status
// res.data: 后端真实返回的数据 类型为any 因为我们不知道是什么类型 无法判断
console.log(res.data)
})
})
</script>
但是我们怎么通过TS来指定返回的类型呢?
解决方案
在common.ts
里调用第二种方法,调用request的API,通过API源码的泛型来指定。
export const getLoginInfo = () => {
return request.get<{
status: number
msg: string
data: {
logo_square: string
}
}>('/login/info')
})
接口文件里存在大量与驼峰命名相冲突的问题,因此我们需要把eslint对于接口文件的规则关闭。不然会一直报错。
module.export = {
globals: {
...
},
....
overrides: [
{
files: ['src/api/**/*.ts'],
rules: {
camelcase: 'off'
}
}
]
}
完事了之后,继续看common.ts
但是如果每一个接口都定义这么多,而且都是一样的status, msg 以及不同的data,会不会太麻烦了呢?
写一个接口来装
interface ResponseData<T = any> {
status: number
msg: string
data:T
}
export const getLoginInfo = () => {
return request.get< ResponseData<{
logo_square: string
}>>('/login/info')
})
但是,我们真正要使用的是数据,而这样的话,我们只能通过res.data.data.logo_square
才能拿到数据,我们能不能直接res.data.logo_square
那么我们需要再一次改进common.ts
export const getLoginInfo = () => {
return request.get< ResponseData<{
logo_square: string
}>>('/login/info').then( res => return res.data)
})
这样就可以了
但是但是,这样是否还是太麻烦了,操作了这么多
最后优化方案
能不能最后在ts里一调,就能拿到结果,不用每次都在接口文件里写xx.then(xxx)这么麻烦呢?
能不能在源头定义呢?
改axios的get方法,或者直接在拦截器里拦截一下。但是我发现可以在request.ts
里,自己封装请求方法。
- 在
request.ts
, 不再导出request了,而是导出函数 并且定义类型
export default <T = any>(config: AxiosRequestConfig) => {
return request(config).then(res => {
return res.data.data as T
})
}
- 由于变成了request函数,并没有get方法,因此在
common.ts
里需要改。又回到了最开始的调用方式
export const getLoginInfo = () => {
return request<{
logo_square: string
logo_rectangle: string
login_logo: string
slide: string[]
}>({
method: 'GET',
url: '/login/info' // 接口路径
})
调用request请求方法,通过泛型指定后端返回的数据类型:<{logo…: string}> 然后通过选项式的参数,来指定请求相关的信息。method: ‘GET’, url: ‘xxx’
完事!
接口类型模块
关于以上的接口,仍然存在着问题,若我们需要处理接口的数据,类型成了很大的问题。
比如,当我们想要用一个list来保存data.slide 但是slide是有类型的,string,list是没类型的,无法相等,每次这样的话得重复写类型,是否太麻烦
<script>
import { ref } from 'vue'
const list = ref([])
onMounted(() => {
getLoginInfo().then(data => {
list.value = data.slide
// 这时就会报错,list是never类型,而slide是string
})
})
</script>
类型模块的方法。
将类型存放到接口里导出。想用的时候直接导入即可
// common.ts
export interface LoginInfo {
logo_square: string
logo_rectangle: string
login_logo: string
slide: string[]
}
export const getLoginInfo = () => {
return request<LoginInfo>(...)
}
<!-- index.vue-->
<script>
import type { LoginInfo } from ''
const list = ref<LoginInfo['slide']>([])
onMounted(() => {
getLoginInfo().then(data =>{
list.value = data.slide
})
})
</script>
然而其实这种方法,在common.ts
里不易于维护,可以优化。
在api的文件夹里再创建一个types
文件夹用来存放类型模块,会更易于使用和维护。创建一个common.ts
来存放common的类型。
axios封装总结
这就算基本结束了!
整理一下接口的思路:
utils
在utils
里封装了请求模块,然后请求拦截器(统一设置用户身份啥的 token),响应拦截器(统一处理接口响应错误,比如token过期无效、服务端异常等)
原来是将request导出,而由于request不支持泛型,没法告诉他后端响应什么类型的数据,而request.get、post、put 支持响应数据类型
但是,我们把情况想成比较麻烦的,由于我们的后端可能又会包装一层数据 data,导致我们访问数据比较麻烦。所以,我自己封装了一个request(如果没有又包装一层,就能直接用request.get/post/put即可)。
最后自己封装的
export default <T = any>(config: AxiosRequestConfig) => {
return request(config).then(res => {
return res.data.data as T
})
}
导出一个函数,函数接收泛型,类型为any,config配置对象,他的类型也是原本就配置好的,我们给他复制上。然后传进的参数,then之后,把结果直接拿出来,res.data.data就进到最深,直接能拿到数据,然后转换成泛型T
api接口文件
最终我们用的时候,就在api里,比如common.ts
,引入刚刚的request
// common,ts
import request from '@/untils/request'
import type { LoginInfo } from './types/common'
export const getLoginInfo = () => {
return request<LoginInfo>({
method: 'GET',
url: '/login/info' // 接口路径
})
types类型模块
而为了参数类型好维护,并且好通用,还易于重用。于是把数据的类型单独放在一个文件里,在api里创建一个types,为每个接口创建一个类型模块
// common.ts
export interface LoginInfo{
logo_square: string
logo_rectangle: string
login_logo: string
slide: string[]
}
login组件使用
最后,完成了接口的所有配置之类的,在login组件里正式调用它们。
<script lang="ts" setup>
// 想在挂载完毕后,调用接口
import { getLoginInfo } from '@/api/common'
import type { LoginInfo } from '@/api/types/common'
import { onMounted, ref } from 'vue'
// 创建一个数组list来存放接口数据的slide。LoginInfo记录了接口数据的所有类型,让list的类型与slide类型一致。
const list = ref<LoginInfo['slide']>([])
// 想在挂载完毕后调用,于是用onMounted()
onMounted(() => {
getLoginInfo().then(data => {
// res:axios 包装的响应对象,data、status
// res.data: 后端真实返回的数据 类型为any 因为我们不知道是什么类型 无法判断
list.value = data.slide
console.log(list.value)
})
})
</script>
整体思路就是 在组件里调用getLoginInfo => getLoginInfo函数导入request, 调用request,通过url使用get去请求数据,而我选择自己封装request => request: 导入axios,用request来存放axios,然后对request来进行操作,不会影响到axios,请求拦截器、响应拦截器,最后导出request供别人使用。
环境变量
由于项目需要通过不同的路径去测试,可能在本地是localhost: 8080,也可能上线后变成xxx.com,也可能测试时 test.com
而通过request.ts修改baseURL
,太原始了。
而Vite也提供了环境变量的用法。在一个特殊的import.meta.env
对象上暴露环境变量。并且提供了内置的环境变量:
**import.meta.env.MODE**
: {string} 是一个字符串 获取当前应用运行的模式。比如开发模式development,还是生产构建模式production。**import.meta.env.BASE_URL**
: {string} 部署应用时的基本 URL。他由[base](https://cn.vitejs.dev/config/shared-options.html#base)
配置项决定。与接口的基础路径不是一回事!运行在网站的根目录,子目录一般是/a**import.meta.env.PROD**
: {boolean} 应用是否运行在生产环境。**import.meta.env.DEV**
: {boolean} 应用是否运行在开发环境 (永远与import.meta.env.PROD
相反)。**import.meta.env.SSR**
: {boolean} 应用是否运行在 server 上。
.env文件
Vite 使用 dotenv 从你的 环境目录 中的下列文件加载额外的环境变量:一共四个文件。
.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.[mode] # 只在指定模式下加载
.env.[mode].local # 只在指定模式下加载,但会被 git 忽略
开发模式vs生产模式
什么是开发模式?
当我们npm run dev/serve的时候
什么是生产构建模式?
当我们npm run build的时候,就是生产构建模式。
我们分别在根目录创建这四个文件:.env
, .env.local
, .env.development
, .env.production
规则:
为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_
为前缀的变量才会暴露给经过 vite 处理的代码。例如下面这些环境变量:
VITE_SOME_KEY=123
DB_PASSWORD=foobar
只有 VITE_SOME_KEY
会被暴露为 import.meta.env.VITE_SOME_KEY
提供给客户端源码,而 DB_PASSWORD
则不会。
console.log(import.meta.env.VITE_SOME_KEY) // 123
console.log(import.meta.env.DB_PASSWORD) // undefined
配置接口路径就能:
# .env.development
VITE_API_BASEURL=https://shop.fed.lagounews.com/api/admin
由于是全局的,能直接导入
// request.ts
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASEURL // 显示undefined,因为没有类型。对于自定义的环境变量,需要手动进行类型补充。
})
如何手动进行类型补充?
需要在src
目录下创建一共env.d.ts
文件,按着下面这样增加ImportMetaEnv
的定义:
interface ImportMetaEnv {
readonly VITE_API_BASEURL: string
// 更多环境变量...
}
环境变量总结
模式
默认情况下,开发服务器 (npm run dev
命令) 运行在 development
(开发) 模式,而 build
命令则运行在 production
(生产) 模式。
这意味着当执行 vite build
时,它会自动加载 .env.production
中可能存在的环境变量:
# .env.production
VITE_APP_TITLE=My App
在你的应用(组件)中,你可以使用 import.meta.env.VITE_APP_TITLE
渲染标题。
而我们在项目中则是把接口路径导入进来,在开发模式里,把接口路径写进去,需要的时候就导出import.meta.env.VITE_API_BASEURL
自定义模式
然而,重要的是理解 模式 是一共更广泛的概念,还能自定义模式,比如预发布/预上线模式”staging”
可以通过传递--mode
选项标志来覆盖命令使用的默认模式。
vite build --mode staging
为了使应用实现预期行为,我们还需要一个 .env.staging
文件:
# .env.staging
NODE_ENV=production
VITE_APP_TITLE=My App (staging)
现在,你的 staging 应用应该具有类似于生产的行为,但显示的标题与生产环境不同。
跨域
针对跨域,解决方案有9-10种,然而,数量不如质量,就找到两个主流的解决方案仔细掌握!
开发环境:
在服务端配置CORS。
配置开发 服务器代理,比如vite-server.proxy
生产环境:
在服务端配置CORS。
配置生产 服务器代理,比如nginx
CORS
CORS全称为Cross Origin Resource Sharing(跨域资源共享)。这种方案对于前端来说没什么工作量,和正常发送请求写法上没有任何区别,工作量基本都在后端(其实也没啥工作量,就是配置一些HTTP协议)
服务端配置CORS基本都是通过配置access-control-allow
之类的
服务器代理
可能有些后端开发人员觉得配置CORS麻烦,不想搞,那纯前端也是有解决方案的。
在开发模式下跨域使用开发服务器的proxy功能,比如vite-server.proxy.
但在生产环境下是不能使用的。生产环境需要配置生产服务器(比如nginx, Apache等)进行反向代理。在本地服务和生产服务配置代理的原理是一样的,通过搭建一个中转服务器来转发请求规避跨域的问题。
服务器不仅能接收处理请求,还能发送请求。而服务器请求是不存在跨域问题的。
流程:如果配置了CORS:页面 => 接口服务。 页面直接发送请求到接口服务。接口服务 => 页面
配置了开发/生产服务器:页面 => 开发/生产服务器 => 接口服务
配置服务器代理
参照:https://cn.vitejs.dev/config/server-options.html#server-proxy
export default defineConfig({
server: {
proxy: {
// 字符串简写写法
'/foo': 'http://localhost:4567',
// 选项写法
'/api': {
target: 'http://jsonplaceholder.typicode.com', // 代理的目标地址
// 兼容基于名字的虚拟主机
// 有时候一个服务器里有多个网站,多个网站有多个域名, 域名和网站是通过虚拟主机的方式进行配置
// 配置好当a.com 映射到 localhost:xxxx本地端口服务。怎么拿到a.com的呢?通过HTTP请求头部的origin字段。而我们在开发或生产模式,默认的origin是真实的origin,是localhost:xxx。那么无法通过代理服务。而将changeOrigin设置为true,代理服务会将默认的origin修改为代理的目标地址
changeOrigin: true,
// 路径重写
// 如果默认http://jsonplaceholder.typicode.com/api/xxx是我们想要的路径,那没问题,不需要重写。因为上面配置了/api。但是不一定是api,配置名自己喜欢咋写咋写
// 如果我们想要的是http://jsonplaceholder.typicode.com/xxx,那么就需要重写,通过正则表达式,将/api设置为‘’
rewrite: (path) => path.replace(/^\/api/, '')
}
}
})
/api
,也就是设置.../api
就会被代理到目标地址。比如localhost:3000/api
就会变成www.xxxx.com/api/xxx
于是我对项目进行实操:
在vite.config.ts
进行配置
server: {
proxy: {
'/admin': {
target: 'https://shop.fed.lagounews.com/api', // 代理的目标地址
changeOrigin: true
}
}
}
然后把基础路径删了
// request.ts
const request = axios.create({
// baseURL: import.meta.env.VITE_API_BASEURL // 我直接删了,于是就成了基础路径localhost:3000
})
然后在common.ts
里修改,把url改成admin前缀的
export const getLoginInfo = () => {
return request<LoginInfo>({
method: 'GET',
url: 'admin/login/info' // 接口路径
})
结果真的成功了!200状态码!http://xxxxxxx/admin就会变成我们设置的目标路径,后面拼接`/login/info`