复习:性能:平衡 / 善意欺骗 / 缓存 / 线程和异步 / 队列 / 压缩合并 / 高并发大流量
MVC中可以HttpContext直接获取缓存对象,可以把它当做一个“全局”容器,里面以键(string)值(object)对的形式存储着缓存数据。
可调用其索引器读写缓存数据,其过程非常类似于session/HttpContext.Current.Items
public ActionResult Single(int id) { string key = "article"; object cachedModel = HttpContext.Cache[key]; if (cachedModel == null) { HttpContext.Cache[key] = service.Get(id); }//else nothing return View((SingleModel)HttpContext.Cache[key]);
常见面试题:区别
实际上,上述索引器内部只是简单的调用了两个方法:
而Insert()(或者Add())方法还有很多重载,可以定义更多的缓存配置:
public object Add( string key, object value, //键值对 CacheDependency dependencies, //主要适用于文件,当文件发生变动时,删除当前缓存 DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, //优先级 CacheItemRemovedCallback onRemoveCallback);//当缓存被删除时调用){
ASP.NET中可以设置:
在MVC中,绝对/滑动过期时间不能同时设立,否则会报异常(演示:略):
DateTime.Now.AddSeconds(30), TimeSpan.Zero
DateTime.MaxValue, new TimeSpan(0, 0, 5),
@想一想@:为什么要设计一个滑动过期呢?
滑动过期基于这样一种假设:越是被频繁请求的数据,以后就越有可能被再次请求(类似于“犹太定律”,事实上也确实如此,^_^)。利用滑动过期,就能自动的筛选出最被频繁访问的数据,提高命中率,最大限度的压榨性能。
和Session类似,虽然我们设置了缓存的过期时间,但并不能保证在此期间缓存一定存在——在某些情况下,有效期内的缓存也会被MVC自动清除。
这时候,ASP.NET会清除那些优先级低的,保留优先级高的。
我们可以在插入缓存数据的时候指定其优先级(枚举):
public enum CacheItemPriority
默认的cache数据会一直保存,这样,如果真实/源数据发生改变,缓存数据就会变得“不正确”。
演示:更改数据库中数据,改变页面呈现……
所以,ASP.NET提供了缓存依赖机制,即:为缓存数据设置一个“依赖”,如果依赖发生变化,让缓存失效,以便能获取正确数据。
我们常用的是
即:一旦数据库数据发生变动,就让缓存失效。
因为localDb不支持从数据库发送notification(通知)到.NET运行时,所以我们采用的是poll机制:由ASP.NET定时的轮循检查数据库变更。
所以需要在数据库上设置一个表,专门的记录其最后更改时间……
首先,使用aspnet_regsql.exe命令配置数据库:(复习:数据库中存放session)
aspnet_regsql.exe -S (localdb)\MSSQLLocalDB -E -d 17bang -ed启动数据库的缓存依赖,其中:
检查数据库会发现多了一个表:AspNet_SqlCacheTablesForChangeNotification
aspnet_regsql.exe -S (localdb)\MSSQLLocalDB -E -d 17bang -t Articles -et指定数据库上的表(缓存依赖),其中:
然后,在web.config的system.web中配置poll时间等
<system.web> <caching> <sqlCacheDependency enabled="true" pollTime="1000"> <databases> <add name="17bang" connectionStringName="17bang" pollTime="1000" /> </databases> </sqlCacheDependency> </caching> </system.web>
其中pollTime的单位是毫秒。connectionStringName
最后,可以在Insert时添加SqlCacheDependency实例对象做参数:
HttpContext.Cache.Insert(cacheKey, model, //17bang数据库名,Users是表名 new SqlCacheDependency("17bang", "Articles"));演示:改变数据库中的数据……
常见面试题:Add() vs Insert()
很明显,Insert()更好用。
ASP.NET还为我们提供了一个delegate参数选项
public delegate void CacheItemRemovedCallback( string key, object value, CacheItemRemovedReason reason);
public delegate void CacheItemUpdateCallback(string key, CacheItemUpdateReason reason, ……);
可以设定当cache数据过期/被删除时可调用的方法。我们来演示一下:
(k, v, r) => {Trace.WriteLine( $"cache with key:({k}) and value:({v}) is deleted, reason:({r})"); }演示:output窗口输出……
使用上文所述的API很灵活,但:
所以MVC推出了OutputCache,可以:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] public class OutputCacheAttribute : ActionFilterAttribute, IExceptionFilter
有意思:OutputCache实际上继承自ActionFilterAttribute!
可以看出,OutputCache可适用于Controller(但一般不会)和Action,包括ChildAction。
* |
为所有不同的url参数(名称/个数/值)缓存不同的副本, |
空字符串或者None |
忽略url参数的差异,所有不同url参数都共用同一个缓存副本 |
分号(;)分隔的参数名,如:id;name |
为指定的url参数缓存不同的副本,忽略未指定的url参数差异 |
断点演示:Action中的断点不会被击中,MVC会直接将之前缓存的HTML文件/片段返回给客户端……
<outputCacheSettings> <outputCacheProfiles> <add name="ArticleSingle" duration="100" varyByParam="name;age" /> </outputCacheProfiles> </outputCacheSettings>
然后,在OutputCache中引用:
[OutputCache(CacheProfile = "ArticleSingle")] public ActionResult Single(int id)
这样,能实现和
[OutputCache(Duration = 100)]一样的效果。
@想一想@:为什么还要额外提供这个在web.config中进行配置的CacheProfile选项?
此外,还可以直接禁用OutputCache(一般是因为调试):
<outputCache enableOutputCache="false"></outputCache>
很多时候,我们希望能缓存一个页面,但是其中的某一个部分例外(如:LogonStatus),怎么办呢?
MVC暂时未能提供内置的支持(演示),我们需要(通过NuGet)引入第三方插件MVCDonutCache 。
它的实现非常简单:
[DonutOutputCache(Duration = 5)] //需要添加using DevTrends.MvcDonutCaching;
@Html.Action("_Inviter", "Shared", true)
演示:父Action页面和ChildAction页面都显示DateTime.Now
建议:总是使用MVCDonutCache
MVC内置了js/css文件压缩机制。
以.css文件为例,我们先添加3个.css文件:
里面有注释、空格和一个.b1/.b2/.b3/.bat类定义等。
然后在BundleConfig.RegisterBundles()中进行配置:
bundles.Add(new StyleBundle("~/b").Include( "~/Content/b1.css", "~/Content/b1.css" ));
意思是用“~/b”包含(Include)两css文件:
如果是.js文件就用ScriptBundle替换StyleBundle。
接下来就可以在View中使用:
@Styles.Render("~/b")
F5运行演示:上述代码转化成:
<link href="/Content/b1.css" rel="stylesheet"> <link href="/Content/b2.css" rel="stylesheet">如果是.js文件就使用:@Scripts.Render()。
上述b1.css和b2.css文件还没有被压缩和合并:这是因为还是在调试状态下运行。
在web.config中system.web节点下修改:
<compilation debug="false" targetFramework="4.6.1"/>
然后Ctrl+F5 release状态下运行,生成的HTML标签变成:
<link href="/b?v=eTZCJLM-wumVB4Lk6hPRwROI38FiegKqZ8_EsrepW9c1" rel="stylesheet">
演示:/b的内容为:b1.css和b2.css文件的压缩合并后内容:
body{}.b1{}body{}.b2{}body{}
注意href="/b?v=eTZCJLM-wumVB4Lk6hPRwROI38FiegKqZ8_EsrepW9c1"中v的值是MVC自动生成的。
MVC会监视着“~/b” bundle中的css文件,如果他们的内容发生变化,v值也会相应的变化(反之不会变化),保证浏览器不会使用缓存着的“/b”内容,而是重新获取变化后的最新内容。
演示:
Include()中除了可以逐一列举,还可以使用通配符:
bundles.Add(new StyleBundle("~/b").Include( "~/Content/b*" ));
bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js"));注意:不要同一文件的不同版本放置在相同位置,这样会把所有版本都拉入进来。
RazorPages中没有bundle功能,个人估计是因为bundle会删除所有注释,包括文件头部的版权声明,这可能是违反版权法律规定的。
ASP.NET可以为Ajax请求返回两种格式的数据:
if (Request.IsAjaxRequest()) //判断是否是Ajax请求
方法。返回一个(仍然是ActionResult子类的)JsonResult对象。
Json()方法有参数:
演示:点赞/踩
使用了“困难模式”,在赞和踩的父元素上绑定事件:
<div yz-article-appraise> <a data-direction="up">赞 <span>95</span> </a> <a data-direction="down">踩 <span>3</span> </a> </div>
$('[yz-article-appraise]').click(function (event) {
@想一想:Ajax请求的url应该怎么写,需要告诉后端哪些内容?
url: "/Article/_Appraise?articleId=@ViewContext.RouteData.Values["id"]&direction=" + $(event.target).data("direction"),
#体会#:JavaScript代码和Razor的C#代码混用,^_^
@想一想@:为什么这里不用this而是event.target,如果使用this的话位置应该放在哪里?
ArticleController下声明:
public ActionResult _Appraise(int articleId, Direction direction)
不要忘了利用Model绑定,使用Action参数接收前端数据。
Direction是一个枚举,声明在global中:
public enum Direction { Up, Down
然后Action方法中该做啥做啥,比如:
int amount = service.Appraise(articleId, direction);
返回的amount是当前赞/踩的数量,传入Json()中。演示:因为发起的是GET请求报错
method: "POST",
return Json(amount, JsonRequestBehavior.AllowGet);
如果前端确实不需要数据返回的话,Action方法可以返回void的(但不建议):
public void _Appraise(
赞/踩数量标签仍然用属性定位:
<span yz-article-appraise-amount >95</span>
@想一想@:为什么?
为重用整理过后的代码:
let $target = $(event.target), direction = $target.data("direction");
success: function (data) { $target.find('[yz-article-appraise-amount]').text(data); },
需要using System.Web.Mvc;
[Remote("IsNameDuplicated", "Register", ErrorMessage = "* 用户名重复", HttpMethod = "GET")] public string UserName { get; set; }
//必须返回一个JsonResult public JsonResult IsNameDuplicated(string UserName)//UserName就是需要验证的属性名称 { return Json(UserName != "zyfei", //true:通过验证 JsonRequestBehavior.AllowGet); }
<input data-val="true" data-val-remote="* 用户名重复" data-val-remote-additionalfields="*.UserName" data-val-remote-type="GET" data-val-remote-url="/Register/IsNameDuplicated" id="UserName" name="UserName" type="text" value="">
课前已准备:
还是PerViewPerModel,不要重用列表和发布的Model。
不再将Comments视为SingleModel的一部分:
@foreach (var item in Model.Comments)
而是抽象出:
@Html.Action("_List", "Comment", new {articleId = articleId})其中,articleId由ViewData而来:
int articleId = Convert.ToInt32(ViewContext.RouteData.Values["id"]);
@Html.Partial("~/Views/Comment/_Publish.cshtml", new _PublishModel { ReferId = articleId})以便独立出Comment._PublishModel:
public class _PublishModel { [Required(ErrorMessage = " *必填")] public string Body { get; set; } public int ReferId { get; set; }PartialView中,保证form表单提交后到达:
@using (Html.BeginForm("_Publish", "Comment"))ReferId通过hidden input传递,且有防CSFR机制:
@Html.TextAreaFor(m => m.Body) @Html.ValidationMessageFor(m=>m.Body) @Html.HiddenFor(m => m.ReferId) @Html.AntiForgeryToken()
public class CommentController : BaseController { [NeedLogOn] [HttpPost] [ValidateAntiForgeryToken] public ActionResult _Publish(_PublishModel model)且完成提交后重定向到之前页面:
int CommentId = service.Publish(model); return RedirectToAction("Single", new { id = model.ReferId });
尽量利用MVC已有的功能,在不改变现有代码的基础上进行改造。
$('form').submit(function (event) { event.preventDefault(); let $form = $(this); $.ajax({ url: $form.attr('action'), data: $form.serialize(), method: "post",
@想一想@:为什么要在form.submit(),而不是在button.click()事件中处理?(更保险,没有遗漏,保全Model验证……)
一般来说,MVC中但凡复杂一点的数据,我们就直接返回现成的HTML内容。
所以引入_ListItem.cshtml,从_List.cshtml中复制粘贴并修改:
@model ViewModels.Comment._ItemModel <div data-id="@Model.Id"> <small> 作者: <a href="/User/@Model.Author.Id">@Model.Author.UserName</a> </small> <div> @Html.Raw(Model.Body)
然后通过service获取model填充:
public ActionResult _Publish(_PublishModel model) { int commentId = service.Publish(model); if (Request.IsAjaxRequest()) { return PartialView("_ListItem", service.GetItem(commentId));
不要忘了,之前的评论列表,也使用PartialView重构:
@model List<ViewModels.Comment._ItemModel> @foreach (var item in Model) { @Html.Partial("_ListItem", item)
演示:未登录用户发布评论,插入重定向页面内容
这是因为NeedLogon没有区分Ajax请求,导致发生了页面跳转,并将跳转后页面内容作为Ajax响应返回。
所以首先需要区分是否为Ajax请求,如果是Ajax请求:
if (filterContext.HttpContext.Request.IsAjaxRequest())
一般要两个要求:
filterContext.HttpContext.Response.StatusCode = 403; filterContext.Result = new JsonResult { Data = "该请求需要登录才能响应……"
F12演示:
评论列表可以被缓存;然后可能就有“更严格的”需求,当有新评论被发布时,更新缓存数据!
这就需要我们通过编程的方式实现。
首先,缓存列表数据:
string key = getCacheKey(articleId); if (HttpContext.Cache.Get(key) == null) { HttpContext.Cache.Insert(key, service.GetOf(articleId)); }//else nothing
注意这个key值,带有articleId,被封装成方法:
private string getCacheKey(int articleId) { return $"commentsof-{articleId}";这就是我们后面删除缓存时需要用到的:
public ActionResult _Publish(_PublishModel model) { string key = getCacheKey(model.ReferId); HttpContext.Cache.Remove(key);
要使用异步,只需要将Action方法改为异步方法(复习),如下:
public async Task<ActionResult> Index() { //……await之前其他代码:准备message/client等 await client.SendMailAsync(mail); //……await之后其他代码 return View(); }
复习:async为什么能提升性能?
复习:后台流程
简化成注册时填入email。
[ComplexType] public class Email
ValidCode = new Random().Next(9999).ToString("0000");
cfg.CreateMap<VM.Register.IndexModel, User>() .ForMember(u => u.Email, opt => opt.MapFrom(i => new Email { Addresss = i.Email }))
发件人是系统邮箱,收件人是User.Email.Address。
关键是要生成一个链接,这就需要用到HtmlTemplate:
Body = EmailTemplate.Valid( ConfigurationManager.AppSettings["domain"], Id, Email.ValidCode),
public static string Valid(string domain, int userId, string code) { return $@"点击<a href='{domain}/Email/Valid/{userId}?code={code}'>{domain}/Email/Valid/{userId}?code={code}</a>完成验证"; }注意:
<add key="domain" value="http://localhost:62679"/>
复习:email的发送
将email的发送方法改为:
await client.SendMailAsync(mail);
同时修改方法的定义:
public async Task Register()
不能是void,因为它还要被继续异步调用
public async Task<int> Save(IndexModel model) { await user.Register();
直到生成异步Action:
public async Task<ActionResult> Index(IndexModel model) { int id = await userService.Save(model);
演示:
分别使用AjaxForm和原生JQuery的post()实现求助的应答
键值策略
Cache是全局的:所以要避免不同的。。。使用了相同的键值
演示:
建议使用{Controller}-{Action}-{Parameters}的形式构建cache key值,以免重复冲突。通常我们可以抽象出一个方法:
public static string GetCacheKey( string controller, string action, params object[] parameters) { string key = $"{controller}-{action}-"; return key + string.Join("_", parameters); }
在Action中调用:参数id表示文章编号,int类型;参数all表示是否显示全部内容,bool类型
string key = Keys.GetCacheKey(nameof(ArticleController), nameof(Single), id, all);
为什么呢?
我们知道:IIS本身是多线程(复习)的,且一个线程对应(处理)一个Request请求,请求处理完毕发送response到客户端,over,线程回收。
如果我们直接引入异步方法,就很有可能:
在MVC3以及之前,还需要Controller继承AsyncController
public class RegisterController : AsyncController
多快好省!前端后端,线上线下,名师精讲
更多了解 加: