开发过程中疑难杂症解决方案总结一
一、批次加载列表数据
问题场景:机票列表页面一直处于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等作为技术储备;