bootstrap插件概述

好像没有找到过关于bootstrap插件部分的文章,前两天在做一个input放大镜,是采用bootstrap插件编码规范,从而发现原来bootstrap的编码非常让人愉悦。故而想系统性的了解一下。

一、自定义编译js文件

默认bootstrap.min.js提供12个完整插件脚本,我没有找到可以通过网上直接构建的,应该有只是源代码结构非常漂亮,每一个写前端的都心需要自己动手构建一个比较短小的bootstrap插件脚本库出来。

bootstrap是通过 grunt 来构建的,关于grunt使用见参考资料。

bootstrap: {
    src: [
        'js/transition.js',
        'js/alert.js',
        'js/button.js',
        'js/carousel.js',
        'js/collapse.js',
        'js/dropdown.js',
        'js/modal.js',
        'js/tooltip.js',
        'js/popover.js',
        'js/scrollspy.js',
        'js/tab.js',
        'js/affix.js'
    ],
    dest: 'dist/js/<%= pkg.name %>.js'
}

以上是 Gruntfile.js 的一部分【他JSON格式】,用于拼接所有插件脚本,所以很简单只需要把不需要的加个 // 即可。

当然这里只有JS部分,还得去掉CSS部分, bootstrap采用的是LESS编写CSS,存放于 /less/bootstrap.less 也只需要找到我们并不需要的插件样式的 @import "xxx.less"

完成所有操作所以,只需要在源代码根目录下,输入CMD命令: grunt dist,会自动在 dist 目录下重新生成一份新的文件。

二、闭包

+function ($) {
  'use strict';

  // BUTTON PUBLIC CLASS DEFINITION
  // ==============================

  var Button = function (element, options) {
    this.$element  = $(element)
    this.options   = $.extend({}, Button.DEFAULTS, options)
    this.isLoading = false
  }}(jQuery);

很典型的一个闭包应用,这里将 jQuery 对象做为 $ 参数,这样可以避免闭包类使用的是 jQuery 对象,而不是其他类库或自己定义的 $

细心的人还会发现,为什么 function 前面会有一个 + 号,这是一个保障作用,特别是将bootstrap脚本库作为项目当中的一个库;并将所有脚本压缩成一个文件时,如果紧邻前面脚本没有加上 ; 来区分结束时,就会合在一起。有时我们也会使用 ;function(){} 逗号。

三、避免插件同名冲突

  var old = $.fn.button

  $.fn.button             = Plugin
  $.fn.button.Constructor = Button


  // BUTTON NO CONFLICT
  // ==================

  $.fn.button.noConflict = function () {
    $.fn.button = old
    return this
  }

$.fn.button.noConflict 每一个插件都会相类似的代码,用来处理插件同名冲突问题,比如:button,在 jquery.ui 下也有一个同名,假如你同时引用这两个类库时,就会产生两个问题:

  1. jquery.ui 在 bootstrap 前面,这个时候你使用 $('').button() 调用的是 bootstrap 的 button
  2. bootstrap 在 jquery.ui 前面,这个时候你使用 $('').button() 调用的是 jquery.ui 的 button

为什么?因为加载顺序的关系插件被覆盖了,而对于 jquery.ui 而言并未有同时冲突的处理,所以这里强烈要求将 jquery.ui库放在bootstrap前面,有这个前提,再来看我下面的示例,就会明白 noConflict 有什么作用。

//$.fn.button.noConflict();
$('button').button();

jquery.ui库放在bootstrap前面 的前提下,我注释掉第1行代码时,调用的是 bootstrapbutton 插件。反之调用的是 jquery.ui 下的 button 插件。怎么样?有没有感叹 bootstrap 的编码方式很完美。

四、data-* 属性

所有bootstrap的插件都可以不需要在页面上写上脚本调用代码,包括插件参数,都是以 data-参数名="值" 这种形式出现,有没有觉得很高大上。

<button type="button" class="btn btn-primary" data-toggle="button" aria-pressed="false" autocomplete="off">
  Single toggle
</button>

以上当我点击按钮时会自动加载 .action 样式,再次点击取消样式。而实现这一切要归于以下代码:

  $(document)
    .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) {
      var $btn = $(e.target)
      if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
      Plugin.call($btn, 'toggle')
      e.preventDefault()
    })
    .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) {
      $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type))
    })

这里的 click.bs.button.data-api 带有多级的事件命名空间,虽然不太会有重复的事件名称存在,但是如果你有洁癖不想bootstrap监控这些事件,我们可以直接注销掉,比如:

$(document).off('.bs'); // 所有bootstrap监控事件
$(document).off('click.bs.button'); // 只对button插件有效

另一方面将参数 data-* 化,核心其实就是调用 jQuery.data() 他会自动处理这一方面,比如:

  Button.prototype.setState = function (state) {
    var d    = 'disabled'
    var $el  = this.$element
    var val  = $el.is('input') ? 'val' : 'html'
    var data = $el.data()

    state = state + 'Text'

    if (!data.resetText) $el.data('resetText', $el[val]())

    $el[val](data[state] || this.options[state])

    // push to event loop to allow forms to submit
    setTimeout(function () {
      state == 'loadingText' ?
        $el.addClass(d).attr(d, d) :
        $el.removeClass(d).removeAttr(d);
    }, 0)
  }

其中 var data = $el.data() 就是自动将当前元素的所有 data-* 转化为对象。关于这个详见 jQuery.Data()

五、API接口

bootstrap插件都是单一入口、可链接式,比如我们可以这么调用一个 button 插件。

$('button').button('loading').addClass('active');

关于单一入口,比如:

$('button').button('loading');
$('button').button('reset');

是不是觉得记忆成本非常低,有没有?

六、关于事件

bootstrap 每个插件都有属于自己的一序列自定义事件,比如 modelhidden.bs.modal,而所有这一切都是通过 jQuery.trigger 来触发,关于 jQuery.trigger 事件的一些细节,比如:版本不同事件是否冒泡也不一样等等。

当我们需要监听这些事件时,我们可以这么做:

$('#model').on('hidden.bs.modal', function(e) {});

其中 $('#model') 是当前模态框的元素,这样有效的限定同一页面多个模态分别监听。

当然也可以用 $(document) 监听,这归于 jQuery.trigger 事件冒泡,但如果有多个模态框时那么任何一个关闭都将引时该监听。

一个题外话,在查bootstrap源代码时,发现大量的使用 $.proxy(this.hide, this),作用是保持在事件回调函数内上下文依然是插件本身。

七、CSS3兼容

每个插件都有做如下处理:

      var callbackRemove = function () {
        that.removeBackdrop()
        callback && callback()
      }
      $.support.transition && this.$element.hasClass('fade') ?
        this.$backdrop
          .one('bsTransitionEnd', callbackRemove)
          .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
        callbackRemove()

    } else if (callback) {
      callback()
    }

这里会监测是否支持CSS3中的 transition,当不支持时直接回调用,否则当动画结束后再回调。

参考资料

  1. Grunt 快速入门
  2. bootstrap github

Input放大镜

项目地址

https://github.com/cipchk/inputmagnify

Input放大镜

Input放大镜,像在输入手机号或身份证号码时,每四位插入一个分隔符。

使用

  1. 依赖于jQuery。
  2. 分别引用css.css和src.js两个文件。

javascript 方式

$('#mobile').inputmagnify({
    placement: 'bottom'
});

html 方式

<input type="text" data-cipchk="inputmagnify" data-first_digit="4" />

所有参数都支持 data-* 的形式。

参数

  • animation:显示时是否带动画效果,(boolean:true)。
  • placement:放大镜位置,(字符串:top、left、right、bottom)。
  • first_digit:首次间隔位数,比如手机号前3位需要分隔,当为0时不参与(整数:3)。
  • interval_digit:除首次间隔外,剩下字符间隔位数,(整数:4)。
  • works:间隔字符,(字符串:半角空格)。
  • template:显示器模板文件。
  • container:显示器HTML置放位置,(jQuery对象:默认放到input后面)。

分析插入DOM效率问题:reflow

在富客户端时代,通过JavaScript来改变元素结构是必须的。这是通过DOM或document对象模型来完成操作,而对于当大量操作或者你想写一个比别人更快的应用,那么了解一些对DOM的原理变得尤为重要。

当元素被真正插入到document时会引发浏览器reflow(即重绘元素样式并计算,这是浏览器的事我们不讨论)。比如我们插入一个新元素、改变元素的CSS样式、调整浏览器窗口、访问offsetHeight等等都会引发若干次 reflow。

说了好几次 reflow,那么提高应用效率,减少浏览器触发 reflow 的次数是一个正比关系。

常见几种容易引发 reflow

reflow-chart

从图上看,可以看得出关于一些会引发 reflow 的CSS属性。

一、CSS样式操作

我们应该尽可能的减少对 element.style.* 的操作次数,因为每一行脚本代码都将可能引发 reflow(为什么说可能,因为对于像现代浏览器会对这种操作做适当的优化)。

function setLinkStyle() {
    element.style.fontWeight = 'bold';
    element.style.color = '#000';
    // 更多类似
}

更适合的方式应该是

.link { font-weight: bold; color: #000; }
function setLinkStyle() {
    element.className = 'link';
}

二、尽可能处理好DOM后再插入

这种创建或修改一个也会引发一个 reflow。假如我们有个函数用来创建新元素,链接文本为 Text;并提供 classlink

function addLink(parentElement) {
    var element = document.createElement('a');
    parentElement.appendChild(element);
    element.innerHTML = 'Text';
    element.className = 'link';
}

这将会引发三次 reflow。因此,我们更快的写法应该是:

function addLink(parentElement) {
    var element = document.createElement('a');
    element.innerHTML = 'Text';
    element.className = 'link';
    parentElement.appendChild(element);
}

三、DocumentFragment 方法

DocumentFragment 他是一个轻量级且不需要父代的DOM对象,他对浏览器或手机浏览器的兼容支持非常完美。

比如我们连续创建30个链接,比如:

function addBatchLink(parentElement) {
    for (var i = 0; i < 30; i++) {
        var element = document.createElement('a');
        element.innerHTML = 'Text';
        element.className = 'link';
        parentElement.appendChild(element);
    }
}

这里会引发30次 reflow。而通过 DocumentFragment 的方式只会触发一次 reflow

function addBatchLink(parentElement) {
    var fragment = document.createDocumentFragment();
    for (var i = 0; i < 30; i++) {
        var element = document.createElement('a');
        element.innerHTML = 'Text';
        element.className = 'link';
        fragment.appendChild(element);
    }
    parentElement.appendChild(fragment);
}

四、offsetHeight 等属性

appendChild 一个元素后接着再访问元素的 offsetHeight 样式属性值时也会立刻触发一个 reflow,且脚本会等待 reflow 完成后才会继续,虽然计算会非常快。

jQuery的DOM操作

jQuery对DOM操作的核心文件是 manipulation.js,这里扩展了一些像 appendafterbefore 等关于DOM操作接口。而这里的核心是 domManip() 方法,他会使用 createDocumentFragment 创建一个全新元素对象,最后才执行一次 appendChild 插入元素。

可见jQuery已经做了很多优化。

重绘与reflow

浏览器会引起重绘,可能会是当添加元素、改变元素可见性、改变背景颜色等时。

而reflow是当DOM被改变风格(我们大可指CSS)时会执行,当改变className、浏览器窗口大小等会进行若干次 reflow,当父元素执行 reflow时,也一并会考虑到子元素的变化,甚至前后代元素,最后变成所有都需要 reflow

重绘与reflow,他们就像是相互关联着,都是影响 DOM 操作效率最主要原因。

影响 reflow 时间

  • DOM结构深度越深,修改元素后代越多所需要时间越长。
  • 最小化CSS规则、删除未使用的CSS,尽可能使用className。

总结

关于 reflow 的话已经说了那么多,如果我们抛开UI阻塞、重绘等问题外,对于SPA应用而言操作DOM的效率影响者整个应用的效率。了解为什么 reflow 是一个至关重要问题,这样才能写出更高效的前端代码。

参考资料

[SignalR2] 系列开篇

文章索引

  1. 系列开篇
  2. SignalR 部分核心原理
  3. WebSocket部署问题和优化若干问题
  4. 身份验证和授权
  5. 服务端主动推送
  6. SignalR JavaScript 客户端
  7. AngularJS 应用
  8. 异常处理

想要了解SignalR得先了解WebSocket,因为大可直接说SignalR的实现是为了实现类似WebSocket的实时通信。

什么是WebSocket

它的目标是为了让浏览器和服务端双向通信,即我们可以按传统请求-接收或服务端主动发送数据给客户端。WebSocket他并非是一个功能型东西,是一种协议,得要明白这一点,但凡浏览器和服务器实现这种协议,就可以直接在任何浏览器、平台(Win/Linux等等)上实现WebSocket。

在WebSocket协议中规定,浏览器和服务器只需要一次握手,然后彼此之间就有了爱意、可以相互抛媚眼。因此这种规范带来两大优点:

  • Header 极少,2字节。不言而喻,极大减少宽带。
  • Server Push即服务端可以主动推送。

WebSocket 目前在浏览器上支持情况,可以参考测试页

另一种服务器推技术:Comet

如同WebSocket的Server Push一样。对于客户端而言有两种方式:

一是基于套接口,这需要运用像Flash XMLSocket或Java Applet套接口。

二是基于HTTP长连接,而实现这一技术有三种:AJAX长轮询、iframe、htmlfile流方式。

Comet是一种兼容性很好,像很多新浪微博、QQ邮箱、GTALK等等都使用该技术。可以参考这篇文章具体实现方案。

SignalR

应该说SignalR是上种两种的一个结合,甚至支持更多方案。SignalR提供四种数据传输方式,来解决浏览器兼容问题,他们默认分别按以下来判断该采用哪种方式:

  1. webSockets
  2. serverSentEvents
  3. longPolling
  4. foreverFrame 针对IE有效的一种变态做法,我没有细看。

以上四种具体实施方案百度非常多,很全,这里不再赘述。

一个完整示例

百度上已经有很多关于聊天室的示例,因为聊天室的确非常能说明SignalR的优点,而这里示例是设计一个关于消息提醒功能。

假定需求:用户登录后接收私信或系统异常报告通知及当前服务器时间。

1、首先我们可以通过NuGet很轻松安装SignalR,并在 Startup.cs 启用SignalR的支持。

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ConfigureAuth(app);

        app.MapSignalR(); // 启用SignalR
    }
}

MapSignalR 是默认的注册SignalR入口,这里我们完全可以自定义任意对SignalR具体实现接口。比如重新定义服务器地址(默认是 /signalr)、自定义身份证验证和授权机制等等。对于整个SignalR框架而言,他们都是采用依赖注入,换言之我们可以最大化扩展符合业务逻辑能力。

以下是将服务器地址变更为 /demo,且禁止生成生成客户端代理脚本。

app.MapSignalR("/demo", new Microsoft.AspNet.SignalR.HubConfiguration()
{
    EnableJavaScriptProxies = false
});

2、创建Hub类

为了演示需要,我分别创建 GetNow()GetCount() 两个方法:

GetNow():任何人都可以访问,对所有连接者发送当前时间。

GetCount():需要登录后才可以访问,发送自己的消息数据。

[HubName("msgHub")]
public class MessageHub : Hub
{
    public void GetNow()
    {
        Clients.All.pushNow(DateTime.Now);
    }

    [Authorize]
    public void GetCount()
    {
        List<MessageItem> ls = new List<MessageItem>();
        ls.Add(new MessageItem()
        {
            count = new Random().Next(1, 99),
            title = "异常数量",
            url = "/ex"
        });
        ls.Add(new MessageItem()
        {
            count = new Random().Next(1, 99),
            title = "私信",
            url = "/msg"
        });
        Clients.Caller.pushGet(ls);
    }
}

[Authorize] 默认采用的是ASP.NET的验证机制通道,换句话说当网站采用Windows登录验证,那么这里的 [Authorize] 也直接采用此方式。而对于SignalR自身已经有握手Key,来保证连接安全。

而往往默认的身份验证和授权机制很难符合业务需求,所以也可以直接继承 Microsoft.AspNet.SignalR.AuthorizeAttribute 自定义授权和验证规则,这一点后面会有专门文章来说明。

当采用 [Authorize] 属性后,就可以在方法里头调用 Context.User 对象访问当前用户信息。

另外非常重要的一点:app.MapSignalR(); 必须是 ConfigureAuth(app); 之前,因为我们都懂。

3、HTML代码

当开启 EnableJavaScriptProxies 时(默认就是开启),就可以享受SignalR自动生成的一个Hub客户端脚本代码。所以我需要先引入这些脚本;包括jQuery.js、jquery.signalR.js两个核心文件。

<script src="~/Scripts/jquery-1.10.2.min.js"></script>
<script src="~/Scripts/jquery.signalR-2.1.2.js"></script>
<script src="~/demo/hubs"></script>
<p id="J_Now"></p>
<p id="J_GetCount"></p>

其中 ~/demo/hubs 由SignalR自动生成,这里的 /demo 是指在前面变更的服务器地址。

4、启动Hub

虽然SignalR自动生成Hub客户端脚本,但此时还是需要添加一些 client 以便于服务器推送时接收数据,和启用Hub。

(function () {
    var msgHub = $.connection.msgHub;

    $.extend(msgHub.client, {
        pushNow: function (res) {
            $('#J_Now').html(res);
        },
        pushGet: function (res) {
            $('#J_GetCount').html(JSON.stringify(res));
        }
    });

    $.connection.hub.start().done(function () {
        init();
    }).fail(function () {
        // 连接失败处理
    });


    function init() {
        setInterval(function () { msgHub.server.getNow(); }, 1000); // 按理应该是由服务器直接推送,但我想留到另一节说,不要问为什么,任性。

        msgHub.server.getCount().fail(function (res) {
            console.log(res);
        });
    }
})();

每个Hub都有对应 clientserver,而SignalR会反射类下的所有公共方法,并把这些自动生成拼接成JavaScript放进 server 集合里头。

client:表示由服务端推送数据时,客户端接收对象,比如:Clients.Caller.pushGet(ls); 中的 pushGet

server:表示主动请求服务端的方法,比如:GetCount()

对于 server.getCount() 返回的是一个 promise() 对象,所以这里可以直接 done()fail() 来处理一些失败或成功时操作。

以上,就是一个完整的示例。

总结

SignalR 不光解决了我们多种实时通信的方案,而且兼容性非常好,这是一个相当好的项目,不久将来一定会有越来越多项目使用,甚至我们可以整个项目都采用 SignalR。这对于用户而言是极棒的体验。

关于ASP.NET API的FromBody参数

越来越喜欢 asp.net web api,很大原因是因为非常干净。而其中 POST 是作为修改交互极为重要的一个请求。当然为了体验,和 asp.net web api交互总是离不开 jQuery 或者其实类似 ajax 请求方法,但往往会有一些小小诡异,比如:

咦,为什么都是null?

所以这篇文章会讲解一些 POST 请求时可能的一些问题。

POST原始参数数据到Asp.net web API

public void Post(string value){}

原则上我们只需要按标准的使用 $.post,比如:

$.post('/api/values', { value: 'asdf' });

可结果是:

POST http://localhost:53924/api/Values 404 (Not Found) 

WHY???很明显路由未找到,可这与我们期待不同呀。

必须标记参数[FromBody]

这是一种概念问题,因为对于 POST 时,按我们想当然自然是会自动将 POST 内容直接给 value。而对于路由而言这是一个完整的请求参数,自然就返回一个 404 错误。

我们需要给参数加上 [FromBody] 属性,比如:

public void Post([FromBody]string value){}
$.post('/api/values', { value: 'asdf'}, function(d) { console.log(d); });
// 输出:null

以上我们可以正常的请求成功。但输出的结果还是 null,这不应该我传递是 asdf,这一点我后面再提。

Asp.net web API 参数绑定规则

默认下,ASP.NET web API按以下两种规则绑定参数:

  1. 简单类型主要包括原始类型(int、bool、double等)含(TimeSpan、Guid待)以及String,默认会尝试从 URI 获取。
  2. 复杂类型会尝试从请求体获取,并通过媒体类型格局化器进行解析。

每一个方法只能有一个[FromBody]

除非你自定义一个模型绑定方法,否则你只能使用一个 [FromBody],而对于 POST大部分情况下是多个参数,所以最好的是采用复杂类型。

为什么只允许一个,这应该从认识 RESTful 架构开始,对于一个资源对应一个特定方法,而把数据分成几个松散参数也违背 ASP.NET API 吧。

public class Album {
    public string name {get;set;}
}
public void Post(int id, Album album){}
$.post('/api/values?id=1', { name: 'asdf'}, function(d) { console.log(d); });

以上,这是将简单类型和复杂类型同时使用。

[FromBody]参数必须进行键值编码

回过头看 null 问题,ASP.NET web API需要我们为参数指定 [FromBody],这是一种特定格式。正因为这种特定格式,会依赖 Content-Type类型来解析请求体应该采用什么格式,而绝大部分客户端和服务端采用的是键值对和JSON两种。

  1. 键值对:jQuery默认就是采用这种方式,而对于ASP.NET web API默认是空值做为取值条件,所以我们应该 $.post('/api/values', { '': 'asdf' }); 这样 value 参数才能够正常绑定。

  2. JSON:当Content-Type:application/json时,这也是大多数客户端所习惯采用的比如(AngularJS),对于这种情况我们应该 $.ajax( { type:'post', url: '/api/values', contentType: 'application/json', data: '"asdf"' }).done(function(d) { console.log(d); });

415 Unsupported Media Type

当我们传递的请求体内容无法被正常解析时,会得到这个错误。所以在传递请求体内容时要注意。

jQuery更新表单值后在DOM中无效

这是原先一次使用html()来判断DOM中input是否有某值时发现的一个问题。

假定有一HTML片断,其html()是:

<input type="text" name="val" value="val" placeholder="" >

然后对input设置新值:

$('#val').val('new value');

对DOM的input元素设置值时,并没有直接在DOM上体现,所以设置前和设置后其html()结果完全一样。

假如我是通过 attr 来设置DOM的属性值时,可以达到我预期的结果。

$('#val').attr('va', 'new value');

按我猜想当对DOM的value进行操作时,由于需要保留原始默认值,还记得一个表单的 type="reset" 吗?所以对于value的只存于内存当中。只会对首次加载页面有效。

attr 实际是通过 setAttribute 来修改DOM对象,这里我又尝试一个有意思的代码。

ipt.attr('type', 'number')

原先是一个 type="text" 被我强制修改为 number,页面也立即变为 number 表单样式。

以上。

© 2017 卡片机色彩 沪ICP备13032872号-3

Theme by cipchk

to top