HI,有段时间没有更新了,主要由于第一年前事情比较多,有些事得忙着张罗下;第二呢,对我的网站进行了一次大范围的优化,主要是申请的云服务器资源有限,1m的网络带宽,带上图片展现的话,打开网站的平均速度会达到20s以上,用户的体验不是很好;通过此次优化,已将访问速度控制在1s左右,总体感受速度和用户体验提高了很多。固然,最主要的是,把网站的模式改为了单页应用模式,算得上是比较大的重构了,因此耽搁的时间比较久,请你们见谅。css
那今天主要来讲一下我是怎么重构以前的网站代码,更新为单页模式吧,顺便分享下我的性能优化的经验。html
以前.Net Core自动帮咱们生成的网站代码,主要是多页的应用,咱们定义页面的时候,引入了_layout文件,至关视图区域的模板页,那经过控制器调整页面的同时,会将整个页面包括模板页都会再次加载一遍。因此,以前的多页应用,对应个人我的应用来讲,有如下问题:前端
如今,网页设计总体趋向于单页网站设计,可是这里我仍是要强调一下,我从没说过单页模式就必定比多页模式要好,只是这对我我的网站项目来讲,单页模式可能会更合适。其实2者都有很明显的优缺点,多页模式也有它的优点,好比能够很方便的作SEO,单页模式也有本身的劣势,好比得单独方案作SEO,并且开发难度相对多页会复杂些。jquery
因此说,一切抛开具体应用去谈技术之间的好坏都是耍流氓!!!这里,咱们的网站也不彻底是单页模式,好比登陆页面跳转等等用的仍是多页,因此只能说是以单页模式为主的混合模式。angularjs
那既然咱们决定要改造咱们的网站为单页应用,首先定一下优化的目标吧:ajax
既然咱们舍弃了多页模式,就不能再用微软提供的_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的使用方法,若是须要了解更多的使用方法,请参考官网文档。缓存
首先,咱们引入如下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会自动帮咱们解析,对应作部分视图更新,咱们能够看到,如今整页只有内容区域部分刷新,其余区域再也不会被从新加载。
多页模式下,咱们的控制器方法返回的是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绑定到视图-->内容区域获取到绑定后的视图,在内容区域显示。
最终菜单管理页面效果:
咱们已经将网站改形成单页应用,用户体验已经有了很大的提升,可是仍是有许多值得优化的地方。因为云服务器提供的带宽很是有限,有时资源和图片下载稍微有点慢的状况,会致使整个体验会很是很差。固然,优化网站的手段很是多,实际状况每每比较复杂,那针对本人的网站,这里列出我本身这次实际过程当中采起的优化方法,跟你们分享下。
以前咱们访问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内就完成了加载,实际用户体验良好。 咱们的单页模式改造和性能优化,顺利完成!