当前位置:   article > 正文

Devexpress XAF的前端优化策略

dxr.axd

上次写博竟然是2011年。。。多少年沧海桑田,那我现在为什么要写?确实有点闲得发慌的嫌疑。

言归正传。

话说不管是对XAF框架,还是对各种前端技术,越了解就发现自己越不了解。所以其实本文更多的是一次写给自己看的备忘。当然如果有哪位同道偶然来到此间,愿意指点一二的,当然无比欢迎。

再次言归正传。我们要说的是性能提升。

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

而前端的必要优化,能够带来更加明显的性能提升体验。尤其是现在这个时代有很多系统不再是局限于员工们坐在办公室里电脑前能够带宽保证终端配置保证,而是有可能在任何地方用自己的手机操作,例如集成在企业微信里的应用系统。

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

其实对于XAF来说,由于其自成一体的前端框架体系,集成各种流行js框架会变得不是那么现实。官网上相关KB并不多,比如这篇有所涉及:

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

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

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

所以,我们动手吧?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内部资源暂时无法预加载的问题。

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

转载于:https://www.cnblogs.com/damnedmoon/p/7965475.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/135996
推荐阅读
相关标签
  

闽ICP备14008679号