vue 切换主题 实战【webpack plugin+loader】

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

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值