1. 程式人生 > >今天你上班了嗎?來聊聊一個隱蔽了 5 年的BUG!

今天你上班了嗎?來聊聊一個隱蔽了 5 年的BUG!

前言

今天,我們要揭曉一個 FineUI 隱藏最深的一個BUG,這個問題從 2014-07-30 釋出 FineUIPro v1.0.0 就一直存在,直到最新於 2020-01-10 釋出的 v6.1.1 版本依然存在,之所以一直沒有被提上臺面,是因為這個BUG的重現場景比較少,特別是現在網路速度越來越快的情況下。

 

提出問題

這個問題分別由中山市的一個企業客戶和美國的一個企業客戶獨立發現,我們先來看下中山市客戶的提問:

發現一個小問題,就是當我開啟一個有表格的頁面時,表格還沒載入完成,我切換別的頁面,當有表格的頁面載入完成後,我再切回去,表格就佈局失敗了,擠在一起。

這位客戶還特意做了一個GIF圖片,演示遇到的問題:

 

再來看下美國一個企業客戶的提問:

I wanted to also ask you about an issue with timer. The problem appears when you open a page with a timer which reloads a grid every 10 seconds, and you navigate to another page. If navigating to another page and meanwhile the timer reloads the grid behind the scenes, when going back to the initial page (the one with the timer), the page is not reloaded entirely (the grid is missing).

這位客戶的提問也非常專業,甚至做了一個可重現問題的示例,感興趣的可以自己嘗試下:

 

 

重現問題

由於這個美國客戶給出了可重現問題的示例,我們就來仔細分析一下,這個示例有 3 個檔案:

把這些頁面放到官網示例原始碼的 test 目錄下,開啟 /test/grid_timer.aspx 頁面:

 

然後,點選 Add new tab to parent page 按鈕,會新開一個選項卡,頁面效果如下:

 

在這個頁面停留 10 秒,然後返回第一個頁面,此時的頁面效果如下所示:

 

很明顯,此時表格的寬度不對了,因為這個頁面處於隱藏狀態時更新了表格資料,因此這個問題可能是由於頁面隱藏時寬度計算不對造成的。

 

在著手分析問題之前,先對照上面的頁面效果看下 grid_timer.aspx 頁面的程式碼邏輯:

<f:Grid ID="Grid1" IsFluid="true" CssClass="blockpanel" ShowBorder="true" ShowHeader="true" Title="Grid"
    runat="server" DataKeyNames="Id,Name" DataIDField="Id" EnableCheckBoxSelect="false">
    <Columns>
        <f:RenderField Width="140px" DataField="Id" ColumnID="Id" HeaderText="Id" SortField="Id" />
        <f:RenderField Width="140px" DataField="Name" ColumnID="Name" HeaderText="Name" ExpandUnusedSpace="true" />
        <f:RenderField Width="80px" DataField="EntranceYear" ColumnID="EntranceYear" HeaderText="Entrance year" />
        <f:CheckBoxField Width="80px" RenderAsStaticField="true" DataField="AtSchool" ColumnID="AtSchool" HeaderText="At school" />
    </Columns>
</f:Grid>

<f:Timer ID="timer1" Interval="10" Enabled="true" OnTick="Timer1_Tick" EnableAjaxLoading="false" runat="server" />

<f:Button runat="server" Text="Add new tab to parent page" OnClientClick="openHelloPage();" />

 

其中,openHelloPage 是一個自定義JS函式,用來新增一個新的選項卡(addExampleTab 是定義在外部框架頁面的一個JS函式,用來新增選項卡):

var basePath = '<%= ResolveUrl("~/") %>';

function openHelloPage() {
    parent.addExampleTab({
        id: 'hello_fineui_tab',
        iframeUrl: basePath + 'test/grid_timer_hello.aspx',
        title: 'New Page',
        refreshWhenExist: true
    });
}

 

這個頁面有一個 Timer 控制元件,會每隔 10 秒回發頁面,現在看下後臺的程式碼邏輯:

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        BindGrid();
    }
    else
    {
        if (Request.Form["__EVENTARGUMENT"] == "MyTimer")
        {
            BindGrid();
        }
    }

}
private void BindGrid()
{
    Grid1.DataUrl = "./grid_timer_handler.ashx";
    Grid1.DataBind();
}

protected void Timer1_Tick(object sender, EventArgs e)
{
    BindGrid();
}

可以看出,Timer控制元件會每隔 10 秒返回後臺,並重新對錶格進行資料繫結。

 

分析問題

經過測試,我們發現 iframe 中的頁面處於隱藏狀態時,此時獲取的頁面寬度為0,所以表格重新佈局時高度也為零。

 

為了解決這個問題,可以自定義timer,在頁面處於隱藏狀態時不更新表格資料,程式碼如下:

1. 自定義JS指令碼:

window.setInterval(function () {
        // Check if the current page is visible
        if ($('body').width()) {
            __doPostBack('', 'MyTimer');
        }
    }, 10000);

 

2. 後臺接受自定義回發:

protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                BindGrid();
            }
            else
            {
                if (Request.Form["__EVENTARGUMENT"] == "MyTimer")
                {
                    BindGrid();
                }
            }

        }

 

由於這個頁面邏輯其實還是蠻特殊的,所以我們通過上述邏輯是可以解決這個問題,我們也及時答覆了客戶。

 

顯然,這個答覆並沒有讓客戶滿意,隨後我們收到如下反饋:

 if there would be a solution to redraw the grid if we let the timer make the updates behind the scenes? Maybe if there is an event on the main TabStrip for changing the active tab, and if the active tab is the one with the timer, then set the width for the grid to the initial value?

 

其實使用者的訴求也很正常:希望表格處於隱藏狀態下也能得到更新,而在切換選項卡時重新設定表格的寬度。

 

這樣可行嗎?

顯然是不行的,我們不可能記錄所有控制元件的初始寬度,也不可能對某一兩個控制元件進行特殊處理。

 

那該怎麼辦,我們也陷入了深思......

 

解決問題

其實,明眼人都能看明白,最直接的解決辦法就是:切換選項卡時重新對其中的某些控制元件進行佈局。

問題的關鍵,如果知道哪些控制元件需要佈局呢?

哪些控制元件在頁面處於隱藏狀態時進行了無效的佈局操作呢?顯然這要從 FineUI 控制元件層面給出通用的解決辦法。

 

經過一番嘗試,我們給出瞭如下解決辦法:

1. 在控制元件基類 F.Component 的佈局操作中,攔截處於隱藏 IFrame 中控制元件的佈局操作:

doLayout: function () {
    
    if(F.util.insideIFrame() && !$('body').is(':visible')) {
        F.cmpsLayoutInHiddenIFrame = F.cmpsLayoutInHiddenIFrame || [];
        F.cmpsLayoutInHiddenIFrame.push(cmp.id);
        return;
    }
    
    // ...

}

 其中 F.cmpsLayoutInHiddenIFrame 用來記錄隱藏狀態下進行佈局的控制元件ID列表,以便在選項卡切換時重新佈局。

 

2. 容器基類中定義一個函式,用來執行重新佈局操作(redoLayoutInHiddenIFrame會遍歷F.cmpsLayoutInHiddenIFrame並執行佈局操作):

checkIFrameHiddenLayout: function() {
    var me = this;
    
    // 內部的iframe頁面已經載入完畢
    if(me.iframe && me.iframeLoaded) {
        var iframeWnd = me.getIFrameWindow();
        // 如果目標頁面不可訪問(跨域限制,或者目標頁面沒有引入FineUIPro),則不作處理
        if(F.util.canIFrameWindowAccessed(iframeWnd)) {
            iframeWnd.F.redoLayoutInHiddenIFrame();
        }
    }
}

 

3. 在啟用選項卡時,檢查是否有需要佈局的控制元件:

setActiveTab: function(tab) {

    if (tab.iframe) {
            
            if(!tab.iframeLoaded) {
                tab.setIFrameUrl(tab.iframeUrl);
            } else {
                tab.checkIFrameHiddenLayout();
            }
    }
    
    // ...
    
}

注意:上述程式碼都是 FineUI 內部使用的,這裡為了方便理解進行了改寫和簡化,並非實際使用的原始程式碼。

 

經過這個改造,上述客戶提出的兩個問題都能完美解決,請看下面兩個對比圖。

老版本:

新版本:

 

 

老版本:

 

新版本:

 

這樣就搞定了,慢著.....

 

 

重新思考 & 新的解決方案

雖然上面的思路非常直觀,程式碼實現也並不複雜,但是總有點打補丁的感覺,生怕哪天這個新打的補丁再破了。

我也一直在思考這個問題,為啥隱藏的IFrame頁面,裡面元素的寬度都計算不對?

 

有沒有讓隱藏狀態的 IFrame 行為表現的就像一直顯示的那樣?這樣,我們不需要這一堆補丁程式碼了,也就少了一個可能出錯的點。

答案還真有!

 

一般我們控制頁面上元素的顯示隱藏有 3 種方法:

1. display: none/block:最常用顯示隱藏元素的方法

2. visibility: hidden/visible:隱藏的元素還會佔據原來的位置,只不過不可見而已,不常用。

3. position: absolute;  top: -10000px; 通過將元素絕對定位,並遠遠的浮動到可見區域的外面,來實現元素的不可見,不常用。

 

而 FineUI 中一直用的就是第一種方法,也是最常用的做法:

 

 

而 display: none; 會導致其中的 IFrame 頁面的寬度計算不對。如果我們採用第三種方式,問題是不是就迎刃而解了呢?

答案是肯定的。

 

因為將元素浮動到可視區域外面,雖然我們看不到這個元素,但是本質上這個元素的各種行為應該和可見元素一模一樣!!

下個版本,我們會採用這個新的實現方式,來解決問題:

 

 

 

One more thing...

新的解決辦法更加簡潔,不僅減少了一堆補丁程式碼,而且還帶來一個意想不到的好處,那就是切換選項卡時,IFrame頁面的滾動條能保持位置了!!

老版本:

 

新版本:

 

道理也很簡單,新的隱藏方式只是讓元素距離可見區域遠一點,其實元素還是顯示的,所以之前的狀態都能保持。

 

是不是很酷!

 

 

官網示例已更新,現在就可以訪問了:

FineUIPro:https://pro.fineui.com/

FineUIMvc:https://mvc.fineui.com/

FineUICore:https://core.fineui.com/

FineUICore (Razor Pages & Tag Helpers):https://pages.fineui.com/

F.js:https://js.fineui.com/

&n