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.tsconst 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 = pathskinCssEle.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) returnconst 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

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

  1. 【Android Studio安装部署系列】八、Android Studio主题皮肤更换

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 概述 Android Studio具有自己的主题皮肤,但是如果想要更换自己喜欢的主题皮肤,可以参考下面的步骤. 注意,更换主题皮肤,之前的 ...

  2. 上传服务器后字体文件丢失,详解Vue+elementUI build打包部署后字体图标丢失问题...

    错误描述: Vue+elementUI  build打包部署后字体图标丢失,控制台显示文件element-icons.woff和element-icons.ttf文件404 错误展现: 控制台报错截图 ...

  3. Eclipse复制或修改项目后,把项目部署后发现还是原来的项目名称

    Eclipse复制或修改项目后,把项目部署后发现还是原来的项目名称 解决: 到项目根目录打开.setting文件夹,找到"org.eclipse.wst.common.component&q ...

  4. aspx-cs-dll :在部署后就让所有的aspx处于已经编译成dll的状态

    aspx->cs->dll asp.net项目在部署后,aspx文件并没有被编译,这种情形要一直维持到用户第一次访问页面,该页面文件aspx才会转化成cs,并编译成dll,这次访问速度不会 ...

  5. 【Android 插件化】Hook 插件化框架 ( 合并 “插件包“ 与 “宿主“ 中的 Element[] dexElements | 设置合并后的 Element[] 数组 )

    Android 插件化系列文章目录 [Android 插件化]插件化简介 ( 组件化与插件化 ) [Android 插件化]插件化原理 ( JVM 内存数据 | 类加载流程 ) [Android 插件 ...

  6. 解决django配合nginx部署后admin样式丢失

    解决django配合nginx部署后admin样式丢失 1.  在项目的settings.py文件里添加以下内容: STATIC_URL = '/static/' STATICFILES_DIRS = ...

  7. weblogic自带的jdk是在工程的包部署后编译使用

    weblogic自带的jdk是在工程的包部署后编译使用的.当用户把项目打包部署到weblogic上面,运行该项目的java环境jdk就是用的weblogic自带的jdk了,工程中的jdk和编译时的jd ...

  8. 通过XShell将linux服务器上的Tomcat下项目部署后,怎样对日志操作来调试

    场景 使用XShell将linux服务器上的Tomcat下的项目部署后, 发现项目跑不起来. 此时你想通过查看Tomcat运行的日志来调试. 实现 可以进入到Tomcat下的logs目录下 输入: l ...

  9. gulp项目部署服务器,关于部署:部署后如何在远程服务器上触发gulp / grunt任务?...

    我刚刚切换到Wordpress的Roots Sage入门主题:roots.io/sage/docs/ 我目前正在阅读有关部署过程的信息. 我的流程通常是: - 做出改变 -用咕unt声/大嘴巴建造 - ...

最新文章

  1. mysql8.0 tar安装_CentOS7安装MySQL8.0 tar包
  2. 平均薪资29036的Python,零基础初学者如何入门?
  3. ReentrantLock可重入锁的使用
  4. 将 Entity Framework、LINQ 和 Model-First 用于 Oracle 数据库
  5. iOS开发——策略模式
  6. 超级LINUX VOD系统介绍
  7. kali安装zmap
  8. Android开发环境搭建笔记总结
  9. 【python 去除文件名后缀或提取后缀】
  10. VRAR行业喷发剃须刀品牌结合VR推广_VRAR123
  11. 如何设置Google浏览器支持跨域
  12. 易车上面可以买车吗?
  13. 关于node链接数据库Handshake inactivity timeout
  14. 网易云音乐 ios android 通用,网易云游戏苹果和安卓不通用吗 | 手游网游页游攻略大全...
  15. js中对象的可枚举和不可枚举属性
  16. 国内常见的一些代码托管平台
  17. linux操作系统的7种运行级别的详细说明
  18. 视频教程-嵌入式读图基础-智能硬件
  19. 标准化拉普拉斯矩阵特征值范围为什么小于等于2?(证明)
  20. 中国食品级磷酸盐市场深度评估及预测报告(2022版)

热门文章

  1. java课程设计 博客园_Java课程设计博客(团队)
  2. 【题解】《算法零基础100讲》(第44讲) 位运算 (位或) 入门
  3. 逻辑运算符号“”、“||”和“!=”的相对优先级是怎样的,代码解释。
  4. 吃海鲜视频软件测试,IDE硬盘用HD Tune Pro检测健康状态警告分析。是否需要低级格式化?...
  5. php计算机毕业设计基于thinkph框架的学生宿舍公寓管理系统
  6. 三星在5G设备市场站稳脚跟,提前研发6G挑战华为和诺基亚等
  7. 基于正点原子STM32F1精英版秒表(库函数版)
  8. 查看IC中文文档的网站
  9. MyBatis入门-association标签使用
  10. python编程实例集合-编程小白如何结合量化实例学习python量化建模?