在团队设计BrnShop的web项目之初,我们碰到了两个问题,第一个是数据的复用和传递,第二个是大mvc框架和小mvc框架的选择。下面我依次来说明下。
首先是数据的复用和传递:对于BrnShop的每一次请求,程序都要分成好几个阶段执行,例如验证,执行动作方法等等,在各个阶段我们可能需要重复使用同一信息,而我们的愿景就是希望此信息只需获取一次,然后沿着流程管道一直流动,这样在后面的阶段中就可以直接使用,不用再重新获取了,提高程序的性能。举例来说:在授权验证阶段,我们为对用户进行验证,从而获取了用户信息,当验证结束后,此用户信息并不被抛弃,而是保留下来,这样在后面的动作方法中我们就不需要再次获取用户信息,而是直接使用刚才在授权中保留下来的用户信息就可以了。
具体实现是这样的:首先我们给这些需要公用的数据定义个上下文类,它们分别是BrnShop.Web.Framework项目中的WebWorkContext类和AdminWorkContext类,其中WebWorkContext是前台项目使用的上下文,AdminWorkContext是后台项目使用的上下文。代码很简单,就是定义了一些公共字段,具体如下:
有了上下文类后,我们需要找一个可以保证上下文流动的地方。在翻看了asp.net mvc的源码后,我们找到一个好地方,这个地方就在控制器的基类Controller中。在Controller中微软定义了六个方法,具体如下:
- protected override void Initialize(RequestContext requestContext);说明:初始化调用构造函数后可能不可用的数据。
- protected virtual void OnAuthorization(AuthorizationContext filterContext);说明:在进行授权时调用。
- protected virtual void OnActionExecuted(ActionExecutedContext filterContext);说明:在调用操作方法后调用。
- protected virtual void OnActionExecuting(ActionExecutingContext filterContext);说明:在调用操作方法前调用。
- protected virtual void OnResultExecuted(ResultExecutedContext filterContext);说明:在执行由操作方法返回的操作结果后调用。
- protected virtual void OnResultExecuting(ResultExecutingContext filterContext);说明:在执行由操作方法返回的操作结果前调用。
这些都是虚方法,所以我们可以定义一个继承自Controller的新控制器,然后重写这些方法。由于这些方法是在同一个类中,所以它们可以共享同一个字段(这个字段就是上下文),而且其他的控制器都是继承自这个新控制器类,所以在动作方法中也是可以访问这个共享字段(父类的字段)。新控制器类分别是BrnShop.Web.Framework项目中BaseWebController类和BaseAdminController类,其中BaseWebController为前台控制器类,BaseAdminController为后台控制器类,具体实现如下:
到此事情还没完,那就是这个上下文是控制器的字段,在视图中如果想访问它需要强制类型转换下,代码为:((BaseWebController)(this.ViewContext.Controller)).WorkContext;试想一下我们每次访问上下文都需要这么长的一段代码那是怎样的煎熬呀?不过幸好有解决办法,那就是重写mvc的WebViewPage页(如果你不知道WebViewPage和mvc的编译过程请阅读大神“Artech”的相关文章,地址如下:http://www.cnblogs.com/artech/)。具体代码在BrnShop.Web.Framework项目中WebViewPage类和AdminViewPage类,其中WebViewPage为前台视图类,AdminViewPage为后台视图类:
定义好新的视图类后,我们需要通知编译器使用这个新类,通知方式在视图文件的web.config中,具体见下图:
通过将"pageBaseType"的值设置为我们的新类名,我们就可以在视图文件中直接使用上下文了。例:@WorkContext.ShopConfig.SEOKeyword
说完了数据的复用和传递,我们再来说说大mvc框架和小mvc框架的问题。首先何为大mvc框架,何为小mvc框架?
- 大mvc框架指的是尽量完整的一套asp.net mvc框架,包含路由,控制器,模型绑定,模型校验,筛选器等等。
- 小mvc框架指的是只包含项目所必须使用的mvc部分,对于使用不到的部分尽量不用或移除。
大家可能觉得这有什么难的?但是对于一个开源项目来说这确实是一个很重要的问题,因为开源项目的产品面向的是全国甚至是全世界的开发者,大家的技术参差不齐,有的高,有个低。为了保证尽可能多的覆盖开发者,只有原汁原味的mvc才对开发者更亲切和熟悉,所以应该使用大mvc框架。可是一款优秀的产品不只是面向初级开发者,还需要面对高级开发者,对于高级开发者来说他们希望获得项目最大的可控权,所以框架应该尽量只使用最核心的mvc部分,这样留给开发者的空间才能更大,这样这样看来又应该使用小mvc框架。下面我从两个方面来说明我们是如何解决这个问题的。
首先是mvc筛选器:看过我们源码的园友已经发现,我们项目中没有定义任何一个筛选器类。那我们的筛选器在哪儿?答案就在上面的上下文流动中,在上面重写的筛选器方法中我们实现所有筛选。如果你想针对某个控制器A单独筛选你可以在A中再一次重写筛选器方法添加自己的代码。如果你想只针对某一方法进行筛选你只需要单独在方法中筛选就可以了。这样通过使用内置在controller中的筛选方法我们实现了和第三方筛选器的隔离,也减少了反射获取筛选器的次数。
其次是模型绑定和校验:我们首先通过手动获取request集合的方式去除所有模型绑定,以登陆代码为例:
/// <summary> /// 登录 /// </summary> public ActionResult Login()//注意此方面没有任何参数 { string returnUrl = WebHelper.GetQueryString("returnUrl"); if (returnUrl.Length == 0) returnUrl = "/"; if (WorkContext.ShopConfig.LoginType == "") return PromptView(returnUrl, "商城目前已经关闭登陆功能!"); if (WorkContext.Uid > 0) return PromptView(returnUrl, "您已经登录,无须重复登录!"); if (WorkContext.ShopConfig.LoginFailTimes != 0 && LoginFailLogs.GetLoginFailTimesByIp(WorkContext.IP) >= WorkContext.ShopConfig.LoginFailTimes) return PromptView(returnUrl, "您已经输入错误" + WorkContext.ShopConfig.LoginFailTimes + "次密码,请15分钟后再登陆!"); //get请求 if (WebHelper.IsGet()) { ViewData.Add("oAuthPluginList", Plugins.GetOAuthPluginList()); return View(new LoginModel()); } //post请求 LoginModel model = new LoginModel(); //模型绑定 手动绑定 model.AccountName = WebHelper.GetFormString(WorkContext.ShopConfig.ShadowName).Trim(); model.Password = WebHelper.GetFormString("password"); model.IsRemember = WebHelper.GetFormInt("isRemember"); model.VerifyCode = WebHelper.GetFormString("verifyCode"); //模型验证 PartUserInfo partUserInfo = VerifyLogin(model); if (!ModelState.IsValid)//验证失败时 { ViewData.Add("oAuthPluginList", Plugins.GetOAuthPluginList()); return View(model); } else//验证成功时 { //当用户等级是禁止访问等级时 if (partUserInfo.UserRid == 1) return PromptView("您的账号当前被锁定,不能访问"); //删除登陆失败日志 LoginFailLogs.DeleteLoginFailLogByIP(WorkContext.IP); //更新用户最后访问 int regionId = WorkContext.Region != null ? WorkContext.Region.RegionId : -1; Users.UpdateUserLastVisit(partUserInfo.Uid, WorkContext.IP, regionId, DateTime.Now); //更新购物车中用户id Orders.UpdateShopCartUidBySid(partUserInfo.Uid, WorkContext.Sid); //将用户信息写入cookie中 ShopUtils.SetUserCookie(partUserInfo, (WorkContext.ShopConfig.IsRemember == 1 && model.IsRemember == 1) ? 30 : -1); return Redirect(returnUrl); } }
其次是模型校验,校验又分为两部分。第一部分是验证,对此我们也是采用手动校验的方式,同样以登陆为例:
/// <summary> /// 登录验证 /// </summary> private PartUserInfo VerifyLogin(LoginModel model) { PartUserInfo partUserInfo = null; //验证账户名 if (string.IsNullOrWhiteSpace(model.AccountName)) { ModelState.AddModelError(WorkContext.ShopConfig.ShadowName, "账户名不能为空"); } else if (model.AccountName.Length < 4 || model.AccountName.Length > 50) { ModelState.AddModelError(WorkContext.ShopConfig.ShadowName, "账户名必须大于3且不大于50个字符"); } else if ((!SecureHelper.IsSafeSqlString(model.AccountName))) { ModelState.AddModelError(WorkContext.ShopConfig.ShadowName, "账户名不存在"); } //验证密码 if (string.IsNullOrWhiteSpace(model.Password)) { ModelState.AddModelError("password", "密码不能为空"); } else if (model.Password.Length < 4 || model.Password.Length > 32) { ModelState.AddModelError("password", "密码必须大于3且不大于32个字符"); } //验证验证码 if (CommonHelper.IsInArray(WorkContext.PageKey, WorkContext.ShopConfig.VerifyPages)) { if (string.IsNullOrWhiteSpace(model.VerifyCode)) { ModelState.AddModelError("verifyCode", "验证码不能为空"); } else if (model.VerifyCode.ToLower() != Sessions.GetValueString(WorkContext.Sid, "verifyCode")) { ModelState.AddModelError("verifyCode", "验证码不正确"); } } //当以上验证全部通过时 if (ModelState.IsValid) { if (BSPConfig.ShopConfig.LoginType.Contains("2") && ValidateHelper.IsEmail(model.AccountName))//邮箱登陆 { partUserInfo = Users.GetPartUserByEmail(model.AccountName); if (partUserInfo == null) ModelState.AddModelError(WorkContext.ShopConfig.ShadowName, "邮箱不存在"); } else if (BSPConfig.ShopConfig.LoginType.Contains("3") && ValidateHelper.IsMobile(model.AccountName))//手机登陆 { partUserInfo = Users.GetPartUserByMobile(model.AccountName); if (partUserInfo == null) ModelState.AddModelError(WorkContext.ShopConfig.ShadowName, "手机不存在"); } else if (BSPConfig.ShopConfig.LoginType.Contains("1"))//用户名登陆 { partUserInfo = Users.GetPartUserByName(model.AccountName); if (partUserInfo == null) ModelState.AddModelError(WorkContext.ShopConfig.ShadowName, "用户名不存在"); } //判断密码是否正确 if (partUserInfo != null && Users.CreateUserPassword(model.Password, partUserInfo.Salt) != partUserInfo.Password) { LoginFailLogs.AddLoginFailTimes(WorkContext.IP, DateTime.Now);//增加登陆失败次数 ModelState.AddModelError("password", "密码不正确"); } } return partUserInfo; }
通过上面代码大家可以看出所有的验证都是手动进行的。
校验的第二部分是验证信息显示,在mvc中大家经常使用Html.ValidationMessageFor之类的方法来显示验证信息,所以为了保证上述方法还能够正常使用,我们需要将所有验证信息都添加到ModelState中(因为Html.ValidationMessageFor之类的方法实现本质就是通过获取ModelState指定键值的内容来判断是否显示和显示什么内容)。到此我们已经有了校验数据,剩下的就是在视图中显示了。关于显示我们仍然可以使用Html.ValidationMessageFor之类的方法;如果你想获得更大的灵活性你可以使用视图页面的“GetVerifyErrorList”方法,此方法在我们新定义的视图基类中,它的功能就是将校验信息构建成一个json对象。代码如下:
/// <summary> /// 获得验证错误列表 /// </summary> /// <returns></returns> public MvcHtmlString GetVerifyErrorList() { ModelStateDictionary modelState = ((Controller)(this.ViewContext.Controller)).ModelState; if (modelState == null || modelState.Count == 0) return new MvcHtmlString("null"); StringBuilder errorList = new StringBuilder("["); foreach (KeyValuePair<string, ModelState> item in modelState) { errorList.AppendFormat("{0}‘key‘:‘{1}‘,‘msg‘:‘{2}‘{3},", "{", item.Key, item.Value.Errors[0].ErrorMessage, "}"); } errorList.Remove(errorList.Length - 1, 1); errorList.Append("]"); return new MvcHtmlString(errorList.ToString()); }
下面给出一个使用例子,代码是登陆视图的代码:
//脚本代码 <script type="text/javascript"> var verifyErrorList= @GetVerifyErrorList(); $(function(){ if (verifyErrorList != null) { for(var i = 0; i < verifyErrorList.length; i++){ $("#"+verifyErrorList[i].key+"Error").html(verifyErrorList[i].msg) } } }) </script> //html代码 <tr> <td>密码:</td> <td> <input type="password" name="password" id="password" value="@Model.Password"/> </td> <td><span style="color: Red;" id="passwordError"></span></td> </tr>
通过以上实现我们既保证框架能够兼容mvc各个功能,又为高级开发者提供了足够的扩展空间。PS:团队中有位同事曾经将asp.net mvc源码中有关模型绑定和模型校验的代码全部删除,并完美运行实例,性能和开销都少了不少,有兴趣的朋友可以去试试!