當接到這個需求的時候,百度到業界關於主題切換的方案還挺多的,css連接替換、className更改、less.modifyVars、css in js等等,但每一種方案聽起來都是又累又貴。有沒有那種代碼侵入低,小白無腦又好維護的方案呢?那天然是有的,確切的說是css它自己就支持。css
定義一個全局顏色變量,改變這個變量的值頁面內全部引用這個變量的元素都會進行改變。好簡單是否是?vue
// 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。node
是的,還真有polyfill能兼容IE:css-vars-ponyfill。它搞定IE的方式大概是這樣子的webpack
+-------------------------+
| 獲取頁面內style標籤內容 |
| 請求外鏈css內容 |
+-------------------------+
|
|
v
+-------------------------+ 是 +-------------------------+
| 內容是否含有var() | ----> | 標記爲src |
+-------------------------+ +-------------------------+
| |
| 否 |
v v
+-------------------------+ +-------------------------+
| 標記爲skip | | 將var(*)替換爲變量值, |
| | | 新增style標籤添加到head |
+-------------------------+ +-------------------------+
複製代碼
效果大概是這個樣子的 git
// 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項目中用上述方案你可能會在IE中看到這樣的狀況 github
由於css-vars-ponyfill
是依賴dom元素來實現轉換的,在node中沒法使用,因此從server直出未轉換的css代碼到client加載js文件轉換css間存在一段樣式空檔。
+- - - - - - - - - - - - - - - - - - - -+
' 樣式空窗期: '
' '
+----------+ ' +----------------+ +------------+ ' +-------------+
| 發起請求 | --> ' | SSR直出頁面 | --> | 加載js依賴 | ' --> | 替換css變量 |
+----------+ ' +----------------+ +------------+ ' +-------------+
' '
+- - - - - - - - - - - - - - - - - - - -+
複製代碼
解決這個問題也很簡單,只須要在每一個用到css var
的地方加上一個兼容寫法web
@_primary: red
@primary: var(--primary)
:root{
--primary: @_primary
}
.theme {
color: @primary;
}
// 改成
.theme {
color: @_primary;
color: @primary;
}
複製代碼
在不支持css var的瀏覽器上會渲染默認顏色red
,等待js加載完畢後ponyfill替換樣式覆蓋。api
手動在每一個用到的地方添加兼容寫法既幸苦又很差維護,這個時候咱們須要瞭解一些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}`
}
)
}
複製代碼
至此,咱們能夠愉快自如的切換主題了。 bash
經過如何「懶得寫更多代碼」來吸取新知識會更加有趣, 但願這篇文章可以幫助到你。