本文来研究写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追踪
然后我们很快就在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》 中来进行吧
网友评论