前端组件库自定义主题切换探索-02-webpack-theme-

2023-01-09  本文已影响0人  东扯葫芦西扯瓜

本文来研究写webpack-theme-color-replacer webpack 的实现逻辑和原理。
上一篇我们讲过, webpack-theme-color-replacer webpack 基本思路就是,webpack构建时,在emit事件(准备写入dist结果文件时)中,将即将生成的所有css文件的内容中 带有指定颜色的css规则单独提取出来,再合并为一个theme-colors.css输出文件。然后在切换主题色时,下载这个文件,并替换为需要的颜色,应用到页面上,但是具体的细节确并不清楚,我们想要看看是否可以改造达到自己的需求和期望,就得具体看下里面的实现过程逻辑

1、注册插件

首先,我们还是在项目根目录下建config文件夹,里面有plugin.config.js文件
同样,要在vue.config.js注册插件

以上两点代码参考第一篇:前端组件库自定义主题切换探索-01

改造用于测试组件

为了方便研究,我们将ant-design-pro的 setting-draw组件挪过来,并做下改造只保留主题设置功能,目录结构如下:


image.png

这里测试代码,是vue2+typescript+javascript混写(项目是typescript+vue2搭建,但是移植的代码是javascript),搭建可参考:Vue2+typescript写法总结

index.ts

import SettingDrawer from "./SettingDrawer.vue"
export default SettingDrawer

settingConfig.js

import themeColor from "./themeColor.js"
const colorList = [
  {
    key: "薄暮", color: "#F5222D"
  },
  {
    key: "火山", color: "#FA541C"
  },
  {
    key: "日暮", color: "#FAAD14"
  },
  {
    key: "明青", color: "#13C2C2"
  },
  {
    key: "极光绿", color: "#52C41A"
  },
  {
    key: "拂晓蓝(默认)", color: "#1890FF"
  },
  {
    key: "极客蓝", color: "#2F54EB"
  },
  {
    key: "酱紫", color: "#722ED1"
  },
  {
    key: "浅紫", color: "#9890Ff"
  }
]

const updateTheme = newPrimaryColor => {
  themeColor.changeColor(newPrimaryColor).finally(() => {
    setTimeout(() => {
    }, 10)
  })
}

export { updateTheme, colorList }

themeColor.js

import client from "webpack-theme-color-replacer/client"
import generate from "@ant-design/colors/lib/generate"

export default {
  getAntdSerials (color) {
    // 淡化(即less的tint)
    const lightens = new Array(9).fill().map((t, i) => {
      return client.varyColor.lighten(color, i / 10)
    })
    // colorPalette变换得到颜色值
    // console.log("lightens", lightens)
    const colorPalettes = generate(color)
    // console.log("colorPalettes", colorPalettes)
    const rgb = client.varyColor.toNum3(color.replace("#", "")).join(",")
    // console.log("rgb", rgb)
    return lightens.concat(colorPalettes).concat(rgb)
  },
  changeColor (newColor) {
    var options = {
      newColors: this.getAntdSerials(newColor), // new colors array, one-to-one corresponde with `matchColors`
      changeUrl (cssUrl) {
        return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path
      }
    }
    return client.changer.changeColor(options, Promise)
  }
}

settingDraw.vue

<template>
  <div class="setting-drawer">
    <div class="setting-drawer-index-content">
      <div :style="{ marginTop: '24px' }">
        <h3 class="setting-drawer-index-title">切换颜色列表</h3>
        <div>
          <a-tooltip class="setting-drawer-theme-color-colorBlock" v-for="(item, index) in colorList" :key="index">
            <template slot="title">
              {{ item.key }}
            </template>
            <a-tag :color="item.color" @click="changeColor(item.color)">
              <a-icon type="check" v-if="item.color === color"></a-icon>
              <a-icon type="check" style="color: transparent;" v-else></a-icon>
            </a-tag>
          </a-tooltip>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { updateTheme, colorList } from "./settingConfig"

export default {
  data () {
    return {
      colorList,
      color: "",
    }
  },
  methods: {
    changeColor (color) {
      if (this.color !== color) {
        this.color = color
        updateTheme(color)
      }
    },
  }
}
</script>

然后我们将theme-example.vue配置成路由页面
theme.example.vue

<template>
  <basic-container>
    <div>
      <a-button type="primary">主色-primary</a-button>
      <a-button type="danger">警告色-danger</a-button>
    </div>
    <!--颜色设置组件-->
    <setting-drawer/>
  </basic-container>
</template>
<script lang="ts">
import BasicContainer from "../../components/layouts/basic-container.vue"
import { Component, Vue } from "vue-property-decorator"
import SettingDrawer from "../../../packages/setting-drawer"

@Component({
  components: {
    BasicContainer,
    SettingDrawer
  },
})
export default class ThemeExample extends Vue {

}
</script>

basic-container.vue

<template>
  <pro-layout :menus="menus" :collapsed="collapsed" :handleCollapse="handleCollapse" :style="{ background: '#0B1C40' }" class="menu-slider">
    <!-- 页面内容-->
    <slot></slot>
  </pro-layout>
</template>

<script>

import ProLayout from "@ant-design-vue/pro-layout"
import exampleRoutes from "../../router/example-routes"

export default {
  name: "BasicContainer",
  components: {
    ProLayout
  },
  data () {
    return {
      menus: [], // 菜单
      collapsed: false, // 侧栏收起状态
    }
  },
  created() {
    this.menus = this.handleMenus([...exampleRoutes])
  },
  methods: {
    /**
     * 处理菜单数据
     */
    handleMenus(routes) {
      const menuRoutes = JSON.parse(JSON.stringify(routes))
      const menus = []
      for (let i = 0; i < menuRoutes.length; i++) {
        delete menuRoutes[i].component
        const { meta, children } = menuRoutes[i]
        const newMenus = { ...menuRoutes[i] }
        if (meta && meta.menu) {
          if (children && children.length) {
            newMenus.children = this.handleMenus(children)
          }
          menus.push(newMenus)
        }
      }
      return menus
    },
    /**
     * 窗口尺寸搜索展开
     * @param val
     */
    handleCollapse (val) {
      this.collapsed = val
    }
  }
}
</script>


菜单数据仅供参考,可自行处理。然后先看下效果

[video(video-b6ofzHj3-1673234646284)(type-csdn)(url-https://live.csdn.net/v/embed/268814)(image-https://video-community.csdnimg.cn/vod-84deb4/e876f8f08fbf71ed80040764a0fd0102/snapshots/dff5edb95111439c846d5582efabd016-00001.jpg?auth_key=4826828998-0-0-1653dcf315a7e0650ef9d205253f2049)(title-切换主题setting-drawer)]

2、插件相关调用栈

可以正常切换主题色,然后我们来看下调用栈

image.png

从上图可以看出,最终由replaceCssText完成样式替换,而cetCssTo调用了replacCssText,我们先看下这两个函数代码


image.png

可以看到这两个函数仅做赋值和替换工作,无其他逻辑
然后我们来看下getCssString


image.png

这里有个判断逻辑,就是是否将css嵌入js,那到底走哪个,我们来操作看下即可。如果没有嵌入,肯定会发起请求。然后因为getCssString在第一次操作才会调用,所以我们要先清空页面(刷新),然后操作看看浏览器的网络请求


image.png

3、颜色提取原理

可以看到,确实发起了请求,并且名字是theme-colors-8addcf28.css

image.png

上图是请求的css文件内容,搜索ant-btn-primary我们发现该内容包含了ant-btn-primary及ant-btn-primary相关的比如hover样式内容,当然还有其他色号是1890ff的内容,以及其他颜色如40a9ff

然后我们尝试搜索ant-btn-danger,却查不到任何结果。当然如果我们将plugin.config中的getAntdSerials函数调用参数改为#F5222D,重启项目后,再测试,这时候搜索结果就会发现,ant-btn-primary差不到任何内容,ant-btn-danger就可以查到

这里说明了一点,webpack-theme-color-replacer确实是通过我们在调用getAntdSerials时传递的颜色参数来提取颜色数据的,我们看下getAantdSerials的返回结果,加上3个打印

const getAntdSerials = (color) => {
  // 淡化(即less的tint)
  const lightens = new Array(9).fill().map((t, i) => {
    return ThemeColorReplacer.varyColor.lighten(color, i / 10)
  })
  console.log("lightens", lightens)
  const colorPalettes = generate(color)
  console.log("colorPalettes", colorPalettes)
  const rgb = ThemeColorReplacer.varyColor.toNum3(color.replace("#", "")).join(",")
  // console.log("rgb", rgb)
  const matchColors = lightens.concat(colorPalettes).concat(rgb)
  console.log("matchColors", matchColors)
  return matchColors
}

重启后看下运行控制台,注意这里看的时运行控制台,不是浏览器控制台,因为这段代码在项目启动时vue.config.js里面就调用了


image.png

结合刚才看到的css返回结果里面有其他颜色的情况,我们不妨对比查找一下,果然像40a9ff,e6f7ff等颜色,两边都存在。也就是插件内部通过matchColors的颜色结果去提取颜色样式

matchColors包含两部分,一部分是webpack-theme-color-replacer的计算颜色,另一部分是ant-design-vue的计算颜色,其中ant-design-vue的计算颜色和ant-design-vue的颜色设计体系相关,比如hover颜色,active颜色。webpack-theme-color-replacer的计算颜色的依据暂不清楚,不过我们可以看到,他们都是根据我们提供的颜色的深浅变化颜色


image.png

另外,在setCssText里面,将css代码注入到body里面,便于读取,我们点开页面html,可以看到


image.png

这里的css内容,和theme-colors-8addcf28.css文件的内容一致

4、思路问题重新整理

到这里,我们暂时先对之前的分析做下整理
a、我们注册插件时,插件通过我们提供的颜色 getAntdSerials("#1890ff") 去做样式筛选
b、筛选后的颜色,存放在一个css文件中
c、在第一次替换时,请求提取出来的css文件内容
d、将请求到的css内容提取出来放在页面的style标签里面
e、读取style标签的css内容,根据正则匹配替换后,重新赋值回去,完成颜色替换

想要达到我们的目标,比如可以分别对primary和danger的颜色进行替换,就要弄清楚以下几点

a、theme-colors-8addcf28.css 的内容是在哪里生成的?
我们现在知道是根据我们提供的颜色筛选出来的,但是在哪筛选?还不知道,之前看过的文件里面没有找到筛选的具体代码,一上来就是直接请求theme-colors-8addcf28.css的内容
b、theme-colors-8addcf28.css 文件名是如何定义的?
当前插件只支持一种颜色及其变化颜色的替换,并且theme-colors-8addcf28.css里面只有一种颜色(包括变化颜色),想要分别支持多种,怕是要有多个文件才行

5、theme-colors-8addcf28.css url 来源查找

既然如此,我们就先根据请求theme-colors-8addcf28.css文件的url参数进行追踪,结合之前的调用栈分析代码,我们很快就找到了目标代码


image.png

这里首先theme_COLOR_config是文件内的变量,它是由win()[WP_THEME_CONFIG]赋值而来
第二,WP_THEME_CONFIG 是一个全局的变量,也就是window.WP_THEME_CONFIG,当前文件没有,我们得去其他地方查找
第三,cssUrl有两个来源,theme_COLOR_config.url 或者 options.cssUrl,至于是哪个,我们打印确认一下


image.png

添加打印代码后,我们操作一下,看下浏览器控制台


image.png

显然,url和 WP_THEME_CONFIG 有关

**查找WP_THEME_CONFIG **
当前文件没有 WP_THEME_CONFIG 的定义 ,那我们只能去其他地方查找,首先我们看下vue.config.js,这里面显然没有,plugin.config.js也没有。这两处项目主题插件注册相关的文件没有,那就只能去插件内部找找看了。


image.png

上面是插件的文件结构,themeColorChanger.js我们已经看过,formElementUI不用看,这个看名字就知道是专门给element-ui写的插件,其他文件,我们就逐个翻一遍吧。最终我们在src下的index.js里面找到这个变量的定义


image.png

这是注册webpack插件的时候挂载进去的,由JSON.stringify(this.handler.options.configVar)赋值而来,接下来我们对this.handler.options.configVar进行追踪

configVar追踪

image.png

然后我们很快就在Handler.js里面找到了相关代码
第一我们看到了configVar的定义
第二,我们看到了和theme-colors-8addcf28.css很像的fileName

回到themeColorChange.js,theme_COLOR_config 是通过调用win函数,然后取WP_THEME_CONFIG 变量属性得来,我们不妨先看下win函数调用得结果


image.png

win执行的结果就是window对象,点开后,我们在一大堆属性里面,找到tc_cfg_7781740664726529,即configVar


image.png

之所以找tc_cfg_7781740664726529,是根据configVar: 'tc_cfg_' + Math.random().toString().slice(2)、WP_THEME_CONFIG: JSON.stringify(this.handler.options.configVar)和win()[WP_THEME_CONFIG]几行代码推断而来,查看结果后也证明了我们的猜测,configVar是挂载到window下的属性键名,而fileName则是属性里面的url

下面我们将css改为css2,tc_cfg_改为tc_cfg_test_


image.png

重启项目测试一下


image.png image.png

确实已经被更改

然后我们在Handler.js的this.options下面看到这行代码,this.assetsExtractor = new AssetsExtractor(this.options),也就是optins的配置是在AssetsExtractor类中处理的

由于篇幅太长,我们接下来的进一步追踪在下一篇:《前端组件库自定义主题切换探索-02-webpack-theme-color-replacer webpack 的实现逻辑和原理-02》 中来进行吧

上一篇下一篇

猜你喜欢

热点阅读