Vue3+Vite+Ts 项目实战 03 初始化 Element Plus、API 和组件自动导入、ElMessage 组件使用、Vue 全局属性、Axios、环境变量、跨域

初始化 Element Plus

Element Plus 是针对 Vue 3 的 Element UI 升级版。

安装和配置自动按需导入

# 安装
npm install element-plus --save

建议使用按需导入,官方推荐使用 unplugin-vue-componentsunplugin-auto-import这两款插件实现自动导入,来弥补按需导入的一些缺点(手动注册组件等)。

# 安装插件
npm install -D unplugin-vue-components unplugin-auto-import

配置 Vite:

// vite.config.ts
...
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {
    
     ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
    
    
  plugins: [
    ...
    // ElementPlus 自动导入
    AutoImport({
    
    
      resolvers: [ElementPlusResolver()]
    }),
    Components({
    
    
      resolvers: [ElementPlusResolver()]
    })
  ],
  ...
})

测试:

<el-button type="primary">Primary</el-button>

国际化

Element Plus 组件默认使用英语,例如日期组件:

在这里插入图片描述

要想使用其他语言(如中文)需要全局配置国际化。

完整导入方式可以在注册的时候通过 locale 选项配置。

按需导入需要使用官方提供的 Vue 组件进行配置:

<!-- src\App.vue -->
<template>
  <el-config-provider :locale="locale">
    <router-view />
  </el-config-provider>
</template>

<script setup lang="ts">
import locale from 'element-plus/lib/locale/lang/zh-cn'
</script>

效果(需要刷新页面使配置生效):

在这里插入图片描述

图标

如果想要像官方案例一样在项目中直接使用 Element Plus 的图标,需要全局注册组件,官方自动导入的插件还在开发中,当前手动全局注册:

// src\plugins\element-plus.ts
import {
    
     App } from 'vue'
import * as ElIconModules from '@element-plus/icons-vue'

export default {
    
    
  install(app: App) {
    
    
    // 批量注册 Element Plus 图标组件
    // 或者自定义 ElIconModules 列表
    for (const iconName in ElIconModules) {
    
    
      app.component(iconName, (ElIconModules as any)[iconName])
    }
  }
}

// src\main.ts
import {
    
     createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import elementPlus from './plugins/element-plus'

// 加载全局样式
import './styles/index.scss'

createApp(App)
  .use(router)
  .use(store)
  .use(elementPlus)
  .mount('#app')

API 和 组件自动导入

Element Plus 实现自动导入安装用的插件不仅仅用于它自己,实际上这两个插件是可以应用到各种框架和库中。

unplugin-vue-components

unplugin-vue-components 插件用于自动识别 Vue 模板中使用的组件,自动按需导入和注册。

它提供了多个可配置的 UI 库的解析器,查看 《Importing from UI Libraries》 部分。

在 TypeScript 项目中会生成 components.d.ts 文件,自动补充更新组件的类型声明文件。

**注意:**插件自动识别的是 template 模板中使用的组件,可以查看 components.d.ts 确认是否已被识别。

unplugin-auto-import

unplugin-auto-import 插件可以在 Vite、Webpack、Rollup 和 esbuild 环境下自动按需导入配置库的常用 API,如 Vue 的 ref,不需要手动 import

可以通过 imports 配置项配置自动导入的 API(预设或自定义规则),也可以通过 resolvers 配置组件库的解析器(如 Element Plus)。

支持 TypeScript 的项目中,插件安装后会在项目根目录生成 auto-imports.d.ts 文件,当配置自动导入时,会自动补充配置库的 API 对应的类型声明。

注意:auto-imports.d.ts 文件默认会被 ESLint 校验,报错 <变量> is defined but never used.,可以忽略 ESLint 对该文件的校验。

配置API 自动导入

配置 Vue 、Vue Router 和 Pinia 的 API 自动导入:

// vite.config.ts
...
import AutoImport from 'unplugin-auto-import/vite'
...

// https://vitejs.dev/config/
export default defineConfig({
    
    
  plugins: [
    ...
    AutoImport({
    
    
      imports: [
        // presets
        'vue',
        'vue-router',
        'pinia'
      ],
      eslintrc: {
    
    
        enabled: true,
        filepath: './.eslintrc-auto-import.json',
        globalsPropValue: true
      },
      // ElementPlus 自动导入
      resolvers: [ElementPlusResolver()]
    }),
    ...
  ],
  ...
})

保存生效后,auto-imports.d.ts 会自动填充内容,并且会在项目根目录生成 .eslintrc-auto-import.json eslint 全局变量配置。

需要手动的将这两个文件添加到 TypeScript 和 ESLint 配置文件中:

// tsconfig.json
{
    
    
  ...
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "auto-imports.d.ts"],
  "references": [{
    
     "path": "./tsconfig.node.json" }]
}

// .eslintrc.js
module.exports = {
    
    
  ...
  extends: [
    ...
    // unplugin-auto-import
    './.eslintrc-auto-import.json'
  ],
  ...
}

忽略 auto-imports.d.ts ESLint 校验。

# .eslintignore
auto-imports.d.ts

建议重启编辑器使配置生效。

现在就可以直接使用 Vue、Vue Router 和 Pinia 的 API,而不需要手动 import 了。

例如,可以注释下面几个 API 的导入:

<!-- src\layout\AppHeader\Breadcrumb.vue -->
...

<script setup lang="ts">
// import { useRouter } from 'vue-router'
// import { computed } from 'vue'

const router = useRouter()

const routes = computed(() => {
      
      
  return router.currentRoute.value.matched.filter(item => item.meta.title)
})

</script>

// src\store\index.ts
// import { defineStore } from 'pinia'

const useStore = defineStore('main', {
    
    
...
})

export default useStore

// src\main.ts
// import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// import { createPinia } from 'pinia'
import elementPlus from './plugins/element-plus'

...

注意:

  1. 不是全部 API,例如 Vue Router 的 createRouter 就不会导入。具体可以自动导入的 API 参考 unplugin-auto-import/src/presets
  2. 生成 .eslintrc-auto-import.json 文件后如不需要增加配置建议将 enabled: true 设置为 false,否则每次都会生成这个文件。

单独引用 ElementPlus 组件

自动按需引入的原理是通过识别 <template> 中使用的组件自动导入,这就导致如果使用的是 ElMessage 这类直接在 JS 中调用方法的组件,插件并不会识别并完成自动导入。

例如:

  • 在 Vue 单文件组件的 <script> 中使用
  • 在 Axios 拦截器中使用

所以这类组件需要进行手动操作:

  • 手动 import 导入组件(完整引入也需要手动 import
  • 手动导入样式文件(element-plus/theme-chalk/xxx.css

例如常用的 ElMessage 组件:

import {
    
     ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/el-message.css'

ElMessage.success('成功使用')

不过这样还有个问题,因为有的组件中还使用了其他 Element 组件,如 ElMessageBox 中的确认按钮使用了 ElButton 组件,虽然渲染成功,但由于不是通过插件自动导入的,所以没有任何样式。

在这里插入图片描述

如果是在 Vue 单文件组件中使用的,并且模板中使用了 <el-button>,则会触发自动导入组件的样式文件。

所以建议按需引入的方式,仍然引入完整的样式文件,避免这类边界问题。

// src\plugins\element-plus.ts
import {
    
     App } from 'vue'
import * as ElIconModules from '@element-plus/icons-vue'
import 'element-plus/theme-chalk/index.css'

export default {
    
    
  install(app: App) {
    
    
    // 批量注册 Element Plus 图标组件
    // 或者自定义 ElIconModules 列表
    for (const iconName in ElIconModules) {
    
    
      app.component(iconName, (ElIconModules as any)[iconName])
    }
  }
}

import {
    
     ElMessage } from 'element-plus'
// 不再需要单独引入组件样式
// import 'element-plus/theme-chalk/el-message.css'

ElMessage.success('成功使用')

Vue 全局属性(globalProperties)

官方文档:

Vue 2 中使用 Vue.prototype 可以一次性设置所有 Vue 实例全局(this)访问的变量和方法。

Vue 3 改为通过根实例上的 app.config.globalProperties 对象注册该实例(app)内所有组件实例访问的全局属性,以代替 Vue 2 修改全部根实例的方式。

全局注册 ElementPlus 消息提示组件

如果完整引入了 Element Plus,就会自动为 app.config.globalProperties 添加一些组件的全局方法(如 ElMessage$messageElMessageBox$msgbox$alert 等)。

但如果使用的按需引入的方式,使用这类组件时,则需要在使用的模块中手动导入组件及组件样式。

为了减少在 Vue 组件中重复的导入,可以将它们注册为 Vue 实例的全局变量(变量名参考完整引入注册的名称):

// src\plugins\element-plus.ts
import {
    
     App } from 'vue'
import * as ElIconModules from '@element-plus/icons-vue'
import {
    
     ElMessage, ElMessageBox } from 'element-plus'
import 'element-plus/theme-chalk/index.css'

export default {
    
    
  install(app: App) {
    
    
    // 批量注册 Element Plus 图标组件
    // 或者自定义 ElIconModules 列表
    for (const iconName in ElIconModules) {
    
    
      app.component(iconName, (ElIconModules as any)[iconName])
    }

    // 将消息提示组件注册为全局方法
    app.config.globalProperties.$message = ElMessage
    app.config.globalProperties.$msgBox = ElMessageBox
  }
}

使用全局变量

可以在选项式 API 中通过 this.<globalProperty>访问全局变量,或者在模板中直接使用:

<template>
  <button @click="$message.success('可以在模板中直接使用')">
    提示
  </button>
</template>
<script lang="ts">
export default defineComponent({
    
    
  mounted() {
    
    
    this.$message.success('Options API 成功使用')
  }
})
</script>

在 setup 中使用全局变量

官方并没有介绍如何在 setup()<script setup> 中使用全局变量的方式。

因为 app.config.globalProperties 的目的就是为了替代 2.x 的 Vue.prototype 使用,随着全局 API 的更新,不会再有一次性的 Vue 全局配置,而应为每个根实例进行单独配置。

这本就不是为了应用在 setup 中的,你应该直接导入内容或者在 setup 中使用 provide/inject

参考 Issues:

provide/inject 方式

<!-- 父级组件,如 App.vue -->
<script setup>
import {
      
       ElMessage } from 'element-plus'

provide('$message', ElMessage)
</script>

<!-- 子孙组件 -->
<script setup>
const $message = inject('$message')

onMounted(() => {
      
      
  $message.success('setup - provide/inject 成功使用')
})
</script>

setup 中获取实例方式(不推荐)

还有一种的方式,Vue 对外暴露了一个 API getCurrentInstance() 可以访问内部组件实例,通过它可以访问到根实例的全局变量:

<script setup lang="ts">
const instance = getCurrentInstance()

onMounted(() => {
      
      
  instance.proxy.$message.success('setup - getCurrentInstance() 成功使用')
  // 也可以使用 appContext
  console.log(instance.appContext.config.globalProperties.$message === instance.proxy.$message) // true
})
</script>

<script lang="ts">
export default defineComponent({
      
      
  mounted() {
      
      
    console.log(this.instance.proxy === this) // true
  }
})
</script>

建议

使用的方式有很多,建议使用安全的方式,如直接导入或在选项 API 下使用。

全局变量 TypeScript 类型声明

添加类型声明文件:

// src\types\global.d.ts
import {
    
     ElMessage, ElMessageBox } from 'element-plus'

declare module 'vue' {
    
    
  // vue 全局属性
  export interface ComponentCustomProperties {
    
    
    $message: typeof ElMessage
    $msgBox: typeof ElMessageBox
  }
}

基于 Axios 封装请求模块

npm i axios

基本配置

// src\utils\request.ts
import axios from 'axios'
import {
    
     ElMessage } from 'element-plus'
// 在 plugins/element-plus.ts 引入了全部组件样式,这里不需额外引入

// 创建 axios 实例
const request = axios.create({
    
    
  baseURL: 'http://localhost:5000/api/admin'
})

// 请求拦截器
request.interceptors.request.use(function (config) {
    
    
  // 统一设置用户身份 token
  return config
}, function (error) {
    
    
  return Promise.reject(error)
})

// 响应拦截器
request.interceptors.response.use(function (response) {
    
    
  // 统一处理接口响应错误,如 token 过期无效、服务端异常等
  if (response.data.status && response.data.status !== 200) {
    
    
    ElMessage.error(response.data.msg || '请求失败,请稍后重试')
    return Promise.reject(response.data)
  }
  return response
}, function (error) {
    
    
  return Promise.reject(error)
})

export default request

接口请求全部放在 src/api 目录下组织:

// src\api\common.ts
// 公共基础接口封装
import request from '@/utils/request'

export const demo = () => {
    
    
  return request({
    
    
    method: 'GET',
    url: '/demo'
  })
}

使用:

<!-- src\views\login\index.vue -->
<template>
  <div>
    登录
  </div>
</template>

<script setup lang="ts">
import {
      
       demo } from '@/api/common'
import {
      
       onMounted } from 'vue'

onMounted(() => {
      
      
  demo().then(res => {
      
      
    console.log(res.data)
    // {"msg":"ok","status":200,"data":{"title":"Hello World","date":1649412637487}}
  })
})

</script>

封装响应数据的接口类型

当前获取的 res 是 Axios 包装的响应对象,后端返回的真实数据没有声明类型,所以 IDE 无法提供智能提示,所以要手动声明响应数据的类型。

request 不支持响应数据泛型,所以要改用支持泛型的 request.<method> 发送请求。

export const demo = () => {
    
    
  return request.get<{
    
    
    status: number
    msg: string
    data: {
    
    
      title: string
      date: number
    }
  }>('/demo')
}

现在 IDE 就可以自动提示 res.data 下的字段了。

但是每个接口都会返回 statusmsgdata 字段,为了避免重复声明,可以将它们封装成一个接口类型(interface),将 data 定义为一个泛型:

interface ResponseData<T = any> {
    
    
  status: number
  msg: string
  data: T
}

export const demo = () => {
    
    
  return request.get<ResponseData<{
    
    
    title: string
    date: number
  }>>('/demo')
}

封装泛型请求方法

现在访问响应数据的 data 字段需要通过 res.data.data.title

如果想要简洁一些,如 res.title,可以在请求后返回 .then(res => res.data.data)

这样就必须在每个请求后添加这个操作。

通常我们在 axios 拦截器中进行处理,但是 request.get() 返回类型仍会是 Axios 封装的对象(AxiosResponse)。

虽然运行正常,但智能提示会失效。

可以封装一个接收泛型的方法,内部调用 request()

// src\utils\request.ts
import axios, {
    
     AxiosRequestConfig } from 'axios'

...

export default <T = any>(config: AxiosRequestConfig) => {
    
    
  return request(config).then(res => (res.data.data || res.data) as T)
}

不过这样就不能使用 request.get() 方式发送请求了:

// 之前定义的 interface ResponseData 就不需要了
interface DemoData {
    
    
  title: string
  date: number
}

export const demo = () => {
    
    
  return request<DemoData>({
    
    
    method: 'GET',
    url: '/demo'
  })
}

这个方式无法使用 request.<method> 方式发送请求,有利有弊,根据个人习惯选择使用。

提取接口类型模块

一般接口的响应数据的格式可能会在多个地方用到,为了可以复用它们的接口类型,可以将其单独提取到一个模块。

src/api 目录下创建 types 文件夹用于存放 API 相关的类型模块:

// src\api\types\common.ts
export interface DemoData {
    
    
  title: string
  date: number
}

使用:

// src\api\common.ts
// 公共基础接口封装
import request from '@/utils/request'
import {
    
     DemoData } from '@/api/types/common'

export const demo = () => {
    
    
  return request<DemoData>({
    
    
    method: 'GET',
    url: '/demo'
  })
}

<!-- src\views\login\index.vue -->
<template>
  <div>
    登录
  </div>
</template>

<script setup lang="ts">
import {
      
       demo } from '@/api/common'
import {
      
       DemoData } from '@/api/types/common'
import {
      
       onMounted, ref } from 'vue'

const data = ref<DemoData>()

onMounted(() => {
      
      
  demo().then(res => {
      
      
    data.value = res
    console.log(data.value.title)
  })
})

</script>

环境变量和模式

环境变量和模式 | Vite 官方中文文档

一般会给项目的接口配置不同环境的基础地址(baseUrl),这通常会配置到环境变量中。

Vite 在一个特殊的 import.meta.env.[variable] 对象上暴露环境变量,构建时这些环境变量会作为字符串识别被静态替换,所以不能使用动态的 key 取值,如 import.meta.env[variable]

Vite 支持和 Vue CLI 一样的方式,通过 .env.[mode] 文件指定环境变量。

与 Vue CLI 不同的是,后者的自定义变量必须以 VUE_APP_ 开头,而 Vite 则必须以 VITE_ 开头。

配置环境变量

值会作为字符串替换,可以不加引号,除非包含 #

# .env.development
# 开发模式下加载的环境变量
VITE_API_BASEURL=http://localhost:5000/api/admin

# .env.production
# 生产模式下加载的环境变量
VITE_API_BASEURL=http://localhost:5000/api/admin

// src\utils\request.ts
import axios, {
    
     AxiosRequestConfig } from 'axios'

// 创建 axios 实例
const request = axios.create({
    
    
  baseURL: import.meta.env.VITE_API_BASEURL
})

...

注意:修改环境变量需要重启 Vite(npm run dev)才会生效。

环境变量 TypeScript 支持

Vite 只提供了默认的环境变量(MODEBASE_URLPRODDEV)的 TS 类型定义,用户自定义的环境变量需要手动添加类型定义。

// src\env.d.ts
...

// Vite 环境变量
// eslint-disable-next-line no-unused-vars
interface ImportMetaEnv {
    
    
  readonly VITE_API_BASEURL: string
}

由于定义了 interface 而没有使用,eslint 会报错,可以对这行代码禁用这个规则。

跨域问题

通常情况下,前端项目和后端项目都是分开部署的,而服务端接口都有跨域限制,解决跨域的方式很多,前端最主流的解决方案就是两种:

开发环境 生产环境
在服务端配置 CORS 在服务端配置 CORS
配置开发服务器代理,如 vite 的 server.proxy 和 Vue CLI 的 devServer.proxy 配置生产服务器代理,如 nginx

一般后端开发懒得配 CORS,前端常用的解决方案就是配置服务器反向代理(proxy)。

原理就是搭建一个中转服务器来转发请求规避跨域的问题,接口请求本地地址,由运行前端代码的服务器转发到目标服务器。

修改请求基础路径为本地路径(约定 /api 开头的请求路径都是需要转发的接口请求):

# .env.development
# 开发模式下加载的环境变量
VITE_API_BASEURL=/api

记得重启使环境变量生效。

配置 /api 的反向代理:

// vite.config.ts
...

export default defineConfig({
    
    
  ...
  server: {
    
    
    proxy: {
    
    
      '/api': {
    
    
        // 目标地址
        target: 'http://localhost:5000/api/admin',

        // 有的服务器会验证 origin
        // 默认接收到的是真实的 origin 即 http://localhost:3000
        // 设置 changeOrigin 为 true 后,代理服务器就会把 origin 修改为 target 的 origin(http://localhost:5000)
        // 一般建议加上此设置
        changeOrigin: true,

        // 路径重写
        // 路径即请求路径,如 /api/demo
        // 默认会这样拼接: <target.path><path> 如 http://localhost:5000/api/admin/api/demo
        // 重新后为 http://localhost:5000/api/admin/demo
        rewrite: path => path.replace(/^\/api/, '')
      }
    }
  }
})

PS:当前案例使用的服务端代码默认配置了 CORS,可以删掉 app.use(cors()) 测试跨域效果。

猜你喜欢

转载自blog.csdn.net/u012961419/article/details/124300061
今日推荐