今天你上班了吗?来聊聊一个隐蔽了 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/

你可能感兴趣的:(今天你上班了吗?来聊聊一个隐蔽了 5 年的BUG!)