利用CSS预处理技术实现项目换肤功能(less css + asp.net mvc4.0 bundle)

一、背景

    在越来越重视用户体验的今天,换肤功能也慢慢被重视起来。一个 web 系统用户可以选择一个自己喜欢的系统主题,在用户眼里还是会多少加点分的。我们很开心的是 easyui v1.3.4 有自带 default gray black bootstrap metro 五款皮肤,但是它并不像 bootsrap 提供了很完整的 css 框架,不能提供项目需要的所有的 css,所以还需要自己编写控件之外的一些 css。给系统换肤时,easyui 控件都没问题,问题就在于自己编写的这部分 css 怎么实现换肤,当然,最简单的办法就是为每一款主题都写对应的一份自定义 css 然后在项目中加载,这样是可以实现。
    但是我觉得这样有点罗嗦了,当你添加新的 css 或修改 css 时,你要同时修改 N 份 css, 每一个主题对应一份,而且 easyui 除了这 5 款默认的主题还有其它主题或者我们还可以自定义主题,那这样修改 css 就更不现实了。所以我们就想到的动态 css, 也就是 css 预处理技术。

二、CSS 预处理技术

CSS 预处理器技术已经非常的成熟,常用的预处理器框架有:
1、Less 官网:http://lesscss.org/
2、Sass 官网:http://sass-lang.com/
3、Stylus 官网:http://learnboost.github.io/stylus/

我研究比较多的只有 less, 后两者也只是了解了下,所以这里我还是选用 less 来实现

我们先来看看用 less 带来了哪些方便
1、用 Less 我们可以实现用变量去写 css, 可以很方便的实现换肤功能(只要改变变量的值即可)

@the-border: 1px;
@base-color: #111;
@red:        #842210;

#header {
color
: (@base-color * 3);
border-left
: @the-border;
border-right
: (@the-border * 2);
}
#footer
{
color
: (@base-color + #003300);
border-color
: desaturate(@red, 10%);
}

2、Less 提供了很多很有用的函数比如 lighten darken fadein fadeout…等等,比如把字体 A 颜色设置为 #1382CE,字体 B 跟 A 同一颜色,只是比较淡:

@fontcolor:#1382CE;
.font1 {
    color: @fontcolor;
}
.font2 {
    color: lighten(@fontcolor,30%);
}

3、我们也可以自己定义自己的 less 函数,比如我们写一个背景渐变的 css, 我们可以定义一个渐变的函数,如下:

.gradient(@color: #F5F5F5, @start: #EEE, @stop: #FFF) {
  background: @color;
  background: -webkit-gradient(linear,
                               left bottom,
                               left top,
                               color-stop(0, @start),
                               color-stop(1, @stop));
  background: -ms-linear-gradient(bottom,
                                  @start,
                                  @stop);
  background: -moz-linear-gradient(center bottom,
                                   @start 0%,
                                   @stop 100%);
  background: -o-linear-gradient(@stop,
                                 @start);
  filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",@stop,@start));
}

然后我们只需要这样用:

.head-north
{
    .gradient(colorDefault,colorFrom, colorTo);
}

我就简单介绍这些,less 还有很多用法,大家自己去探究。

三、系统换肤实现思路

当然不光是引入 less 就完了,事情远没有这么简单,我们知道既然是 dynamic css 那么一定是需要编译的,less 最终也是只是编译生成 css 代码,那么就有一个问题,什么时候编译
1、使用编译工具比如 koala、SimpleLess 等,在项目发布前编译好放在项目中


2、前端解析编译,需要在项目中引入 less.js


3、后台动态解析,在 java 环境下的编译引擎比较多,.net 下好像我就找到一个 dotless,而且实现的还不是很完整,只能说是 less v1.5 的部分实现。

先分析下这三种方式,第一种用编译工具,就是我发布项目要做的事情变多了,我一向比较懒,喜欢简单的,万一我忘记了怎么办,总觉得是多了一趟事情。
第二种前端实时解析,这种其实是很理想的一种方便,也很方便,但是带来的一个问题就是前端的效率,如果 css 少还好说,多了肯定会影响效率的。


第三种呢,后台动态编译,后台只编译一次后缓存起来,对服务器基本没有影响,这样很好,问题是我这个框架是.net 的,是 dotless 实现不完整, 但是我们不一定会用到 less 所有的功能,有基本功能就够用了,比如条件判断等更高级的使用,我们可以在处理前自己先预处理一下,再给 less 类库去解析。

好吧,那么我就选择第三种在后台动态解析了。
具体思路:


1、根据当前用户选择的 theme 取得 easyui.css 文件并根据特征获得主题的相关变量 @body-background-color 或 @body-text-color 等等,这些变量我在我自定义的 css 中会常用到,取得这些变量是很容易做到的。


2、利用这些变量,我们可以 theme.less 中编写自定义的 css


3、利用 asp.net mvc4.0 中的 bundle 中来处理 less


4、页面引用输出 css

四、具体实现

1、引入 dotless 类库
2、定义 easyui 中的变量,应该包括以下变量

/*common*/
@border-color
@border-radius
@font-size
@shadow-background-color
@mask-background-color
@toolbar-background-color
@toolbar-border-color
@split-color
@split-proxy-color

/Header/
@header-background-color
@header-text-color
@header-gradient-used
@header-gradient-from
@header-gradient-to

/body/
@body-background-color
@body-text-color

/grid/
@grid-header-background-color
@grid-header-gradient-from
@grid-header-gradient-to
@cell-border-color
@alt-background-color

/state/
@selected-background-color
@selected-text-color
@selected-border-color
@hover-background-color
@hover-text-color
@hover-border-color
@invalid-background-color
@invalid-border-color
@invalid-text-color

/menu/
@menu-background-color
@menu-text-color
@menu-border-color

/button/
@button-background-color
@button-selected-color
@button-text-color
@button-gradient-used
@button-gradient-from
@button-gradient-to
@button-radius
@button-split-color1
@button-split-color2

这些变量要通过用户的 theme 取得 easyui.css 文件并解析这个文件去给这些 less 变量赋值

3、自定义自己的动态 css,下面是我的项目中 theme.less 文件的片段

.z-body
{
    background:@body-background-color;
}

.z-toolbar,.z-toolbar-dialog{
border-color
:@border-color;
background
:@header-background-color;
}

.z-txt {
border-color
:@border-color;
background
:white;
}
.head-left, .head-right, .head-right a
{
color
: $when(@theme=gray,default,black,bootstrap| #fff | #000);
}

.head-north
{

    .gradient(@selected-background-color,@header-background-color, @selected-background-color);
}

.head-south,.head-south a
{
    background:@header-background-color;
    color: lighten(@body-text-color,30%);
}
 
……

4、在项目中的 BundleConfig.cs 中的 RegisterBundles 中注册 bundles

using System;
using System.IO;
using System.Web;
using System.Web.Hosting;
using System.Web.Mvc;
using System.Web.Optimization;
using Zephyr.Utils;

namespace Zephyr.Web.Mvc
{
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
var dirBase = new DirectoryInfo(HttpContext.Current.Server.MapPath(
string.Format("~/Content/js/easyui/{0}/themes",AppSettings.EasyuiVersion)));
var dirs
= dirBase.GetDirectories();
foreach (var dir in dirs)
{
if (dir.Name == "icons") continue;
var theme = dir.Name;
var themeBundle = new Bundle(string.Format("~/Content/css/theme/{0}", theme)).Include(
"~/Content/css/less/elements.less",
"~/Content/css/less/theme.less");
themeBundle.Transforms.Add(
new EasyuiLessTransform(theme));
themeBundle.Transforms.Add(
new LessTransform());
themeBundle.Transforms.Add(
new CssMinify());
bundles.Add(themeBundle);
}
}
}
}

这里在 bundle 的 Transforms 中添加了三个 BundleTransform 处理,
其中 EasyuiLessTransform 是我对 easyui 变量及自定义条件判断 $when 的处理


LessTransform 则是调用 dotless 库解析 less 代码

using System;
using System.Web.Optimization;
using dotless.Core;

namespace Zephyr.Web.Mvc
{
public class LessTransform : IBundleTransform
{
public void Process(BundleContext context, BundleResponse response)
{
var compiled = Less.Parse(response.Content);
if (string.IsNullOrEmpty(compiled))
throw new Exception("less 文件中语法有错误!");
response.Content
= compiled;
response.ContentType
= "text/css";
}
}
}

第三个 CssMinify 则是 System.Web.Optimization 下面的对 css 混淆压缩处理。

5、在页面中引用,razor 页面中只需要以下代码即可

@Styles.Render("~/Content/css/theme/" + AppLoginer.Theme)

至此,换肤功能已完成,我们可以看看效果

五、各种主题下的效果
1、默认主题


image

image

2、gray 风格image

image

3、bootstrap 风格
image

image

4、black 风格,这个好像口味比较重
image

image

6、metro 风格,这款很干净简洁,我自己很喜欢

image

image

六、后述

这样一来,这个功能就算是很灵活了,就算是以后再加入一款新主题,代码也完全不用修改,而且想效果更好点还可以 p 几张题头的图片换上。
总体效果当然和专业美工做的当然没法比,不过做做业务管理系统忽悠忽悠客户已经足够了。