Prototype methods missing in IE via SignalR

2019-06-27 13:27发布

问题:

I've run into a problem dealing with prototype methods disappearing (in this case Array.prototype methods) only in IE and only when the array is coming through SignalR.

I wrote a small/stupid but simple proof of concept web app that demonstrates this problem (code is all below). Notice that when you click "Update all clients" and then "Fruits containing the letter 'r'" the prototype methods in _list are missing causing an exception. In that case the array came from SignalR. Now when you click "Reset" and it resets the array to the hard-coded value the "Fruits containing the letter 'r'" button suddenly works - the prototype methods are back. Remember, this problem only occurs in IE.

HINT: When I first wrote the proof of concept I couldn't reproduce the issue. IE still had the prototype methods when the array came via SignalR but I did have another error when the page loaded. I was accidentally including jQuery twice. When I took out the redundant script to include the second jQuery it fixed that error (obviously) but now the problem could be reproduced. IE was then missing the Array prototype methods I created but only when the array comes via SignalR.

myExtensions.js:

Array.prototype.where = function (del)
{
    var ret = new Array();
    for (var i = 0; i < this.length; i++)
    {
        if (del(this[i])) ret.push(this[i]);
    }
    return ret;
}

Array.prototype.select = function (del)
{
    var ret = new Array();
    for (var i = 0; i < this.length; i++)
    {
        ret.push(del(this[i]));
    }
    return ret;
}

_Layout.cshtml

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>@ViewBag.Title - My ASP.NET MVC Application</title>
        <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
        <meta name="viewport" content="width=device-width" />

        @Styles.Render("~/Content/css")

        @Scripts.Render("~/Scripts/myExtensions.js")
        @Scripts.Render("~/bundles/modernizr")
        @Scripts.Render("~/Scripts/jquery-1.7.1.js")
        @Scripts.Render("~/Scripts/jquery.signalR-1.0.0-rc1.js")
        <script src="~/signalr/hubs"></script>

    </head>
    <body>
        <header>
            <div class="content-wrapper">
                <div class="float-left">
                    <p class="site-title">@Html.ActionLink("IE/SignalR error POC", "Index", "Home")</p>
                </div>
            </div>
        </header>
        <div id="body">
            @RenderSection("featured", required: false)
            <section class="content-wrapper main-content clear-fix">
                @RenderBody()
            </section>
        </div>
        <footer>
            <div class="content-wrapper">
                <div class="float-left">
                    <p>&copy; @DateTime.Now.Year - My ASP.NET MVC Application</p>
                </div>
            </div>
        </footer>
        @*@Scripts.Render("~/Scripts/myExtensions.js")*@
    </body>
</html>

ListHub.cs

using System.Linq;
using Microsoft.AspNet.SignalR.Hubs;

namespace SignalR_Bug_POC.Hubs
{
    public class ListHub : Hub
    {
        public void RunTest()
        {
            Clients.All.updateList(new string[]
                {
                    "apple", "pear", "grape", "strawberry", "rasberry", "orange", "watermelon"
                }.Select(f => new { Name = f }).ToList());
        }
    }
}

Index.cshtml

@{
    ViewBag.Title = "Home Page";
}

@if(false)
{
    @Scripts.Render("~/Scripts/jquery.signalR-1.0.0-rc1.js")
    @Scripts.Render("~/Scripts/myExtensions.js")
    <script src="~/signalr/hubs"></script>
}

<script type="text/javascript">

    var _fruits = ["blueberry", "grape", "orange", "strawberry"].select(function (f) { return { "Name": f } });
    var _list;

    var conn = $.connection.listHub;
    $.connection.hub.start();

    conn.client.updateList = function (data)
    {
        _list = data;
        $("#theList").html("");
        for (var i = 0; i < _list.length; i++)
        {
            $("#theList").append("<li>" + _list[i].Name + "</li>");
        }
    }

    $(document).ready(function ()
    {
        $("#cmdUpdateClients").click(function ()
        {
            conn.server.runTest();
        });
        $("#cmdReset").click(function ()
        {
            conn.client.updateList(_fruits);
        });
        $("#cmdRunTest").click(function ()
        {
            var message = "";
            var fruitsContaining = _list
                .where(function (f) { return f.Name.indexOf('r') >= 0 })
                .select(function (f) { return f.Name });
            for (var i = 0; i < fruitsContaining.length; i++)
            {
                message += " - " + fruitsContaining[i] + "\n";
            }
            alert(message);
        });
        conn.client.updateList(_fruits);
    });


</script>

<input type="button" id="cmdUpdateClients" value="Update All Clients" />
<input type="button" id="cmdReset" value="Reset" />
<input type="button" id="cmdRunTest" value="Fruits containing the letter r." />
<ul id="theList"></ul>

I'm not sure if it's something I'm doing wrong in the code (i.e. something I'm doing in the wrong order) or if it's an IE bug or a SignalR bug. When I set a breakpoint for instance on the first line of the conn.client.updateList JS method and track the call stack up to the very top and see that even there (in the SignalR receive method) arrays in the 'data' object don't have my prototype methods.

回答1:

With the following modification, it works just fine for me:

var fruitsContaining = _list
    .where(function (f) { return f.indexOf('r') >= 0 })
    .select(function (f) { return f});

The where and select were actually there, it was f.Name that wasn't, as the array members are strings.

Update:

Okay, ignore the above. Here's the fix:

var list = list
if (navigator.appName == 'Microsoft Internet Explorer') {
    list = Array.prototype.slice.call(_list);
}
var fruitsContaining = list
    .where(function (f) { return f.Name.indexOf('r') >= 0 })
    .select(function (f) { return f.Name });
for (var i = 0; i < fruitsContaining.length; i++) {
    message += " - " + fruitsContaining[i] + "\n";
}

I don't entirely understand the issue, but I believe it may be a bug in jquery. Though the question is a bit different, I stole the solution from this question: Why does this change to the Array prototype not work in my jQuery plugin?

Additional Update

Added the IE check.



回答2:

I encountered the same problem: when I used SignalR to pass arrays from C# to an Angular app, I couldn't use methods defined in Array.prototype on the received objects. Furthermore, the objects were indeed "array-like" in the sense that some of the array tests described here would fail. For example, arr instanceof Array would return false, but Array.isArray(arr) would return true.

The problem starts when the web application is hosted in IIS without WebSockets support. In this case, SignalR defaults in Chrome and Firefox to serverSentEvents, and in Internet Explorer and Edge to ForeverFrame.

As this question indicates, ForeverFrame is causing the arrays to be deserialized incorrectly. This is because ForeverFrame uses a different frame to maintain the SignalR connection, and arrays in different frames are created using different Array objects.

There are number of solutions here:

  1. If possible, enable WebSockets for IIS. This can be done starting from IIS 8 and Windows Server 2012.
  2. If WebSockets is not available, you can specify in the $.connection.hub.start() parameters that ForeverFrame should not be used, defaulting to LongPolling on IE and Edge.
  3. You can supply your own JSON parser to ForeverFrame, and call to window.JSON:

    $.connection.hub.json = {
        parse: function(text, reviver) {
            console.log("Parsing JSON");
            return window.JSON.parse(text, reviver);
        },
        stringify: function(value, replacer, space) {
            return window.JSON.stringify(value, replacer, space);
        }
    };
    
  4. And, as suggested in Pete's answer, you can call Array.prototype.slice on the received object, converting it into an Array of the same frame. This has to be done for any array received from SignalR, so it is not scalable like the other two options.