vite+vue3从零开始搭建项目(三)

2,297 阅读3分钟

vite+vue3从零开始搭建项目(三)

前面两篇文章大致介绍了项目的搭建和组件的安装,这篇主要是一些补充。

以下是系列文章和GitHub仓库地址,点击即可跳转链接。

vite+vue3从零开始搭建项目(一)

vite+vue3从零开始搭建项目(二)

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