Devexpress XAF的前端优化策略

上次写博居然是2011年。。。多少年沧海桑田,那我如今为何要写?确实有点闲得发慌的嫌疑。javascript

言归正传。css

话说无论是对XAF框架,仍是对各类前端技术,越了解就发现本身越不了解。因此其实本文更多的是一次写给本身看的备忘。固然若是有哪位同道偶然来到此间,愿意指点一二的,固然无比欢迎。html

再次言归正传。咱们要说的是性能提高。前端

这里打算单独把前端部分拎出来聊聊,是由于后端的优化技术在官网上各类解决方案仍是资料很丰富的。固然,后端的坑也是很多,好比对于XAF的新手而言最容易犯的错就是Controller中各类事件绑定后没有对应解绑致使内存泄漏。但总的来讲总能在官方KB中找到solution和best practice指引,因此再也不赘述。java

而前端的必要优化,可以带来更加明显的性能提高体验。尤为是如今这个时代有不少系统再也不是局限于员工们坐在办公室里电脑前可以带宽保证终端配置保证,而是有可能在任何地方用本身的手机操做,例如集成在企业微信里的应用系统。react

因此,若是有这样的应用场景,那么是值得研究研究基于XAF的Web系统如何前端优化。jquery

其实对于XAF来讲,因为其自成一体的前端框架体系,集成各类流行js框架会变得不是那么现实。官网上相关KB并很少,好比这篇有所涉及:webpack

https://www.devexpress.com/Support/Center/Question/Details/T514882/running-custom-scripts-reactjs-in-this-case-on-callback-in-xafgit

写于7个月前,官网的回答是“没什么好建议”。github

对于我等码农,也许所以能够不用紧跟风云变幻的各类前端框架,但现实就是,不追流行能够,但性能差仍是不能忍。

因此,咱们动手吧?Go!

Bundle & Minify

1. 本身的资源

本身的js, css,虽然可能和XAF相比是小儿科,但蚊子腿再小也是肉,并且也是最好处理的,能处理固然要处理。就从这里开始吧。

既然用的是XAF,高大上的webpack,grunt之流能够忽略了(反正我不会,哈哈),用土土的BundleConfig吧。这个应该有好多文章可查,好比能够参考这篇:http://blog.csdn.net/zhou44129879/article/details/16818987

另外,须要添加须要的dll引用。没玩过的本身VS建立一个Web项目而后抄吧。

BundleConfig类:

    public class BundleConfig
    {
        // 有关 Bundling 的详细信息,请访问 http://go.microsoft.com/fwlink/?LinkID=303951
        public static void RegisterBundles(BundleCollection bundles)
        {
            //不须要在这里指定静态cdn url。直接在ResourceFilter中替换成指向阿里云cdn连接
            //bundles.UseCdn = true;
            bundles.Add(new StyleBundle("~/bundles/paceTheme")
                .Include("~/Styles/pace/themes/blue/pace-theme-material.css"));
            bundles.Add(new StyleBundle("~/bundles/allCss")
                .Include("~/Styles/*.css"));
            bundles.Add(new ScriptBundle("~/bundles/HeadJs").Include(
                            "~/Scripts/qiniu.ui_video.js",
                            "~/Scripts/qiniu.jssdk.js",
                            "~/ckplayer/ckplayer.js",
                            "~/Scripts/d3.js",
                            "~/Scripts/index.js"));

            bundles.Add(new ScriptBundle("~/bundles/TailJs").Include(
                            "~/Scripts/NoSleep.min.js",
                            "~/Scripts/qiniu.uploader.main.js",
                            "~/Scripts/ycoms.voting.js",
                            "~/Scripts/ycoms.comments.js",
                            "~/Scripts/ycoms.ranking.js"));

        }
    }

以后在Application_Start中加入这个:

#if !DEBUG
            BundleTable.EnableOptimizations = true;
#endif
            BundleConfig.RegisterBundles(BundleTable.Bundles);

注意,对于WebForm项目而言,实测EnableOptimizations须要显式指定为true。

页面端Render示例。此处是css,js与此相似:

    <asp:PlaceHolder runat="server">
        <%: Styles.RenderFormat("<link href=\"{0}\" type=\"text/css\" rel=\"stylesheet\"/>", "~/bundles/allCss") %>
    </asp:PlaceHolder>

注意,这里用的是RenderFormat的方法。能够指定任意渲染模板。记住这个方法,特定场景之下,咱们后面会用来把它渲染成input hidden。

Web.config:

    <modules>
...
      <remove name="BundleModule" />
      <add name="BundleModule" type="System.Web.Optimization.BundleModule" />
    </modules>

 

最终,可以发现本身的脚本和css都被捆绑为一个文件,并已经作了minify处理:

 

2. XAF的资源

 XAF自身已经提供了相应选项,这个你们应该都很清楚。这里想提一提的是其中的enableResourceMerging。

  <devExpress>
...
    <compression enableHtmlCompression="true" enableCallbackCompression="true" enableResourceCompression="true" enableResourceMerging="false" />
...
  </devExpress>

 

这个选项,至少可以完成捆绑的功能。至因而否minify,由XAF自行判断,好比上面截图中第一个高达681K的脚本资源(版本17.1.7)就是Minify过的。

可是,是否捆绑,这是一个见仁见智的问题。以前作过一个测试,比较捆绑与否的请求状况。

首先,enableResourceMerging = "false":

Login.aspx:

 

以后登陆,访问Default.aspx:

 

以后,enableResourceMerging = "true":

Login.aspx:

 

以后登陆,访问Default.aspx:

 

比较一下,能够发现,请求数固然是为true的时候大大减小,但同时,仔细研究发现,XAF会根据当前页面的须要动态捆绑须要的js。也就是说,针对Login.aspx页面捆绑的js,对于Default.aspx页面来讲不必定适用,哪怕其中有大部分js内容多是重复的,但XAF也会从新捆绑一次,致使浏览器重复加载。这从访问Default.aspx页面先后两次相差近1M的数据量就能看出来。

因此,弊利之间的取舍,就见仁见智了。我目前是设置为false。

 CDN

 可是,如上图所示,哪怕不捆绑,登陆进来总共也须要加载2M的内容,若是不仅是内网访问,CDN貌似是必须的。而若是是内网访问也想从这方面提高性能,下降对服务器的压力,则能够用反向代理。

因为个人场景是外部访问更多,并且手持设备众多,因此各类终端和网络情况不可预计,CDN必须有。

可是,本身的资源还好办,占大头的XAF自带资源怎样也经过CDN访问呢?

首先,CDN应该有回源机制。我没有怎么充分调研过,应该都有吧?回源机制就是可以配置一个回源IP(好比10.10.10.10),而CDN绑定一个域名(好比cdn.mydomain.com),这样,有一个请求如http://cdn.mydomain.com/js/myscript.js会先到cdn去请求,若是cdn发现没有缓存该资源,或者已通过期,会自动请求http://10.10.10.10/js/myscript.js并将结果返回。下次再有请求就直接从缓存中返回。

其次,CDN应该能支持GZIP。要知道以前那个6百多K的脚本是压缩后为6百多K,没压缩的话是2.6M。

没怎么挑,这里选择了阿里云CDN。除了知足上面两条以外,本身的服务器也在上面,这样直接能开通CDN不用审查。

 

那么,如今的问题变为,怎么把XAF页面中的形如 /DXR.axd?XXX……的引用指定为http://cdn.mydomain.com/DXR.axd?XXX……?

例如,一个没有处理过的XAF页面多是这样的:

 

没错,用Response.Filter。

 

    /// <summary>
    /// 把资源连接替换成指向阿里云cdn的连接
    /// </summary>
    public class ResourceFilter : Stream
    {
        Stream responseStream;
        long position;
        StringBuilder responseHtml;

        public ResourceFilter(Stream inputStream)
        {
            responseStream = inputStream;
            responseHtml = new StringBuilder();
        }

        #region Filter overrides
        public override bool CanRead
        {
            get { return true; }
        }

        public override bool CanSeek
        {
            get { return true; }
        }

        public override bool CanWrite
        {
            get { return true; }
        }

        public override void Close()
        {
            responseStream.Close();
        }

        public override void Flush()
        {
            responseStream.Flush();
        }

        public override long Length
        {
            get { return 0; }
        }

        public override long Position
        {
            get { return position; }
            set { position = value; }
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            return responseStream.Seek(offset, origin);
        }

        public override void SetLength(long length)
        {
            responseStream.SetLength(length);
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            return responseStream.Read(buffer, offset, count);
        }
        #endregion

        bool? isHtml = null;

        #region Dirty work
        public override void Write(byte[] buffer, int offset, int count)
        {
            string strBuffer = System.Text.UTF8Encoding.UTF8.GetString(buffer, offset, count);

            if (isHtml == null)
            {
                //第一次解析。首先判断文件头有没有html标签。
                Regex sof = new Regex("<html>", RegexOptions.IgnoreCase);
                if (sof.IsMatch(strBuffer))
                {
                    isHtml = true;
                }
                else
                    isHtml = false;
            }

            if (!isHtml.Value)
            {
                //若是不是html,直接处理后返回。
                responseStream.Write(buffer, offset, count);
                return;
            }

            //不然
            // ---------------------------------
            // Wait for the closing </html> tag
            // ---------------------------------
            Regex eof = new Regex("</html>", RegexOptions.IgnoreCase);
            
            if (!eof.IsMatch(strBuffer))
            {
                responseHtml.Append(strBuffer);
            }
            else
            {
                responseHtml.Append(strBuffer);
                string finalHtml = responseHtml.ToString();

                //here's where you'd manipulate the response.
                finalHtml = finalHtml.Replace("/DXR.axd?",
                   "http://cdn.mydomain.com/DXR.axd?")
                   .Replace("DXX.axd?",
                   "http://cdn.mydomain.com/DXX.axd?")
                   .Replace("/bundles/",
                   "http://cdn.mydomain.com/bundles/")
                   ;

                byte[] data = Encoding.UTF8.GetBytes(finalHtml);

                responseStream.Write(data, 0, data.Length);
            }
        }
        #endregion
    }

 

 在一个HttpModule中注册:

    public class YCHttpModule : IHttpModule
    {
        public void Dispose()
        {
        }

        public void Init(HttpApplication context)
        {
            context.PostAuthenticateRequest += Context_PostAuthenticateRequest;
#if !DEBUG
            context.ReleaseRequestState += Context_ReleaseRequestState;
#endif
        }

        private void Context_ReleaseRequestState(object sender, EventArgs e)
        {
#if !DEBUG
            HttpResponse response = HttpContext.Current.Response;

            if (response.ContentType == "text/html")
                response.Filter = new ResourceFilter(response.Filter);
#endif
        }
...

 

关于Response.Filter的应用网上资料不少,这里很少说了。效果就是最终全部须要CDN访问的资源均可以经过替换成特定的url(cdn.mydomain.com/xxx)来走cdn线路。包括咱们以前用bundle捆绑的本身的资源。这就是为何以前的捆绑代码并无指定useCDN的缘由。

预加载

有了CDN,发现页面的载入时间从平均十几秒缩短到了4秒之内,好开心。但,一旦网络情况很差,再CDN也是白搭。怎么办?

能作的都作了,但用户有可能在烂网络下,好比3G下访问,或者信号很差——那这个时候只能从提高用户体验入手了。

如何不着痕迹地预加载也是一门学问(至少对我来讲),能够参看这篇:

http://www.jianshu.com/p/ba9759384ecf?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

但因为XAF Web应用的特殊性,咱们并不对全部的js都有任意处置的能力(固然,更广泛的状况是大多数脚本具体干吗的都不甚了了。。。),因此想用webpack, promise等等估计有难度。

Pace.js

Pace.js能自动显示当前页面加载进度,并且有各类不一样的样式选择,原本是想直接用它就搞定的。但发如今网络很差的状况下效果很差,由于我没法保证pace.js下载后才下载其余资源文件,哪怕我把pace.js都直接放在<head>以前了(由于XAF会在<head>下就插入资源文件连接),这样的话常常页面都花了很久加载得差很少了Pace的效果才显示出来。而我要解决的偏偏是网络很差时候的问题。

可是Pace仍是颇有用的,如今关键是要让Pace的效果出来以前浏览器里显示的不是大白页。

Link Preload

这里,就考虑须要有一个轻量级的初始页面,可以迅速显示内容,顺便作些资源的加载就更好了。那么,预加载须要用到什么技术呢?

参见下文:
Preloading content with rel="preload"

来自 <https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content>

动态加载,但不执行(关于Preload, 你应该知道些什么?)

来自 <http://www.jianshu.com/p/24ffa6d45087>

 link加preload能够只加载资源但不执行,这对与js资源来讲特别有意义。

咱们这里能够用脚原本添加link,如上文中的代码所示:

var preloadLink = document.createElement("link");
preloadLink.href = "myscript.js";
preloadLink.rel = "preload";
preloadLink.as = "script";
document.head.appendChild(preloadLink);

 

之因此用脚本,是由于能够在页面加载完毕后才动态添加link,这样用来显示进度的function和element都已经ready。能够确保显示加载进度。
既然要显示进度,咱们须要响应其加载完毕事件。值得庆幸的是,它有onload事件。参见下文:

https://www.w3cplus.com/performance/reloading/preload-prefetch-and-priorities-in-chrome.html

可是——

通过第一次测试,遗憾地发现只有微信浏览器(安卓X5)支持,IOS和windows微信浏览器都不支持。普通浏览器方面,chrome支持,其余没测。

不支持怎么办?

最粗糙的作法,不支持就直接跳转到根页面呗,放弃此功能。但关键是最须要支持的手机浏览器中iphone不行。是iphone不行,这样岂不是会被人诟病是屌丝应用?

那么只能换个思路。这里最关键的是要避免脚本被执行,由于咱们这个页面是纯加载的页面,不想去雕琢执行顺序和依赖关系,维护很长的一个XAF脚本列表已经够头疼了。那么,是否是能够考虑下文的这个hack:

Here's what is, in my opinion, a better solution for this issue that uses the IMG tag and its onerror event. This method will do the job without looping, doing contorted style observance, or loading files in iframes, etc. This solution fires correctly when the file is loads, and right away if the file is already cached (which is ironically better than how most DOM load events handle cached assets). Here's a post on my blog that explains the method - Back Alley Coder post - I just got tired of this not having a legit solution, enjoy!

var loadCSS = function(url, callback){ var link = document.createElement('link'); link.type = 'text/css'; link.rel = 'stylesheet'; link.href = url;
document.getElementsByTagName('head')[0].appendChild(link); var img = document.createElement('img'); img.onerror = function(){ if(callback) callback(link); } img.src = url; }

来自 <https://stackoverflow.com/questions/2635814/javascript-capturing-load-event-on-link> 

咱们能够把js的link传给img的src,而后监听onerror事件。通过测试,思路是正确的,可是浏览器缓存的行为会变得不那么可控。好比最大的68xk的那个文件居然没有缓存成功,后续页面依然再次加载。

再通过第二次测试,发现XAF本身的资源url会变化。例如,形如"/DXR.axd?r=24_359-k45Jf"的url,"k45jf"是一个周期性变化的部分。在明白其变化机制或者创建动态监测机制以前,没法预加载该类资源。

 因此就简单了,按照类型建立对应element,再也不搞弯弯绕绕的花活。哪怕js报错,但对于手机浏览器(尤为是微信浏览器)的用户来讲,这些是不可见的,不影响效果。

而因为XAF内部资源暂时没法在此处预加载,而这些又才是大头,这个页面,如前文所说,最重要的做用就仅仅是在Pace起做用前避免给用户”大白页“。

思路明确,接下来就是写启动页了:

Starter Page

对于XAF来讲,参考:

https://www.devexpress.com/Support/Center/Question/Details/T541642/html-start-page-for-xaf-web

而这个页面内容,是这样的:

 

<%@ Page Language="C#" Async="true" AutoEventWireup="true" Inherits="Start" EnableViewState="false" CodeBehind="Start.aspx.cs" %>

<!DOCTYPE html>
<html>
<head id="Head1" runat="server">
    <asp:PlaceHolder runat="server">
        <%: Styles.RenderFormat("<link href=\"{0}\" type=\"text/css\" rel=\"stylesheet\"/>", "~/bundles/paceTheme") %>
    </asp:PlaceHolder>
    <asp:PlaceHolder runat="server">
        <%: Styles.RenderFormat("<input class=\"allCss\" type=\"hidden\" value=\"{0}\" />", "~/bundles/allCss") %>
    </asp:PlaceHolder>
    <asp:PlaceHolder runat="server">
        <%: Scripts.RenderFormat("<input class=\"HeadJs\" type=\"hidden\" value=\"{0}\" />", "~/bundles/HeadJs") %>
    </asp:PlaceHolder>
    <asp:PlaceHolder runat="server">
        <%: Scripts.RenderFormat("<input class=\"TailJs\" type=\"hidden\" value=\"{0}\" />", "~/bundles/TailJs") %>
    </asp:PlaceHolder>
    <title>艺超教学系统</title>
    <link rel="shortcut icon" href="/favicon.ico" />
    <link rel="bookmark" href="/favicon.ico" />
    <style type="text/css">
        #infoPanel {
            position: absolute;
            top: 50%;
            left: 50%;
            margin: -100px 0 0 -230px;
            width: 460px;
            height: 200px;
            z-index: 99;
            text-align: center;
            color: darkgreen;
            font-size: 40px;
        }

        #imgLogo {
            position: absolute;
            left: 50%;
            margin: 250px 0 0 -130px;
            z-index: 99;
            text-align: center;
            color: darkgreen;
            font-size: 40px;
        }
    </style>
</head>
<body class="Dialog">
    <form id="form2" runat="server">
        <asp:HiddenField runat="server" ID="hidCdnUrl" ClientIDMode="Static" />
    </form>
    <img id="imgLogo" src="/Images/Logo260.png" />
    <div id="infoPanel">
        <span id="text">系统加载中</span>
        <span id="percentage" style="color: black; font-size: 60px"></span>
    </div>
    <%--This part is added.--%>
    <script type="text/javascript">
        function showText(txt) {
            document.getElementById('percentage').innerText = txt;
        }

        var csses = [
            "https://cdn.bootcss.com/font-awesome/4.3.0/css/font-awesome.min.css"
        ];
        var jses = [
            "http://cdn.staticfile.org/plupload/2.1.9/plupload.min.js",
            "https://cdn.bootcss.com/font-awesome/4.3.0/css/font-awesome.min.css",
            "http://res.wx.qq.com/open/js/jweixin-1.2.0.js",
            "Scripts/jquery.signalR-2.2.1.min.js",
            "http://cdn.staticfile.org/plupload/2.1.9/moxie.min.js",
            "/scripts/ycoms.comments.simple.js"
        ];

        var imgs = [
            "/Images/Logo.png",
            "/images/progressPieceYellow.png"
        ];

        var allCss = document.getElementsByClassName("allCss");
        for (var i = 0; i < allCss.length; i++) {
            csses.push(allCss[i].value);
        }
        var headJs = document.getElementsByClassName("HeadJs");
        for (var i = 0; i < headJs.length; i++) {
            jses.push(headJs[i].value);
        }
        var tailJs = document.getElementsByClassName("TailJs");
        for (var i = 0; i < tailJs.length; i++) {
            jses.push(tailJs[i].value);
        }

        var total = csses.length + jses.length + imgs.length;
        var currentCount = 0;

        var timer = setTimeout(function () {
            document.getElementById('text').innerText = "好像网络有点不给力,咱们在努力加载,请耐心等等……";
        }, 10000);

        var handler = function (e) {
            currentCount++;
            if (currentCount == total) {
                clearTimeout(timer);
                document.getElementById('text').innerText = "请稍候,系统启动中";
                document.getElementById('percentage').style.display = "none";
                window.location.href = "/";
            }
            var percent = parseInt(currentCount * 100 / total);
            showText(percent + "%");
        };
        var cdnUrl = document.getElementById('hidCdnUrl').value;
        var loadThem = function (arr, type) {
            for (var i = 0; i < arr.length; i++) {
                var url = arr[i];
                if (cdnUrl && !url.startsWith('http', 0)) {
                    if (!url.startsWith('/'))
                        url = '/' + url;
                    //记得确保cdnUrl不以/结尾
                    url = cdnUrl + url;
                }
                var preloadLink;
                if (type == "stylesheet") {
                    preloadLink = document.createElement("link");
                    preloadLink.rel = type;
                    preloadLink.href = url;
                }
                else if (type == "text/javascript") {
                    preloadLink = document.createElement("script");
                    preloadLink.src = url;
                }
                else if (type == "image") {
                    preloadLink = document.createElement("img");
                    preloadLink.src = url;
                    preloadLink.style.display = "none";
                }
                preloadLink.onload = handler;
                document.body.appendChild(preloadLink);
            }
        };

        loadThem(csses, "stylesheet");
        loadThem(jses, "text/javascript");
        loadThem(imgs, "image");

    </script>
</body>
</html>

 

这段代码首先注意一下,咱们在Bundle Render的时候用了RenderFormat的方法。前文有提到过,这个方法的强大之处在于能Render成任意文本,咱们这里就是把它们Render成了对应的input type=hidden的element。这样就能够在js中动态获取其值再动态加载。

另外,Render的时候没有给hidden元素id,而是经过class来找到他们,这是由于在非bundle模式下(debug状态下,我以前的代码设置为禁用捆绑),系统会原样Render出来多个文件而不是单个,因此用classname能获取多个元素。

最后,固然就是没有用jquery...显而易见,这个页面就是用来加载资源的,自身的内容越少越好,因此用的是原生js。

这样,Start页面可以迅速呈现内容,并在跳转到default页面(系统经过微信访问是自动登陆,固然这是另外一个话题)以后,在Pace出效果以前可以在浏览器中一直显示,从而完成了和pace的良好衔接。

固然,预加载若是能作得充分些就完美了。

相关工具

开发环境网络太好。。。须要模拟糟糕环境。这个工具不错:

clumsy 0.2

来自 <http://jagt.github.io/clumsy/cn/index.html>

遗留问题

 解决XAF内部资源暂时没法预加载的问题。

或者索性不解决——毕竟按需加载也许才是最好的选择。

相关文章
相关标签/搜索