vue 切换主题 实战【webpack plugin+loader】
-------------------------------------------------------------------------------------------------------------------------
项目地址:https://github.com/halvee-L/vue-theme-loader.git
为了各位看官方便食用,整理发布在npm
webpack-vue-them: https://www.npmjs.com/package/webpack-vue-theme
vue-loader-extend:https://www.npmjs.com/package/vue-loader-extend
将原有vue脚手架中vue-loader修改为vue-loader-extend ,并在webpack配置中生产环境下加入插件即可
const ExtractThemePlugin = require('webpack-vue-theme')
///webpack.prod.config.js
{
...
plugins:[...,new ExtractThemePlugin({
filename: utils.assetsPath('css/app.[theme].css')
})]
...
}
-------------------------------------------------------------------------------------------------------------------------
最近项目上又双叕遇到主题切换需求了,为什么说又...因为在上一家公司刚做完不久 =.=|| 采用的是通过sass定义颜色值,通过改变变量颜色来达到主题切换的目的。先规纳整理下当初走踩的坑:
1.须先定义所有场景的颜色值,如1级背景 2级背景 1级字体 2级字体 等等(如下)。若规纳全面,后期还比较顺利,但若需要再增加颜色值(特别是介于中间)时 ,比较麻烦。
$color-bg1: #ecf0f1;
$color-bg2: white;
$color-bg3: #f4f4f4;
$color-bg4: #fafbfb;
2.场景不匹配:如1级背景与1级字体对应使用,但可能会有业务(弹窗,小提示??)会出现2级背景对应1级字体情况。切换主题后效果不(hen)友(nan)好(kan)。
3.研发支撑不友好: 如按主题调整完再切换调整其它主题,场景太多(上家公司产品10+前端支撑,功能复杂度高,场景多),需要对所有场景进行排查,耗时耗力,还可能出现遗漏问题。或按场景调整,需要不断切换配置重启项目,也是耗时耗力...(通过sass-resources-loader全局sass变量注入,若有其它方案,请大佬告知。小生感激不敬~~)
终上所述,此方案不尽人意。当然前端主题还有其它的方案,不再做阐述,但都各有优劣。
-------------------------------------------------------------------------------------------------------------------------
好了,上正题,插件参考 vue的style.scoped与extract-text-webpack-plugin。
实现目的在研发时声明主题与主题样式(组件级),再通过插件实现主题样式的提取。
初始设想声明主题分样式块级(style)、类选择器(class) 及样式属性(如color:red;)
约定写法如下;[!red]代码red主题生效的样式(最终实现不是这样,见后面详解)
// 块级申明
<style theme="red">
// 类选择器
.[!red]class{
[!red]color:red;
}
</style>
首先实现theme="red"的处理,此处参考style.scoped处理,但最后发现vue-loader在代码里通过query.scoped判断添加了scopedId插件,而query.scoped在loader解析vue文件时写入(有兴趣的童鞋可以去看vue-loader源码),扩展参数theme无法保存也无法传入
自定义插件,最终也只能通过将vue-loader拷出本地修改。另外,如果不实现style.theme功能,可以直接通过.postcssrc.js配置插件。
vue-loader/loader.js,追加theme参数
// add 此处追加theme参数
function getRawLoaderString(type, part, index, scoped) {
let lang = part.lang || defaultLang[type]
let styleCompiler = ''
if (type === 'styles') {
// style compiler that needs to be applied for all styles
styleCompiler =
styleCompilerPath +
'?' +
JSON.stringify({
// a marker for vue-style-loader to know that this is an import from a vue file
vue: true,
id: moduleId,
theme: part.attrs.theme,
scoped: !!scoped,
hasInlineConfig: !!query.postcss
}) +
'!'
...
}
vue-loader/lib/style-compiler/index.js
// add plugin for vue-loader scoped css rewrite
if (query.scoped) {
plugins.push(scopeId({ id: query.id }))
}
// add 本地修改新增主题抽取插件
plugins.push(theme({ theme: query.theme || '' }))
.postcssrc.js配置
// .postcssrc.js配置 可在此处配置vue-loader解析style样式块用的插件
module.exports = {
plugins: [
require('postcss-import'),
require('postcss-url'),
// to edit target browsers: use "browserslist" field in package.json
require('autoprefixer')
]
}
好了,theme拿到后,可以正式开始处理插件,此处我们需要对[!red]格式的类名及样式名进行处理。
然厄...
我是谁 我在哪 我在干什么???
仔细一看,原来是老天不允许我这么厉(zhuang)害(bi),啊呸 明明语法检验和css解析报错...
怎么办,css-loader、postcss解析 、AST语法树、eslint检验处理等等在脑海里迅速闪过,最后决定,修改语法为--theme--[机智如我!]
.panel3 {
border: 1px solid #498dff;
--red--border-color: red;
--black--border-color: black;
color: #498dff;
--red--color: red;
--black--color: black;
}
.--black--panel4 {
background-color: black;
}
.--red--panel5 {
border-color: red;
color: red;
}
正题,按原有思路以[!theme]格式应该也可以,只是复杂得多,需要解析及校验两处都需要做对应处理,所以这里先偷个懒,毕竟功能优先[手动狗头]
成功绕过校验与解析后就轻松了,首先是loader对css处理,参考postcss处理http://api.postcss.org
对所有类名遍历处理
root.walkRules(node => {
let match = node.selector.match(THEMEREG);
// 类名带主题时
if (match && match[1]) {
node.selector = replaceNormal(node.selector);
//若主题与style.theme相同且为研发环境时,直接生效
if (match[1] === theme && !isProduction) {
walkDeclsCurrent(node);
} else {
//若不是当前主题 则提取样式
chunkNode(node, match[1]);
}
} else {
// 遍历样式
walkDeclsChunk(node, theme);
}
});
研发环境当前主题样式直接生效,暴力方案
function walkDeclsCurrent(node) {
if (node.nodes[0]) {
node.insertBefore(node.nodes[0], { text: theme })
}
node.walkDecls(decl => {
decl.prop = decl.prop.replace(THEMEREG, '')
decl.value = decl.value + ' !important'
})
}
不是当前主题时,研发环境直接加入主题名,生产环境提取样式
function chunkNode(node, theme) {
if (isProduction) {
themer.add(theme, utils.getDeepNode(node), node.source)
node.remove()
} else {
node.selector = '.' + theme + ' ' + node.selector
}
}
样式属性处理,规则和类名处理规则类似
function walkDeclsChunk(node, theme) {
let ruleCache = {}
node.walkDecls(decl => {
let match = decl.prop.match(THEMEREG)
if (match && match[1]) {
decl.prop = replaceNormal(decl.prop)
if (theme === match[1] && !isProduction) {
decl.value = decl.value + ' !important'
} else {
let rule = ruleCache[match[1]]
if (rule) {
rule.append(utils.getDeepNode(decl))
} else {
rule = utils.getDeepNode(decl.parent)
rule.removeAll()
rule.selector = '.' + match[1] + ' ' + rule.selector
ruleCache[match[1]] = rule
rule.append(decl.clone())
}
decl.replaceWith({
text:
'replace by postcss-theme[ ' +
rule.selector +
']' +
decl.toString()
})
}
}
})
// todo 判断环境 {
if (isProduction) {
Object.keys(ruleCache).forEach(key => {
let rule = ruleCache[key]
themer.add(key, rule, node.source)
})
} else {
Object.keys(ruleCache).forEach(key => {
let rule = ruleCache[key]
node.parent.append(rule)
})
}
}
loader处理完成后,考虑生产环境打包,需要对样式进行提取操作,此处参考extract-text-webpack-plugin插件。核心的就两点,
1是通过webpack.ConcatSource与webpack.SourceMapSource生成资源
2是通过assets添加资源文件
themer.each((name, node) => {
var source = new _webpackSources.ConcatSource()
for (var i = 0, len = node.length; i < len; i++) {
let _source = that.source(node[i])
source.add(_source)
}
var file = isFunction(filename)
? filename(getPath.bind(null, value))
: getPath(name, filename)
compilation.assets[file] = source
})
-------------------------------------------------------------------------------------------------------------------------
整个过程整理完成后,东西也不多,比较耗时的是对vue-loader的分析借鉴scopeId的处理以及webpack.SourceMapSource提取操作。
参考文档:
postcss API:http://api.postcss.org
webpack源码分析:https://www.cnblogs.com/QH-Jimmy/category/1129698.html