ASP.NET Core 一步步搭建我的網站(6)_單頁模式和優化

 前言

HI,有段時間沒有更新了,主要由於第一年前事情比較多,有些事得忙着張羅下;第二呢,對我的網站進行了一次大範圍的優化,主要是申請的雲服務器資源有限,1m的網絡帶寬,帶上圖片展現的話,打開網站的平均速度會達到20s以上,用戶的體驗不是很好;通過此次優化,已將訪問速度控制在1s左右,總體感受速度和用戶體驗提高了很多。固然,最主要的是,把網站的模式改爲了單頁應用模式,算得上是比較大的重構了,因此耽擱的時間比較久,請你們見諒。css

那今天主要來講一下我是怎麼重構以前的網站代碼,更新爲單頁模式吧,順便分享下我的性能優化的經驗。html

重構出發點和目標

以前.Net Core自動幫咱們生成的網站代碼,主要是多頁的應用,咱們定義頁面的時候,引入了_layout文件,至關視圖區域的模板頁,那經過控制器調整頁面的同時,會將整個頁面包括模板頁都會再次加載一遍。因此,以前的多頁應用,對應個人我的應用來講,有如下問題:前端

  • 頁面導航跳轉時,須要從新加載整個頁面包含的css、js和html元素,雲服務器帶寬有限,從新加載資源有些浪費;
  • 用戶體驗很差,在網速帶寬不夠的狀況下,頁面刷新慢,用戶須要等待很長一段時間才能看到內容區域;
  • 做爲後臺管理系統,頁面佈局包含導航欄,頭部區域、尾部區域和內容區域,除了內容區域的視圖會常常變換,其餘區域不會有大的變化,卻每次須要從新加載和刷新;
  • 當初網站設計須要在移動端提供支持,多頁應用在移動設備中頁面切換的表現加載慢、不流暢、用戶體驗差;

如今,網頁設計總體趨向於單頁網站設計,可是這裏我仍是要強調一下,我從沒說過單頁模式就必定比多頁模式要好,只是這對我我的網站項目來講,單頁模式可能會更合適。其實2者都有很明顯的優缺點,多頁模式也有它的優點,好比能夠很方便的作SEO,單頁模式也有本身的劣勢,好比得單獨方案作SEO,並且開發難度相對多頁會複雜些。jquery

因此說,一切拋開具體應用去談技術之間的好壞都是耍流氓!!!這裏,咱們的網站也不徹底是單頁模式,好比登陸頁面跳轉等等用的仍是多頁,因此只能說是以單頁模式爲主的混合模式。angularjs

那既然咱們決定要改造咱們的網站爲單頁應用,首先定一下優化的目標吧:ajax

  • 以簡單、容易和可操做的方式展現給用戶,提供用戶簡單的線性體驗;
  • 整個頁面有固定的導航欄,頭部區域、尾部區域和變化的內容區域,實現頁面片斷局部刷新;
  • 公共資源首次加載後,頁面更新後不在從新加載;
  • 選擇專門的UI框架,下降單頁模式的開發難度;
  • 很好的響應式設計,支持移動端良好的用戶體驗;

引入UI路由框架

既然咱們捨棄了多頁模式,就不能再用微軟提供的_layout模板頁取渲染視圖了,很方便的Razor引擎也無法再用了(這裏稍微解釋下,.Net Core以前版本的,是能夠繼續用Razor來渲染數據模型的,可是.Net Core中,會將cshtml文件一塊兒編譯成dll文件,因此cshtml文件在發佈路徑中是找不到的,因此路由後的url是找不到的),那選用那種UI框架,來知足單頁模式應用呢?json

 

咱們理一下思路:如圖,咱們經過導航欄side的url地址,經過一種路由方式找到對應的html文件,而後後臺加載數據,經過UI渲染,在內容區域content中顯示。這裏能夠看出,主要要知足一是url的路由,二是數據模型的渲染。固然,知足這樣需求的UI框架不少,我這裏用的一種主流框架angular-ui-router(url路由,能夠支持多樣化視圖和嵌入式視圖)+angular(數據模型和UI界面雙向綁定),你們也能夠用別的方式,條條大道通羅馬嘛!bootstrap

 angular-ui-router配置

本文只是簡單介紹angular的使用方法,若是須要了解更多的使用方法,請參考官網文檔緩存

首先,咱們引入如下2個腳本:angular.js和angular-ui-router.js。導航菜單結構重構以下:性能優化

 1     /// <summary>
 2     /// 導航菜單項
 3     /// </summary>
 4     public class NavMenu
 5     {
 6         public string Name { get; set; }
 7         public string TemplateUrl { get; set; }
 8         public string Icon { get; set; }
 9 
10         /// <summary>
11         /// 子菜單
12         /// </summary>
13         public IList<NavMenu> SubNavMenus { get; set; }
14     }
15 
16     /// <summary>
17     /// 左側導航菜單視圖模型
18     /// </summary>
19     public class NavBarMenus
20     {
21         public IList<NavMenu> NavMenus { get; set; }
22     }

在site.js裏,作以下配置:

 1 //Angular相關配置
 2 var app = angular.module('app', ['ui.router']);
 3 //路由配置
 4 app.config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) {
 5     $urlRouterProvider.otherwise('/');
 6     $stateProvider.state('Home', {
 7         //主頁
 8         url: '/',
 9         templateUrl: 'App/Home/Home.html'
10     }).state('MenuManagement', {
11         //菜單管理
12         url: '/Configuration/MenuManagement',
13         templateUrl: 'App/Configuration/MenuManagement/MenuManagement.html'
14     }).state('ApiSimulator', {
15         //API模擬
16         url: '/Tools/ApiSimulator',
17         templateUrl: 'App/Tools/ApiSimulator/ApiSimulator.html'
18     }).state('SiteAnalytics', {
19         //網站分析
20         url: '/Tools/SiteAnalytics',
21         templateUrl: 'App/Tools/SiteAnalytics/SiteAnalytics.html'
22     }).state('Error', {
23         url: '/Error',
24         templateUrl: 'App/Home/Error.html'
25     });
26 }]);

以前的嵌套菜單也須要重構,注意這裏操做菜單再也不用<a>標籤的默認導航屬性href,而是改用ui-sref屬性:

 1 @foreach (var navMenu in Model.NavMenus)
 2 {
 3     if (navMenu.SubNavMenus != null && navMenu.SubNavMenus.Any())
 4     {
 5         <li class="treeview">
 6             <a href="#">
 7                 <i class="fa @navMenu.Icon"></i><span>@SharedLocalizer[navMenu.Name]</span>
 8                 <span class="pull-right-container">
 9                     <i class="fa fa-angle-left pull-right"></i>
10                 </span>
11             </a>
12             <ul class="treeview-menu">
13                 @await Html.PartialAsync("_NavMenu", new NavBarMenus { NavMenus = navMenu.SubNavMenus })
14             </ul>
15         </li>
16     }
17     else
18     {
19         <li>
20             @if (navMenu.TemplateUrl != null && navMenu.TemplateUrl.StartsWith("http"))
21             {
22                 <a href="@navMenu.TemplateUrl" target="_blank">
23                     <i class="fa @navMenu.Icon"></i><span>@SharedLocalizer[navMenu.Name]</span>
24                 </a>
25             }
26             else
27             {
28                 <a ui-sref="@navMenu.TemplateUrl">
29                     <i class="fa @navMenu.Icon"></i><span>@SharedLocalizer[navMenu.Name]</span>
30                 </a>
31             }
32         </li>
33     }
34 }

以上,咱們的路由配置就能夠了,點擊左側的導航菜單,angular-ui-router會自動幫咱們解析,對應作部分視圖更新,咱們能夠看到,如今整頁只有內容區域部分刷新,其餘區域再也不會被從新加載。

angularjs數據視圖雙向綁定

多頁模式下,咱們的控制器方法返回的是View視圖,即整個html頁面。改形成單頁模式,控制器須要返回的是數據模型的json,經過angularjs提供的數據雙向綁定,能夠很方便的改造,如下用菜單管理功能做爲示例。

 咱們新增一個html視圖_MenuPartial.html,做爲菜單項的部分視圖(至關於葉節點),能夠看到節點會判斷有沒有子菜單,若是有子菜單,會再次引用_MenuPartial.html,實現了子菜單的嵌套:

 1 <div class='dd-handle dd3-handle'></div>
 2 <div class="dd3-content" ng-click="editMenu(item)">
 3     <i class="fa {{item.icon}} margin-r-5"></i>{{item.name}}
 4     <span class='pull-right'>
 5         <a style="cursor: pointer"
 6            ng-click="deleteMenu(item,$event);$event.stopPropagation();">
 7             <i class="fa fa-minus text-success margin-r-5"></i>
 8         </a>
 9     </span>
10 </div>
11 <ol class='dd-list' ng-if="item.subNavMenus">
12     <li class="dd-item dd3-item"
13         ng-repeat="item in item.subNavMenus"
14         ng-include="'App/Configuration/MenuManagement/_MenuPartial.html'"></li>
15 </ol>

有了菜單項模板,接下來重構一下菜單管理頁面MenuManagement.html,控制器指定MenuManagementController:

 1 <section class="content-header">
 2     <h1>
 3         菜單管理
 4         <small>Preview</small>
 5     </h1>
 6     <ol class="breadcrumb">
 7         <li><a href="#"><i class="fa fa-home"></i></a></li>
 8         <li>後臺管理</li>
 9         <li class="active">菜單管理</li>
10     </ol>
11 </section>
12 <section class="content">
13     <div class="box" ng-controller="MenuManagementController">
14         <div class="box-body row">
15             <div class="col-md-7">
16                 <div class="dd">
17                     <ol class='dd-list'>
18                         <li class="dd-item dd3-item" ng-repeat="item in navMenus" 
19                             ng-include="'App/Configuration/MenuManagement/_MenuPartial.html'"></li>
20                     </ol>
21                 </div>
22             </div>
23             <div class="col-md-5">
24                 <div class="box box-solid">
25                     <div class="box-header">
26                         <i class="fa fa-bars"></i>
27                         <h3 class="box-title">編輯菜單</h3>
28                     </div>
29                     <form class="form">
30                         <div class="form-group">
31                             <label for="name">菜單名稱:</label>
32                             <input class="form-control" type="text" id="name" 
33                                    ng-model="selectedMenu.name">
34                         </div>
35                         <div class="form-group">
36                             <label for="icon">菜單圖標:</label>
37                             <input class="form-control" type="text" id="icon" 
38                                    ng-model="selectedMenu.icon">
39                         </div>
40                         <div class="form-group">
41                             <label for="templateUrl">菜單路徑:</label>
42                             <input class="form-control" type="text" id="templateUrl" 
43                                    ng-model="selectedMenu.templateUrl">
44                         </div>
45                         <div class="pull-right">
46                             <a ng-click="addMenu()"
47                                class="btn btn-social btn-instagram margin-r-5">
48                                 <i class="fa fa-plus margin-r-5"></i>新增
49                             </a>
50                             <a ng-click="save(navMenus)"
51                                ng-disabled="submitting"
52                                class="btn btn-social btn-instagram">
53                                 <i class="fa fa-save margin-r-5"></i>保存
54                             </a>
55                         </div>
56                     </form>
57                 </div>
58             </div>
59         </div>
60     </div>
61 </section>

最後,咱們實現MenuManagementController控制器邏輯:

 1 app.controller('MenuManagementController', ['$scope', function ($scope) {
 2     //編輯菜單
 3     $scope.editMenu = function (item) {
 4         $scope.selectedMenu = item;
 5     };
 6     //新增菜單
 7     $scope.addMenu = function () {
 8         var newMenu = { name: "<新增菜單>", icon: "fa-circle-o", templateUrl: "" };
 9         $scope.navMenus.push(newMenu);
10         $scope.selectedMenu = newMenu;
11     };
12     //刪除菜單
13     $scope.deleteMenu = function (item, $event) {
14         $.confirm({
15             icon: 'fa fa-info',
16             title: '刪除',
17             content: '確認刪除菜單[' + item.name + ']?',
18             type: 'dark',
19             closeIcon: true,
20             typeAnimated: true,
21             buttons: {
22                 confirm: {
23                     text: '肯定',
24                     btnClass: 'btn-dark',
25                     action: function () {
26                         $($event.target).closest('li').remove();
27                     }
28                 },
29                 cancel: {
30                     text: '取消',
31                     btnClass: 'btn-dark'
32                 }
33             }
34         });
35     };
36     //保存菜單
37     $scope.save = function (menus) {
38         $scope.submitting = true;
39         $.ajax({
40             type: 'POST',
41             url: '/Configuration/MenuManagement/Save',
42             data: {},
43             success: function (data) {
44                 //TODO:
45             },
46             complete: function () {
47                 $scope.submitting = false;
48                 $scope.$apply();
49             }
50         });
51     }
52     $scope.selectedMenu = {};
53     //加載菜單
54     $.ajax({
55         type: 'GET',
56         url: '/Configuration/MenuManagement/GetMenus',
57         success: function (data) {
58             $scope.navMenus = data;
59             $scope.$apply();
60             setTimeout(function () {
61                 $('.dd').nestable();
62             }, 100);
63         }
64     });
65 }]);

簡單說下大致的加載流程:用戶點擊左側導航菜單-->angular-ui-router會根據我們site.js中的路由配置,找到對應的MenuManagement.html頁面進行局部刷新-->根據視圖中的控制器MenuManagementController,執行對應控制器中的邏輯並返回數據,由angularjs綁定到視圖-->內容區域獲取到綁定後的視圖,在內容區域顯示。

最終菜單管理頁面效果:

網站性能優化

咱們已經將網站改形成單頁應用,用戶體驗已經有了很大的提升,可是仍是有許多值得優化的地方。因爲雲服務器提供的帶寬很是有限,有時資源和圖片下載稍微有點慢的狀況,會致使整個體驗會很是很差。固然,優化網站的手段很是多,實際狀況每每比較複雜,那針對本人的網站,這裏列出我本身這次實際過程當中採起的優化方法,跟你們分享下。

引入CDN資源

以前咱們訪問css/js資源,咱們都是直接去=訪問的是網站服務器,這樣的話,在帶寬不足的狀況下,下載特別慢,另外也佔用了其餘html元素和圖片下載時間,好比bootstrap.min.css文件,可能就須要幾秒的下載時間,一但css/js文件多的狀況下,總體頁面下載常常超過20s。

那像這種常用的外部庫文件,如今有許多免費的前端開源項目 CDN 加速服務,能夠提供咱們穩定、快速訪問這些庫文件的功能,咱們直接引用這些CDN服務便可:

 1 <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
 2 <script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
 3 <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
 4 <script src="https://cdn.bootcss.com/angular.js/1.6.8/angular.min.js"></script>
 5 <script src="https://cdn.bootcss.com/angular-ui-router/1.0.3/angular-ui-router.min.js"></script>
 6 <script src="https://cdn.bootcss.com/spin.js/2.3.2/spin.min.js"></script>
 7 <script src="https://cdn.bootcss.com/Ladda/1.0.5/ladda.min.js"></script>
 8 <script src="https://cdn.bootcss.com/angular-ladda/0.4.3/angular-ladda.min.js"></script>
 9 <script src="https://cdn.bootcss.com/jquery_lazyload/1.9.7/jquery.lazyload.min.js"></script>
10 <script src="https://cdn.bootcss.com/Nestable/2012-10-15/jquery.nestable.min.js"></script>
11 <script src="https://cdn.bootcss.com/jquery-confirm/3.3.2/jquery-confirm.min.js"></script>
12 <script src="https://cdn.bootcss.com/Chart.js/2.7.1/Chart.min.js"></script>
13 <script src="https://cdn.bootcss.com/jquery-sparklines/2.1.2/jquery.sparkline.min.js"></script>
14 <script src="https://cdn.bootcss.com/moment.js/2.20.0/moment.min.js"></script>
15 <script src="https://cdn.bootcss.com/moment.js/2.20.0/locale/zh-cn.js"></script>
16 <script src="https://cdn.bootcss.com/bootstrap-daterangepicker/2.1.27/daterangepicker.min.js"></script>
17 <script src="https://cdn.bootcss.com/jQuery-slimScroll/1.3.8/jquery.slimscroll.min.js"></script>
18 <script src="https://cdn.bootcss.com/fastclick/1.0.6/fastclick.min.js"></script>
19 <script src="https://cdn.bootcss.com/iCheck/1.0.2/icheck.min.js"></script>
20 <script src="https://cdn.bootcss.com/pace/1.0.2/pace.min.js"></script>
1 <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" />
2 <link rel="stylesheet" href="https://cdn.bootcss.com/Ladda/1.0.5/ladda-themeless.min.css" />
3 <link rel="stylesheet" href="https://cdn.bootcss.com/jquery-confirm/3.3.2/jquery-confirm.min.css" />
4 <link rel="stylesheet" href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css">
5 <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap-daterangepicker/2.1.27/daterangepicker.min.css" />
6 <link rel="stylesheet" href="https://cdn.bootcss.com/ionicons/4.0.0-9/css/ionicons.min.css">
7 <link rel="stylesheet" href="https://cdn.bootcss.com/iCheck/1.0.2/skins/all.css" />
8 <link rel="stylesheet" href="https://fonts.cat.net/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic" />

引入後,咱們監測下網絡傳輸,能夠看到,如今訪問這些資源直接從CDN服務器中去取,大大提升了訪問速度:

 

使用對象存儲服務

 網站不免包含一些圖片或稍微大一點的資源,幾十kb到幾M。沒有緩存狀況下,訪問這些資源,我那可憐的帶寬基本就佔用差很少了,打開這種界面,常常就卡死或超時很嚴重。其實國內外不少雲服務器服務商已經提供了這種解決方案,好比雲存儲服務。您能夠將任意數量和形式的非結構化數據存入對象存儲服務,並對數據進行管理和處理,知足各種場景的存儲需求。

咱們將圖片存儲到申請的對象存儲服務中,這樣咱們訪問網站圖片的時候,就能夠直接訪問雲存儲服務器。再來看下訪問速度,能夠明顯看出,如今圖片下載速度控制在幾百ms內,比以前幾秒甚至十幾秒才能下載一張圖片,有了很大的性能提升。

 圖片延遲加載

 相對來講,通常圖片算是比較大的文件,每每網頁傳輸過程當中,佔了很大的帶寬,若是一個頁面圖片比較多,出現圖片集中加載的時候,網站總體感受加載偏慢。

咱們考慮用圖片延遲加載的方法,只有滾動條快要滾動到圖片位置時,纔去加載對應的圖片,不然暫時不須要加載圖片,由於視圖區域還沒有存在改圖片,即便加載了也看不到。

這裏咱們須要引入jquery.lazyload.min.js,在須要延遲加載的圖片,實現對應的代碼:  $("img.lazyload").lazyload();

監測一下網絡傳輸,剛開始未加載圖片,發現只有滾動條到達圖片內容區域時,纔會實時加載圖片,相對來講,它幫助減輕服務器負載。

合併壓縮資源

爲了合理的管理本身的代碼,咱們能夠將css和js文件打包並壓縮,這樣咱們能夠將全部的css或js壓縮成1個文件,文件體積也比原先小不少。

.Net Core已經提供了合併和壓縮資源的工具,咱們在程序包管理控制檯,安裝BundlerMinifier工具:Install-Package BundlerMinifier.Core -Version 2.6.362

安裝完成後,咱們配置根目錄下的bundleconfig.json,關於BundlerMinifier的使用方法和配置說明,參考官方文檔

 1 [
 2   {
 3     "outputFileName": "wwwroot/dist/bundles.min.css",
 4     // An array of relative input file paths. Globbing patterns supported
 5     "inputFiles": [
 6       "wwwroot/css/skins/_all-skins.css",
 7       "wwwroot/css/adminlte.css",
 8       "wwwroot/css/site.css"
 9     ]
10   },
11   {
12     "outputFileName": "wwwroot/dist/bundles.min.js",
13     "inputFiles": [
14       "wwwroot/js/adminlte.js",
15       "wwwroot/js/site.js",
16       "wwwroot/App/**/*.js"
17     ],
18     // Optionally specify minification options
19     "minify": {
20       "enabled": true,
21       "renameLocals": true
22     },
23     // Optionally generate .map file
24     "sourceMap": false
25   }
26 ]

編譯代碼,能夠看到會自動將配置的全部css和js打包:

這裏注意一個小細節,angular依賴注入的js文件,必須嚴格按照標準格式來寫,不然的話,打包後會執行報錯。

咱們能夠設置在開發環境下,用原始的css/js資源,方便調試,發佈時用打包後的資源

 1 <environment names="Development">
 2     <script>window.environment = 'Development'</script>
 3     <script src="~/js/adminlte.js"></script>
 4     <script src="~/js/site.js"></script>
 5     <script src="~/App/Home/Home.js"></script>
 6     <script src="~/App/Home/Error.js"></script>
 7     <script src="~/App/Configuration/MenuManagement/MenuManagement.js"></script>
 8     <script src="~/App/Tools/ApiSimulator/ApiSimulator.js"></script>
 9     <script src="~/App/Tools/SiteAnalytics/SiteAnalytics.js"></script>
10 </environment>
11 <environment names="Production">
12     <script>window.environment = 'Production'</script>
13     <script src="~/dist/bundles.min.js" asp-append-version="true"></script>
14 </environment>

咱們再監測下網站傳輸,能夠看到,打包後的文件大小,較以前壓縮了不少,很大提升了傳輸效率。

 總結

 咱們最後看下優化後的效果,主頁在沒有緩存狀況下,2s內就完成了加載,實際用戶體驗良好。 咱們的單頁模式改造和性能優化,順利完成!

 

相關文章
相關標籤/搜索