前言
HI,有段時間沒有更新了,主要因為第一年前事情比較多,有些事得忙着張羅下;第二呢,對個人網站進行了一次大范圍的優化,主要是申請的雲服務器資源有限,1m的網絡帶寬,帶上圖片展示的話,打開網站的平均速度會達到20s以上,用戶的體驗不是很好;經過這次優化,已將訪問速度控制在1s左右,整體感覺速度和用戶體驗提升了不少。當然,最主要的是,把網站的模式改成了單頁應用模式,算得上是比較大的重構了,所以耽擱的時間比較久,請大家見諒。
那今天主要來說一下我是怎么重構之前的網站代碼,更新為單頁模式吧,順便分享下個人性能優化的經驗。
重構出發點和目標
之前.Net Core自動幫我們生成的網站代碼,主要是多頁的應用,我們定義頁面的時候,引入了_layout文件,相當視圖區域的模板頁,那通過控制器調整頁面的同時,會將整個頁面包括模板頁都會再次加載一遍。所以,之前的多頁應用,對應我的個人應用來說,有以下問題:
- 頁面導航跳轉時,需要重新加載整個頁面包含的css、js和html元素,雲服務器帶寬有限,重新加載資源有些浪費;
- 用戶體驗不好,在網速帶寬不夠的情況下,頁面刷新慢,用戶需要等待很長一段時間才能看到內容區域;
- 作為后台管理系統,頁面布局包含導航欄,頭部區域、尾部區域和內容區域,除了內容區域的視圖會經常變換,其他區域不會有大的變化,卻每次需要重新加載和刷新;
- 當初網站設計需要在移動端提供支持,多頁應用在移動設備中頁面切換的表現加載慢、不流暢、用戶體驗差;
現在,網頁設計整體趨向於單頁網站設計,但是這里我還是要強調一下,我從沒說過單頁模式就一定比多頁模式要好,只是這對我個人網站項目來說,單頁模式可能會更合適。其實2者都有很明顯的優缺點,多頁模式也有它的優勢,比如可以很方便的做SEO,單頁模式也有自己的劣勢,比如得單獨方案做SEO,而且開發難度相對多頁會復雜些。
所以說,一切拋開具體應用去談技術之間的好壞都是耍流氓!!!這里,我們的網站也不完全是單頁模式,比如登錄頁面跳轉等等用的還是多頁,所以只能說是以單頁模式為主的混合模式。
那既然我們決定要改造我們的網站為單頁應用,首先定一下優化的目標吧:
- 以簡單、容易和可操作的方式展示給用戶,提供用戶簡單的線性體驗;
- 整個頁面有固定的導航欄,頭部區域、尾部區域和變化的內容區域,實現頁面片段局部刷新;
- 公共資源首次加載后,頁面更新后不在重新加載;
- 選擇專門的UI框架,降低單頁模式的開發難度;
- 很好的響應式設計,支持移動端良好的用戶體驗;
引入UI路由框架
既然我們舍棄了多頁模式,就不能再用微軟提供的_layout模板頁取渲染視圖了,很方便的Razor引擎也沒法再用了(這里稍微解釋下,.Net Core之前版本的,是可以繼續用Razor來渲染數據模型的,但是.Net Core中,會將cshtml文件一起編譯成dll文件,所以cshtml文件在發布路徑中是找不到的,所以路由后的url是找不到的),那選用那種UI框架,來滿足單頁模式應用呢?
我們理一下思路:如圖,我們通過導航欄side的url地址,通過一種路由方式找到對應的html文件,然后后台加載數據,通過UI渲染,在內容區域content中顯示。這里可以看出,主要要滿足一是url的路由,二是數據模型的渲染。當然,滿足這樣需求的UI框架很多,我這里用的一種主流框架angular-ui-router(url路由,可以支持多樣化視圖和嵌入式視圖)+angular(數據模型和UI界面雙向綁定),大家也可以用別的方式,條條大道通羅馬嘛!
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內就完成了加載,實際用戶體驗良好。 我們的單頁模式改造和性能優化,順利完成!