上章節我們已經定制好動態配置的菜單,用戶登錄網站的第一步就是進入首頁內容,那我們先搭建一下我們的首頁內容。想着自己的網站內容主要是個人博客類型,所以,首頁就展示博主本人的一些基本信息吧,哈哈。當然,做成靜態的界面很簡單,直接將信息填進html中就行了,基本沒有什么技術含量,那我們這里要做成可配置的:將個人信息配置在json文件中(也可以存儲在數據庫,考慮信息內容結構的不可預期性和易變性,這里不采用數據庫保存)。這樣,以后我們要更新主頁上的信息時,就不用編譯發布網站,只要修改對應的json配置文件即可。
.NET Core 配置機制介紹
提到“配置”二字,我們腦海中會立馬浮現出兩個特殊文件,那就是我們再熟悉不過的app.config和web.config,一直以來我們已經習慣了將結構化的配置信息定義在這兩個文件之中。到了.NET Core的時候,很多我們習以為常的東西都發生了改變,其中也包括定義配置的方式。有興趣的同學可以移步官方文檔介紹:.net core 配置
.NET Core不僅支持以鍵-值對的形式、結構化的形式讀取相關的配置信息(這里自己去研究,不在祥述),更重要的是,如今可以將定義的結構化配置綁定為對象。之前我們必須逐條的讀取配置信息,如果配置項太多的話,讀取配置項其實是一項非常繁瑣的工作。現在只用再定義對應結構的Option對象,框架自動幫我們綁定配置信息到這個對象,我們直接訪問對象中的屬性即可。除此之外,.NET Core采用依賴注入的方式來使用Option模型,這樣我們就和方便的在需要的地方使用配置信息了。
接下來看具體怎么使用,首先,在項目中增加json配置文件,這里結構如下:
我們按照Json的格式,定義一個接收配置信息的類(結構完全等同json中的定義)
1 /// <summary> 2 /// 個人資料 3 /// </summary> 4 public class MyProfile 5 { 6 public IEnumerable<Project> Projects { set; get; } 7 } 8 9 /// <summary> 10 /// 項目經歷 11 /// </summary> 12 public class Project 13 { 14 /// <summary> 15 /// 持續至日期 16 /// </summary> 17 public string LastToDate { set; get; } 18 /// <summary> 19 /// 項目名稱 20 /// </summary> 21 public string Name { set; get; } 22 /// <summary> 23 /// 持續時間 24 /// </summary> 25 public int Lasting { set; get; } 26 /// <summary> 27 /// 所在公司 28 /// </summary> 29 public string Company { set; get; } 30 /// <summary> 31 /// 項目職務 32 /// </summary> 33 public string MyTitle { set; get; } 34 /// <summary> 35 /// 項目職責 36 /// </summary> 37 public string MyDuty { set; get; } 38 /// <summary> 39 /// 項目技術 40 /// </summary> 41 public string Technology { set; get; } 42 /// <summary> 43 /// 項目規格 44 /// </summary> 45 public int NumOfPeople { set; get; } 46 /// <summary> 47 /// 項目描述 48 /// </summary> 49 public IEnumerable<string> ProjectDescs { set; get; } 50 /// <summary> 51 /// 項目圖片 52 /// </summary> 53 public IEnumerable<string> ProjectImgs { set; get; } 54 }
那怎么建立json數據和對象之間的映射呢,見證奇跡的時候到了,在Startup.cs文件中ConfigureServices方法中,增加以下代碼:
1 var builder = new ConfigurationBuilder() 2 .SetBasePath(Directory.GetCurrentDirectory()) 3 .AddJsonFile("Datas/Config/MyProfile.json"); 4 5 var config = builder.Build(); 6 7 services.Configure<MyProfile>(config.GetSection("MyProfile"));
在需要使用配置信息地方通過構造器注冊,可以看到相關信息完整的加載到對象中,是不是新的配置系統顯得更加輕量級,具有更好的擴展性,並且支持多樣化的數據源。
個人信息展示
有了可配置的個人信息數據,我們只需要在主頁上將之展示出來即可,后台管理--菜單管理,添加一個主頁菜單,排序最前,設置訪問路徑/Home/Index,圖標樣式,保存后左側導航便出現新增的主頁菜單項。在HomeController控制器的Index方法中,已經讀取到個人信息數據,返回前端渲染,效果如下:
1 <div class="tab-pane active" id="timeline"> 2 <ul class="timeline timeline-inverse"> 3 4 @foreach (var project in Model.Projects.Reverse()) 5 { 6 <li class="time-label"> 7 <span class="bg-light-blue">@project.LastToDate</span> 8 </li> 9 <li> 10 <i class="fa fa-tags bg-blue"></i> 11 <div class="timeline-item"> 12 <span class="time">持續時間:@(project.Lasting)個月 <i class="fa fa-clock-o"></i></span> 13 <h3 class="timeline-header"> 14 <a>項目名稱</a> @project.Name 15 </h3> 16 <div class="timeline-body"> 17 <p class="text-muted">所在公司: @project.Company</p> 18 <p class="text-muted">項目職務: @project.MyTitle</p> 19 <p class="text-muted">項目職責: @project.MyDuty</p> 20 <p class="text-muted">項目技術: @project.Technology</p> 21 </div> 22 <div class="timeline-footer"> 23 <a class="btn btn-primary btn-xs">更多 >></a> 24 </div> 25 </div> 26 </li> 27 <li> 28 <i class="fa fa-thumb-tack bg-aqua"></i> 29 <div class="timeline-item"> 30 <span class="time">規模:@(project.NumOfPeople)個人 <i class="fa fa-cube"></i></span> 31 <h3 class="timeline-header no-border"> 32 <a>項目描述</a> 33 </h3> 34 <div class="timeline-body"> 35 @if (project.ProjectDescs != null) 36 { 37 38 var descIndex = 0; 39 40 foreach (var projectDesc in project.ProjectDescs) 41 { 42 <p class="text-muted">@(++descIndex). @projectDesc.</p> 43 } 44 } 45 </div> 46 </div> 47 </li> 48 if (project.ProjectImgs != null) 49 { 50 <li> 51 <i class="fa fa-camera bg-purple"></i> 52 <div class="timeline-item"> 53 <span class="time"><i class="fa fa-photo"></i></span> 54 <h3 class="timeline-header"> 55 <a>效果預覽</a> 56 </h3> 57 <div class="timeline-body"> 58 <div class="row"> 59 @foreach (var projectImg in project.ProjectImgs) 60 { 61 <div class="col-sm-6 col-md-4"> 62 <img style="cursor: pointer; height: 150px;" src="@projectImg" 63 alt="..." class="img-responsive thumbnail center-block img-more"> 64 </div> 65 } 66 </div> 67 </div> 68 </div> 69 </li> 70 } 71 72 } 73 74 <!-- start --> 75 <li class="time-label"> 76 <span class="bg-green">2009.02</span> 77 </li> 78 <li> 79 <i class="fa fa-clock-o bg-gray"></i> 80 </li> 81 </ul> 82 </div>
目前圖片是用壓縮后的縮略圖顯示的(為了網頁快速加載),可能用戶想要展開看原始圖,所有我們這塊可以優化一下,點擊縮略圖,彈出模態框,展示原始尺寸圖片。ok,主頁部分大功告成,后台修改json文件配置,我們的主頁內容也和方便的更新了。
1 <script> 2 $('.img-more').click(function () { 3 var file = $(this).attr('src') 4 var fileNames = file.split('.') 5 $("#myModal").find("#img_show").html("<image src='" + 6 fileNames[0] + '-lg.' + fileNames[1] + 7 "' class='carousel-inner img-responsive img-rounded' />") 8 $("#myModal").modal(); 9 }) 10 </script>
登錄驗證功能
還記得第2章內容嗎,我們已經實現了用戶的注冊和登錄,但是目前沒有做相關的登錄驗證和權限管理(權限管理以后等后面完成用戶角色單獨開一章說,本章主要說一下登錄驗證)。比如我們注銷用戶時,再次通過瀏覽器鏈接,輸入http://localhost:16546/Configuration/Menu/Index,就可以跳過登錄,直接訪問菜單界面。這塊我們要自己實現的話,不外乎2種方案:
第1種:定義一個公共的控制器,其他所有的控制器繼承它,公共的控制器實現重寫如下方法:
1 public class AuthenticationControllor : Controller 2 { 3 protected override void OnActionExecuting(ActionExecutingContext filterContext) 4 { 5 if (filterContext.HttpContext.Session["username"] == null) 6 filterContext.Result = new RedirectToRouteResult("Login", new RouteValueDictionary { { "from", Request.Url.ToString() } }); 7 8 base.OnActionExecuting(filterContext); 9 } 10 }
第2種:定義認證特性,繼承ActionFilterAttribute,在需要驗證的地方,增加屬性認證:
1 // 登錄認證特性 2 public class AuthenticationAttribute : ActionFilterAttribute 3 { 4 public override void OnActionExecuting(ActionExecutingContext filterContext) 5 { 6 if (filterContext.HttpContext.Session["username"] == null) 7 filterContext.Result = new RedirectToRouteResult("Login", new RouteValueDictionary { { "from", Request.Url.ToString() } }); 8 9 base.OnActionExecuting(filterContext); 10 } 11 }
那我們采用哪種方法呢?都不用,哈哈,因為Indentity幫我們已經完成了所有的認證工作,直接在需要驗證的控制器或方法上,增加屬性過濾器[Authorize]即可。
我們再試驗下,注銷登錄后,我們瀏覽器中輸入菜單界面地址鏈接,網站判斷此時尚未登錄,就會跳轉到登錄界面。用戶輸入登錄信息無誤后,跳轉到需要訪問的菜單界面。
加載Loading效果和form表單重復提交防護
上圖我們可以看到,每次頁面加載的時候頁面頭部會出現一條進度條和一個轉動的loading,這樣給用戶的體驗會很好,這是怎么實現的呢。其實AdminLTE已經提供了很方便的使用:首先我們在母版頁Layout.cshtml引入<script src="~/lib/AdminLTE/plugins/pace/pace.js"></script>,然后加入以下腳本,就實現上述的效果,很簡單吧:
1 //ajax請求Pace效果 2 $(document).ajaxStart(function () { 3 Pace.restart() 4 })
之前實現的菜單管理,存在一個小問題,比如用戶在保存的時候,網速慢或服務器反應延遲的情況,用戶等不急再次點擊form表單中的提交按鈕,就會造成重復提交的場景,一般會導致異常。雖然說在后台增加邏輯判斷也可以解決,但是重復提交造成的異常各種各樣,要就增加各種驗證,得不償失。我的想法是在前端提交后,服務器未返回結果之前,將提交按鈕置為不可用狀態,這樣用戶無法再次點擊,從而避免服務器端異常。但是還有個問題,必須在jquery前端驗證之后,才能開始置為不可用,否則前端驗證失效。所以我們修改下jquery前端驗證的默認的submitHandler。每當用戶點擊提交按鈕時,會首先觸發前端驗證,驗證不通過,直接顯示錯誤提示;驗證通過后,將按鈕置不可用,直到服務器返回結果。
1 //防止重復提交 2 $.validator.setDefaults({ 3 submitHandler: function (form) { 4 $(form).find('[type="submit"]').attr('disabled', true); 5 form.submit(); 6 } 7 });
404,500等錯誤頁面提示
之前我們建了一些菜單項,因為沒有指定訪問路徑,點擊后會有404錯誤;另外,一些服務器異常,網頁拋出500錯誤。這些需要捕獲一下,並提供友好的頁面顯示給用戶。在Startup.cs文件Configure方法中,配置如下:
1 if (env.IsDevelopment()) 2 { 3 app.UseExceptionHandler("/Home/Error/500"); 4 app.UseStatusCodePagesWithReExecute("/Home/Error/{0}"); 5 6 app.UseDeveloperExceptionPage(); 7 app.UseBrowserLink(); 8 app.UseDatabaseErrorPage(); 9 } 10 else 11 { 12 app.UseExceptionHandler("/Home/Error/500"); 13 app.UseStatusCodePagesWithReExecute("/Home/Error/{0}"); 14 }
對應的控制器代碼更新如下:
1 [Route("Home/Error/{statusCode}")] 2 public IActionResult Error(int statusCode) 3 { 4 return View(new ErrorViewModel 5 { 6 StatusCode = statusCode, 7 RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier 8 }); 9 }
對應視圖代碼也需要更新一下:
1 @model MyWebSite.ViewModels.ErrorViewModel 2 @{ 3 ViewData["Title"] = "錯誤"; 4 } 5 <div class="error-page" style="margin-top: 100px;"> 6 @if (Model.StatusCode == 500) 7 { 8 <!-- 500類型錯誤 --> 9 <h2 class="headline text-red"> @Model.StatusCode</h2> 10 11 <div class="error-content"> 12 <h3> 13 <i class="fa fa-warning text-red"></i> Oops! Something went wrong. 14 </h3> 15 <div> 16 StatusCode:<code>@Model.StatusCode</code> 17 </div> 18 <div> 19 Request ID: <code>@Model.RequestId</code> 20 </div> 21 <p> 22 We will work on fixing that right away. 23 Meanwhile, you may <a asp-area="" asp-controller="Home" asp-action="Index">return to index</a> or try using the search form. 24 </p> 25 <form class="search-form"> 26 <div class="input-group"> 27 <input type="text" name="search" class="form-control" placeholder="Search"> 28 <div class="input-group-btn"> 29 <button type="submit" name="submit" class="btn btn-danger btn-flat"> 30 <i class="fa fa-search"></i> 31 </button> 32 </div> 33 </div> 34 </form> 35 </div> 36 } 37 else 38 { 39 <!-- 其他類型錯誤 --> 40 <h2 class="headline text-yellow"> @Model.StatusCode</h2> 41 42 <div class="error-content"> 43 <h3> 44 <i class="fa fa-warning text-yellow"></i> Oops! Page not found. 45 </h3> 46 <div> 47 StatusCode:<code>@Model.StatusCode</code> 48 </div> 49 <div> 50 Request ID: <code>@Model.RequestId</code> 51 </div> 52 <p> 53 We could not find the page you were looking for. 54 Meanwhile, you may <a asp-area="" asp-controller="Home" asp-action="Index">return to index</a> or try using the search form. 55 </p> 56 <form class="search-form"> 57 <div class="input-group"> 58 <input type="text" name="search" class="form-control" placeholder="Search"> 59 <div class="input-group-btn"> 60 <button type="submit" name="submit" class="btn btn-warning btn-flat"> 61 <i class="fa fa-search"></i> 62 </button> 63 </div> 64 </div> 65 </form> 66 </div> 67 } 68 </div>
小結
以上,這期關於網站的調整已介紹完畢,最后,后台管理--菜單管理,添加對應博客鏈接的菜單項,為了方便用戶通過鏈接訪問我的博客,看下演示效果吧: