美文网首页
前端组件库自定义主题切换探索-02-webpack-theme-

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

作者: 东扯葫芦西扯瓜 | 来源:发表于2023-01-09 17:21 被阅读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》 中来进行吧

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/civucdtx.html