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。这对于用户而言是极棒的体验。

JavaScript的Number.prototype必须使用valueOf()来获取原始数值

本来对原先对一个字符串做扩展时,是这样子:

String.prototype.format = function() {
    return this;
}

其中 this 表示当前字符串对象,而相对于 Number.prototype 而言,有些不同。

this 被解释为一个完整的 Number 对象,不能够直接通过 this 直接访问数字值;必须通过 valueOf() 来获取原始值。

Number.prototype.format = function() {
    return this.valueOf();
}

附录:
1.Number valueOf()

pjax处理json返回体

项目一直使用的是pjax版本,虽然在github上有很多pjax实现版本,但是唯有这个版本最适合项目。

在项目运用中,很多对于一些像处理开关标识,比如:文章的关闭开关。

这种请求是不应该做为一种历史记录中的一种,而对于pjax而言默认是按 html 的处理 success 事件的,即使你根据 options 传入不一样的 dataType,他依然会按这种方式来处理。

我目前的做法是,拦截 success 事件交由新事情 pjax:success_object 来处理后面的事情。

这是部分被我修改的代码片断:

  options.success = function(data, status, xhr) {
    // 当返回体为Object时,适为返回体JSON
    // 项目中,对于这部分请求不认为是有效可后退对象,所以全部转给success_object处理
    if (typeof(data) === 'object') {
      fire('pjax:success_object', [data, xhr, options])
      return
    }
   }

或者由我Fork出来的另一版本

什么是pjax

Ajax是Web开发非常重要的工具,只不过有一点单纯拿AJAX来请求一个页面时特别是单页应用时,无法正常使用浏览器历史记录、后退、刷新时同时替换文档标题。

而pjax就是用来解决这个问题,使用的是jQuery ajax 和 pushState(查看支持pushState浏览器版本情况)。

一、pushState、replaceState

pushState往历史堆栈的顶部添加一条记录,replaceState更改当前历史记录,包括三个参数:

  • data为一个可以被解析的对象或null,不能够是一个jQuery对象。在触发popstate事件时,会做为state参数传递过去;
  • title为页面的标题;
  • url为页面的URL,不写则为当前页。

二、pjax应用

pjax并不是完全自动的,需要自己调用。

首先:需要给页面设定一下被替换的位置,例:

<!DOCTYPE html>
<html>
<head>
  <!-- styles, scripts, etc -->
</head>
<body>
  <h1>My Site</h1>
  <div class="container" id="pjax-container">
    Go to <a href="/page/2">next page</a>.
  </div>
</body>
</html>

如果我们希望点击 /page/2 链接,会将 /page/2 HTML代码替换到 #pjax-container 容器中,则还需要告知pjax监听 a 标签。

$(document).pjax('a', '#pjax-container')

以上是一个完整的例子;如果你希望跟Unobtrusive JavaScript兼容,那么可以尝试使用符合Unobtrusive JavaScript写法,当然并不能完整的利用data-ajax-loading、data-ajax-mode、data-ajax-update属性。

每个PJAX请求都会自动加入一个 X-PJAX 请求头,以便于我们可以来判断是否需要引入 layout 文件,这里我是通过一个Filter来判断:

    public class PJaxFilter : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            base.OnActionExecuting(filterContext);
            filterContext.Controller.ViewBag.layout = "~/Views/Shared/_Layout.cshtml";
            string pjaxHeader = filterContext.RequestContext.HttpContext.Request.Headers["X-PJAX"];
            if (!String.IsNullOrEmpty(pjaxHeader)) filterContext.Controller.ViewBag.layout = null;
        }
    }

cshtml页面对于布局需要根据 @{ Layout = ViewBag.layout; } 来,这样当我们直接通过 /page/2 请求页面时也能够正常显示。

页面标题可以通过在HTML中输出 <title>页面标题</title>,pjax会自动获取。

pjax也提供一系列事件,比如 pjax:sendpjax:complete 来处理加载状态:

$(document).on('pjax:send', function() {
  $('#loading').show()
})
$(document).on('pjax:complete', function() {
  $('#loading').hide()
})

二、pjax原理

pjax的AJAX请求是基于jquery的ajax,所以pjax的options参数大部分才是jquery ajax请求的原样参数。

1、每次PAJX发生请求时会先通过 pushState 将请求页压入历史堆栈的顶部。

  if (xhr.readyState > 0) {
    if (options.push && !options.replace) {
      // Cache current container element before replacing it
      cachePush(pjax.state.id, context.clone().contents())
      window.history.pushState(null, "", stripPjaxParam(options.requestUrl))
    }
    fire('pjax:start', [xhr, options])
    fire('pjax:send', [xhr, options])
  }

2、请求成功后会:替换当前历史记录页、修改页面标题、把HTML写入相应容器当中。

    if (options.push || options.replace) {
      window.history.replaceState(pjax.state, container.title, container.url)
    }
    // Clear out any focused controls before inserting new page contents.
    try {
      document.activeElement.blur()
    } catch (e) { }
    // 这里的页面标题,会依次按<head><title /></head>、按Response片断根节点的title属性、Response带有<title />。
    if (container.title) document.title = container.title
    fire('pjax:beforeReplace', [container.contents, options], {
      state: pjax.state,
      previousState: previousState
    })
    context.html(container.contents)

这里要特意提一下pjax对脚本的处理方式。

因为 .html() 是基于innerHTML来改变容器代码,所以如果在页面使用 document.write 变成很不可取的方式,因为不同版本浏览器解析会有不可预知的情况发生,参考

还有针对 <script src="" /> 的处理是将未加载过的脚本写入到 <head> 中。

这里pjax的顺序是先写入HTML,再加载script,这好处就是避免DOM渲染和脚本资源请求受阻,倒置页面假死。

所以这里要特别注意加载顺序问题,如果某个页面每次请求都需要引用外部脚本,建议用 getScript。

3、监控window.onpopstate事件,后退时重新发送AJAX请求。

pjax会先从缓存中获取数据,而缓存并非无限制的,可以通过调用 pjax 时指定 maxCacheLength 参数大小(默认:20);和重新发送请求。

详见 onPjaxPopstate 函数。

4、重新加载布局

我们可以重新指定Response的请求头pjax版本号,来强制重新刷新页面。

if request.headers['X-PJAX']
  response.headers['X-PJAX-Version'] = "v123"
end

或html代码加入:

<meta http-equiv="x-pjax-version" content="v123">

其实pjax和模块化编程(例:require.js)真的是天生一对,之前有好几个小项目已经做了大量开发,效率、用户体验真的上升一个档次。

以上。

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

Theme by cipchk

to top