和服务端交互
整理了大概学习的几大问题:
- 基于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'})// 请求拦截器interceptorrequest.interceptors.request.use(function (config) {// 统一设置用户身份 tokenreturn config}, function (error) {// Do something with request errorreturn 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 errorreturn 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: numbermsg: stringdata: {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: numbermsg: stringdata: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: stringlogo_rectangle: stringlogin_logo: stringslide: 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.tsexport interface LoginInfo {logo_square: stringlogo_rectangle: stringlogin_logo: stringslide: 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,tsimport 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.tsexport interface LoginInfo{logo_square: stringlogo_rectangle: stringlogin_logo: stringslide: 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.slideconsole.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=123DB_PASSWORD=foobar
只有 VITE_SOME_KEY 会被暴露为 import.meta.env.VITE_SOME_KEY 提供给客户端源码,而 DB_PASSWORD 则不会。
console.log(import.meta.env.VITE_SOME_KEY) // 123console.log(import.meta.env.DB_PASSWORD) // undefined
配置接口路径就能:
# .env.developmentVITE_API_BASEURL=https://shop.fed.lagounews.com/api/admin
由于是全局的,能直接导入
// request.tsconst 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.productionVITE_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.stagingNODE_ENV=productionVITE_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.tsconst 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`

