1. Unobtrusive JavaScript
介绍
说到Unobtrusive Ajax
,就要谈谈UnobtrusiveJavaScript
了,所谓Unobtrusive JavaScript
即为非侵入式JavaScript
,是目前在Web开发领域推行的一种思想。
相信大家一定都有直接在HTMLElements
上面写上事件调用代码的情况,比如:
<!--page.html-->
<!doctype html>
<html>
<head>
<title>Obtrusive JavaScript Page</title>
<script type="text/ecmascript">
function onload() {
console.log("Page has be loaded");
}
</script>
</head>
<body onload="onload">
<button onclick="alert('hello');">Hello</button>
<div onclick="this.style.color='red'">
Click to Change Color
</div>
</body>
</html>
这种代码就叫做侵入式的JavaScript
,因为这种情况下JavaScript
代码和HTML代码混在在一起,不利于代码阅读以及然后维护,还会导致HTML代码混乱不堪。
而相对与上面的做法,非侵入式JavaScript
则采用如下的做法来避免这种混乱:
<!--page.html-->
<!doctype html>
<html>
<head>
<title>Unobtrusive JavaScript Page</title>
<script src="page.js"></script>
</head>
<body>
<button id="btnHello">Hello</button>
<div id="colorChg">
Click to Change Color
</div>
</body>
</html>
//page.js
document.onload = function (e) {
var body = document.getElementsByTagName('body')[0];
body.onload = function(e) {
console.log("Page has be loaded");
}
var btn = document.getElementById('btnHello');
btn.onclick = function (e) {
alert('hello');
}
var div = document.getElementById('colorChg');
div.onclick = function (e) {
this.style.color = 'red';
}
};
当然,为了让JavaScript
看起来更加的优美,写起来更加的舒畅,更加的不那么费劲,可以使用现成的JavaScript
类库完成上面的操作,比如jQuery:
//page.js
$(function (e) {
$('body').click(function (e) {
console.log("Page has be loaded");
});
$('#btnHello').click(function (e) {
alert('hello');
});
$('#colorChg').click(function (e) {
$(this).css('color', 'red');
});
});
2. Unobtrusive JavaScript
在ASP.NET MVC中的应用
使用VS新建一个ASP.NET MVC项目就会在~/Scripts/
目录下面看到很多以unobtrusive
结尾的javascript脚本文件,如:
今天的主角是ASP.NET MVC Unobtrusive Ajax
,那么大家应该可以猜到主要还是jquery.unobtrusive-ajax.js
和jquery.unobtrusive-ajax.min.js
这两个文件。这就是ASP.NETMVC实现非侵入式Ajax
的主要手段,若要在项目中使用Unobtrusive Ajax
,那么一定要用到这两个文件中的一个,至于具体用哪个就不多废话了。下面来看看Unobtrusive Ajax
在ASP.NET MVC中的使用。
ASP.NET MVC中使用Unobtrusive Ajax
主要用到的是Ajax
辅助方法,这些方法由AjaxHelper
提供,定义在System.Web.Mvc.Ajax.AjaxExtensions
类中。Ajax
主要用到的方法有ActionLink,RouteLink,BeginForm,BeginRouteForm
,当然这些方法都有各自重载的版本,具体参见MSDN文档,本文主要以具有代表性的BeginForm
为例来进行说明,其他的用法大同小异。
ASP.NET MVC对BeginForm
的使用提供了11个重载的版本,但是细细观察11个重载版本就会发现,这些重载中共同点是有一个AjaxOptions
类型的参数,除此之外Ajax
辅助方法的BeginForm
和Html辅助方法的BeginForm
相同参数版本之间并无差别,而这个AjaxOptions
就是ASP.NET MVC实现Ajax
方法依据。
为了便于观察测试效果,在新建的MVC项目的HomeController
中增加如下Action
:
public ActionResult Index()
{
return View();
}
public string s(string q)
{
return "The Query String is : " + q;
}
其中Action
‘Index
’用于显示Ajax
操作页面,Action
‘s’用来响应Ajax
请求结果。Index
页面的主要内容如下:
@using (Ajax.BeginForm(
new AjaxOptions
{
Url = "Home/s",
HttpMethod = "GET",
UpdateTargetId = "searchResult",
InsertionMode = InsertionMode.Replace
}))
{
<input type="text" name="q" />
<input type="submit" value="查询" />
}
<div id="searchResult"></div>
其作用就是生成一个表单,提交这个表单的时候执行异步的Ajax
请求,并将请求结果回显到id为searchResult
的div
元素内。运行如下:
此处只用到了AjaxOptions
的四个属性,AjaxOptions
的其他属性如下表所示:
各个参数的具体意思都已经有解释了,深入的用法将在本文第三部分予以说明。上表中对参数进行了分组和着色以示区分其在BeginForm
中的作用:前两个Url
和HttpMethod
算是Ajax
请求的基础了,指示了Ajax
请求的Url路径及所采用的Http
方法;UpdateTargetId
和InsertionMode
是对请求成功后回显的设置,正如上例所示的那样;OnBegin、OnComplete、OnFailure
以及OnSuccess
四个属性则是对Ajax
请求过程JavaScript回调的设置,具体是什么大家可望文生义了,这些值可以是具体的JavaScript语句或者是一个JavaScript函数;而Confirm
则是在发起Ajax
请求前页面进行确认的消息,页面通过window.confirm
显示确认信息;最后,LoadingElementDuration
和LoadingElementId
这两个则属于锦上添花的东西了,用于在请求过程中显示页面动态请求情况,比如一个‘Loading…
’的文字或者一个显示进度的图片。
下面是一个完整的例子:
@using (Ajax.BeginForm(
new AjaxOptions
{
Url = "Home/s",
HttpMethod = "GET",
UpdateTargetId = "searchResult",
InsertionMode = InsertionMode.Replace,
OnBegin = "alert('开始请求');",
OnComplete = "Ajax.GetData_Com",
OnSuccess = "alert('请求成功');",
OnFailure = "alert('请求失败');",
Confirm = "确认发起搜索",
LoadingElementDuration = 500,
LoadingElementId = "waiting"
}))
{
<input type="text" name="q" />
<input type="submit" value="查询" />
<span id="waiting" style="display: none">Loading...</span>
}
<div id="searchResult"></div>
<script>
Ajax = {};
Ajax.GetData_Com = function (data, status, xhr) {
console.log(data);
};
</script>
当然,真正的Unobtrusive
自然是要将下面的脚本单独放到一个js文件中的,此处为了示例就免去这个麻烦了_。
3. ASP.NET MVC Unobtrusive Ajax
原理剖析
看看上面的Ajax
辅助方法,的确给设计带来了不小的便利和帮助,省去了很多手工使用$.ajax
或者$.get
或者$.post
的不便,那么这种便利是怎么实现的,鄙人有一毛病,遇到问题总要刨根问题搞个清楚。下面权当是自己分析的一些心得,与大家分享,不善之处,还望不吝赐教。
前面说到ASP.NET MVC实现Unobtrusive Ajax
主要使用的是jquery.unobtrusive-ajax.js
这个JavaScript脚本,这个当然是首先要看的喽。可是打开文件发现里面有不少类似data-ajax-*
这样的Html属性,接触过HTML5的筒子们都应该知道这是HTML5预留给用户程序使用的属性,这自然是和Html相关,看来有必要看看Ajax.BeginForm
辅助方法生成的Html代码哦。
原谅我还拿前面的例子,Razor
引擎生成的Html
脚本如下(原谅我为了便于阅读做了稍微的调整):
<form action="/"
data-ajax="true"
data-ajax-url="Home/s"
data-ajax-method="GET"
data-ajax-begin="alert('开始请求');"
data-ajax-complete="Ajax.GetData_Com"
data-ajax-update="#searchResult"
data-ajax-mode="replace"
data-ajax-success="alert('请求成功');"
data-ajax-failure="alert('请求失败');"
data-ajax-confirm="确认发起搜索"
data-ajax-loading="#waiting"
data-ajax-loading-duration="500"
method="post">
<input type="text" name="q" />
<input type="submit" value="查询" />
<span id="waiting" style="display: none">Loading...</span>
</form>
<div id="searchResult"></div>
<script>
Ajax = {};
Ajax.GetData_Com = function (data, status, xhr) {
console.log(data);
};
</script>
可见,Ajax.BeginForm
辅助方法的确为表单添加了很多以data-ajax-
开头的属性,你一定想到了,这些属性的生成肯定和AjaxOptions
有关,Bingo,对应关系如下表:
可以说,ASP.NET MVC实现一个Unobtrusive Ajax
所需的全部东西都在这里了。下面就来分析一下jquery.unobtrusive-ajax.js
中的实现机制。
回想一下,通过jQuery
发起一个Ajax
请求的过程,ASP.NET MVC实现Unobtrusive Ajax
则是基于jQuery
的,你一定猜到了,这个过程的核心就是一次$.ajax()
调用!!而如你所知,$.ajax()
的调用其实也就是准备一个ajaxOptions
!!
ASP.NET MVC完成这一过程的核心有两步:
- 截获
<form>
的submit
事件或者<a>
的click
事件,并获取Ajax.BeginForm
或者Ajax.BeginRouteForm
或者Ajax.ActionLink
或者Ajax.RouteLink
中设置AjaxOptions
属性(这些属性分别对应类似data-ajax-*
的html5
属性,见上面表格),并通过这些属性设定$.ajax()
调用所需的ajaxOptions
。 - 通过前面的
ajaxOptions
发起$.ajax()
调用,完成Ajax
请求。
jquery.unobtrusive-ajax.js
中与此过程涉及的主要有两段代码(其他的代码都是辅助性代码,都为这两个服务)。
第一段在jquery.unobtrusive-ajax.js
文件的最后面,如下:
代码段一:
$("form[data-ajax=true]").live("submit", function (evt) {
var clickInfo = $(this).data(data_click) || [];
evt.preventDefault();
if (!validate(this)) {
return;
}
asyncRequest(this, {
url: this.action,
type: this.method || "GET",
data: clickInfo.concat($(this).serializeArray())
});
});
或(注意是或)
$("a[data-ajax=true]").live("click", function (evt) {
evt.preventDefault();
asyncRequest(this, {
url: this.href,
type: "GET",
data: []
});
});
作为jQuery的专家,你一定一眼就能看出此段代码的意图,对,监控页面中所有具有data-ajax
属性的表单的提交操作,此处用的是$.live()
方法(关于$.live
就不废话了),这就是页面中不需要手工再去截获表单的submit
事件或者链接的click
事件的原因所在!
第二段就是第一段中出现的asyncRequest()
函数,该函数中有两处代码,大家一看就会明白整个Ajax
请求发起的过程了:
代码段二:
function asyncRequest(element, options) {
var confirm, loading, method, duration;
confirm = element.getAttribute("data-ajax-confirm");
if (confirm && !window.confirm(confirm)) {
return;
}
loading = $(element.getAttribute("data-ajax-loading"));
duration = element.getAttribute("data-ajax-loading-duration") || 0;
$.extend(options, {
type: element.getAttribute("data-ajax-method") || undefined,
url: element.getAttribute("data-ajax-url") || undefined,
beforeSend: function (xhr) {
var result;
asyncOnBeforeSend(xhr, method);
result = getFunction(element.getAttribute("data-ajax-begin"), ["xhr"]).apply(this, arguments);
if (result !== false) {
loading.show(duration);
}
return result;
},
complete: function () {
loading.hide(duration);
getFunction(element.getAttribute("data-ajax-complete"), ["xhr", "status"]).apply(this, arguments);
},
success: function (data, status, xhr) {
asyncOnSuccess(element, data, xhr.getResponseHeader("Content-Type") || "text/html");
getFunction(element.getAttribute("data-ajax-success"), ["data", "status", "xhr"]).apply(this, arguments);
},
error: getFunction(element.getAttribute("data-ajax-failure"), ["xhr", "status", "error"])
});
options.data.push({ name: "X-Requested-With", value: "XMLHttpRequest" });
method = options.type.toUpperCase();
if (!isMethodProxySafe(method)) {
options.type = "POST";
options.data.push({ name: "X-HTTP-Method-Override", value: method });
}
$.ajax(options);
}
没错,这两段就完成ASP.NET MVC 的Ajax
请求。而从这两段代码中你也一定大致看出了前面的OnBegin、OnComplete、OnFailure
以及OnSuccess
四个属性对应的Ajax请求过程JavaScript
回调对应的是Ajax
请求的哪个部分了:
这里有一个关键的函数地方‘getFunction
’,它定义在jquery.unobtrusive-ajax.js
文件的最前面:
function getFunction(code, argNames) {
var fn = window, parts = (code || "").split(".");
while (fn && parts.length) {
fn = fn[parts.shift()];
}
if (typeof (fn) === "function") {
return fn;
}
argNames.push(code);
return Function.constructor.apply(null, argNames);
}
这个函数的功能就是从data-ajax-*
中和JavaScript回调相关的属性中获取JavaScript回调函数。拿OnComplete
来说,通过Html Element
的getAttribute( "data-ajax-complete")
获取data-ajax-complete
属性(设为oncomplete
),首先假设oncomplete
为定义在外部JavaScript脚本文件中的函数,然后一级一级的查找该函数,回到前面例子里面对AjaxOptions
的设定OnComplete ="Ajax.GetData_Com"
,那么,此处getFunction
函数将返回的是window.Ajax.GetData_Com
这个函数(注意window
是JavaScript
默认的全局名称空间),那么请求完成后,window.Ajax.GetData_Com
将会被调用,即:
window.Ajax.GetData_Com.apply(this, arguments);
这里的arguments
就是$.ajax
请求中complete
回调的参数。假如,这里没有找到符合条件的函数,那么,getFunction
将会把oncomplete
的字符串值作为脚本进行执行,即:getFunction
的最后一句:
return Function.constructor.apply(null, argNames);
这就是为什么可以直接在OnSuccess
中写JavaScript脚本的原因哦。
PS:
OK,主体框架分析完成了,来分析一些其他的东西,主要是Ajax
辅助方法里面的AjaxOptions
设置问题。
1)上面的例子主要以Form
来分析,对于Link
来说,以上所有的分析都成立,只是<form>
变成了<a>
,这也是他们的唯一区别,所有的原理是一样的,也就是监听所有具有data-ajax
的<a>
标签的click
事件,然后发起ajax
请求,这部分的代码和前面的代码段一作用是一样的,只是后者用于<form>
而已,相应的代码段一就变成了下面这个样子:
$("a[data-ajax=true]").live("click", function (evt) {
evt.preventDefault();
asyncRequest(this, {
url: this.href,
type: "GET",
data: []
});
});
2)AjaxOptions
设置问题
》Url
:默认为form
的action
,上面提到的jquery.unobtrusive-ajax.js
两段代码中对其进行了设置,如果有设置data-ajax-url
属性,则取data-ajax-url
属性作为ajax
请求的url
,否则取<form>
的action
属性(如果是链接的话,则取的是<a>
的href
属性)。
》HttpMethod
:默认值为GET
,这部分的设置和AjaxOptions.Url
是相似的,对于Form
来说优先级依次为data-ajax-method
属性,form .action
,"GET
",对于Link
来说就是 “GET
”。
》UpdateTargetId
:原则上应是需要更新的html元素的id(MVC的Aajx
辅助方法会自动在生成的data-ajax-update
属性前面插入字符’#
’,参看前面Ajax.BeginForm
生成的Html
代码),但是细细查看代码,你会发现此处可为多个html
元素,只要第一个不加#
,之后的依次加上即可,如UpdateTargetId ="div1,#div2, #div3"
,可实现多个div
同时更新,甚至于后面的可以是任何jQuery
支持的css selector
。这部分代码定义在文件jquery.unobtrusive-ajax.js
里的函数asyncOnSuccess
中:
mode = (element.getAttribute("data-ajax-mode") || "").toUpperCase();
$(element.getAttribute("data-ajax-update")).each(function (i, update) {
var top;
switch (mode) {
case "BEFORE":
top = update.firstChild;
$("<div />").html(data).contents().each(function () {
update.insertBefore(this, top);
});
break;
case "AFTER":
$("<div />").html(data).contents().each(function () {
update.appendChild(this);
});
break;
default:
$(update).html(data);
break;
}
});
核心是前两行代码:mode
获取更新方式,共有三种模式,InsertBefore,InsertAfter,Replace
(在枚举InsertionMode
中定义),从上面的代买来看,默认的是Replace
模式;再看下面的element.getAttribute( "data-ajax-update")
,对于上面的例子,获取到的应该是"#searchResult
",如果将上例中的<UpdateTargetId = "searchResult">
改成<UpdateTargetId="searchResult, div.update">
,那么被更新的除了 # searchResult
之外,还有所有具有‘update
’这样的css
类的 div
元素,原因就在那个each()
!
InsertionMode
:只有在设置了UpdateTargetId
之后才会生效LoadingElementDuration
:只有在设置了LoadingElementId
之后才会生效