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)
},
}
}
完成阐明
皮肤的插件比生成多语言还要简略一点。这儿仍是仿制了部分多语言插件中的阐明。
- 皮肤文件实际上便是原封不动的从
srcPath
放到distPath
目录罢了。 - 插件在运用closeBundle钩子,是rollup钩子中的最后一步。rollup钩子阐明。触发closeBundle钩子的时分,打包已经完毕,dist目录中已经已经有了打包后的文件。选择钩子时,留意有必要在新的dist文件生成之后才干履行。
- 插件中的代码是打包时履行,是node环境,不是浏览器环境,不能运用
import.meta.glob
,因而运用fs读取文件。 - 仿制整个文件夹的操作运用node.js原生的
fs.cpSync
更适宜。可是这个功用在node.js 16.7版别才有,考虑到很多人的node版别号小于16.7,因而仍是引进了fs-extra
。
参阅
- 运用vite和vue-i18n,完成布置后新增多语言包功用
/post/717356… - Element Plus组件库 经过CSS变量设置换肤
element-plus.gitee.io/zh-CN/guide… - rollup钩子阐明
rollupjs.org/guide/en/#o…