一、批次加载列表数据
问题场景:机票列表页面一直处于loading状态,服务器日志显示已经返回了数据,但是ajax请求一直处于挂起状态,导致页面也一直loading中。
分析原因:通过排查,发现服务器返回的数据量过大(大约2M)导致ajax假死,网上有人说ajax请求的响应时间为10秒,超过了就会假死。
解决方案:
1、把原先的ajax请求分成多批次,每次请求让服务器返回5条数据,这样就避免了一次性响应数据量过大的问题;
2、服务端把数据做缓存,分批次从缓存读取数据给客户端;
3、客户端ajax每次请求返回后根据服务端返回的完成标识判断是否再次发送请求;
相关代码:
客户端:
function getCalendarPriceListAndFlightList() {
var info = {
"depCity": $("#hiddepCity").val(),
"arrCity": $("#hidarrCity").val(),
"depDate": $("#hiddepDate").val(),
"queryModule": "1", //查询类型 1-单程;2-往返去程;3-往返回程
"lineType": "OW", //查询类型 OW-单程; RT-往返
"uniqueKey": "", //携程舱位标识,返程查询需要
"queryuuid": "", //查询的唯一标识
"retType": "1", //数据返回模式 0:多批次返回,覆盖刷新界面展示 1:一次性全部返回(默认)
"enterpriseId": $("#hidEnterpriseid").val(),
"isLoaded": -1,
"guidStr": "",
"userName": $("#hidUserName").val(),
"action": "getCalendarPriceListAndFlightList"
};
$.ajax({
url: "../FlightHandler/JDFlightHandler.ashx",
type: "post",
data: info,
dataType: "text",
beforeSend: function (XMLHttpRequest) {
$('.page-list-con').html('');
createLoading();
},
success: function (data) {
var dataArr = data.split('|');
if (dataArr[2] != "") {
if (dataArr[0] == "-1")
$("#topPrice").text("¥");
else
$("#topPrice").text("¥" + dataArr[0]);
if (dataArr[1] == "-1")
$("#EndPrice").text("¥");
else
$("#EndPrice").text("¥" + dataArr[1]);
$('.page-list-con').html(dataArr[2]);
if (dataArr[5] == "-1")
$("#dqPrice").text("¥");
else
$("#dqPrice").text("¥" + dataArr[5]);
if (dataArr[6] == "-1")
$('#time_sel_airway').html();
else
$('#time_sel_airway').html(dataArr[6]);
//保存点击的出发城市、到达城市、开始时间、结束时间
$("#hidTempdepCity").val($("#hiddepCity").val());
$("#hidTemparrCity").val($("#hidarrCity").val());
$("#hidTempdepDate").val($("#hiddepDate").val());
$("#hidTemparrDate").val($("#hidarrDate").val());
if (dataArr[3] == "0") {
reloadFlightList(dataArr[3], dataArr[4]);
}
} else {
$('.page-list-con').html(SetErroHtml());
alert("读取数据超时,请尝试重新查询!");
}
},
complete: function (XMLHttpRequest, textStatus) {
//removeLoading();
},
error: function () {
}
});
}
function reloadFlightList(isLoaded, guidStr) {
var info = {
"depCity": $("#hiddepCity").val(),
"arrCity": $("#hidarrCity").val(),
"depDate": $("#hiddepDate").val(),
"arrDate": $("#hidarrDate").val(),
"enterpriseId": $("#hidEnterpriseid").val(),
"isLoaded": isLoaded,
"guidStr": guidStr,
"userName": $("#hidUserName").val(),
"action": "getCalendarPriceListAndFlightList"
};
$.ajax({
url: "../FlightHandler/JDFlightHandler.ashx",
type: "post",
data: info,
dataType: "text",
success: function (data) {
var dataArr = data.split('|');
if (dataArr[3] == "0") {
if (dataArr[2] != "") {
$("#tbFlightContainer").append(dataArr[2]);
}
reloadFlightList(isLoaded, guidStr);
}
else {
removeLoading();
}
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
removeLoading();
return;
}
});
}
服务端:
public string getFlightList(HttpRequest Request, ref string interval, ref int isFinished, out decimal minPrice, string AccountName)
{
LogHelper.WriteLog("进入getFlightList方法");
string retStr = string.Empty;
string guidStr = "";
int iswc = 1;
minPrice = 0;
try
{
string depCity = string.Empty;
string arrCity = string.Empty;
string depDate = string.Empty;
string queryModule = string.Empty;
string lineType = string.Empty;
string uniqueKey = string.Empty;
string queryuuid = string.Empty;
string retType = string.Empty;
if (!string.IsNullOrEmpty(Request.Form["depCity"]))
{
depCity = Request.Form["depCity"].ToString();
}
if (!string.IsNullOrEmpty(Request.Form["arrCity"]))
{
arrCity = Request.Form["arrCity"].ToString();
}
if (!string.IsNullOrEmpty(Request.Form["depDate"]))
{
depDate = Request.Form["depDate"].ToString();
}
if (!string.IsNullOrEmpty(Request.Form["queryModule"]))
{
queryModule = Request.Form["queryModule"].ToString();
}
if (!string.IsNullOrEmpty(Request.Form["lineType"]))
{
lineType = Request.Form["lineType"].ToString();
}
if (!string.IsNullOrEmpty(Request.Form["uniqueKey"]))
{
uniqueKey = Request.Form["uniqueKey"].ToString();
}
if (!string.IsNullOrEmpty(Request.Form["queryuuid"]))
{
queryuuid = Request.Form["queryuuid"].ToString();
}
if (!string.IsNullOrEmpty(Request.Form["retType"]))
{
retType = Request.Form["retType"].ToString();
}
string enterpriseId = Request.Form["enterpriseId"].ToString();
string isLoaded = Request.Form["isLoaded"].ToString();
string RguidStr = Request.Form["guidStr"].ToString();
string userName = Request.Form["userName"].ToString();
List<Common.JsonFlightInfo.FlightInfo> lstFlightInfo = null;
List<Common.JsonFlightInfo.FlightInfo> lstFlightInfoEx = null;
List<Common.JsonFlightInfo.FlightInfo> lstFlightInfoExFilter = null;
List<JsonFlightInfo.FlightInfo> lst = null;
//首次加载航班信息
if (string.IsNullOrEmpty(RguidStr))
{
lst = PublicListDataBind(depCity, arrCity, depDate, queryModule, lineType, uniqueKey, queryuuid, retType, ref interval, ref isFinished);//获取机票数据
if (lst!=null && lst.Count > 0)
{
lstFlightInfo = lst;
lstFlightInfoEx = lst;
lstFlightInfoExFilter = lst;
//生成GUID缓存键
guidStr = System.Guid.NewGuid().ToString();
CacheHelper.SetCache("cache" + userName, lstFlightInfoExFilter, new TimeSpan(0, 10, 0));
}
else
{
return "|1|";
}
}
else
{
//非首次加载航班信息,读缓存
lstFlightInfo = CacheHelper.GetCache(RguidStr) as List<Common.JsonFlightInfo.FlightInfo>;
lstFlightInfoEx = CacheHelper.GetCache(RguidStr) as List<Common.JsonFlightInfo.FlightInfo>;
//用回传过来的GUID键,继续读取缓存
guidStr = RguidStr;
}
//集合里航班信息大于5条,每次就读取前5条,然后把其余数据放到缓存下次读取用。
if (lstFlightInfoEx.Count > 5)
{
lstFlightInfo = lstFlightInfoEx.Skip(0).Take(5).ToList();
CacheHelper.SetCache(guidStr, lstFlightInfoEx.Skip(5).ToList()); //航班信息保存到缓存
if (lstFlightInfoEx.Skip(5).Count() < 1)
iswc = 1; //已完成
else
iswc = 0; //未完成
}
else
{
//集合里航班信息小于5条时,就直接全部输出给页面,清除缓存
if (!string.IsNullOrEmpty(RguidStr))
{
CacheHelper.RemoveAllCache(RguidStr);
}
iswc = 1; //已完成
}
if (isLoaded == "-1")
{
List<string> listString = new List<string>();
if (lst != null)
{
foreach (Common.JsonFlightInfo.FlightInfo flightInfo in lst)
{
flightInfo.minPrice = Convert.ToDecimal(Common.Application.GetLeastPrice(flightInfo.flightNo, Request.Form["classFC"] == null ? "" : Request.Form["classFC"].ToString(),userName));
if (!listString.Contains(flightInfo.airwaysCn))
{
listString.Add(flightInfo.airwaysCn);
sbairway.Append("<li name="airway" hkdm="" + flightInfo.airways + "">" + flightInfo.airwaysCn + "</li>");
}
}
}
}
retStr = Common.HtmlFlightList.getHtmlFlightListWAP(lstFlightInfo, depCity, arrCity, "1", out minPrice, enterpriseId, userName);
LogHelper.WriteLog("返回航班信息。");
}
catch (Exception ex)
{
LogHelper.WriteLog("getFlightList异常!异常信息:" + ex.Message + ",堆栈:" + ex.StackTrace);
throw;
}
if (!string.IsNullOrEmpty(guidStr))
return retStr + "|" + iswc + "|" + guidStr;
else
return retStr + "|" + iswc + "|";
}
问题总结:
1、ajax请求响应时间为10秒,超过后请求就挂起,浏览器进入假死状态;
2、jQuery ajax实现分批次请求数据;
3、从Cache中利用linq分批次读取数据;
注:
用ajax设置请求头部超时时间,如:
ajax.setRequestHeader("connectionTimeout","5000");
二、APP自动登录实现和登录跳转问题
需求场景:公司要做一个APP,套个壳,把现有的几个H5站点加进去。这样APP和H5端用户的登录信息需共享,并且当H5站点重启时不再跳转原H5的登录页面,而是跳转到APP的登录界面。
解决方案:
1、APP端通过把用户登录信息写入H5的cookie中来传递,当用户退出APP再次登录时cookie会更新。
2、H5端站点通过HttpModule拦截HTTP请求,如果H5站点登录失效(比如MemCache或Session过期)就调用自动登录API来更新H5端的登录状态;
3、如果H5站点重启(比如重新发布站点或修改web.config),则跳转到APP登录界面;
相关代码:
HttpModule实现:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Net;
using System.Text;
using System.Web;
using System.Web.Script.Serialization;
using System.Web.SessionState;
using XFK.AppLYTravel.Common.CommonModel;
using XFK.AppLYTravel.Common.Configuration;
using XFK.AppLYTravel.Common.helper;
using XFK.AppLYTravel.Common.Logs;
using XFK.AppLYTravel.Common.Util;
using XinfuMall.XinfuWeb.PublicClass;
namespace XFK.AppLYTravel.Common
{
/// <summary>
/// AutoLoginModule 的摘要说明
/// </summary>
public class AutoLoginModule : IHttpModule, IRequiresSessionState
{
public static readonly object syncObject = new object();
public void Init(HttpApplication context)
{
LogHelper.WriteLog("进入HttpModule_Init方法。");
context.EndRequest+=context_EndRequest;
}
void context_EndRequest(object sender, EventArgs e)
{
lock (syncObject)
{
HttpApplication app = (HttpApplication)sender;
HttpContext _context = app.Context;
string originReq = app.Request.Headers["User-Agent"];
LogHelper.WriteLog("HttpModule获取请求头user_agent:" + originReq);
try
{
if (!string.IsNullOrEmpty(originReq))
{
//如果是来自APP的请求
if (originReq.ToLower().IndexOf("xinfukaapp") > -1)
{
string userNo = string.Empty;
string xf_sid = string.Empty;
if (_context.Request.Cookies["userNo"] != null)
{
userNo = _context.Request.Cookies["userNo"].Value;
}
if (_context.Request.Cookies["xf_sid"] != null)
{
xf_sid = _context.Request.Cookies["xf_sid"].Value;
}
LogHelper.WriteLog(string.Format("HttpModule获取到的cookie数据:userNo={0},xf_sid={1}", userNo, xf_sid));
bool isGoLogin = false;
if (!string.IsNullOrEmpty(userNo) && !string.IsNullOrEmpty(xf_sid))
{
HttpContext.Current.Items["xf_sid"] = xf_sid;
if (SessionManager.Read(UserUtil.user_sessionStr) != null)
{
LogHelper.WriteLog(string.Format("用户{0}处于已登录状态。", userNo));
}
else
{
LogHelper.WriteLog(string.Format("用户{0}登录已过期,需要自动登录。", userNo));
string pstr = string.Format("userNo={0}&sessionID={1}", userNo, xf_sid);
var url = ConfigurationManager.AppSettings["LoginAppAutoApi"];
LogHelper.WriteLog(string.Format("调用APP自动登录接口:{0},入参:{1}", url, pstr));
string strResult = JDAPI.PostWebRequest(url.ToString(), pstr);
LogHelper.WriteLog("返回数据:" + strResult);
JavaScriptSerializer js = new JavaScriptSerializer();
GeneralResult loginResult = js.Deserialize<GeneralResult>(strResult);
if (loginResult.Ret == "200" && loginResult.data != null)
{
LogHelper.WriteLog("用户" + userNo + "自动登录成功!");
}
else
{
isGoLogin = true;
}
}
}
else
{
isGoLogin = true;
}
if (isGoLogin)
{
LogHelper.WriteLog("用户" + userNo + "自动登录失败!需重新手工登录。");
string mimeStr = _context.Response.ContentType;
LogHelper.WriteLog("获取到的请求中mime类型:" + mimeStr);
if (!string.IsNullOrEmpty(mimeStr))
{
//当请求的是页面
if (mimeStr.IndexOf(@"text/html") > -1)
{
if (originReq.Equals("XinfukaAppAndroid"))
{
LogHelper.WriteLog("检测到客户端是安卓系统,跳转APP登录。");
_context.Response.Write("<script type="text/javascript">window.AndroidInterface.tokenTimeOut();</script>");
_context.Response.End();
}
else if (originReq.Equals("XinfukaAppIos"))
{
LogHelper.WriteLog("检测到客户端是IOS系统,跳转APP登录。");
StringBuilder sb = new StringBuilder();
sb.Append("<script type="text/javascript"> function loadURL(url) {");
sb.Append("var iFrame;");
sb.Append("iFrame = document.createElement("iframe");");
sb.Append("iFrame.setAttribute("src", url);");
sb.Append("iFrame.setAttribute("style", "display:none;");");
sb.Append("iFrame.setAttribute("height", "0px");");
sb.Append("iFrame.setAttribute("width", "0px");");
sb.Append("iFrame.setAttribute("frameborder", "0");");
sb.Append("document.body.appendChild(iFrame);");
sb.Append("iFrame.parentNode.removeChild(iFrame);");
sb.Append("iFrame = null;");
sb.Append("}");
sb.Append("loadURL("haleyAction://tokenTimeOut");</script>");
_context.Response.Write(sb.ToString());
_context.Response.End();
}
else
{
LogHelper.WriteLog("无效识别。");
_context.Response.Write("无效识别。");
_context.Response.End();
}
}
}
}
}
}
}
catch (System.Threading.ThreadAbortException) { }
catch (Exception ex)
{
LogHelper.WriteLog("用户自动登录异常,信息:" + ex.Message + ",堆栈:" + ex.StackTrace);
}
}
}
public void Dispose()
{
}
}
}
H5站点引入:
<system.webServer>
<modules>
<add name="AutoLoginModuleNew" type="XFK.AppLYTravel.Common.AutoLoginModule,XFK.AppLYTravel.Common" />
</modules>
</system.webServer>
H5跳转方法
public static void AppRedirect(HttpContext context, Action cusAction)
{
LogHelper.WriteLog("进入AppRedirect方法。");
HttpContext _context = context;
string originReq = _context.Request.Headers["User-Agent"];
LogHelper.WriteLog("AppRedirect获取请求头user_agent:" + originReq);
try
{
if (!string.IsNullOrEmpty(originReq))
{
//如果是来自APP的请求
if (originReq.ToLower().IndexOf("xinfukaapp") > -1)
{
string mimeStr = _context.Response.ContentType;
LogHelper.WriteLog("AppRedirect获取到的请求中mime类型:" + mimeStr);
if (!string.IsNullOrEmpty(mimeStr))
{
//当请求的是页面
if (mimeStr.IndexOf(@"text/html") > -1)
{
LoginInfo sessionInfo = _context.Session["UserLoginInfo"] as LoginInfo;
if (sessionInfo == null)
{
LogHelper.WriteLog("session信息为空!");
if (originReq.Equals("XinfukaAppAndroid"))
{
LogHelper.WriteLog("AppRedirect检测到客户端是安卓系统,跳转APP登录。");
_context.Response.Write("<script type="text/javascript">window.AndroidInterface.tokenTimeOut();</script>");
_context.Response.End();
}
else if (originReq.Equals("XinfukaAppIos"))
{
LogHelper.WriteLog("AppRedirect检测到客户端是IOS系统,跳转APP登录。");
StringBuilder sb = new StringBuilder();
sb.Append("<script type="text/javascript"> function loadURL(url) {");
sb.Append("var iFrame;");
sb.Append("iFrame = document.createElement("iframe");");
sb.Append("iFrame.setAttribute("src", url);");
sb.Append("iFrame.setAttribute("style", "display:none;");");
sb.Append("iFrame.setAttribute("height", "0px");");
sb.Append("iFrame.setAttribute("width", "0px");");
sb.Append("iFrame.setAttribute("frameborder", "0");");
sb.Append("document.body.appendChild(iFrame);");
sb.Append("iFrame.parentNode.removeChild(iFrame);");
sb.Append("iFrame = null;");
sb.Append("}");
sb.Append("loadURL("haleyAction://tokenTimeOut");</script>");
_context.Response.Write(sb.ToString());
_context.Response.End();
}
else
{
LogHelper.WriteLog("AppRedirect无效识别。");
_context.Response.Write("AppRedirect无效识别。");
_context.Response.End();
}
}
}
}
}
else
{
cusAction.Invoke();
}
}
}
catch (System.Threading.ThreadAbortException) { }
catch (Exception ex)
{
LogHelper.WriteLog("AppRedirect异常,信息:" + ex.Message + ",堆栈:" + ex.StackTrace);
}
}
H5端在BasePage中调用,如:
using Common;
using Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Script.Serialization;
using System.Web.SessionState;
using XinfuMall.XinfuWeb.PublicClass;
/// <summary>
/// PageBase 的摘要说明
/// </summary>
public class PageBase : System.Web.UI.Page
{
public PageBase()
{
}
private string accountName;
private string enterpriseid;
public string AccountName
{
get { return this.accountName; }
}
public string Enterpriseid
{
get { return this.enterpriseid; }
}
protected override void OnInit(EventArgs e)
{
if (HttpContext.Current.Request.Cookies["xf_sid"] == null && XFK.Infrastructure.Util.SessionManager.Read(UserUtil.user_sessionStr) == null)
{
Response.Redirect(System.Configuration.ConfigurationManager.AppSettings["xinfuUrl"] + "mall/Login?originUrl=" + HttpUtility.UrlEncode(Request.Url.AbsoluteUri, System.Text.Encoding.UTF8));
}
else
{
LoginInfo loginfo = Session["UserLoginInfo"] as LoginInfo;
if (loginfo == null)
{
XFK.AppLYTravel.Common.CommUtil.AppRedirect(HttpContext.Current,WebRedirect);
}
this.accountName = loginfo.AccountName;
this.enterpriseid = loginfo.Enterpriseid;
var limitEnterprise = String.IsNullOrEmpty(System.Configuration.ConfigurationManager.AppSettings["limitEnterprise"]) ? "" : System.Configuration.ConfigurationManager.AppSettings["limitEnterprise"];
if (limitEnterprise.Split(',').Contains(loginfo.Enterpriseid.ToString()))
{
System.Web.HttpContext.Current.Response.Write("对不起,所属的企业没有订购此产品的权限!");
System.Web.HttpContext.Current.Response.End();
}
}
}
void WebRedirect()
{
Response.Redirect(System.Configuration.ConfigurationManager.AppSettings["xinfuUrl"] + "mall/Login?originUrl=" + HttpUtility.UrlEncode(Request.Url.AbsoluteUri, System.Text.Encoding.UTF8));
}
}
遇到的问题:
1、H5如何判断请求来自APP;
2、HttpModule获取请求资源mime类型的时机;
3、Http请求管道事件(如BeginRequest、EndRequest)对于js脚本执行的影响;
例如IOS端跳转APP登录页的方法是这样的:
<script>
function loadURL(url) {
var iFrame;
iFrame = document.createElement("iframe");
iFrame.setAttribute("src", url);
iFrame.setAttribute("style", "display:none;");
iFrame.setAttribute("height", "0px");
iFrame.setAttribute("width", "0px");
iFrame.setAttribute("frameborder", "0");
document.body.appendChild(iFrame);
iFrame.parentNode.removeChild(iFrame);
iFrame = null;
}
loadURL("haleyAction://tokenTimeOut");
</script>
这段js代码在EndRequest事件中可以输出到</html>标签外,页面最底部,如下图:
这时候所有的DOM元素都已加载完毕,所以执行没问题;
但是放在BeginRequest里,js代码被输出到了页面最顶部,如:
这时候因为DOM元素还未加载好,会有如下错误:

解决方案:
1、APP端通过修改http请求头User-Agent来标识;
2、精确获取mime类型要在EndRequest事件里,如:string mimeStr = _context.Response.ContentType;
在BeginRequest里不管请求资源是页面、脚本、图片或是其他,获取的都是"text/html"
3、这种情况,用window.onload包装一下就可以正常执行了,如:
<script>
window.onload=function(){
function loadURL(url) {
var iFrame;
iFrame = document.createElement("iframe");
iFrame.setAttribute("src", url);
iFrame.setAttribute("style", "display:none;");
iFrame.setAttribute("height", "0px");
iFrame.setAttribute("width", "0px");
iFrame.setAttribute("frameborder", "0");
document.body.appendChild(iFrame);
iFrame.parentNode.removeChild(iFrame);
iFrame = null;
}
loadURL("haleyAction://tokenTimeOut");
}
</script>
总结:
1、HttpModule的BeginRequest、EndRequest事件对于页面元素(HTML和js)加载顺序的影响;
2、获取mime类型一定要在EndRequest事件里,并且用Response.ContentType方法;
3、session依赖于cookie,当cookie在页面中失效后session也会失效;
三、用到的或了解到的跨域知识总结
1、WEB站点调用远程API,通过Ajax请求本地Handler或Controller,而本地Handler或Controller利用HttpWebRequest和HttpWebResponse与远程API交互;
2、WEB站点在调用远程登录接口时,HTTP请求中带上cookie,实现统一登录;
3、实现跨域的技术如JSONP、CORS、postMessage等作为技术储备;