前端组件库自定义主题切换探索-03-webpack-theme-color-replacer webpack 同时替换多个颜色改造

32 篇文章 0 订阅

接上一篇《前端组件库自定义主题切换探索-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变量来实现这些逻辑!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值