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

使用 Twitter Bootstrap 实现响应式 Web 设计

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (74投票s)

2013年12月2日

CPOL

15分钟阅读

viewsIcon

188097

downloadIcon

1695

介绍如何使用 ASP MVC / Twitter Bootstrap / WebAPI 和 Knockout.js 创建响应式网站。

目录

您可以在此处下载演示代码:TwitterBootstrapDemo_ASPMVC4.zip

引言

在工作中,我混合使用 ASP MVC / WCF 和 WPF。我们已经基本接受了 JavaScript 的 MVxx 框架,例如 Knockout.jsAngular.js,但最近我们不得不进行一些真正响应式的设计工作,这些工作需要针对大量不同的外形尺寸。我们公司确实有优秀的软件工程师,但我们都没有特别擅长 CSS,因为我猜我们更喜欢编写代码。所以我们着手招聘一些非常擅长 UI/UX 的人员。新招聘的 UI/UX 人员做的第一件事就是引入 Bootstrap 的使用,这是一个由 Twitter 的一些员工开发的 HTML 框架。

对于那些没听说过响应式框架的人来说,它通常是一个用于处理 HTML 的框架,其中包含许多助手、CSS、JavaScript 以及经过充分验证和测试的模式,用于开发在不同外形尺寸下表现良好且行为可预测的网站。这些响应式框架通常包含以下内容:

  • 一个响应式网格
  • 导航菜单
  • 弹出窗口/对话框
  • 标准的 CSS 控件/样式集

它们可能包含更多或更少的功能,但这些是这些框架的基本构建块。

我显然听说过 Bootstrap 和其他大型响应式框架,如 Foundation.js。然而,新来的同事选择了 Bootstrap 路线。既然我没有怎么玩过 Bootstrap,我决定自己动手玩玩,并创建了一些我决定分享的代码。本文中的代码并不高明,但如果你是 Bootstrap(或响应式 HTML 设计)的新手,你可能会从中找到一些有用的东西。对于已经使用过 Bootstrap 或其他响应式框架的资深 Web 开发人员来说,我不知道你是否能从中获得太多东西。

在我们深入研究演示代码之前,我只想指出,我决定的演示应用程序可能最终会作为我博客的配套网站(如果喜欢的话可以链接)使用。因此,它包含了很多对我个人来说才相关的信息,例如我写过的文章的链接等。

正如我所说,很抱歉进行这种赤裸裸的自我推销,我只是觉得如果我要写点东西,最好写一些我真正能用得上的东西,所以我就是这么做的,我希望大家都能接受。

话虽如此,我认为演示代码仍然可以为那些以前没用过 BootstrapKnockout.js 的人提供一些不错的入门材料。

先决条件

演示应用程序使用 SQL Server,并要求您更改 Web.Config 中的连接字符串以指向您自己的 SQL Server 连接实例。

  • SQL Server

演示应用程序

正如我所提到的,演示应用程序非常专注于满足我个人需求,以便制作一个我博客的配套网站。那么这具体意味着什么呢?对我来说,这意味着我想要一个小型微型网站,满足以下要求:

  • 网站将使用 ASP MVC / Entity Framework / Web API / Bootstrap / Knockout.js
  • 我希望它是一个小型网站,不要太花哨
  • 我希望能够快速添加内容
  • 我希望它能显示一个类别列表,例如 C# / WPF 等,每个类别都会显示我写过的这些领域文章的一些图片
  • 每篇文章图片应显示工具提示
  • 每篇文章图片应可点击以显示有关文章的更详细描述,并应提供查看完整文章的机制
  • 希望它能在不同的外形尺寸(桌面/手机/平板电脑)下工作

加载中

这是首次运行时它的样子,在创建数据库并使用数据填充(这是 Entity Framework 的一项功能,稍后会详细介绍)时,会显示加载图标。

点击图片放大查看

数据加载后

这是加载完所有内容后的样子

点击图片放大查看

这是我们点击其中一张文章图片后的样子

点击图片放大查看

响应式设计

这是它在智能手机或小型浏览器窗口中的样子。

这里有几点需要注意,例如:

  1. 导航栏(NavBar 是 Bootstrap 的术语)已更改(稍后详细介绍)
  2. 图像不再是每行多个,而是单列显示(稍后详细介绍)
  3. 内容仍然适应良好

并且在点击文章图片后,请注意弹出对话框仍然很好,不会超出屏幕边界,弹出对话框的内容也能很好地适应其新尺寸。

正如我所说,我完全意识到这个演示应用程序具有非常个人化的性质(即对我个人而言),但我仍然认为它是一个有用的载体,可以用来教授 Knockout.jsBootstrap 的一些工作原理,尽管它显然是一个非常专注于我个人需求的演示。

它是如何工作的

以下各节将概述演示应用程序的内部工作原理。

数据库

我想要做的一件事是使用 SQL Server 和 Entity Framework(Code First),并希望用一些初始种子数据填充数据库。这便是该演示应用程序特定的 DbContext 的样子:

public class DatabaseContext : DbContext
{
    public DatabaseContext() : base("DefaultConnection")
    {
        this.Configuration.LazyLoadingEnabled = true;
        this.Configuration.ProxyCreationEnabled = true;
    }

    protected override void OnModelCreating(DbModelBuilder mb)
    {
        // Code here
    }

    public DbSet<Article> Articles { get; set; }
    public DbSet<Category> Categories { get; set; }
}

可以看到它非常简单,只有两个 DbSets,一个用于类别,一个用于文章,其中 CategoryArticle 类看起来如下:

类别

[Table("Categories")]
public class Category
{
    [Key]
    public int Id { get; set; }

    [Required(ErrorMessage = "SlideOrder is a required field.")]
    public int SlideOrder { get; set; }

    [Required(ErrorMessage = "Title is a required field.")]
    public string Title { get; set; }

    [Required(ErrorMessage = "Description is a required field.")]
    public string Description { get; set; }

    public virtual ICollection<Article> Articles { get; set; }
}

文章

[Table("Articles")]
public class Article
{
    [Key]
    public int Id { get; set; }

    [Required(ErrorMessage = "Title is a required field.")]
    public string Title { get; set; }

    [Required(ErrorMessage = "ShortDescription is a required field.")]
    public string ShortDescription { get; set; }

    [Required(ErrorMessage = "LongDescription is a required field.")]
    public string LongDescription { get; set; }

    [Required(ErrorMessage = "ImageUrl is a required field.")]
    public string ImageUrl { get; set; }

    [Required(ErrorMessage = "ArticleUrl is a required field.")]
    public string ArticleUrl { get; set; }

    [ForeignKey("Category")]
    public int CategoryId { get; set; }

    public virtual Category Category { get; set; }

}

这里使用 DataAnnotation 属性对于演示应用程序来说有点过度,因为它从不允许输入新的 Category / Article 对象,但由于这个演示应用程序是我为自己制作的,我想也许将来会扩展它以允许用户创建新的 Category / Article 对象,因此我认为这些属性的使用在以后可能会派上用场。

数据库创建

对于种子数据,我显然可以运行一些 SQL 脚本,但这次我决定使用内置的 Entity Framework 数据库初始化器,所以我写了这个简单的初始化器,它将在每次运行时从头开始删除并重新创建数据库,并且还将使用通过构造函数传入的种子数据来填充数据库。

public class DatabaseInitializer : DropCreateDatabaseAlways<DatabaseContext> 
{
    private List<Category> categoriesSeedData;

    public DatabaseInitializer(List<Category> categoriesSeedData)
    {
        this.categoriesSeedData = categoriesSeedData;
    }

    protected override void Seed(DatabaseContext context)
    {
        foreach (var category in categoriesSeedData)
        {
            context.Categories.Add(category);
        }
        context.SaveChanges();
    }
}

我们需要确保使用这个数据库初始化器,所以我们需要在 global.asax.cs 中设置它,方法如下:

public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        var categoriesSeedData = XmlParser.ObtainSeedData();

        Database.SetInitializer(new DatabaseInitializer(categoriesSeedData));

        WebApiConfig.Register(GlobalConfiguration.Configuration);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
}

我们将在下一节看到种子数据是如何生成的。

XML 种子数据

现在我们知道我们将要使用一些初始数据来填充数据库,那么这些数据从哪里来呢?

答案就在一个小的 XML 文件中,如下所示:

<?xml version="1.0" encoding="utf-8" ?>
<seedData>
  <!-- CATEGORIES -->
  <categories>
    <category>
      <slideOrder>1</slideOrder>  
      <title>About</title>  
      <description>
        <![CDATA[Category text here..........]]>
      </description>
    </category>
    <category>
      <slideOrder>2</slideOrder>  
      <title>C#</title>  
      <description>
        <![CDATA[Category text here..........]]>
      </description>
    </category>
    ....
    ....
    ....
    ....
    ....
    ....
    ....
  </categories>

  <!-- C# ARTICLES -->
  <articles>
    <article>
      <title>Threading 1 of 5</title>
      <shortDescription>Beginners guide to threading in .NET (Part 1)</shortDescription>
      <longDescription><![CDATA[Introduction into threading in .NET]]>
      </longDescription>
      <imageUrl>Content/images/Thread1.png</imageUrl>
      <articleUrl>https://codeproject.org.cn/Articles/.......</articleUrl>
    </article>
    ....
    ....
    ....
    ....
    ....
    ....
    ....
  </articles>
</seedData>

然后我们使用这个小小的辅助类来解析这些种子数据,ObtainSeedData() 方法的返回值用于初始化数据库(参见我们之前看到的 global.asax.cs 代码)。

public static class XmlParser
{
    public static List<Category> ObtainSeedData()
    {
        var appPath = HttpRuntime.AppDomainAppPath;
        var doc = XElement.Load(string.Format(@"{0}\App_Data\SeedData.xml",appPath));
        var serverUtility = HttpContext.Current.Server;

        var categories = doc.Descendants("category").Select(x =>
        {
            Category category = new Category
            {
                SlideOrder = int.Parse(x.Element("slideOrder").Value),
                Title = x.Element("title").Value.Trim(),
                Description = x.Element("description").Value.Trim()
            };


            var articles = x.Descendants("articles").Descendants("article").Select(y =>
                {
                    return new Article
                        {
                            Title = y.Element("title").Value.Trim(),
                            ShortDescription = y.Element("shortDescription").Value.Trim(),
                            LongDescription = y.Element("longDescription").Value.Trim(),
                            ArticleUrl = y.Element("articleUrl").Value,
                            ImageUrl = y.Element("imageUrl").Value,
                            Category = category
                        };
                });

            category.Articles = articles.ToList();
            return category;

        }).ToList();

        return categories;
    }
}

Web API

为了公开 CategoryArticle 对象,我选择了使用 WebAPI。这里涉及到几个决定, namely:

  • 是否允许自动内容协商
    • 我选择不这样做,而是强制将结果序列化为 JSON,因为我知道我想使用 Knockout.js 来处理这些结果,JSON 会处理得更好。而且,如果允许 XML,我还需要担心延迟加载/贪婪加载。
  • 是否尝试遵循纯 REST 动词,如 PUT / POST / GET 等
    • 我选择为文章创建自定义路由,因为我基本上想要延迟加载文章集合,但仅在我从 JavaScript 需要它们时。所以为了满足这个需求,在我看来,为 ArticleController 创建一个新的自定义路由是有意义的。

CategoryController

这是 CategoryController 的样子:

public class CategoryController : ApiController
{
    public HttpResponseMessage Get()
    {
        IEnumerable<Category> categories;
        using (var context = new DatabaseContext())
        {
            context.Categories.Include("Articles");
            categories = context.Categories.OrderBy(x => x.SlideOrder).ToList();
            if (categories.Any())
            {

                var slimCats = categories.Select(x => new
                    {
                        Id = x.Id,
                        Title = x.Title,
                        Description = x.Description,
                        ArticleIds = x.Articles.Select(y => y.Id).ToList()
                    }).ToList();
                    
                return Request.CreateResponse(HttpStatusCode.OK, slimCats,
                    Configuration.Formatters.JsonFormatter);
            }

            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
    }
}

ArticleController

这是 ArticleController 的样子:

public class ArticleController : ApiController
{
    // GETALL api/article/GetAll/5
    public HttpResponseMessage GetAll(int categoryId)
    {
        IEnumerable<Article> articles;
        using (var context = new DatabaseContext())
        {
            articles = context.Articles.Where(x => x.CategoryId == categoryId).ToList();
            var slimArticles = articles.Select(x => new
                {
                    x.CategoryId,
                    x.Id,
                    x.Title, 
                    x.ImageUrl,
                    x.ShortDescription,
                    x.LongDescription,
                    x.ArticleUrl
                }).ToList();

            return Request.CreateResponse(HttpStatusCode.OK, slimArticles,
                    Configuration.Formatters.JsonFormatter);
        }
    }
}

WebAPI 路由

这是这两个 Web API 控制器的路由设置,可以看到有标准路由,还有一个我为 ArticleController 创建的特定路由。

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
        name: "Articles",
        routeTemplate: "api/Article/GetAll/{categoryId}",
            defaults: new
            {
                controller = "Article",
                action = "GetAll",
                categoryId = 1
            }
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

Knockout JavaScript ViewModels

如前所述,我决定在客户端使用 Knockout.js 进行 MVVM 绑定。在我们深入研究 ViewModel 的代码之前,我想先回顾一下 ViewModel 应该尝试做什么。我决定创建以下 Knockout.js ViewModel:

DemoMainPageViewModel

此 ViewModel 由 Bootstrap 的 NavBar 和 Bootstrap 的 Carousel 控制,本质上这意味着它必须加载当前选定的 Bootstrap Carousel 或点击的 Bootstrap NavBar 项的 Article。其理念是,一个 Category 由一个 Bootstrap NavBar 菜单项或一个 Bootstrap Carousel 幻灯片表示。每次点击这两者之一时,我们都会获取当前 CategoryArticle

//=================================================================
// SUMMARY 
//=================================================================
//
// A simple Knockout.js overall page viewmodel, which simply acts as
// a host for a number of CategoryViewModel items, which are displayed
// within an embedded Twitter Boostrap Carousel. The first slide
// will have its articles fetched on initial creation of the 
// DemoMainPageViewModel. Subsequent slides will have their articles
// loaded when the slide is requested. The loading of a slides articles
// will only happen once. Essentially once the articles for a given slide
// (category) are loaded the first time they are never asked to load again
var DemoMainPageViewModel = function () {

    this.Categories = ko.observableArray();
    this.Loading = ko.observable(true);
    this.Loaded = ko.observable(false);
    this.HasArticle = ko.observable(false);
    this.CurrentArticle = ko.observable();

    this.SetArticle = function (article) {
            
        this.HasArticle(true);
        this.CurrentArticle(article);

        $('#articleModal').modal({
            keyboard: true
        }).modal('show');

    };

    this.Initialise = function(jsonCats) {

        this.Loading(false);
        this.Loaded(true);
        for (var i = 0; i < jsonCats.length; i++) {
            this.Categories.push(new CategoryViewModel(this, jsonCats[i]));
        }
            
        // Load the articles for the 1st slide (remember KO observableArray, 
        // are functions, so we need to call a function)
        this.Categories()[0].loadArticles();
    };

    // When the Category slide becomes active (requested to be shown via Nav menu)
    // load the Article(s) for the Category slide
    this.mainActionClicked = function (data, event) {
        var sourceElement = event.srcElement;
        var slideNumber = $(sourceElement).data('maincarouselslide');
        $('#mainCarousel').carousel(slideNumber);
        this.Categories()[slideNumber].loadArticles();
    };
};

CategoryViewModel

CategoryViewModel 是一个简单的 ViewModel,能够保存多个 Article,并负责显示与点击的 Article 相关的数据。基本思想是,Article 在第一次加载时会被延迟加载,**并且仅**在第一次加载时,调用以获取它们才会命中 WebAPI ArticleController。我们在这里使用了可爱的 jQuery when/then 语法。我喜欢它,因为它来自 C# 和 Task Parallel Library 及其 continuations 的使用,对我来说很有意义。总之,这是 CategoryViewModel 的代码:

//=================================================================
// SUMMARY 
//=================================================================
//
// A simple Knockout.js Category viewmodel. Each Category is 1 slide
// of an embedded Twitter Boostrap Carousel. Each Category also lazy
// loads an ObservableArray of Article items, when the Category slide
// becomes active in the Twitter Boostrap Carousel. The lazy loading
// of the Articles is only done once per slide.
var CategoryViewModel = function (parent, jsonCat) {

    this.Id = jsonCat.Id;
    this.Parent = parent;
    this.LoadedArticles = ko.observable(false);
    this.Title = ko.observable(jsonCat.Title);
    this.Description = ko.observable(jsonCat.Description);
    this.Articles = ko.observableArray();
    var context = this;

    this.hasArticles = ko.computed(function() {
        return context.Articles().length > 0;
    }, this);

        
    // Shows a modal dialog of the clicked Article
    this.articleClicked = function (article) {
        context.Parent.SetArticle(article);
    };

    // Do the lazy load of the article. Basically only do it
    // when this fuction is called externally
    this.loadArticles = function () {

            
        if (this.LoadedArticles() == true) {
            return;
        }

        this.LoadedArticles(true);

        // Push stuff to articles
        $.when($.ajax('/api/article/GetAll/' + this.Id))
            .then(function (data, textStatus, jqXHR) {
                for (var i = 0; i < data.length; i++) {
                    context.Articles.push(data[i]);

                    $('#img' + data[i].Id).tooltip({
                        title: data[i].ShortDescription,
                        trigger: 'hover focus'
                    });
                }
        });
    };
};

开始进行

为了启动 DemoMainPageViewModel 的初始化(即,使用 CategoryController 中的 Category 数据加载它),我们使用以下代码,这是一个简单的 JavaScript 自执行函数和初始的 jQuery AJAX 调用来获取 Category 数据。这段代码在获取 Category 数据时会显示一个忙碌指示器,**只有**在 Category 数据可用后,忙碌指示器才会隐藏,并显示主用户界面。

(function ($) {
    
    $(document).ready(function () {
        createViewModel();
    });


    //=================================================================
    // SUMMARY 
    //=================================================================
    //
    // A simple Knockout.js overall page viewmodel, which simply acts as
    // a host for a number of CategoryViewModel items, which are displayed
    // within an embedded Twitter Boostrap Carousel. The first slide
    // will have its articles fetched on initial creation of the 
    // DemoMainPageViewModel. Subsequent slides will have their articles
    // loaded when the slide is requested. The loading of a slides articles
    // will only happen once. Essentially once the articles for a given slide
    // (category) are loaded the first time they are never asked to load again
    var DemoMainPageViewModel = function () {

        ....
        ....
        ....

    };


    //=================================================================
    // SUMMARY 
    //=================================================================
    //
    // Initialise Carousel, and listen to the carousel controls, that should
    // load the current category slides Article(s) when the slide becomes active.
    // The Article(s) are loaded using a REST based WebApi call
    function hookUpCarouselControls(demoVM) {

        $('#mainCarousel').carousel({
            interval: false,
            pause:true
        }).on('slid.bs.carousel', function (event) {

            var active = $(event.target)
                .find('.carousel-inner > .item.active');
            var from = active.index();
            demoVM.Categories()[from].loadArticles();
        });
    }



    //=================================================================
    // SUMMARY 
    //=================================================================
    //
    // Shows a busy indicator, and then does an initial AJAX call to get
    // the initial Categories using REST based WebApi call. When the initial
    // categories are fetched, a Twitter Boostrap Carousel is created, which
    // is done using a simple Knockout.js ViewModel
    function createViewModel() {     

        var demoVM = new DemoMainPageViewModel();
        ko.applyBindings(demoVM);

        $.when($.ajax("/api/category"))
            .then(function (data, textStatus, jqXHR) {
                demoVM.Initialise(data);
                hookUpCarouselControls(demoVM);
        });
    }
    
})(jQuery);

_Layout.cshtml

这是使用 DemoMainPageViewModel 的主 ASP MVC 布局页面。不过,当我们在下面讨论轮播图时,您会看到更多 DemoMainPageViewModel 的用法。下面可以看到有一个 Bootstrap NavBar,它利用了 DemoMainPageViewModel 中的一些 Knockout.js 绑定。我们将在稍后更详细地介绍这一点。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
</head>
<body>


    <div id="wrap">


        <div class="container mainContainer">
            <div class="row">
                <div class="col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2">

                    <div class="navbar navbar-default navbar-inverse navbar-static-top" 
                            data-bind="visible: Loaded">
                        <div class="container">
                            <div class="navbar-header">
                                <button type="button" 
                                    class="navbar-toggle" 
                                    data-toggle="collapse" 
                                    data-target=".navbar-collapse">
                                    <span class="icon-bar"></span>
                                    <span class="icon-bar"></span>
                                    <span class="icon-bar"></span>
                                </button>
                                <a class="navbar-brand" href="#">MY PORTFOLIO</a>
                            </div>
                            <div class="navbar-collapse collapse">
                                <ul class="nav navbar-nav">
                                    <li class="active">
                                        <a data-bind="click: mainActionClicked" 
                                            href="#" 
                                            data-maincarouselslide="0">About</a>
                                    </li>
                                    <li>
                                        <a data-bind="click: mainActionClicked" 
                                           href="#" 
                                           data-maincarouselslide="1">C#</a>
                                    </li>
                                    <li><a data-bind="click: mainActionClicked" 
                                        href="#" data-maincarouselslide="2">Web</a></li>
                                    <li class="dropdown">
                                        <a href="#" 
                                           class="dropdown-toggle" 
                                            data-toggle="dropdown">XAML <b class="caret"></b></a>
                                        <ul class="dropdown-menu">
                                            <li><a data-bind="click: mainActionClicked" 
                                                href="#" data-maincarouselslide="3">WPF</a></li>
                                            <li><a data-bind="click: mainActionClicked" 
                                                href="#" data-maincarouselslide="4">Silverlight</a></li>
                                            <li><a data-bind="click: mainActionClicked" 
                                                href="#" data-maincarouselslide="5">WinRT</a></li>
                                        </ul>
                                    </li>
                                </ul>
                            </div>
                            <!--/.nav-collapse -->
                        </div>
                    </div>


                </div>
            </div>
        </div>
        <div id="seperator"></div>

        @RenderBody()


    </div>
    <div id="footer" data-bind="visible: Loaded">
        <div class="container">
            <div class="row">
                <div class="col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2">
                    <span>
                        <img src="~/Content/images/Info.png" 
                             width="30px" 
                            height="30px"></img><span>Sacha Barbers portfolio</span>
                    </span>
                </div>
            </div>
            <div class="row">
                <div class="col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2">
                    <span>
                        <img src="~/Content/images/Link.png" 
                            width="30px" height="30px">
                            <a class="footerAnchor" 
                               href="https://codeproject.org.cn/script/Articles/MemberArticles.aspx?amid=569009" 
                                target="_blank">View my articles here</a>
                        </img>
                    </span>
                </div>
            </div>
        </div>
    </div>
    @Scripts.Render("~/bundles/Js")
    @RenderSection("scripts", required: false)
</body>
</html>

Bootstrap 的一些用法

正如我在本文开头提到的,Bootstrap 是新兴(已新兴)HTML 框架中的一员,它提供了一套核心的 CSS/JavaScript 库和助手,使开发人员能够快速上手。这些库通常包含许多不同的控件。我肯定不会涵盖 Bootstrap 中**所有**的功能,但我们会讨论几个核心项目。

开始使用 Bootstrap 的第一步是获取代码。然后我们需要做以下事情:

包含 JavaScript/CSS 文件

由于我使用的是 ASP MVC,对我来说,这实际上意味着在 bundle 中包含正确的文件,如下所示:

public class BundleConfig
{
    // For more information on Bundling, visit http://go.microsoft.com/fwlink/?LinkId=254725
    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.Add(new ScriptBundle("~/bundles/Js").Include(
                    "~/Scripts/jquery-{version}.js",
                    "~/Scripts/bootstrap.js",
                        "~/Scripts/knockout-2.1.0.js",
                        "~/Scripts/TwitterBoostrapDemo.js"));

        // Use the development version of Modernizr to develop with and learn from. Then, when you're
        // ready for production, use the build tool at http://modernizr.com to pick only the tests you need.
        bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                    "~/Scripts/modernizr-*"));


        bundles.Add(new StyleBundle("~/Content/css").Include(
            "~/Content/bootstrap-3.0.1/css/bootstrap.css",
                "~/Content/bootstrap-3.0.1/css/bootstrap-theme.css",
                "~/Content/site.css"));
    }
}

导航栏

Bootstrap 带有一个**非常酷**(至少在我看来)的功能(好吧,它有几个,但这是我最喜欢的)。所以你问,它是什么?

概念上来说,它是一个菜单,但酷之处在于它是完全响应式的。

例如,这是它在大屏幕上的样子:

这是我们在平板电脑/智能手机上看到的景象,看看它多么紧凑且呈单列显示:

实现这一切的代码再简单不过了。代码如下:

<div class="navbar navbar-default navbar-inverse navbar-static-top" 
        data-bind="visible: Loaded">
    <div class="container">
        <div class="navbar-header">
            <button type="button" 
                class="navbar-toggle" 
                data-toggle="collapse" 
                data-target=".navbar-collapse">
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="#">MY PORTFOLIO</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li class="active">
                    <a data-bind="click: mainActionClicked" 
                        href="#" 
                        data-maincarouselslide="0">About</a>
                </li>
                <li>
                    <a data-bind="click: mainActionClicked" 
                        href="#" 
                        data-maincarouselslide="1">C#</a>
                </li>
                <li><a data-bind="click: mainActionClicked" 
                    href="#" data-maincarouselslide="2">Web</a></li>
                <li class="dropdown">
                    <a href="#" 
                        class="dropdown-toggle" 
                        data-toggle="dropdown">XAML <b class="caret"></b></a>
                    <ul class="dropdown-menu">
                        <li><a data-bind="click: mainActionClicked" 
                            href="#" data-maincarouselslide="3">WPF</a></li>
                        <li><a data-bind="click: mainActionClicked" 
                            href="#" data-maincarouselslide="4">Silverlight</a></li>
                        <li><a data-bind="click: mainActionClicked" 
                            href="#" data-maincarouselslide="5">WinRT</a></li>
                    </ul>
                </li>
            </ul>
        </div>
        <!--/.nav-collapse -->
    </div>
</div>

可以看到,这仅仅是使用一些 Bootstrap 类的问题,例如:

  • navbar
  • navbar-default
  • navbar-inverse
  • navbar-static-top
  • navbar-header
  • navbar-brand
  • navbar-collapse
  • nav navbar-nav

完整的详细信息可以在 Bootstrap 网站上找到。

响应式布局,又称“网格”

Bootstrap 的另一个杀手级特性是其响应式布局功能,也称为“网格”。网格确实是一个巧妙的工具。

正确使用“网格”的关键在于使用几个不同的容器 DIV 和几个不同的 CSS 类。

我认为学习网格的最佳方法是直接引用 Bootstrap 网站上的这段描述:

Bootstrap 包括一个响应式、移动优先的流体网格系统,它会随着设备或视口尺寸的增加而相应地扩展到 12 列。它包含预定义的类,方便布局选项,以及强大的 mixins,可用于生成更具语义的布局。

引言

  • 网格系统用于通过一系列行和列来创建页面布局,这些行和列容纳您的内容。Bootstrap 网格系统的工作原理如下:
  • 行必须放置在 .container 中才能实现正确的对齐和内边距。
  • 使用行来创建列的水平组。
  • 内容应放置在列中,并且只有列可以是行的直接子元素。
  • 预定义的网格类,如 .row 和 .col-xs-4,可用于快速创建网格布局。LESS mixins 也可用于更具语义的布局。
  • 列通过内边距创建沟槽(列内容之间的间隙)。这些内边距通过 .rows 上的负边距在行中进行偏移,以适应第一列和最后一列。
  • 通过指定您希望跨越的十二列的数量来创建网格列。例如,三个等宽的列将使用三个 .col-xs-4。

网格和全宽布局

希望创建完全流体布局(即您的网站会拉伸视口的整个宽度)的人必须将他们的网格内容包装在具有 padding: 0 15px; 的容器元素中,以抵消 .rows 上使用的 margin: 0 -15px;。

媒体查询

我们在 LESS 文件中使用以下媒体查询来创建网格系统中的关键断点。

/* Extra small devices (phones, less than 768px) */
/* No media query since this is the default in Bootstrap */

/* Small devices (tablets, 768px and up) */
@media (min-width: @screen-sm-min) { ... }

/* Medium devices (desktops, 992px and up) */
@media (min-width: @screen-md-min) { ... }

/* Large devices (large desktops, 1200px and up) */
@media (min-width: @screen-lg-min) { ... }

我们偶尔会扩展这些媒体查询以包含 max-width,以便将 CSS 限制在更窄的设备集。

@media (max-width: @screen-xs-max) { ... }
@media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { ... }
@media (min-width: @screen-md-min) and (max-width: @screen-md-max) { ... }
@media (min-width: @screen-lg-min) { ... }

网格选项

通过一个方便的表格,了解 Bootstrap 网格系统的各个方面如何在多种设备上运行。

点击图片放大查看

So,这就是 Bootstrap 官方关于如何使用网格系统的说明。让我们看一个使用网格系统的示例。这是我们稍后将要查看的轮播图代码的一小部分摘录。

<div class="container">
    <div class="row">
        <div class="col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2">
            <p class="lead" data-bind="text: Title" />
            <p data-bind="text: Description" />
        </div>

        <div data-bind="visible: hasArticles" >
            <div data-bind="foreach:  $data.Articles" 
			class="row col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2 articleRow">
                <div class="col-xs-12 col-md-3">
                    <img data-bind="attr: {src: ImageUrl, id: 'img' + Id}, 
				click: $parent.articleClicked" 
			class="img-responsive articleImage" 
			alt="Responsive image" />
                </div>
            </div>
        </div>
    </div>
</div>

在上面的示例代码中,我们可以看到有一个主 Container,其中包含两个 Row

每个 Row 都有一些内容,这些内容将在“特小”和“中等”设备上占据八列,并且在每种情况下,在左侧和右侧都会有两个列的偏移。这种布局在所有不同的外形尺寸上都工作得很好(至少在我的测试中是这样)。

轮播图

演示应用程序的核心是使用 Bootstrap Carousel(参见 https://bootstrap.ac.cn/javascript/#carousel),它用于托管通过 Web API CategoryController 获取的每个 Category 的一个幻灯片。然后将其绑定到 Knockout.js DemoMainPageViewModel,该 ViewModel 包含多个 Knockout.js CategoryViewModel。每个 CategoryViewModel 还包含多个 Knockout.js ArticleViewModel

本文前面已经涵盖了不同 ViewModel 的内部工作原理,所以我不会重复。取而代之的是,我们将只查看 Bootstrap Carousel 的标记,您应该特别关注其中的 Knockout.js 绑定。

基本上,它非常简单:一个 Carousel 幻灯片 = 一个 CategoryViewModel。幻灯片中的一幅图像 = 一个 ArticleViewModel。显然,随着幻灯片的切换,它所代表的 CategoryViewModel 以及属于新显示的 CategoryViewModelCategoryViewModel Article 也会随之改变。

这是 Bootstrap Carousel 的 HTML 代码和 Knockout.js 绑定:

<div id="mainCarousel" data-wrap="false" class="slide"  data-bind="visible: Loaded">
    <div class="carousel-inner" data-bind="foreach: Categories">
        <div class="item" data-bind="css: {'active': $index()==0}">
            <div class="container">
                <div class="row">
                    <div class="col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2">
                        <p class="lead" data-bind="text: Title" />
                        <p data-bind="text: Description" />
                    </div>

                    <div data-bind="visible: hasArticles" >
                        <div data-bind="foreach:  $data.Articles" 
				class="row col-xs-8 col-md-8 col-xs-offset-2 col-md-offset-2 articleRow">
                            <div class="col-xs-12 col-md-3">
                                <img data-bind="attr: {src: ImageUrl, id: 'img' + Id}, 
						click: $parent.articleClicked" 
					class="img-responsive articleImage" alt="Responsive image" />
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <a id="leftCaroselControl" class="left carousel-control" href="#mainCarousel" 
		data-slide="prev"><span 
		class="glyphicon glyphicon-chevron-left"></span></a>
    <a id="rightCaroselControl" class="right carousel-control" href="#mainCarousel" 
		data-slide="next"><span 
		class="glyphicon glyphicon-chevron-right"></span></a>
</div>

工具提示

一旦包含了 Bootstrap 所需的所有内容,工具提示就非常容易了,只需一些 JavaScript 代码即可:

$('#img' + data[i].Id).tooltip({
    title: data[i].ShortDescription,
    trigger: 'hover focus'
});

这会在鼠标悬停在元素上时显示一个漂亮的工具提示,如下所示:

弹出对话框

Bootstrap 使显示弹出对话框变得非常容易。下面显示了单击 Article 时的效果(其中 CategoryViewModel 的父级将是 DemoMainPageViewModel 的单例实例):

var CategoryViewModel = function (parent, jsonCat) {

    // Shows a modal dialog of the clicked Article
    this.articleClicked = function (article) {
        context.Parent.SetArticle(article);
    };
}


var DemoMainPageViewModel = function () {

    this.SetArticle = function (article) {
            
        this.HasArticle(true);
        this.CurrentArticle(article);

        $('#articleModal').modal({
            keyboard: true
        }).modal('show');

    };
}

就这样

好了,这就是我这次想说的全部内容。如果您喜欢这篇文章,非常欢迎投票和评论。

顺便说一下,我将暂时休息一下写文章,因为我决定学习 F#。因此,我不会写太多文章,但您可能会看到几篇关于我学习新语言(F#)的博客文章,我希望这些文章对像我一样刚接触 F# 的人有所帮助。这些博客文章将是基础级别的,因为我将从 F# 的新手角度出发。不过,人总得从某个地方开始,对吧?

© . All rights reserved.