Skip to content

配置

环境变量

环境变量是运行时动态的全局变量,在项目启动时,会根据当前环境加载对应的环境变量。

Vite 的环境变量命名遵循 VITE_ 开头,然后加上环境名称,比如 VITE_PORT

配置文件

sunlight-admin 一共有 3 个环境配置文件,具体如下:

text
├── .env.development      # 开发环境
├── .env.production       # 生产环境
├── .env.test             # 测试环境

sunlight-admin 项目使用 .env.* 文件针对不同环境进行配置,每个环境文件对应特定的运行环境:

  • .env.development:开发环境,当运行 pnpm dev 时,项目会加载该文件的变量
  • .env.test:测试环境,当运行 pnpm build:test 时,项目会加载该文件的变量
  • .env.production:生产环境,当运行 pnpm buildpnpm build:prod 时,项目会加载该文件的变量

提示

在 Vite 中,环境变量的加载顺序为:特定环境文件(.env.development 等)会覆盖默认的环境配置。

基础用法

可以在项目里使用 import.meta.env 获取环境变量。

ts
const { VITE_API_URL, VITE_USER_NODE_ENV } = import.meta.env;
console.log("当前环境变量 VITE_API_URL 为:", VITE_API_URL);
console.log("当前环境类型为:", VITE_USER_NODE_ENV);

如果想判断当前项目运行的环境是开发环境还是生产环境,则通过 import.meta.env.MODE 来判断:

ts
if (import.meta.env.MODE === "development") {
  console.log("当前环境是开发环境");
} else if (import.meta.env.MODE === "test") {
  console.log("当前环境是测试环境");
} else if (import.meta.env.MODE === "production") {
  console.log("当前环境是生产环境");
}

当然也可以通过 import.meta.env.DEV 判断是否为开发环境:

ts
if (import.meta.env.DEV) {
  console.log("当前环境是开发环境");
}
if (import.meta.env.PROD) {
  console.log("当前环境是生产环境");
}

新增环境变量

Vite 规范要求是 VITE_ 作为自定义配置前缀,否则无法读取到自定义的配置。

比如要在 .env.production 文件添加一个配置,具体如下

sh
VITE_ENVIRONMENT = private

然后需要加入 TS 类型支持,在 src/types/env.d.ts 的 ImportMetaEnv 里加上 VITE_ENVIRONMENT: string,具体如下

ts
interface ImportMetaEnv {
  /** 环境名称 */
  VITE_ENVIRONMENT: string;
}

这样在项目中就可以通过 import.meta.env.VITE_ENVIRONMENT 获取到 VITE_ENVIRONMENT 的值了。

配置项

sunlight-admin 支持的环境变量有:

ts
interface ImportMetaEnv {
  /**
   * 当前环境类型
   */
  VITE_USER_NODE_ENV: string;
  /**
   * 接口基础地址
   */
  VITE_API_URL: string;
  /**
   * 静态资源路径(仅生产环境)
   */
  VITE_PUBLIC_PATH?: string;
}

环境文件示例

开发环境 (.env.development)

sh
# 开发环境
VITE_USER_NODE_ENV = develop

# 开发环境接口地址
VITE_API_URL = "/"

生产环境 (.env.production)

sh
# 生产环境
VITE_USER_NODE_ENV = production

# 公共基础路径
VITE_PUBLIC_PATH = /

# 生产环境接口地址
VITE_API_URL = "https://"

测试环境 (.env.test)

sh
# 测试环境
VITE_USER_NODE_ENV = test

# 测试环境接口地址
VITE_API_URL = "/"

全局配置文件

sunlight-admin 使用 Pinia 进行全局状态管理,将框架的全局配置统一放到 src/store/modules/global.ts 文件中。

全局状态管理

配置文件

ts
import { defineStore } from 'pinia'
import piniaPersistConfig from '@/plugins/piniaPersist'

export const defaultPrimary: string = '#409eff' // 默认主题颜色

export const useGlobalStore = defineStore({
  id: 'app-global',
  state: (): GlobalState => ({
    title: '网址标题',
    // 语言切换
    language: 'zh',
    // 布局模式 (horizontal | vertical)
    layout: 'vertical',
    // 主题颜色
    primary: defaultPrimary,
    // 暗黑模式
    isDark: false,
    // 折叠
    isCollapse: false,
    // 面包屑导航
    breadcrumb: true,
    // 面包屑导航图标
    breadcrumbIcon: true,
    // 标签页
    tabs: true,
    // 标签页图标
    tabsIcon: true,
    // 页脚
    footer: true
  }),
  getters: {},
  actions: {
    // Set GlobalState
    setGlobalState(...args: GlobalType.ObjToKeyValArray<GlobalState>) {
      this.$patch({ [args[0]]: args[1] })
    }
  },
  persist: piniaPersistConfig('app-global')
})
ts
declare namespace GlobalType {
  type LayoutType = 'horizontal' | 'vertical'

  type LanguageType = 'zh' | 'en'

  /* Generic Tools */
  type ObjToKeyValArray<T> = {
    [K in keyof T]: [K, T[K]]
  }[keyof T]

  type ObjToKeyValUnion<T> = {
    [K in keyof T]: { key: K; value: T[K] }
  }[keyof T]
}

// 全局配置相关接口
interface GlobalState {
  title: string
  language: GlobalType.LanguageType
  layout: GlobalType.LayoutType
  primary: string
  isDark: boolean
  isCollapse: boolean
  breadcrumb: boolean
  breadcrumbIcon: boolean
  tabs: boolean
  tabsIcon: boolean
  footer: boolean
}

路由配置

路由结构

sunlight-admin项目的路由系统采用了静态路由和动态路由分离的设计模式,通过Vue Router实现。

路由文件结构

text
├── src/router/
│   ├── index.ts            # 路由入口文件,包含路由守卫和配置
│   └── modules/
│       └── staticRouter.ts # 静态路由定义

静态路由

静态路由是不需要权限验证的路由,包括首页、登录页和错误页面等。

// src/router/modules/staticRouter.ts
export const staticRouter: RouteRecordRaw[] = [
  {
    path: '/',
    component: Layout,
    redirect: '/home',
    children: [
      {
        path: 'home',
        component: () => import('@/views/home/index.vue'),
        name: 'Home',
        meta: {
          title: '首页',
          icon: 'menu-home'
        }
      }
    ]
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('@/views/login/index.vue'),
    meta: { title: '登录', hidden: true }
  }
]

// 错误路由
export const errorRouter = [
  { path: '/401', name: '401', component: comp401, meta: { title: '401', hidden: true } },
  { path: '/404', name: '404', component: comp404, meta: { title: '404', hidden: true } },
  { path: '/500', name: '500', component: comp500, meta: { title: '500', hidden: true } },
  { path: '/:pathMatch(.*)*', component: comp404 } // 404路由
]

动态路由

动态路由是基于用户角色权限动态生成的路由,需要登录后才能访问。每个动态路由都配置了对应的角色权限,只有拥有相应角色的用户才能访问。

动态路由定义

typescript
// src/router/modules/asyncRouter.ts 示例
const Layout = () => import('@/layout/index.vue')

export const asyncRoutes: MenuType.MenuOptions[] = [
  {
    path: '/project',
    name: 'project',
    component: Layout,
    meta: {
      title: '项目列表',
      icon: 'menu-project',
      roles: ['administrator'], // 只有管理员角色可以访问
      isKeepAlive: true
    },
    children: [
      {
        path: 'project-test',
        component: () => import('@/views/project/index.vue'),
        name: 'ProjectTest',
        meta: {
          title: '项目测试',
          icon: 'menu-project',
          roles: ['administrator'],
          isKeepAlive: true
        }
      }
    ]
  },
  // 更多路由定义...
]

动态路由生成

通过authStore.generateAsyncRoutes方法根据用户角色生成可访问的路由:

typescript
// src/store/modules/auth.ts 示例
export const useAuthStore = defineStore({
  id: 'app-auth',
  state: (): AuthState => ({
    asyncRoutes: [],
    permissions: []
  }),
  actions: {
    // 生成动态路由
    generateAsyncRoutes(roles: string[]) {
      return new Promise<RouteRecordRaw[]>((resolve) => {
        // 过滤出用户角色可访问的路由
        let accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
        this.asyncRoutes = accessedRoutes
        resolve(accessedRoutes)
      })
    }
  }
})

路由守卫

路由守卫用于控制路由的访问权限,包括登录验证和权限检查:

typescript
// src/router/index.ts
router.beforeEach(async (to, from, next) => {
  const globalStore = useGlobalStore()
  const authStore = useAuthStore()
  const userStore = useUserStore()

  NProgress.start()
  // 设置页面标题
  document.title = `${to.meta.title}` || globalStore.title

  const hasToken = getToken()
  if (hasToken) {
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else {
      const hasRoles = userStore.roles && userStore.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        try {
          const { roles } = await userStore.getInfo()
          // 基于角色生成可访问路由数组
          const accessRoutes = await authStore.generateAsyncRoutes(roles)
          // 动态添加可访问路由
          accessRoutes.forEach(item => router.addRoute(item))
          next({ ...to, replace: true })
        } catch (error) {
          await userStore.resetToken()
          ElMessage.error(JSON.stringify(error) || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

插件配置

Axios请求插件

封装了Axios用于网络请求,包含请求拦截器和响应拦截器:

typescript
// src/plugins/request.ts
import axios from 'axios'
import { getToken } from '@/utils/auth'
import { checkStatus } from './request'

const request = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 30 * 1000,
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
  }
})

// 请求拦截器
request.interceptors.request.use(
  config => {
    const token = getToken()
    if (token) {
      config.headers.Authorization = token
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
request.interceptors.response.use(
  response => {
    return response.data
  },
  async error => {
    const { response } = error
    if (response) checkStatus(response.status)
    return Promise.reject(error)
  }
)

export default request

NProgress进度条插件

用于页面切换时显示进度条,提升用户体验:

typescript
// src/plugins/nprogress.ts
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

// 配置NProgress
NProgress.configure({
  easing: 'ease', // 动画方式
  speed: 500, // 递增进度条的速度
  showSpinner: false, // 是否显示加载指示器
  trickleSpeed: 200, // 自动递增间隔
  minimum: 0.3 // 初始化时的最小百分比
})

export default NProgress

Pinia持久化插件

用于将Pinia状态持久化到localStorage中,防止页面刷新后状态丢失:

typescript
// src/plugins/piniaPersist.ts
import type { PersistedStateOptions } from 'pinia-plugin-persistedstate'

/**
 * @description: 持久化参数配置
 * @param {String} key 存储到持久化的name
 * @return persist
 */
export const piniaPersistConfig = (key: string): PersistedStateOptions => {
  const persist: PersistedStateOptions = {
    key,
    storage: localStorage,
    paths: ['']
  }
  return persist
}

export default piniaPersistConfig

认证配置

sunlight-admin 实现了基于Token的认证系统,用于管理用户登录、权限控制等功能。

Token管理

typescript
// src/utils/auth.ts
// 操作token get/set/remove
import Cookies from 'js-cookie'

const TokenKey = 'bk_token' // 自定义修改

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token: any) {
  return Cookies.set(TokenKey, token)
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}

用户状态管理

typescript
// src/store/modules/user.ts
import { defineStore } from 'pinia'
import { reactive, toRefs } from 'vue'
import { resetRouter } from '@/router'
import { removeToken } from '@/utils/auth'
import { mockUser } from '@/assets/mock/userMock'

export const useUserStore = defineStore('user', () => {
  const initData: UserState = reactive({
    token: '',
    name: '',
    avatar: '', // 头像
    introduction: '', // 介绍
    roles: [] // 角色
  })

  // Set Token
  function setToken(token: string) {
    initData.token = token
  }
  // remove token
  function resetToken() {
    initData.token = ''
    removeToken() // 清除cookie中token
  }

  // 获取用户信息
  function getInfo() {
    return new Promise<any>((resolve): any => {
      let info: any = null
      const userToken = initData.token.split('-')[0]
      info = mockUser[userToken] || {}
      const { roles, name, avatar, introduction } = info
      initData.name = name
      initData.avatar = avatar
      initData.introduction = introduction
      initData.roles = roles
      resolve(info)
    })
  }

  // 退出登录
  function logout() {
    resetToken()
    initData.name = ''
    initData.avatar = ''
    initData.introduction = ''
    initData.roles = []
    resetRouter()
  }

  return {
    ...toRefs(initData),
    setToken,
    resetToken,
    getInfo,
    logout
  }
})

总结

本文档详细介绍了sunlight-admin项目的配置系统,包括:

  1. 环境变量配置:使用Vite的环境变量机制,支持开发、测试和生产三种环境
  2. 全局状态管理:基于Pinia的全局状态管理,包含主题、布局、语言等配置
  3. 语言配置:使用Vue I18n实现的多语言切换功能
  4. 主题配置:支持主题颜色自定义和暗黑模式切换的主题系统
  5. 路由配置:静态路由和动态路由分离的设计模式,支持基于角色的权限控制
  6. 插件配置:封装了Axios、NProgress和Pinia持久化等常用插件
  7. 认证配置:基于Token的认证系统,包含用户状态管理和权限控制

通过这些配置,sunlight-admin项目实现了高度的可定制性和扩展性,方便开发者根据实际需求进行调整。项目采用了模块化的设计思想,将不同功能的配置分离到不同的文件中,提高了代码的可维护性和可读性。