背景:
产品A:现在小蓝APP的这个xx功能要同步到小黄APP,你们看看这些页面的主题色给改为小黄的吧?
开发B:没问题!
动态改变站点主题色,主流方案有两种:
方案一:webpack-theme-color-replacer
原理:插件分为两部分,编译时和浏览器运行时。
在打包时,利用webpack钩子,将含有指定css色值的样式提取出来,生成一个单独的theme.css文件,跟最终产物一起丢到服务器上。
运行时,在入口文件调用webpack-theme-color-replacer/client端API,并传入目标色值,请求theme.css文件,利用正则匹配,用目标色值替换指定色值,将最终的css包含在style标签插入index.html,覆盖掉原有色值。
原理详细分析
在理解详细原理之前,先让我们简单了解几个开发webpack 插件相关的定义:
Compiler
webpack的核心模块之一,提供了一系列插件编译过程的钩子,该类扩展自tapable类,因此可以通过tapable API在不同时期的钩子上注册,然后在该时期触发。另外在创建compilation的实例时,会传入cli、node API相关的options。
this.hooks = Object.freeze({
/** @type {SyncHook<[]>} */
initialize: new SyncHook([]),
/** @type {SyncBailHook<[Compilation], boolean>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<[Stats]>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {SyncHook<[Stats]>} */
afterDone: new SyncHook(["stats"]),
/** @type {AsyncSeriesHook<[]>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<[Compiler]>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compiler]>} */
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
emit: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */
assetEmitted: new AsyncSeriesHook(["file", "info"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
afterEmit: new AsyncSeriesHook(["compilation"]),
/** @type {SyncHook<[Compilation, CompilationParams]>} */
thisCompilation: new SyncHook(["compilation", "params"]),
/** @type {SyncHook<[Compilation, CompilationParams]>} */
compilation: new SyncHook(["compilation", "params"]),
/** @type {SyncHook<[NormalModuleFactory]>} */
normalModuleFactory: new SyncHook(["normalModuleFactory"]),
/** @type {SyncHook<[ContextModuleFactory]>} */
contextModuleFactory: new SyncHook(["contextModuleFactory"]),
/** @type {AsyncSeriesHook<[CompilationParams]>} */
beforeCompile: new AsyncSeriesHook(["params"]),
/** @type {SyncHook<[CompilationParams]>} */
compile: new SyncHook(["params"]),
/** @type {AsyncParallelHook<[Compilation]>} */
make: new AsyncParallelHook(["compilation"]),
/** @type {AsyncParallelHook<[Compilation]>} */
finishMake: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
afterCompile: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<[]>} */
readRecords: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<[]>} */
emitRecords: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<[Compiler]>} */
watchRun: new AsyncSeriesHook(["compiler"]),
/** @type {SyncHook<[Error]>} */
failed: new SyncHook(["error"]),
/** @type {SyncHook<[string | null, number]>} */
invalid: new SyncHook(["filename", "changeTime"]),
/** @type {SyncHook<[]>} */
watchClose: new SyncHook([]),
/** @type {AsyncSeriesHook<[]>} */
shutdown: new AsyncSeriesHook([]),
/** @type {SyncBailHook<[string, string, any[]], true>} */
infrastructureLog: new SyncBailHook(["origin", "type", "args"]),
// TODO the following hooks are weirdly located here
// TODO move them for webpack 5
/** @type {SyncHook<[]>} */
environment: new SyncHook([]),
/** @type {SyncHook<[]>} */
afterEnvironment: new SyncHook([]),
/** @type {SyncHook<[Compiler]>} */
afterPlugins: new SyncHook(["compiler"]),
/** @type {SyncHook<[Compiler]>} */
afterResolvers: new SyncHook(["compiler"]),
/** @type {SyncBailHook<[string, Entry], boolean>} */
entryOption: new SyncBailHook(["context", "entry"])
});
Compilation
扩展自Tapable,也提供了一些生命周期钩子,见官方文档。可以访问所有的模块和它们的依赖(大部分是循环依赖)
一个 compilation 对象代表了一次单一的版本构建和生成资源,它储存了当前的模块资源、编译生成的资源、变化的文件、以及被跟踪依赖的状态信息。简单来说,Compilation的职责就是对所有 require 图(graph)中对象的字面上的编译,构建 module 和 chunk,在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。
compilation 编译可以多次执行,如在watch模式下启动 webpack,每次监测到源文件发生变化,都会重新实例化一个compilation对象,从而生成一组新的编译资源。
插件核心代码:webpack编译层(适用webpack5)
const { Compilation } = require('webpack')
const Handler = require('./Handler')
const pluginName = 'ThemeColorReplacer'
class ThemeColorReplacer {
constructor(options) {
this.handler = new Handler(options)
}
apply(compiler) {
compiler.hooks.compilation.tap(pluginName, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: pluginName,
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, // see below for more stages
},
(assets) => {
this.handler.handle(assets)
}
)
})
}
}
ThemeColorReplacer.varyColor = require('../client/varyColor')
module.exports = ThemeColorReplacer
上述代码即在compilation时期,在processAssets阶段,执行Handler实例的handle方法,参数为需要处理的资源文件列表,如下图:
Handler核心逻辑
通过上图,可知在processAssets阶段,webpack会提取能匹配传入色值样式,额外生成一个样式文件,并在匹配的第一个js文件中插入window.__theme_COLOR_cfg变量,上面有样式文件地址和匹配的色值,如果配置传入injectCss,还会插入window.__theme_COLOR_css,即将所有样式直接插入js中。如下图
插件核心代码:浏览器运行层(适用webpack5)
该插件还导出了一个client文件,导出了一个changer类,用来读取webpack插件生成的样式文件,并用style包裹插入index.html中,来覆盖原本样式
总结
webpack-theme-color-replacer 整体原理较为简单,但目前使用下来存在两点问题:
- 需要去拉取提取后的文件或将整体文件塞入js中,对页面性能有一定影响
- 微前端这种多个业务站点需要改变主题色,需要多个站点去配置,并保持一致性,有一定复杂度
方案二:CSS变量
浏览器原生支持的方式,目前vant, ant等改变主题色的主流方案,使用较简单,且除IE浏览器外,兼容性较好。
使用方法
1. root节点定义css变量
:root {
--blue-50: #F0F6FF;
--blue-100: #D6E4FF;
--blue-200: #ACC9FE;
--blue-300: #598BFF;
--blue-400: #3D6FF5;
--blue-500: #305DF0;
--blue-600: #1F4DCC;
--blue-700: #0D40A6;
--gray-500: #666F80;
}
2. 组件样式使用变量设置
// 由于我们组件库最开始使用less定义的样式,因此在这直接用css变量覆盖less定义即可
@blue-50: var(--blue-50);
@blue-100: var(--blue-100);
@blue-200: var(--blue-200);
@blue-300: var(--blue-300);
@blue-400: var(--blue-400);
@blue-500: var(--blue-500);
@blue-600: var(--blue-600);
@blue-700: var(--blue-700);
3. 项目里直接在入口文件改变css变量颜色,即可做到动态替换
- 方式一:root根节点覆盖,跟上面一致
- 方式二:根dom style样式覆盖
<div :style="varStyle">
<router-view></router-view>
</div>
<script lang="tsx">
import Vue from 'vue';
export default {
data() {
return {
varStyle: {
'--blue-500': '#3ab'
}
};
}
};
</script>
总结
该方式较为简单,推荐!