使用vite和Element Plus,实现部署后不修改代码/打包,新增主题/皮肤包

news2025/1/17 3:04:23

Web前端界面切换主题/皮肤,是一个常见的需求。如果希望在打包部署后实现皮肤的修改甚至增加皮肤,不需要修改源码或者重新打包,类似于我们常见的皮肤包扩展,又该如何实现呢?
我使用类似上一期多语言包功能中介绍的方法来实现。

这个方法对Vue2和Vue3都适用,甚至可以适用于非Vue的前端框架。但是如果项目使用了组件库,皮肤包一般配合UI组件库使用,所以需要UI组件库的支持。目前Element Plus(Vue3)可以直接支持这种模式,Element(Vue2)和Ant Design Vue的支持程度不好。

功能和工程结构

工程结构

为了方便后续说明,首先提供一下我这边整个项目的目录结构。目录结构中省略了与本次说明不相关的文件。

├─app
    ├─package.json
    ├─tsconfig.json
    ├─tsconfig.node.json
    ├─vite.config.ts
    ├─src
    |  ├─App.vue
    |  ├─main.ts
    |  ├─style
    |  |   ├─style.scss
    |  |   └─var.scss
    |  ├─skin
    |  |  ├─index.ts
    |  |  ├─whiteSkin
    |  |  |    ├─bcd.css
    |  |  |    └─index.css
    |  |  ├─redSkin
    |  |       ├─abc.css
    |  |       └─index.css
    |  └─pages
    |     └─subject.vue
    └─plugins
       └─rollup-Plugin-skin-build
               └─index.ts

代码中的皮肤

开发时皮肤默认存放在src/skin文件夹中,也可以存放到其他位置。其中index.ts是皮肤的获取逻辑,剩下的每个文件夹都是一种皮肤。皮肤使用index.css引入。里面可以包含任意的子文件夹和文件,只要它们能被index.css获取到。例如:

├─whiteSkin
|  ├─index.css
|  ├─font
|  |    ├─font1.eot
|  |    └─font2.ttf
|  ├─tool
|       ├─tool1.css
|       └─tool2.css

注意皮肤里面不能使用需要编译的格式,必须是纯css文件。里面可以定义CSS变量。

/* 引入同一皮肤下的其他css文件 */
@import './bcd.css';

/* element-plus 变量 */
:root {
 --el-color-primary: #409eff;
}

/* 自定义 变量 */
:root {
  /*  背景 */
  --grey-background-color: rgba(0, 0, 0, 0.07);
  /*  文字颜色 */
  --grey-font-color: rgba(0, 0, 0, 0.7);
}

然后在页面中引用变量,这时候使用纯css或者其他工具(例如scss, less)都可以。

<style lang="scss" scoped>
  .test {
    color: var(--test-color);
  }
</style>

<style scoped>
  .item-label {
    color: var(--grey-font-color);
  }
</style>

这就需要我们前端开发页面的时候,需要抽象出一些可供换肤的皮肤变量。除了皮肤变量之外,我们也可以在皮肤中写一些css样式,也能够进行覆盖。

支持的UI组件库类型

读到这里,我们也能够清楚,这种方法适用于那些支持css全局变量换肤的组件库。我们通过覆盖全局变量的值实现换肤。是否支持打开浏览器的调试就能看到。例如:

  • Element Plus:

在这里插入图片描述

其中Element Plus官方也说明了这种换肤方式: 通过CSS变量设置

构建包(dist)中的皮肤目录

为了统一后端寻址,dist中的皮肤文件默认统一放置在dist/assets/skin,也可以存放到其他位置。目录中即是开发src/skin中的每个皮肤的文件夹,内容也一致。

├─dist
|   ├─index.html
|   └─assets
|      ├─vite.svg
|      └─skin
|         ├─whiteSkin
|         |    ├─bcd.css
|         |    └─index.css
|         └─redSkin
|             ├─abc.css
|             └─index.css

如果希望增加/修改皮肤,就在构建包的皮肤目录中增加/修改皮肤文件即可,不需要修改代码或重新打包。

切换皮肤

切换皮肤开发模式和生产模式基本相同,因此一起介绍。

代码实现

// src/skin/index.ts

const distPath = `${import.meta.env.VITE_NAMESPACE}/assets/skin/`

// 使用路径引入css
function loadCSSPath(path: string, name: string) {
  const head = document.getElementsByTagName('head')[0]
  const linkId = `skin-${name}`
  const linkEle = document.getElementById(linkId)
  if (linkEle) linkEle.parentNode?.removeChild(linkEle)
  const skinCssEle = document.createElement('link')
  skinCssEle.href = path
  skinCssEle.rel = 'stylesheet'
  skinCssEle.type = 'text/css'
  skinCssEle.setAttribute('id', linkId)
  head.appendChild(skinCssEle)
}

// dev模式下获取皮肤
async function getDevSkin(skinKey: string) {
  const reg = /.*skin\/(.*)\/index\.css/
  const modules = import.meta.glob('@/skin/*/index.css', { as: 'url' })
  Object.keys(modules).forEach(async (key: string) => {
    const regMatch = key.match(reg)
    if (!regMatch) return
    const skinKeyGet = regMatch[1] || ''
    if (skinKeyGet !== skinKey) return
    // 找到真实的url路径
    const path = await modules[key]()
    loadCSSPath(path, skinKey)
  })
}

// prod模式下获取皮肤
async function getProdSkin(skinKey: string) {
  const reqUrl = `${distPath}${skinKey}/index.css`
  loadCSSPath(reqUrl, skinKey)
}

// 切换皮肤调用函数
export async function renderSkin(skinKey: string) {
  if (import.meta.env.DEV) {
    getDevSkin(skinKey)
  } else {
    getProdSkin(skinKey)
  }
}

// 默认皮肤
renderSkin('whiteSkin')

实现切换皮肤的方式

切换皮肤的函数是loadCSSPath,使用原生的javascript的DOM操作,在<head>中创建一个<link>标签,放置CSS文件的URL地址即可。这个方法参考了其他人的方法。

如果在开发模式下加载CSS文件,有更简单的方式:

await import(`./${skinKey}/index.css`)

但是这种动态import方法对同一种皮肤只能生效一次,第二次再引入同样的文件就无效了。因此还是上面的DOM操作更合适。

开发模式和生产模式的区别

  • 生产模式很简单,我们知道URL地址,直接赋值即可。
  • 开发模式下不知道url,反而麻烦一点。需要用import.meta.glob把皮肤文件作为URL加载,再进行赋值。

rollup插件生成构建包(dist)皮肤

同样的,虽然标题写了vite(因为vite对于Vue开发者更熟悉),但插件本身并没有使用vite特性,所以它是一个同时支持vite和rollup的插件。

调用方式

// vite.config.ts
import { defineConfig, ConfigEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import skinBuildPlugin from './plugins/rollup-plugin-skin-build'

export default ({ mode }: ConfigEnv) => {
  return defineConfig({
    plugins: [vue(), skinBuildPlugin(mode)],
    ...['其它vite配置']
  })
}

插件入参

  • mode
    模式,只在生产模式production时执行插件
  • srcPath
    代码中皮肤目录,默认src/skin
  • distPath
    构建包(dist)中的皮肤目录,默认dist/assets/skin

代码实现

// plugins/rollup-plugin-skin-build/index.ts
import fs from 'fs-extra'
import path from 'path'

function setDevSkins(srcPath: string, distPath: string) {
  const dir = fs.readdirSync(srcPath)
  dir.forEach(async (name: string) => {
    const srcNamePath = path.join(srcPath, name)
    const distNamePath = path.join(distPath, name)
    const stats = fs.lstatSync(srcNamePath)
    if (stats.isDirectory()) {
      fs.mkdirSync(distNamePath)
      fs.copy(srcNamePath, distNamePath)
    }
  })
}

export default function skinBuildPlugin(
  mode: string,
  srcPath = path.join('src', 'skin'),
  distPath = path.join('dist', 'assets', 'skin'),
) {
  return {
    name: 'skinBuildPlugin',
    async closeBundle() {
      if (mode !== 'production') {
        return
      }
      fs.mkdirSync(distPath)
      setDevSkins(srcPath, distPath)
    },
  }
}

实现说明

皮肤的插件比生成多语言还要简单一点。这里还是复制了部分多语言插件中的说明。

  1. 皮肤文件实际上就是原封不动的从srcPath放到distPath目录而已。
  2. 插件在使用closeBundle钩子,是rollup钩子中的最后一步。rollup钩子说明。触发closeBundle钩子的时候,打包已经结束,dist目录中已经已经有了打包后的文件。选择钩子时,注意必须在新的dist文件生成之后才能执行。
  3. 插件中的代码是打包时执行,是node环境,不是浏览器环境,不能使用import.meta.glob,因此使用fs读取文件。
  4. 复制整个文件夹的操作使用node.js原生的fs.cpSync更合适。但是这个功能在node.js 16.7版本才有,考虑到很多人的node版本号小于16.7,因此还是引入了fs-extra

参考

  • 使用vite和vue-i18n,实现部署后新增多语言包功能
    https://blog.csdn.net/qq278672818/article/details/128187194
  • Element Plus组件库 通过CSS变量设置换肤
    https://element-plus.gitee.io/zh-CN/guide/theming.html#通过CSS变量设置
  • rollup钩子说明
    https://rollupjs.org/guide/en/#output-generation-hooks

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/93710.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于Xlinx的时序分析与约束(3)----基础概念(下)

1、4种基本的时序路径 下图是一张典型的FPGA与上游器件、下游器件通信的示意图&#xff1a; 其可以划分为4条基本的数据路径&#xff0c;这4条路径也是需要进行时序约束的最基本路径。 &#xff08;1&#xff09;寄存器到寄存器 路径2&#xff0c;FPGA内部的寄存器到另一个寄存…

[附源码]Node.js计算机毕业设计高校医疗健康服务系统的设计与实现Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

【C++初阶】类和对象(下)再谈构造函数、static成员、C++11的成员初始化新玩法、友元类、内部类

文章目录再谈构造函数static成员C11的成员初始化新玩法友元类内部类再谈构造函数 1.构造函数体赋值 在创建对象时&#xff0c;编译器通过调用构造函数&#xff0c;给对象中各个成员变量一个合适的初始值。 虽然上述构造函数调用之后&#xff0c;对象中已经有了一个初始值&am…

客户管理系统如何提升体验

数字化时代&#xff0c;客户与企业交互的触点爆炸式增长&#xff0c;客户体验正从单一触点走向端到端旅程。众多的产品、海量的数据&#xff0c;导致客户对体验的要求越来越多......CRM客户管理系统是企业提升客户体验的有效工具&#xff0c;它不仅可以帮助您进一步了解客户&am…

App自动化之dom结构和元素定位方式(包含滑动列表定位)

先来看几个名词和解释&#xff1a; dom: Document Object Model 文档对象模型dom应用: 最早应用于html和js的交互。界面的结构化描述&#xff0c; 常见的格式为html、xml。核心元素为节点和属性xpath: xml路径语言&#xff0c;用于xml 中的节点定位&#xff0c;XPath 可在 xml…

javaSE - 三个常用的接口(Comparable,Comparator,Cloneable)

1、Comparable 英 [ˈkɒmpərəbl] 美 [ˈkɑːmpərəbl] 可比较的;可比的;可比性;可比;可比较   2、Comparator 美 [kəmˈpɜrətər] n. 比较器&#xff0c;比色器&#xff0c;比较电路&#xff0c;比长仪&#xff0c;场强计 3、 Cloneable 可复制的 一、Comparable …

MySQL性能优化浅析

1. 硬件 1.1 CPU IO密集型&#xff0c;提升CPU核心数 计算密集型&#xff0c;提升CPU频率 1.2 磁盘 机械硬盘在随机访问时&#xff0c;由于受磁针移动速度的限制&#xff0c;性能会大幅降低。使用固态硬盘可以大幅提升随机访问的能力。按需选择。 1.3 其他 带宽、内存频…

Superset 安装配置

文章目录Superset 安装配置一、Superset 概述1. Superset简介2. 功能概述3. 支持的数据库二、Superset 环境部署步骤三、创建虚拟机&#xff0c;安装CentOS1.下载CentOS2.创建虚拟机3.编辑虚拟机设置4.安装centos7.9mini版本5.启动centos&#xff0c;并进行登录四、CentOS配置1…

小米(Android)刷NetHunter安装指南

一、安装NetHunter 前提&#xff1a;确保手机已经root&#xff0c;已装上magisk。如果没有root&#xff0c;可用尝试magisk root 后执行此文 1、下载Nethunter&#xff1a;Get Kali | Kali Linux 然后push 到sdcard 里&#xff0c; 2、打开magisk&#xff0c;选择刚刚下好的…

Windows下安装libtorch与Clion配置

Windows 安装和使用libtorch 1.下载libtorch libtorch的下载链接&#xff0c;如下图所示&#xff0c;libtorch有release和debug版本可以选择。为了方便调试&#xff0c;下debug版。电脑上没CUDA&#xff0c;下次有需要再更新吧。 2.libtorch使用 在Visual Studio的使用可以参…

云原生周刊 | 让 ChatGPT 以电子邮件的方式来解释 KubeSphere

过去的一周是 ChatGPT 的狂欢&#xff0c;我猜每一位云原生玩家都很好奇他是如何看待 Kubernetes 的。咱们不防换个方式来提问&#xff0c;让它使用电子邮件的方式来向别人推荐 KubeSphere 和 OpenFunction。 开源项目推荐 Tailscale Ingress Controller 这是针对 Tailscale …

【DevOps实战系列】第七章:详解Docker私服Harbor篇

个人亲自录制全套DevOps系列实战教程 &#xff1a;手把手教你玩转DevOps全栈技术 Harbor私服搭建 讲完Nexus3再来看下harbor&#xff0c;其实大同小异&#xff0c;只不过harbor的管理要比Nexus3更专业、功能更完善&#xff0c;大家按需选择即可&#xff0c;Nexus的优势是他能和…

web网站工程项目前期需求分析与规划怎么写?

在当下&#xff0c;判断一份网站工程项目文档是否优秀&#xff0c;项目目录是最直接的体现&#xff0c;同时&#xff0c;工程说明、需求分析和项目规划各版块的内容都缺一不可。工欲善其事必先利其器&#xff0c;前期准备得越充分&#xff0c;后期就会越顺利。 本期&#xff0c…

Centos7安装图形化界面并使用Windows远程桌面连接(包含离线部署)

一、在centos7 中部署远程桌面所使用的程序 1、关闭防火墙和selinux(xrdp是通过3389端口远程桌面连接 ) [rootlocalhost ~]# systemctl stop firewalld #临时关闭防火墙 [rootlocalhost ~]# systemctl disable firewalld.service #永久关闭防火墙 [rootlocalhost ~]# setenf…

SpringMVC:SpringMVC之JSON数据传输参数(5)

JSON数据传输参数1 JSON数据传输参数2 JSON普通数组3 JSON对象数据4 JSON对象数组5 小结1 JSON数据传输参数 现在比较流行的开发方式为异步调用。前后台以异步方式进行交换&#xff0c;传输的数据使用的是JSON,所以前端如果发送的是JSON数据&#xff0c;后端该如何接收? 对于…

面试官:单体架构怎么向分布式微服务架构演变的?(8000字干货)

随着网站规模越来越大&#xff0c;单体应用往往很难再满足要求&#xff0c;就需要向分布式&#xff0c;微服务架构演变。 那么这个演变过程是怎么样的呢&#xff1f;都涉及到哪些组件&#xff0c;会遇到哪些问题&#xff0c;以及相应的解决方案都是什么&#xff0c;本篇文章就…

ESP 低功耗入门

此篇博客以 ESP32 为例来说明 ESP 的睡眠模式。 ESP32 芯片可以大致分成以为五个模块&#xff1a; RF 模块&#xff0c;也就是射频模块&#xff0c;用于蓝牙 / Wi-Fi 的收发(TX / RX)功能。CPU&#xff0c;如 Xtensa 内核 (ESP32 / ESP32-S2 / ESP32-S3)&#xff0c;RISC-V 内…

ssm项目-商城管理系统

1 逆向工程设计 1.1 xml文件配置&#xff08;generatorConfig.xml&#xff09; <?xml version"1.0" encoding"UTF-8"?> <!DOCTYPE generatorConfigurationPUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"&q…

滑动窗口的最大值【滑动窗口问题】

文章目录题目解题思路代码展示题目 给定一个长度为 n 的数组 num 和滑动窗口的大小 size &#xff0c;窗口从最左边滑动到最右边&#xff0c;每次向右边滑动一个位置&#xff0c;找出所有滑动窗口里数值的最大值。 窗口大于数组长度或窗口长度为0的时候&#xff0c;返回空 数…

硬件需知知识 -- 基本元件(电阻)

一、电阻 1.1 贴片电阻 1.1.1 贴片电阻的封装大小是和功率时相关的。 封装大小功率(W)0201120\frac{1}{20}201​0402116\frac{1}{16}161​0603110\frac{1}{10}101​080518\frac{1}{8}81​12060.2518120.5或1201012\frac{1}{2}21​25121或者21.1.2 贴片电阻读数 贴片电阻的读数…