65.9K
CodeProject 正在变化。 阅读更多。
Home

BootBrander Bootstrap .less 生成器 UI (第 2 部分 / 解析 variables.less)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.14/5 (3投票s)

2015年3月2日

CPOL

8分钟阅读

viewsIcon

17676

downloadIcon

182

创建一个 MVC 站点,用户可以通过输入更改 bootstrap 变量,并生成自定义品牌的 bootstrap.css。

引言

如果您还没有阅读本系列的第一部分,您应该在这里阅读。

我们正在构建一个用户界面,通过修改 variables.less 文件中的变量颜色来生成自定义的 bootstrap.css

在本文中,我们将解析此文件,并将其中所有颜色暴露给用户。我假设他可以更改颜色,但不能更改诸如 font-size 和 padding 之类的内容。

文章索引

变量

我尝试通过 less.js 库查找有哪些变量。但我找不到任何文档。所以我决定获取 variables.less 文件并自己解析它。

编写一个代码解析器非常困难。您必须逐个字符地查看代码才能正确完成。

但我们很幸运。Bootstrap 团队提供的 variables.less 文件格式非常规范。它不会出现不应该出现的空格,并且每次声明一个部分时,其语法都完全相同。

所有变量都按类别声明。我将利用这一点。一个类别看起来像这样

//== Colors

所以如果我正在读取这一行,我可以轻松地查看前 4 个字符来识别它。

一个变量看起来像这样

@gray-base:              #000;

这甚至更容易。

开始

如果您有上一篇文章的代码,您应该更改一些内容。首先,更改 viewModel 的声明。

    var viewModel = window.viewModel = {
        "@body-bg": ko.observable('#ffffff'),
        "@text-color": ko.observable('#777777'),
        "@brand-primary": ko.observable('#337ab7')
    },

改为

    var viewModel = {
    },

还要找到进行 knockout 绑定的那一行并将其删除。在我们完全完成之前,它会出错。我们稍后会将其放回。

获取和解析

所以,让我们开始编写一个 Ajax 函数来获取 variables.less 文件。

   $(document).ready(function () {

        $.ajax({
            url: "/Content/bootstrap/variables.less",
            success: function (responseText) {
                var lines = responseText.split('\n'),
                    line,
                    i;

                for (i = 0, j = lines.length; i < j; i++) {
                    line = lines[i];
                    if (line.substr(0, 1) === "@") {
                        console.log(line);
                    }
                } 
            }
        })
    });

测试

如果我们运行此代码,应该会得到以下输出

...Lots up here
main.js:52 @brand-success:         #5cb85c;
main.js:52 @brand-info:            #5bc0de;
main.js:52 @brand-warning:         #f0ad4e;
main.js:52 @brand-danger:          #d9534f;
main.js:52 @body-bg:               #fff;
... and more down here 

它打印了所有变量。其中有许多不同类型的变量。它们可以是像 darken() 这样的函数,可以是像素、数字或对其他变量的引用。

目前,我们将重点关注值以 # 开头的变量。那些肯定是颜色。

我们将在 viewModel 中添加其中任何一个。

       $.ajax({
            url: "/Content/bootstrap/variables.less",
            success: function (responseText) {
                var lines = responseText.split('\n'),
                    line,
                    i,
                    nameValue,
                    name,
                    value;

                for (i = 0, j = lines.length; i < j; i++) {

                    line = lines[i];

                    if (line.substr(0, 1) === "@") {
                        //this is a variable
                        nameValue = line.split(":");
                        name = nameValue[0].trim();
                        value = nameValue[1].trim();

                        if (value.substr(0, 1) === "#") {
                            //this is color
                            viewModel[name] = ko.observable(value);
                        }

                    }
                }
                console.log(viewModel);
            }
        });

如果我们运行此代码,可以看到 viewModel 的控制台输出现在包含许多变量。

绑定 viewModel

在上一篇文章中,我们为每个单独的颜色变量手动编写了一个带有 knockout 绑定的 HTML 片段。这不会非常有用,我们将替换它。

为此,我们将创建一个 HTML 模板,然后将其提供给 knockout。

单个颜色的模板

我们将使用一个模板,稍后将 knockout 绑定变量发送到其中。在 body 的底部创建以下 HTML 代码。

    <script type="text/html" id="color">
        <div class="row">
            <div class="col-xs-9" style="padding-right: 0;">
                <input type="text" class="form-control" data-bind="value: $data" />
            </div>
            <div class="col-xs-3" style="padding-left: 0;">
                <input type="color" class="form-control" data-bind="value: $data" />
            </div>
        </div>
    </script>

现在,在 id 为 toolbar-container 的左侧面板中,我们将遍历 viewModel 的属性。在每次迭代中,我们将设置一个 label 并调用我们的模板;

    <div class="col-xs-2" id="toolbar-container">
        <div class="form-group" data-bind="foreach: {data: Object.keys($data), as: '_propkey'}">
            <label data-bind="text: _propkey"></label>
            <div data-bind="template: { name: 'color', data: $root[_propkey] }"></div>
        </div>
    </div>

现在我们可以重新开始 knockout 绑定了。在 Ajax 调用成功处理程序的末尾执行此操作。就在解析 variables.less 的循环之后。

ko.applyBindings(viewModel);

测试

运行网站,应该看起来像这样

我们得到一个很好的列表,其中包含所有看起来是颜色的项。但有一个问题。或者实际上是两个。

所有颜色选择器都是黑色的,这是因为值。首先,它们以“;”结尾。第二个问题是颜色的简写表示法,例如 white #fff。input type="color" 需要完整地编写它们。

找到这些行

if (value.substr(0, 1) === "#") {
    //this is color
    viewModel[name] = ko.observable(value);
}

并更改为

if (value.substr(0, 1) === "#") {
    //this is color
    value = value.replace(";", "");
    if (value.length === 4) {
        value += value.substr(1, 3);
    }
    viewModel[name] = ko.observable(value);
}

再次运行,然后

好!

迭代属性似乎确实会破坏 knockout 的双向绑定。但现在我们先不要担心。因为我们首先添加类别,这无论如何都会改变事情。

类别

现在,它只是一长串颜色。一些上下文会很有帮助。如前所述,变量以注释形式声明在 variables.less 的类别中:

//== Colors

所以,首先,我们会看到一个类别,然后是一系列属于这些类别的变量。为了反映这一点,我们首先需要稍微更改一下 viewModel。它应该具有以下形式

  • viewModel
    • categories
      • category
        • variableName
        • variableName
      • category
        • variableName
    • variables
      • variable
      • variable

viewModel 声明如下

 var viewModel = window.viewModel = {
 categories: {},
 variables: {}
 }

我们还将更改 variable 对象本身。如果您查看我们现在拥有的列表,会缺少几个变量,@text-color 丢失了。这是因为该颜色不以 # 开头,而是声明为继承自另一个 variable。我们稍后会处理这个问题。但目前,我们将把单个 variable 分割为值和类型。

我们的解析代码现在应该如下所示

   var lines = responseText.split('\n'),
        line,
        i,
        nameValue,
        name,
        value,
        category
    ;

    for (i = 0, j = lines.length; i < j; i++) {

        line = lines[i];

        if (line.substr(0, 4) === "//==") {
            category = line.substr(5, line.length).trim();
            //console.log(line.substr(5, line.length).trim());
            viewModel.categories[category] = {
                variables: ko.observableArray()
            };
            continue;
        }

        if (line.substr(0, 1) === "@") {
            //this is a variable
            nameValue = line.split(":");
            name = nameValue[0].trim();
            value = nameValue[1].trim();
            value = value.replace(";", ""); 

            if (value.substr(0, 1) === "#") {
                //this is color
 
                if (value.length === 4) {
                    value += value.substr(1, 3);
                }

                //add the name to the categories
                viewModel.categories[category].variables.push(name);

                //add the variable to the variables object
                viewModel.variables[name] = {
                    type: "color",
                    value: ko.observable(value)
                }
            }
        }
    }

    console.log(viewModel);

    //Apply the viewModel
    ko.applyBindings(viewModel);
}

您可能会注意到,我没有简单地将 variable 保存到类别本身,而是仅保存名称。并且我将 variable 本身添加到对象中。

这是有两个原因。

第一个原因是引用 variables(其值为另一个 variable 名称的那些)。为了解决这些问题,对象更易于查找。

第二个原因是序列化。我无法将引用保存在 category.variables 列表中,因为这会创建一个循环对象,我们无法将其通过 JSON.stringify。

更新 UI

现在我们需要更改我们的 HTML 和 knockout 绑定以反映此更改。首先是我们的模板

<script type="text/html" id="color">
    <div class="row">
        <div class="col-xs-9" style="padding-right: 0;">
            <input type="text" class="form-control" 
            data-bind="value: value" />
        </div>
        <div class="col-xs-3" style="padding-left: 0;">
            <input type="color" class="form-control" 
            data-bind="value: value" />
        </div>
    </div>
</script>

现在是左侧列。我选择将类别和变量放入 details 部分。但您可以通过使用手风琴来改进它

<div class="col-xs-2" id="toolbar-container">

    <div data-bind="foreach: {data: Object.keys(categories), as: '_propkey'}">
        <details>
            <summary data-bind="text: _propkey"></summary>

            <div data-bind="foreach: $root.categories[_propkey].variables">
                <div class="form-group">

                    <label data-bind="text: $data"></label>

                    <div data-bind="template: 
                    { name: $root.variables[$data].type, data: $root.variables[$data] }">
                    </div>

                </div>

            </div>

        </details>
    </div>

</div>

测试

运行页面,UI 应该看起来像这样

但是,如果我们运行第一个文章中的测试

  • 通过在文本字段中键入“red”来更改 @body-bg 变量
    • 您的页面背景应该变为红色
  • 通过颜色选择器更改 @body-bg 变量
    • 您的页面背景应变为选定的颜色
  • 通过在文本字段中键入“red”来更改 @brand-primary 变量
    • 主按钮应变为“blue
    • UI 应保留先前设置的 @body-bg
  • 通过颜色选择器更改 @brand-primary 变量
    • 主按钮应变为选定的颜色
    • UI 应保留先前设置的 @body-bg

所有这些都不起作用。这是因为我们还没有设置对可观察对象的订阅。在上一篇文章中,我们有以下代码:

    function onViewModelChanged() {
        var viewData = ko.toJS(viewModel);
        less.modifyVars(viewData);
        localStorage.setItem("viewData", JSON.stringify(viewData));
    };
    for (var prop in viewModel) {
        if (viewModel.hasOwnProperty(prop)) {
            viewModel[prop].subscribe(onViewModelChanged);
            if (storedViewData.hasOwnProperty(prop)) {
                viewModel[prop](storedViewData[prop]);
            };
        }
    }

然后我说这是根本错误的。之所以这样,是因为它为存储的数据设置了值。订阅首先发生。然后对于每个变量,它设置存储的值。这将导致 onViewModelChanged 函数针对 viewModel 中的每个颜色触发。在我们之前的例子中,这影响不大,因为我们只有 3 种颜色。但现在,我们可能会开始注意到这一点。

但首先,让我们将执行订阅的循环包装在一个函数中,我们称之为 applySubscriptions

    function applySubscriptions() {
        var observableVars = viewModel.variables;

        for (var prop in observableVars) {

            if (observableVars.hasOwnProperty(prop)) {

                observableVars[prop].value.subscribe(onViewModelChanged);
            }
        }
    }

现在,我们需要一些东西来反序列化我们的 viewModel。我们将变量拆分为类型和值。因此,仅仅调用 less.modifyVars(ko.toJS(viewModel)) 将不起作用。

    function viewModelToJS() {
        var obj = {},
            observableVars = viewModel.variables
            ;
        for (var prop in observableVars) {

            if (observableVars.hasOwnProperty(prop)) {

                obj[prop] = observableVars[prop].value();

            }
        }
        return obj;
    }

然后,我们更改 onViewModelChanged 函数,使其使用这个而不是 ko.toJS

    function onViewModelChanged() {

        var viewData = viewModelToJS();
        
        less.modifyVars(viewData);

        localStorage.setItem("viewData", JSON.stringify(viewData));
    };

运行我们的测试

  • 通过在文本字段中键入“red”来更改 @body-bg 变量
    • 您的页面背景应该变为红色
  • 通过颜色选择器更改 @body-bg 变量
    • 您的页面背景应变为选定的颜色
  • 通过在文本字段中键入“red”来更改 @brand-primary 变量
    • 主按钮应变为“blue
    • UI 应保留先前设置的 @body-bg
  • 通过颜色选择器更改 @brand-primary 变量
    • 主按钮应变为选定的颜色
    • UI 应保留先前设置的 @body-bg
  • 在顶部导航中移至其他页面
    • UI 应保留先前设置的 @body-bg
    • UI 应保留先前设置的 @brand-primary

我们的测试进行了到切换页面这一步。如前所述,这个 MVC 站点不是 ajax 的,所以它会从服务器拉取页面。

我们需要修复从存储中重置变量的问题。上一篇文章中获取存储变量并将其放入 storedViewData 的代码应该仍然有效,所以我们在解析器循环中,在检查变量类型之前,可以恢复先前的值

value = value.replace(";", "");

if (storedViewData.hasOwnProperty(name)) {
    value = storedViewData[name];
}

if (value.substr(0, 1) === "#") {

然后在 Ajax 成功处理程序的末尾,调用 less.modifyVars 来应用所有变量;就在 applyBindings 下方。

//Apply the viewModel
ko.applyBindings(viewModel);

//Set the current values
less.modifyVars(viewModelToJS());

现在执行我们的 testplan,它应该可以工作。

总结

我现在结束这篇文章。因为我看到它变得相当长。查找我们的引用变量,如 @text-color,以及处理像 'darken' 和 'lighten' 这样的函数将在下一篇文章中介绍。

在下载中,您会找到一个经过优化且带有 jsdoc 注释的版本。它将是我们下一篇文章的起点。

© . All rights reserved.