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內部資源暫時沒法預加載的問題。

或者索性不解決——畢竟按需加載也許纔是最好的選擇。

相關文章
相關標籤/搜索