基于Css Variable的主题切换完美解决方案
当接到这个需求的时候,百度到业界关于主题切换的方案还挺多的,css链接替换、className更改、less.modifyVars、css in js等等,但每一种方案听起来都是又累又贵。有没有那种代码侵入低,小白无脑又好维护的方案呢?那自然是有的,确切的说是css它本身就支持。
Css3 Variable
定义一个全局颜色变量,改变这个变量的值页面内所有引用这个变量的元素都会进行改变。好简单是不是?
// base.less
:root {
--primary: green;
--warning: yellow;
--info: white;
--danger: red;
}
// var.less
@primary: var(--primary)
@danger: var(--danger)
@info: var(--info)
// page.less
.header {
background-color: @primary;
color: @info;
}
.content {
border: 1px solid @danger;
}
// change.js
function changeTheme(themeObj) {
const vars = Object.keys(themeObj).map(key => `--${key}:${themeObj[key]}`).join(';')
document.documentElement.setAttribute('style', vars)
}
本文结束
个P,它不支持IE啊!!0202年还要兼容IE吗?是的,就是要兼容IE。
css vars ponyfill
是的,还真有polyfill能兼容IE:css-vars-ponyfill。它搞定IE的方式大概是这样子的
+-------------------------+
| 获取页面内style标签内容 |
| 请求外链css内容 |
+-------------------------+
|
|
v
+-------------------------+ 是 +-------------------------+
| 内容是否含有var() | ----> | 标记为src |
+-------------------------+ +-------------------------+
| |
| 否 |
v v
+-------------------------+ +-------------------------+
| 标记为skip | | 将var(*)替换为变量值, |
| | | 新增style标签添加到head |
+-------------------------+ +-------------------------+
效果大概是这个样子的
简单粗暴又不失优雅,在支持css var的浏览器中不会进行处理,所以不需要担心性能问题(是IE的问题,不是我的问题)。
我们来改造一下代码
// store/theme.js
import cssVars from 'css-vars-ponyfill'
export default {
state: {
'primary': 'green',
'danger': 'white'
},
mutations: {
UPDATE_THEME(state, payload) {
const variables = {}
Object.assign(state, payload)
Object.keys(state).forEach((key) => {
variables[`--${key}`] = state[key]
})
cssVars({
variables
})
}
},
actions: {
changeTheme({ commit }, theme = {}) {
commit('UPDATE_THEME', theme)
}
}
}
// router.js
// 因为路由跳转后的页面会按需加载新的css资源,重新转换
const convertedPages = new Set()
router.afterEach((to) => {
if (convertedPages.has(to.path)) return
convertedPages.add(to.path)
context.store.dispatch('theme/changeTheme')
})
SSR项目闪屏问题优化
在SSR项目中用上述方案你可能会在IE中看到这样的情况
因为css-vars-ponyfill
是依赖dom元素来实现转换的,在node中无法使用,所以从server直出未转换的css代码到client加载js文件转换css间存在一段样式空档。
+- - - - - - - - - - - - - - - - - - - -+
' 样式空窗期: '
' '
+----------+ ' +----------------+ +------------+ ' +-------------+
| 发起请求 | --> ' | SSR直出页面 | --> | 加载js依赖 | ' --> | 替换css变量 |
+----------+ ' +----------------+ +------------+ ' +-------------+
' '
+- - - - - - - - - - - - - - - - - - - -+
解决这个问题也很简单,只需要在每个用到css var
的地方加上一个兼容写法
@_primary: red
@primary: var(--primary)
:root{
--primary: @_primary
}
.theme {
color: @primary;
}
// 改为
.theme {
color: @_primary;
color: @primary;
}
在不支持css var的浏览器上会渲染默认颜色red
,等待js加载完毕后ponyfill替换样式覆盖。
Webpack插件开发
手动在每个用到的地方添加兼容写法既幸苦又不好维护,这个时候我们需要了解一些webpack生命周期以及插件开发相关的知识,我们可以通过手写一个webpack插件,在normalModuleLoader
(v5版本被废弃,使用NormalModule.getCompilationHooks(compilation).loader)的hooks中为所有css module添加一个loader来处理兼容代码。
笔者项目使用了less,注意webpack中loader执行顺序是类似栈的先进后出,所以我需要把转换loader添加到less-loader之前,确保我们处理的是编译后的css var写法而非less变量。
// plugin.js
export default class HackCss {
constructor (theme = {}) {
this.themeVars = theme
}
apply(compiler) {
compiler.hooks.thisCompilation.tap('HackCss', (compilation) => {
compilation.hooks.normalModuleLoader.tap(
'HackCss',
(_, moduleContext) => {
if (/\.vue\?vue&type=style/.test(moduleContext.userRequest)) {
// ssr项目同构会有2次compiler,如果module中存在loader则不继续添加
if (hasLoader(moduleContext.loaders, 'hackcss-loader.js')) {
return
}
let lessLoaderIndex = 0
// 项目用了less,找到less-loader的位置
moduleContext.loaders.forEach((loader, index) => {
if (/less-loader/.test(loader.loader)) {
lessLoaderIndex = index
}
})
moduleContext.loaders.splice(lessLoaderIndex, 0, {
loader: path.resolve(__dirname, 'hackcss-loader.js'),
options: this.themeVars
})
}
}
)
})
}
})
}
// loader.js
const { getOptions } = require('loader-utils')
module.exports = function(source) {
if (/module\.exports/.test(source)) return source
const theme = getOptions(this) || {}
return source.replace(
/\n(.+)?var\(--(.+)?\)(.+)?;/g,
(content, before, name, after = '') => {
const [key, indent] = before.split(':')
const add = after.split(';')[0]
return `\n${key}:${indent}${theme[name]}${after}${add};${content}`
}
)
}
至此,我们可以愉快自如的切换主题了。
后记
通过如何“懒得写更多代码”来吸收新知识会更加有趣,
希望这篇文章能够帮助到你。
0条评论