和服务端交互

整理了大概学习的几大问题:

  • 基于axios的二次封装
  • ts: 关于接口的类型问题
  • 多环境baseURL
  • 跨域处理
  • 数据mock

基于axios的二次封装

基于axios封装请求模块

  1. 安装axios:
  1. npm install axios
  1. 封装模块

在utils里创建request.ts,封装模板

  1. import axios from 'axios'
  2. // 创建一个实例,操作实例,不会影响axios本身
  3. const request = axios.create({
  4. baseURL: 'https://shop.fed.lagou.com/api/admin'
  5. })
  6. // 请求拦截器interceptor
  7. request.interceptors.request.use(function (config) {
  8. // 统一设置用户身份 token
  9. return config
  10. }, function (error) {
  11. // Do something with request error
  12. return Promise.reject(error)
  13. })
  14. // // 响应拦截器 处理服务端异常
  15. request.interceptors.response.use(function (response) {
  16. // 统一处理接口响应错误 比如 token过期无效、服务端异常等
  17. return response
  18. }, function (error) {
  19. // Any status codes that falls outside the range of 2xx cause this function to trigger
  20. // Do something with response error
  21. return Promise.reject(error)
  22. })
  23. export default request

请求拦截器和响应拦截器在axios的github说明中的interceptors里就有。

使用思路:

  • 把所有的接口封装成请求方法,然后放到模块里,放置在api的目录下进行组织。
  • 比如创建一个用户相关的接口,user.ts
  1. /**
  2. * 用户相关请求模块
  3. */

比如要登录,创建一个common.ts公共基础接口封装。

  1. /**
  2. * 公共基础接口封装
  3. */
  4. import request from '@/untils/request'
  5. export const getLoginInfo = () => {
  6. return request({
  7. method: 'GET',
  8. url: '/login/info' // 接口路径
  9. })
  10. // 第二种写法:调用request的API,get: return request.get('/login/info')
  11. }

这就封装好了,然后找到login.vue

  1. <script lang="ts" setup>
  2. // 想在挂载完毕后,调用接口
  3. import { getLoginInfo } from '@/api/common'
  4. import { onMounted } from 'vue'
  5. onMounted(() => {
  6. getLoginInfo().then(res => { // res:axios 包装的响应对象,data、status
  7. // res.data: 后端真实返回的数据 类型为any 因为我们不知道是什么类型 无法判断
  8. console.log(res.data)
  9. })
  10. })
  11. </script>

但是我们怎么通过TS来指定返回的类型呢?

解决方案

common.ts里调用第二种方法,调用request的API,通过API源码的泛型来指定。

  1. export const getLoginInfo = () => {
  2. return request.get<{
  3. status: number
  4. msg: string
  5. data: {
  6. logo_square: string
  7. }
  8. }>('/login/info')
  9. })

接口文件里存在大量与驼峰命名相冲突的问题,因此我们需要把eslint对于接口文件的规则关闭。不然会一直报错。

  1. module.export = {
  2. globals: {
  3. ...
  4. },
  5. ....
  6. overrides: [
  7. {
  8. files: ['src/api/**/*.ts'],
  9. rules: {
  10. camelcase: 'off'
  11. }
  12. }
  13. ]
  14. }

完事了之后,继续看common.ts

但是如果每一个接口都定义这么多,而且都是一样的status, msg 以及不同的data,会不会太麻烦了呢?

写一个接口来装

  1. interface ResponseData<T = any> {
  2. status: number
  3. msg: string
  4. dataT
  5. }
  6. export const getLoginInfo = () => {
  7. return request.get< ResponseData<{
  8. logo_square: string
  9. }>>('/login/info')
  10. })

但是,我们真正要使用的是数据,而这样的话,我们只能通过res.data.data.logo_square才能拿到数据,我们能不能直接res.data.logo_square

那么我们需要再一次改进common.ts

  1. export const getLoginInfo = () => {
  2. return request.get< ResponseData<{
  3. logo_square: string
  4. }>>('/login/info').then( res => return res.data)
  5. })

这样就可以了

但是但是,这样是否还是太麻烦了,操作了这么多

最后优化方案

能不能最后在ts里一调,就能拿到结果,不用每次都在接口文件里写xx.then(xxx)这么麻烦呢?

能不能在源头定义呢?

改axios的get方法,或者直接在拦截器里拦截一下。但是我发现可以在request.ts里,自己封装请求方法。

  1. request.ts, 不再导出request了,而是导出函数 并且定义类型
  1. export default <T = any>(config: AxiosRequestConfig) => {
  2. return request(config).then(res => {
  3. return res.data.data as T
  4. })
  5. }
  1. 由于变成了request函数,并没有get方法,因此在common.ts里需要改。又回到了最开始的调用方式
  1. export const getLoginInfo = () => {
  2. return request<{
  3. logo_square: string
  4. logo_rectangle: string
  5. login_logo: string
  6. slide: string[]
  7. }>({
  8. method: 'GET',
  9. url: '/login/info' // 接口路径
  10. })

调用request请求方法,通过泛型指定后端返回的数据类型:<{logo…: string}> 然后通过选项式的参数,来指定请求相关的信息。method: ‘GET’, url: ‘xxx’

完事!

接口类型模块

关于以上的接口,仍然存在着问题,若我们需要处理接口的数据,类型成了很大的问题。

比如,当我们想要用一个list来保存data.slide 但是slide是有类型的,string,list是没类型的,无法相等,每次这样的话得重复写类型,是否太麻烦

  1. <script>
  2. import { ref } from 'vue'
  3. const list = ref([])
  4. onMounted(() => {
  5. getLoginInfo().then(data => {
  6. list.value = data.slide
  7. // 这时就会报错,list是never类型,而slide是string
  8. })
  9. })
  10. </script>

类型模块的方法。

将类型存放到接口里导出。想用的时候直接导入即可

  1. // common.ts
  2. export interface LoginInfo {
  3. logo_square: string
  4. logo_rectangle: string
  5. login_logo: string
  6. slide: string[]
  7. }
  8. export const getLoginInfo = () => {
  9. return request<LoginInfo>(...)
  10. }
  1. <!-- index.vue-->
  2. <script>
  3. import type { LoginInfo } from ''
  4. const list = ref<LoginInfo['slide']>([])
  5. onMounted(() => {
  6. getLoginInfo().then(data =>{
  7. list.value = data.slide
  8. })
  9. })
  10. </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即可)。

最后自己封装的

  1. export default <T = any>(config: AxiosRequestConfig) => {
  2. return request(config).then(res => {
  3. return res.data.data as T
  4. })
  5. }

导出一个函数,函数接收泛型,类型为any,config配置对象,他的类型也是原本就配置好的,我们给他复制上。然后传进的参数,then之后,把结果直接拿出来,res.data.data就进到最深,直接能拿到数据,然后转换成泛型T

api接口文件

最终我们用的时候,就在api里,比如common.ts,引入刚刚的request

  1. // common,ts
  2. import request from '@/untils/request'
  3. import type { LoginInfo } from './types/common'
  4. export const getLoginInfo = () => {
  5. return request<LoginInfo>({
  6. method: 'GET',
  7. url: '/login/info' // 接口路径
  8. })

types类型模块

而为了参数类型好维护,并且好通用,还易于重用。于是把数据的类型单独放在一个文件里,在api里创建一个types,为每个接口创建一个类型模块

  1. // common.ts
  2. export interface LoginInfo{
  3. logo_square: string
  4. logo_rectangle: string
  5. login_logo: string
  6. slide: string[]
  7. }

login组件使用

最后,完成了接口的所有配置之类的,在login组件里正式调用它们。

  1. <script lang="ts" setup>
  2. // 想在挂载完毕后,调用接口
  3. import { getLoginInfo } from '@/api/common'
  4. import type { LoginInfo } from '@/api/types/common'
  5. import { onMounted, ref } from 'vue'
  6. // 创建一个数组list来存放接口数据的slide。LoginInfo记录了接口数据的所有类型,让list的类型与slide类型一致。
  7. const list = ref<LoginInfo['slide']>([])
  8. // 想在挂载完毕后调用,于是用onMounted()
  9. onMounted(() => {
  10. getLoginInfo().then(data => {
  11. // res:axios 包装的响应对象,data、status
  12. // res.data: 后端真实返回的数据 类型为any 因为我们不知道是什么类型 无法判断
  13. list.value = data.slide
  14. console.log(list.value)
  15. })
  16. })
  17. </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 从你的 环境目录 中的下列文件加载额外的环境变量:一共四个文件。

  1. .env # 所有情况下都会加载
  2. .env.local # 所有情况下都会加载,但会被 git 忽略
  3. .env.[mode] # 只在指定模式下加载
  4. .env.[mode].local # 只在指定模式下加载,但会被 git 忽略

开发模式vs生产模式

什么是开发模式?

当我们npm run dev/serve的时候

什么是生产构建模式?

当我们npm run build的时候,就是生产构建模式。

我们分别在根目录创建这四个文件:.env, .env.local, .env.development, .env.production

规则

为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理的代码。例如下面这些环境变量:

  1. VITE_SOME_KEY=123
  2. DB_PASSWORD=foobar

只有 VITE_SOME_KEY 会被暴露为 import.meta.env.VITE_SOME_KEY 提供给客户端源码,而 DB_PASSWORD 则不会。

  1. console.log(import.meta.env.VITE_SOME_KEY) // 123
  2. console.log(import.meta.env.DB_PASSWORD) // undefined

配置接口路径就能:

  1. # .env.development
  2. VITE_API_BASEURL=https://shop.fed.lagounews.com/api/admin

由于是全局的,能直接导入

  1. // request.ts
  2. const request = axios.create({
  3. baseURL: import.meta.env.VITE_API_BASEURL // 显示undefined,因为没有类型。对于自定义的环境变量,需要手动进行类型补充。
  4. })

如何手动进行类型补充?

需要在src目录下创建一共env.d.ts文件,按着下面这样增加ImportMetaEnv的定义:

  1. interface ImportMetaEnv {
  2. readonly VITE_API_BASEURL: string
  3. // 更多环境变量...
  4. }

环境变量总结

模式

默认情况下,开发服务器 (npm run dev 命令) 运行在 development (开发) 模式,而 build 命令则运行在 production (生产) 模式。

这意味着当执行 vite build 时,它会自动加载 .env.production 中可能存在的环境变量:

  1. # .env.production
  2. VITE_APP_TITLE=My App

在你的应用(组件)中,你可以使用 import.meta.env.VITE_APP_TITLE 渲染标题。

而我们在项目中则是把接口路径导入进来,在开发模式里,把接口路径写进去,需要的时候就导出import.meta.env.VITE_API_BASEURL

自定义模式

然而,重要的是理解 模式 是一共更广泛的概念,还能自定义模式,比如预发布/预上线模式”staging”

可以通过传递--mode选项标志来覆盖命令使用的默认模式。

  1. vite build --mode staging

为了使应用实现预期行为,我们还需要一个 .env.staging 文件:

  1. # .env.staging
  2. NODE_ENV=production
  3. 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:页面 => 接口服务。 页面直接发送请求到接口服务。接口服务 => 页面

配置了开发/生产服务器:页面 => 开发/生产服务器 => 接口服务
image.png

配置服务器代理

参照:https://cn.vitejs.dev/config/server-options.html#server-proxy

  1. export default defineConfig({
  2. server: {
  3. proxy: {
  4. // 字符串简写写法
  5. '/foo': 'http://localhost:4567',
  6. // 选项写法
  7. '/api': {
  8. target: 'http://jsonplaceholder.typicode.com', // 代理的目标地址
  9. // 兼容基于名字的虚拟主机
  10. // 有时候一个服务器里有多个网站,多个网站有多个域名, 域名和网站是通过虚拟主机的方式进行配置
  11. // 配置好当a.com 映射到 localhost:xxxx本地端口服务。怎么拿到a.com的呢?通过HTTP请求头部的origin字段。而我们在开发或生产模式,默认的origin是真实的origin,是localhost:xxx。那么无法通过代理服务。而将changeOrigin设置为true,代理服务会将默认的origin修改为代理的目标地址
  12. changeOrigin: true,
  13. // 路径重写
  14. // 如果默认http://jsonplaceholder.typicode.com/api/xxx是我们想要的路径,那没问题,不需要重写。因为上面配置了/api。但是不一定是api,配置名自己喜欢咋写咋写
  15. // 如果我们想要的是http://jsonplaceholder.typicode.com/xxx,那么就需要重写,通过正则表达式,将/api设置为‘’
  16. rewrite: (path) => path.replace(/^\/api/, '')
  17. }
  18. }
  19. })

/api,也就是设置.../api就会被代理到目标地址。比如localhost:3000/api就会变成www.xxxx.com/api/xxx

于是我对项目进行实操:

vite.config.ts进行配置

  1. server: {
  2. proxy: {
  3. '/admin': {
  4. target: 'https://shop.fed.lagounews.com/api', // 代理的目标地址
  5. changeOrigin: true
  6. }
  7. }
  8. }

然后把基础路径删了

  1. // request.ts
  2. const request = axios.create({
  3. // baseURL: import.meta.env.VITE_API_BASEURL // 我直接删了,于是就成了基础路径localhost:3000
  4. })

然后在common.ts里修改,把url改成admin前缀的

  1. export const getLoginInfo = () => {
  2. return request<LoginInfo>({
  3. method: 'GET',
  4. url: 'admin/login/info' // 接口路径
  5. })

结果真的成功了!200状态码!http://xxxxxxx/admin就会变成我们设置的目标路径,后面拼接`/login/info`

image.png