开发过程中疑难杂症解决方案总结一
一、批次加载列表数据
问题场景:机票列表页面一直处于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等作为技术储备;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端