接上一篇《前端组件库自定义主题切换探索-02-webpack-theme-color-replacer webpack 的实现逻辑和原理-02》
这篇我们来开始改造,让这个插件最终能达到我们的目的:
首先修改plugin.config.js。
插件首先要在vue.config.js引用注册,因此先对这里做改造。这里我们指定了四种颜色,primary,danger,warning,other,然后生成配置数据,数据格式如下
{
primary: {
matchColors: getAntdSerials(colorTypes[type]), // colors array for extracting css file, support rgb and hsl.
fileName: `css/${type}-colors-[contenthash:8].css`, // optional. output css file name, suport [contenthash] and [hash].
configVar: `tc_cfg_${type}` + Math.random().toString().slice(2),
}
}
后面的代码逻辑都会根据对象的键名(比如primary)来读取每个键名下面的配置来做批量操作
最终代码如下:
const ThemeColorReplacer = require("./webpack-theme-color-replacer/src/index")
// const ThemeColorReplacer = require("webpack-theme-color-replacer")
const generate = require("@ant-design/colors/lib/generate").default
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
}
const getRandomString = () => Math.random().toString().slice(2)
const colorTypes = {
primary: "#1890ff",
danger: "#F5222D",
warning: "#F2A830",
other: "#35964f",
}
const option = {}
for (const type in colorTypes) {
option[type] = {
matchColors: getAntdSerials(colorTypes[type]), // colors array for extracting css file, support rgb and hsl.
fileName: `css/${type}-colors-[contenthash:8].css`, // optional. output css file name, suport [contenthash] and [hash].
configVar: `tc_cfg_${type}` + getRandomString(),
}
}
const createThemeColorReplacerPlugin = () => new ThemeColorReplacer({
option
})
module.exports = createThemeColorReplacerPlugin
在这个验证测试代码过程中,也遇到了不少问题,这里为了省事点,我们就不赘述中间的曲折,直接上改好的代码,后面再附加一些注意的地方
第二个改scr下面的index.js,
这个文件主要是根据option生成LC_THEME_CONFIG变量里面的内容,关键代码是
const { option } = this.handler.options
for (const i in option) {
webpackDefineConfig[i] = option[i].configVar
}
new webpack.DefinePlugin({
LC_THEME_CONFIG: JSON.stringify(webpackDefineConfig)
}).apply(compiler)
完整的代码如下:
"use strict"
var Handler = require("./handler")
var webpack = require("webpack")
class ThemeColorReplacer {
constructor(options) {
this.handler = new Handler(options)
}
getBinder(compiler, event) {
return compiler.hooks
? compiler.hooks[event].tapAsync.bind(compiler.hooks[event], "ThemeColorReplacer")
: compiler.plugin.bind(compiler, event)
}
apply(compiler) {
const webpackDefineConfig = {}
const { option } = this.handler.options
for (const i in option) {
webpackDefineConfig[i] = option[i].configVar
}
new webpack.DefinePlugin({
LC_THEME_CONFIG: JSON.stringify(webpackDefineConfig)
}).apply(compiler)
compiler.hooks.thisCompilation.tap("ThemeColorReplacer", (compilation) => {
compilation.hooks.processAssets.tapAsync(
{
name: "ThemeColorReplacer",
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS
},
(compilationAssets, callback) => {
this.handler.handle(compilation)
callback()
})
})
}
}
ThemeColorReplacer.varyColor = require("../client/vary-color")
module.exports = ThemeColorReplacer
第三改造index.js直接引用的Handler.js
这里需要改造的关键地方是
const { option } = this.options
// let { injectToHtml } = this.options
for (const i in option) {
const { fileName, matchColors, configVar } = option[i]
const output = this.assetsExtractor.extractAssets(compilation.assets, matchColors, i)
// console.log("handle output", output)
// console.log("Extracted theme color css content length: " + output.length)
const outputName = compilation.getPath(replaceFileName(fileName, output), {})
this.emitSource(compilation, outputName, new wpSources.RawSource(output))
// console.log("fileName", fileName)
// console.log("matchColors", matchColors)
// 记录动态的文件名,到每个入口js
// console.log("outputName", outputName)
this.addToEntryJs(outputName, compilation, output, matchColors, configVar)
}
另外,其他地方需要传递matchColors和configVar,之前因为只改一种颜色,所以是直接用最外面传递进来的,现在要改变多个则需要在for循环里面传递,最终修改代码如下:
"use strict"
var webpack = require("webpack")
var AssetsExtractor = require("./assets-extractor")
var replaceFileName = require("./replace-file-name")
var LineReg = /\n/g
var wpSources = webpack.sources
if (!wpSources) {
wpSources = require("webpack-sources") // for webpack 4
}
module.exports = class Handler {
constructor(options) {
this.options = {
isJsUgly: !(process.env.NODE_ENV === "development" || process.argv.find(arg => arg.match(/\bdev/))),
...options
}
this.assetsExtractor = new AssetsExtractor(this.options.isJsUgly, this.options.changeSelector)
}
// Add Webpack5 Support
emitSource(compilation, name, source) {
console.log("emitSource name", name)
var exists = compilation.assets[name]
if (compilation.updateAsset) { // webpack.version[0] >= '5'
if (exists) compilation.updateAsset(name, source)
else compilation.emitAsset(name, source)
} else {
if (exists) delete compilation.assets[name]
compilation.assets[name] = source
}
}
handle(compilation) {
// Add to assets for output
const { option } = this.options
// let { injectToHtml } = this.options
for (const i in option) {
const { fileName, matchColors, configVar } = option[i]
const output = this.assetsExtractor.extractAssets(compilation.assets, matchColors, i)
// console.log("handle output", output)
// console.log("Extracted theme color css content length: " + output.length)
const outputName = compilation.getPath(replaceFileName(fileName, output), {})
this.emitSource(compilation, outputName, new wpSources.RawSource(output))
// console.log("fileName", fileName)
// console.log("matchColors", matchColors)
// 记录动态的文件名,到每个入口js
// console.log("outputName", outputName)
this.addToEntryJs(outputName, compilation, output, matchColors, configVar)
}
}
// 自动注入js代码,设置css文件名
addToEntryJs(outputName, compilation, cssCode, matchColors, configVar) {
const onlyEntrypoints = {
entrypoints: true,
errorDetails: false,
modules: false,
assets: false,
children: false,
chunks: false,
chunkGroups: false
}
const entrypoints = compilation.getStats().toJson(onlyEntrypoints).entrypoints
Object.keys(entrypoints).forEach(entryName => {
const entryAssets = entrypoints[entryName].assets
for (let i = 0, l = entryAssets.length; i < l; i++) {
const assetName = entryAssets[i].name || entryAssets[i]
if (assetName.slice(-3) === ".js" && assetName.indexOf("manifest.") === -1) { //
const assetSource = compilation.assets[assetName]
if (assetSource && !assetSource._isThemeJsInjected) {
const cSrc = this.getEntryJs(outputName, assetSource, cssCode, matchColors, configVar)
// cSrc._isThemeJsInjected = true
this.emitSource(compilation, assetName, cSrc)
break
}
}
}
})
}
getConfigJs(outputName, cssCode, matchColors, configVar) {
const config = { url: outputName, colors: matchColors }
if (this.options.injectCss) {
config.cssCode = cssCode.replace(LineReg, "")
}
return "\n(typeof window=='undefined'?global:window)." + configVar + "=" + JSON.stringify(config) + ";\n"
}
getEntryJs(outputName, assetSource, cssCode, matchColors, configVar) {
const ConcatSource = wpSources.ConcatSource
const CachedSource = wpSources.CachedSource
const configJs = this.getConfigJs(outputName, cssCode, matchColors, configVar)
if (assetSource instanceof CachedSource) { // CachedSource没有node方法,会报错
return new CachedSource(concatSrc(assetSource._source || assetSource.source(), configJs))
}
return concatSrc(assetSource, configJs)
function concatSrc(assetSource, configJs) {
if (assetSource instanceof ConcatSource) {
assetSource.add(configJs)
return assetSource
} else {
return new ConcatSource(assetSource, configJs)
}
}
}
}
这里需要注意的是,在addToEntryJs函数里面,需要吧cSrc._isThemeJsInjected = true去掉。这里是之前插件为了做缓存用的,在测试中发现,加上这一行,window里面最多挂两个tc_cfg_变量,
在原本的判断里面,执行cSrc._isThemeJsInjected = true时,assetSource._isThemeJsInjected 也会变为true,因为this.getEntryJs返回的是一个浅拷贝,且和assetSource同源,从下面的代码可以看到数据的来源
数据最终通过webpack处理而来,至于为什么是浅拷贝,是同源的,没有得去深究。
replace-file-name 不需要更改,源代码即可。
第四需要改的是assets-extractor.js
(原插件是AssetsExtractor,这里为了符合开发规范改了名字)文件。这里没有大改的地方,而是将参数改为从调用的地方传递,而不是直接取自option,改动后代码如下:
var Extractor = require("./extractor")
var cssLoaderRegDev = /\bn?(?:exports|___CSS_LOADER_EXPORT___)\.push\(\[module\.id?, \\?"(.+?\})(?:\\?\\n)?(?:[\\n]*\/\*#\s*sourceMappingURL=.+?\*\/)?\\?", \\?"\\?"(?:\]\)|,\s*\{)/g
// css-loader: n.exports=t("FZ+f")(!1)).push([n.i,"\n.payment-type[data-v-ffb10066] {......}\n",""])
var cssLoaderRegUgly = /\.push\(\[\w+\.i,['"](.+?\})[\\rn]*['"],['"]['"](?:\]\)|,\{)/g
var CssExtReg = /\.css$/i; var JsExtReg = /\.js$/i
function assetToStr(asset) {
var src = asset.source() || ""
return src.toString()
}
const extractAsset = function (fn, asset, matchColors, isJsUgly, changeSelector) {
const src = assetToStr(asset)
var cssSrcs = []
var CssCodeReg = isJsUgly ? cssLoaderRegUgly : cssLoaderRegDev
src.replace(CssCodeReg, (match, $1) => {
cssSrcs = cssSrcs.concat(Extractor(changeSelector, $1, matchColors))
})
// console.log("cssSrcs", cssSrcs.filter(item=>item))
return cssSrcs.filter(item => item)
}
function extractAll(assets, matchColors, isJsUgly, changeSelector, type) {
// console.log("extractAll matchColors", matchColors)
var cssSrcs = []
Object.keys(assets).map(fn => {
// 原本每修改一点源码,都需要对整个项目的assets翻一遍css,影响性能。
// 故改为在asset上缓存上一次的结果,对没发生变化的asset直接取缓存(已发生变化的asset已经是新对象,无缓存)。
// console.log("fn", fn)
const asset = assets[fn]
// console.log("asset._themeCssCache", asset._themeCssCache)
let cssRules = ""
if (asset._themeCssCache && asset._themeCssCache[type]) {
cssRules = asset._themeCssCache[type]
} else {
cssRules = extractAsset(fn, asset, matchColors, isJsUgly, changeSelector)
}
// asset._themeCssCache || extractAsset(fn, asset, matchColors, isJsUgly, changeSelector)
// console.log("cssRules", cssRules)
if (asset._themeCssCache) {
asset._themeCssCache[type] = cssRules
} else {
asset._themeCssCache = {
[type]: cssRules
}
}
cssSrcs = cssSrcs.concat(cssRules)
})
// console.log("cssSrcs", cssSrcs)
// console.log("cssSrcs.filter(item => item)", cssSrcs.filter(item => item))
return cssSrcs
}
module.exports = function AssetsExtractor(isJsUgly, changeSelector) {
this.extractAssets = function (assets, matchColors, type) {
// console.log("assets", assets)
var srcArray = this.extractToArray(assets, matchColors, type)
// console.log("srcArray", srcArray)
// 外部的css文件。如cdn加载的
var output = dropDuplicate(srcArray).join("\n")
return output
}
this.extractToArray = function (assets, matchColors, type) {
var srcArray = extractAll(assets, matchColors, isJsUgly, changeSelector, type)
// console.log("srcArray------------------------------------------------------", srcArray)
if (srcArray.length === 0 && !this._uglyChanged) {
// 容错一次
this._uglyChanged = true
isJsUgly = !isJsUgly
// 清空缓存
Object.keys(assets).map(fn => assets[fn]._themeCssCache = 0)
srcArray = extractAll(assets, matchColors, isJsUgly, changeSelector, type)
}
return srcArray
}
}
function dropDuplicate(arr) {
var map = {}
var r = []
for (var s of arr) {
if (!map[s]) {
r.push(s)
map[s] = 1
}
}
return r
}
这里需要注意的一点是,asset._themeCssCache 需要按照外面传递的类型来存储,否则后面的颜色不生效
const asset = assets[fn]
// console.log("asset._themeCssCache", asset._themeCssCache)
let cssRules = ""
if (asset._themeCssCache && asset._themeCssCache[type]) {
cssRules = asset._themeCssCache[type]
} else {
cssRules = extractAsset(fn, asset, matchColors, isJsUgly, changeSelector)
}
// asset._themeCssCache || extractAsset(fn, asset, matchColors, isJsUgly, changeSelector)
// console.log("cssRules", cssRules)
if (asset._themeCssCache) {
asset._themeCssCache[type] = cssRules
} else {
asset._themeCssCache = {
[type]: cssRules
}
}
第五需要改的是assets-extractor.js直接引用的extractor.js。
这里需要改动的是将matchColors从外部直接传入,而不是从option取用
改动后代码如下
var extractorCss = require("./css-extractor")
const testCssCode = function (cssCode, matchColors) {
var matchColorRegs = matchColors // ['#409EFF', '#409eff', '#53a8ff', '#66b1ff', '#79bbff', '#8cc5ff', '#a0cfff', '#b3d8ff', '#c6e2ff', '#d9ecff', '#ecf5ff', '#3a8ee6', '#337ecc']
.map(c => new RegExp(c.replace(/\s/g, "").replace(/,/g, ",\\s*") + "([\\da-f]{2})?(\\b|\\)|,|\\s)", "i")) // 255, 255,3
for (var colorReg of matchColorRegs) {
if (colorReg.test(cssCode)) return true // && !ExclueCssReg.test(cssCode)
}
return false
}
module.exports = function Extractor(changeSelector, src, matchColors) {
return extractorCss(src, changeSelector).map(function (css) {
var rules = css.rules.filter(cssCode => testCssCode(cssCode, matchColors))
if (!rules.length) return ""
return css.selector + "{" + rules.join(";") + "}"
})
}
第六个需要改的是extractor.js直接引用的css-extractor.js,
同样是需要改动传参方式,最终代码如下
// \n和备注
var regLfRem = /\\\\?n|\n|\\\\?t|\\\\?r|\/\*[\s\S]+?\*\//g
var SpaceReg = /\s+/g
var TrimReg = /(^|,)\s+|\s+($)/g // 前空格,逗号后的空格; 后空格
var SubCssReg = /\s*>\s*/g // div > a 替换为 div>a
var DataUrlReg = /url\s*\([\\'"\s]*data:/ // url("data:image/svg+xml;base64,PHN2")
var QuotReg = /\\+(['"])/g
// var ExclueCssReg = /(?:scale3d|translate3d|rotate3d|matrix3d)\s*\(/i;
module.exports = function extractCss(src, changeSelector) {
src = src.replace(regLfRem, "")
var ret = []
var nameStart; var nameEnd; var cssEnd = -1
while (true) {
nameStart = cssEnd + 1
nameEnd = src.indexOf("{", nameStart)
cssEnd = findCssEnd(src, nameEnd)
if (cssEnd > -1 && cssEnd > nameEnd && nameEnd > nameStart) {
var cssCode = src.slice(nameEnd + 1, cssEnd)
if (cssCode.indexOf("{") > -1) { // @keyframes
var rules = extractCss(cssCode, changeSelector)
} else {
rules = getRules(cssCode)
}
if (rules.length) {
var selector = src.slice(nameStart, nameEnd)
selector = selector.replace(TrimReg, "$1")
selector = selector.replace(SubCssReg, ">")
selector = selector.replace(SpaceReg, " ") // lines
var p = selector.indexOf(";") // @charset utf-8;
if (p > -1) {
selector = selector.slice(p + 1)
}
// 改变选择器
if (changeSelector) {
var util = {
rules: rules,
changeEach: changeEach
}
selector = changeSelector(selector.split(",").sort().join(","), util) || selector
}
ret.push({ selector, rules: rules })
}
} else {
break
}
}
return ret
// 查找css尾部,兼容 @keyframes {10%{...}}
function findCssEnd(src, start) {
var level = 1
var cssEnd = start
while (true) {
cssEnd++
var char = src[cssEnd]
if (!char) {
return -1
} else if (char === "{") {
level++
} else if (char === "}") {
level--
if (level === 0) {
break
}
}
}
return cssEnd
}
function changeEach(selector, surfix, prefix) {
surfix = surfix || ""
prefix = prefix || ""
return selector.split(",").map(function (s) {
return prefix + s + surfix
}).join(",")
}
}
function getRules(cssCode) {
var rules = cssCode.split(";")
var ret = []
for (var i = 0; i < rules.length; i++) {
var rule = rules[i].replace(/^\s+|\s+$/, "")
if (!rule) continue
if (rule.match(DataUrlReg)) {
rule += ";" + rules[i + 1]
rule = rule.replace(QuotReg, "$1")
i++
}
ret.push(rule.replace(SpaceReg, " "))
}
return ret
}
现在src下面的文件更改完了,我们先去改调用处的vue文件。
第七是路由文件theme-example.vue
<template>
<basic-container>
<div>
<a-button type="primary">主色-primary</a-button>
<a-button type="danger">报错色-danger</a-button>
<span class="my-theme-color">测试自定义主题色</span>
<span class="my-warning-color">测试自定义警告色</span>
<span class="my-other-color">测试自定义其他颜色</span>
</div>
<setting-drawer ref="settingDrawer"/>
</basic-container>
</template>
<script lang="ts">
import BasicContainer from "../../components/layouts/basic-container2.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>
<style scoped lang="less">
.my-theme-color{
color: #1890ff;
}
.my-warning-color{
color: #F2A830;
}
.my-other-color{
color: #35964f;
}
</style>
第八是setting-drawer.vue,至于basic-container2.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,index)">
<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, index) {
updateTheme({
primary: color,
danger: this.colorList[index + 1] ? this.colorList[index + 1].color : this.colorList[0].color,
warning: this.colorList[index + 2] ? this.colorList[index + 2].color : (this.colorList[index + 1] ? this.colorList[0].color : this.colorList[1].color),
other: this.colorList[index + 3] ? this.colorList[index + 3].color : (this.colorList[index + 2] ? this.colorList[index + 2].color : this.colorList[index + 1] ? this.colorList[0].color : this.colorList[2].color),
})
},
}
}
</script>
第九需要改动的是setting-drawer.vue直接引用的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 = (changeColors) => {
// no-undef
themeColor.changeColor(changeColors).finally(() => {
})
}
export { updateTheme, colorList }
第十需要改动的是settingConfig.js直接引用的themeColor.js
import client from "../../../config/webpack-theme-color-replacer/client"
import generate from "@ant-design/colors/lib/generate"
export default {
/**
* 获取变化的颜色
* @param color
* @returns {T[]}
*/
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)
},
/**
* 改变颜色
* @param changeColors
* @returns {Promise<unknown>|Promise<unknown>}
*/
changeColor (changeColors) {
const options = {}
for (const i in changeColors) {
options[i] = {
newColors: this.getAntdSerials(changeColors[i]),
changeUrl (cssUrl) {
return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path
}
}
}
return client.changer.changeColor(options)
}
}
这里的关键改动是根据我们的需要组装插件需要的数据源,关键代码如下:
for (const i in changeColors) {
options[i] = {
newColors: this.getAntdSerials(changeColors[i]),
changeUrl (cssUrl) {
return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path
}
}
}
第十一需要改动的是theme-color-changer.js,
这里的关键代码是将themeColorConfig按照类型存储,和按照类型从LC_THEME_CONFIG和window中取用数据,关键代码如下:
for (const i in option) {
if (!themeColorConfig[i]) {
// eslint-disable-next-line no-undef
console.log("LC_THEME_CONFIG[i]", LC_THEME_CONFIG[i])
// eslint-disable-next-line no-undef
themeColorConfig[i] = win()[LC_THEME_CONFIG[i]]
const later = retry(i)
// 重试直到themeColorConfig加载
console.log("later", later)
if (later) return later
}
const { oldColors, newColors, cssUrl, changeUrl } = option[i]
oldColorsObj[i] = oldColors || themeColorConfig[i].colors || []
newColorsObj[i] = newColors || []
const cssUrlValue = themeColorConfig[i].url || cssUrl
cssUrlObj[i] = changeUrl ? changeUrl(cssUrlValue) : cssUrlValue // url可能被changeUrl改变
}
这里的option就是注册插件时传递的option,即代码const createThemeColorReplacerPlugin = () => new ThemeColorReplacer({
option
})。
另外 oldColorsObj ,newColorsObj ,cssUrlObj 也需要改为对象形式,需要按照键名存储
其他改动后的完整代码如下:
const _urlColors = {} // {[url]: {id,colors}}
const themeColorConfig = {}
module.exports = {
_tryNum: 0,
_tryNumObj: {},
changeColor: function (option) {
const _this = this
const oldColorsObj = {}
const newColorsObj = {}
const cssUrlObj = {}
console.log("wen()", win())
// eslint-disable-next-line no-undef
console.log("LC_THEME_CONFIG", LC_THEME_CONFIG)
console.log("option", option)
for (const i in option) {
if (!themeColorConfig[i]) {
// eslint-disable-next-line no-undef
console.log("LC_THEME_CONFIG[i]", LC_THEME_CONFIG[i])
// eslint-disable-next-line no-undef
themeColorConfig[i] = win()[LC_THEME_CONFIG[i]]
const later = retry(i)
// 重试直到themeColorConfig加载
console.log("later", later)
if (later) return later
}
const { oldColors, newColors, cssUrl, changeUrl } = option[i]
oldColorsObj[i] = oldColors || themeColorConfig[i].colors || []
newColorsObj[i] = newColors || []
const cssUrlValue = themeColorConfig[i].url || cssUrl
cssUrlObj[i] = changeUrl ? changeUrl(cssUrlValue) : cssUrlValue // url可能被changeUrl改变
}
console.log("themeColorConfig", themeColorConfig)
return new Promise(function (resolve, reject) {
const optionKeys = Object.keys(option || {})
const isSameArrReturn = optionKeys.every(key => isSameArr(oldColorsObj[key], newColorsObj[key])) // 判断是不是所有的都相同
// console.log("isSameArrReturn", isSameArrReturn)
if (isSameArrReturn) {
resolve()
} else {
for (const i in option) {
const last = _urlColors[cssUrlObj[i]]
if (last) {
// 之前已替换过
oldColorsObj[i] = last.colors
}
if (!isSameArr(oldColorsObj[i], newColorsObj[i])) {
setCssText(last, cssUrlObj[i], oldColorsObj[i], newColorsObj[i], i, resolve, reject)
}
}
}
})
function retry(type) {
if (!themeColorConfig[type]) {
if (_this._tryNumObj[type] < 9) {
_this._tryNumObj[type] = _this._tryNumObj[type] + 1
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(_this.changeColor(option))
}, 100)
})
} else {
themeColorConfig[type] = {}
}
}
}
function setCssText(last, url, oldColors, newColors, type, resolve, reject) {
// console.log("last=", last, ",url=", url, ",oldColors=", oldColors, ",newColors=", newColors, ",type=", type,)
let elStyle = last && document.getElementById(last.id)
if (elStyle && last.colors) {
setCssTo(elStyle.innerText)
last.colors = newColors
resolve()
} else {
// 第一次替换
const id = "css_" + type + (+new Date())
console.log("第一次替换")
console.log("id", id)
elStyle = document.querySelector(option.appendToEl || "body")
.appendChild(document.createElement("style"))
// console.log("elStyle", elStyle)
elStyle.setAttribute("id", id)
// console.log("url", url)
_this.getCssString(url, function (cssText) {
// console.log("cssText", cssText)
setCssTo(cssText)
_urlColors[url] = { id: id, colors: newColors }
resolve(cssText)
}, reject)
}
function setCssTo(cssText) {
cssText = _this.replaceCssText(cssText, oldColors, newColors)
elStyle.innerText = cssText
}
}
},
replaceCssText: function (cssText, oldColors, newColors) {
oldColors.forEach(function (color, t) {
// #222、#222223、#22222350、222, 255,3 => #333、#333334、#33333450、211,133,53、hsl(27, 92.531%, 52.745%)
const reg = new RegExp(color.replace(/\s/g, "").replace(/,/g, ",\\s*") + "([\\da-f]{2})?(\\b|\\)|,|\\s)", "ig")
cssText = cssText.replace(reg, newColors[t] + "$1$2") // 255, 255,3
})
return cssText
},
getCssString: function (url, resolve, reject) {
// console.log("url", url)
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.responseText)
} else {
reject(xhr.status)
}
}
}
xhr.onerror = function (e) {
reject(e)
}
xhr.ontimeout = function (e) {
reject(e)
}
xhr.open("GET", url)
xhr.send()
},
}
function win() {
return typeof window === "undefined" ? global : window
}
function isSameArr(oldColors, newColors) {
if (oldColors.length !== newColors.length) {
return false
}
for (let i = 0, j = oldColors.length; i < j; i++) {
if (oldColors[i] !== newColors[i]) {
return false
}
}
return true
}
到这里就结束了,vary-color.js不需要改动。然后我们运行后看下效果(注意每次改动均需重启项目)
颜色批量替换演示
我们可以看到,页面上成功同时替换了4种颜色。到此这个测试完成。但是我们在页面上实现让用户自定义各种颜色的期望能实现吗?
答案是No!!
虽然这不是我们想要的答案,但是还是要接受目前的现实。下面讲下原因:
1、webpack-theme-color-replacer 这个插件只会根据颜色色号去替换,也就是说如果用户自己在主题色和危险色上选了相同的颜色,那么出现的结果会是项目上的主题色和危险色都被同时更改,并且分不开了,除非用户重置。而且如果我们修改css查找正则,比如只找css属性名带primary的,又会导致没有用ant-design样式的颜色不会被替换
2、其他的比如圆角等的修改,估计会波及无辜。因为比如 border-radius: 2px,这样的如果按照目前插件的替换逻辑,只会替换2px,会殃及一大片无辜的样式,就算按照规则改成替换border-radius: 2px整个字符,也会殃及不少的无辜样式。所以并不现实。
所以,好想赶紧升级到vue3啊,可以很爽快的使用css变量来实现这些逻辑!!