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






4.14/5 (3投票s)
创建一个 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
 
- UI 应保留先前设置的 
我们的测试进行了到切换页面这一步。如前所述,这个 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 注释的版本。它将是我们下一篇文章的起点。


