vite+vue3从零开始搭建项目(三)
前面两篇文章大致介绍了项目的搭建和组件的安装,这篇主要是一些补充。
以下是系列文章和GitHub仓库地址,点击即可跳转链接。
环境变量
创建环境变量
简单创建开发环境.env.development和生产环境.env.production两个环境变量的配置文件,需要注意的环境变量名需要以VITE_开始。
# 是否加载本地资源(不走API)
VITE_API_ISLOCAL = false
# API请求地址
VITE_API_BASEURL = http://192.168.1.1:9528/
使用环境变量
在vite中对环境变量处理依赖于dotEnv第三方库,vite中内置了这个第三方得库,dotEnv会读取.env文件,并解析这个文件中对应的环境变量,然后将其注入process对象下。一般使用方法为import.meta.env.VITE_API_ISLOCAL,特殊的地方在vite.config.js中需要有特殊的处理方式。
// vite.config.js
// 需要调用loadEnv
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ command, mode }) => {
// 赋值
const env = loadEnv(mode, process.cwd(), '')
return {
server: {
proxy: {
'^/api': {
target: env.VITE_API_BASEURL // 引用
}
}
}
}
})
自定义指令
// src/directives/index.js
import { useUserStore } from '@/pinia'
import { isLocal } from '@/utils/helper'
export default {
install(app) {
// 普通的按钮权限(一个按钮对应一个button值)
app.directive('permission', {
mounted(el, binding) {
if (!binding.value || isLocal()) return false
const userStore = useUserStore()
if (!userStore.buttons.includes(binding.value)) {
el.parentNode.removeChild(el)
}
}
})
// 多对一的按钮权限(多个button值来决定是否显示例如下拉菜单、表格后面的操作栏等等)
app.directive('permissions', {
mounted(el, binding) {
if (!binding.value || isLocal()) return false
const userStore = useUserStore()
let flag = false
for (const item of binding.value) {
if (userStore.buttons.includes(item)) {
flag = true
return
}
}
if (!flag) el.parentNode.removeChild(el)
}
})
}
}
组件的自动安装
创建标准的公共组件,并自动引用。
创建公共组件库
├─ 📁src
│ ├─ 📁components
│ │ ├─ 📁FtIcon
│ │ │ ├─ 📄index.js
│ │ │ ├─ 📄index.jsx
这里展示一个多图标库的集成(iconfont+elementPlus)。
这里只是一个简单的导出。
// src/components/FtIcon/index.js
import FtIcon from './index.jsx'
export default FtIcon
主体部分
// src/components/FtIcon/index.jsx
// elementPlus官方的使用方式el-icon + 组件
const ElementPlusIcon = (name, size, color, className) => (
<el-icon class={[className, 'ft-icon']} size={size} color={color}>
{h(resolveComponent(name))}
</el-icon>
)
// 传统的使用方式,i标签+class名
const OtherIcon = (name, size, color, className) => <i class={[className, `ft-icon ${name}`]} style={`font-size:${size}; color: ${color};vertical-align: middle;`}></i>
export default defineComponent({
name: 'FtIcon', // 组件名:关键点,引用的组件名都是来自这里
props: {
name: { // icon的名称,elementPlus的前加el-icon
type: String,
required: true
},
size: { // 字体大小
type: String,
default: ''
},
color: { // 字体颜色
type: String,
default: ''
},
class: { // 附加class
type: String,
default: ''
}
},
setup(props) {
if (props.name.indexOf('el-icon') === 0) {
return () => ElementPlusIcon(props.name.split('el-icon ')[1], props.size, props.color, props.class)
} else {
return () => OtherIcon(props.name, props.size, props.color, props.class)
}
}
})
自动批量导入
将放入components文件夹下面的组件自动注册到全局。
// 引入elementPlus图标库(仅仅是上一步其实没有导入element的icon)
import { ElIcon } from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 引入组件库
const components = import.meta.glob('@/components/*/index.js', { eager: true })
export default {
install(app) {
// 组件无法识别的el-icon组件黄色警告(加不加都不影响组件渲染)
app.component('ElIcon', ElIcon)
// 注册elementPlus的icon图标
Object.entries(ElementPlusIconsVue).forEach(([key, component]) => {
app.component(key, component)
})
// 将components里的组件都自动注册
Object.keys(components).forEach(key => {
const component = components[key].default
app.component(component.name, component)
})
}
}
页面使用
无需引用,直接在template使用即可。
<tempalte>
<div>
<ft-icon name="el-icon Search"></ft-icon>
<FtIcon name="el-icon Search" color="#fff000" size="100px"></FtIcon>
<ft-icon name="iconfont icon-search" color="#ff0000" size="24px" class="big-icon"/>
</div>
</template>
layout布局
标准的layout布局应该包含头部栏,侧边导航栏,标签导航栏,主体部分。
├─ 📁src
│ ├─ 📁layout
│ │ ├─ 📁AppHeader
│ │ │ ├─ 📄index.vue
│ │ ├─ 📁AppMain
│ │ │ ├─ 📄index.vue
│ │ ├─ 📁Siderbar
│ │ │ ├─ 📄index.vue
│ │ │ ├─ 📄MenuItem.vue
│ │ ├─ 📁TagsView
│ │ │ ├─ 📄index.vue
│ │ ├─ 📄index.vue
<template>
<div class="g-app-container">
<AppHeader />
<div class="g-app-container-content">
<Sidebar />
<div class="g-app-container-right">
<TagsView />
<AppMain />
</div>
</div>
</div>
</template>
<script setup>
import AppHeader from './AppHeader/Index.vue'
import Sidebar from './Siderbar/Index.vue'
import TagsView from './TagsView/Index.vue'
import AppMain from './AppMain/Index.vue'
</script>
</style>
里面的代码展开就太多了,而且没什么共通性,我这边随便展示,大家随便看一下就行。
// src/layout/AppMain/Index.vue
<template>
<el-scrollbar ref="scrollbarRef">
<div class="g-app-main">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</el-scrollbar>
</template>
// src/layout/Siderbar/Index.vue
<template>
<div class="g-app-sider" :class="{ collapse: appStore.isCollapse }">
<div class="g-app-sider-content">
<el-scrollbar>
<el-menu :default-active="activeMenu" :collapse="appStore.isCollapse" class="g-sider-menu">
<MenuItem :menus="userStore.routes"></MenuItem>
</el-menu>
</el-scrollbar>
</div>
<div class="g-app-sider-footer">
<div class="toggle-sider">
<template v-if="appStore.isCollapse">
<ft-icon name="el-icon Expand" size="20" @click="toggleSider"></ft-icon>
</template>
<template v-else>
<ft-icon name="el-icon Fold" size="20" @click="toggleSider"></ft-icon>
</template>
</div>
</div>
</div>
</template>
<script setup>
import MenuItem from './MenuItem.vue'
import { useAppStore, useUserStore } from '@/pinia'
const appStore = useAppStore()
const userStore = useUserStore()
const route = useRoute()
const activeMenu = computed(() => {
return route.meta.activePath || route.path
})
const toggleSider = () => {
appStore.toggleCollapse()
}
</script>
// src/layout/Siderbar/MenuItem.vue
<template>
<template v-for="item in menus" :key="item.path">
<el-sub-menu v-if="hasChildren(item)" :index="item.path">
<template #title>
<ft-icon v-if="item.meta.icon" class="el-icon" :name="item.meta.icon" size="20px"></ft-icon>
<span class="nav-title">{{ item.meta.title }}</span>
</template>
<menu-item :menus="item.children"></menu-item>
</el-sub-menu>
<el-menu-item v-else :index="item.path" @click="linkTo(item.path)">
<ft-icon v-if="item.meta.icon" class="el-icon" :name="item.meta.icon" size="20px"></ft-icon>
<span class="nav-title">{{ item.meta.title }}</span>
</el-menu-item>
</template>
</template>
<script setup>
defineProps({
menus: {
type: Array,
default: () => []
}
})
const hasChildren = val => {
let flag = false
if (Array.isArray(val.children) && val.children.length > 0) {
for (const item of val.children) {
if (item.visible !== false) {
flag = true
break
}
}
}
return flag
}
const router = useRouter()
const linkTo = path => {
router.push(path)
}
</script>
页面
以下是我个人的代码风格,不代表代码这么写有什么优势,纯粹打个样。
├─ 📁src
│ ├─ 📁views
│ │ ├─ 📁Login 登录页
│ │ │ ├─ 📄Index.vue
│ │ ├─ 📁Dasheboard 首页
│ │ │ ├─ 📁components 首页看板用到的复杂组件如图表之类的都放在同目录下的component文件夹内
│ │ │ │ ├─ 📄Line.vue
│ │ │ │ ├─ 📄Pie.vue
│ │ │ ├─ 📄index.vue
│ │ ├─ 📁TablePage 经典表格页面
│ │ │ ├─ 📁dialog 表格页面相关的弹窗都放同目录下的dialog文件夹里
│ │ │ │ ├─ 📄Edit.vue
│ │ │ │ ├─ 📄Detail.vue
│ │ │ │ ├─ 📄Config.vue
│ │ │ ├─ 📄index.vue