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:

运用vite和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,完成布置后新增多语言包功用
    /post/717356…
  • Element Plus组件库 经过CSS变量设置换肤
    element-plus.gitee.io/zh-CN/guide…
  • rollup钩子阐明
    rollupjs.org/guide/en/#o…