初始化 Element Plus
Element Plus 是针对 Vue 3 的 Element UI 升级版。
安装和配置自动按需导入
# 安装
npm install element-plus --save
建议使用按需导入,官方推荐使用 unplugin-vue-components
和 unplugin-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'
...
注意:
- 不是全部 API,例如 Vue Router 的
createRouter
就不会导入。具体可以自动导入的 API 参考 unplugin-auto-import/src/presets- 生成
.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
的 $message
、ElMessageBox
的 $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
下的字段了。
但是每个接口都会返回 status
、msg
和 data
字段,为了避免重复声明,可以将它们封装成一个接口类型(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>
环境变量和模式
一般会给项目的接口配置不同环境的基础地址(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 只提供了默认的环境变量(MODE
、BASE_URL
、PROD
、DEV
)的 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())
测试跨域效果。