admin管理员组

文章数量:1193565

I've been dissecting the following code snippet, which is used to asynchronously load the Segment.io analytics wrapper script:

// Create a queue, but don't obliterate an existing one!
var analytics = analytics || [];

// Define a method that will asynchronously load analytics.js from our CDN.
analytics.load = function(apiKey) {

    // Create an async script element for analytics.js.
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.async = true;
    script.src = ('https:' === document.location.protocol ? 'https://' : 'http://') +
                  'd2dq2ahtl5zl1z.cloudfront/analytics.js/v1/' + apiKey + '/analytics.min.js';

    // Find the first script element on the page and insert our script next to it.
    var firstScript = document.getElementsByTagName('script')[0];
    firstScript.parentNode.insertBefore(script, firstScript);

    // Define a factory that generates wrapper methods to push arrays of
    // arguments onto our `analytics` queue, where the first element of the arrays
    // is always the name of the analytics.js method itself (eg. `track`).
    var methodFactory = function (type) {
        return function () {
            analytics.push([type].concat(Array.prototype.slice.call(arguments, 0)));
        };
    };

    // Loop through analytics.js' methods and generate a wrapper method for each.
    var methods = ['identify', 'track', 'trackLink', 'trackForm', 'trackClick',
                   'trackSubmit', 'pageview', 'ab', 'alias', 'ready'];

    for (var i = 0; i < methods.length; i++) {
        analytics[methods[i]] = methodFactory(methods[i]);
    }
};

// Load analytics.js with your API key, which will automatically load all of the
// analytics integrations you've turned on for your account. Boosh!
analytics.load('MYAPIKEY');

It's well commented and I can see what it's doing, but I'm puzzled when it comes to the methodFactory function, which pushes details (method name and arguments) of any method calls made before the main analytics.js script has loaded onto the global analytics array.

This is all well and good, but then if/when the main script does load, it seemingly just overwrites the global analytics variable (see last line here), so all that data will be lost.

I see how this prevents script errors in a web page by stubbing out methods which don't exist yet, but I don't understand why the stubs can't just return an empty function:

var methods = ['identify', 'track', 'trackLink', 'trackForm', 'trackClick',
               'trackSubmit', 'pageview', 'ab', 'alias', 'ready'];

for (var i = 0; i < methods.length; i++) {
    lib[methods[i]] = function () { };
}

What am I missing? Please, help me understand!

I've been dissecting the following code snippet, which is used to asynchronously load the Segment.io analytics wrapper script:

// Create a queue, but don't obliterate an existing one!
var analytics = analytics || [];

// Define a method that will asynchronously load analytics.js from our CDN.
analytics.load = function(apiKey) {

    // Create an async script element for analytics.js.
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.async = true;
    script.src = ('https:' === document.location.protocol ? 'https://' : 'http://') +
                  'd2dq2ahtl5zl1z.cloudfront.net/analytics.js/v1/' + apiKey + '/analytics.min.js';

    // Find the first script element on the page and insert our script next to it.
    var firstScript = document.getElementsByTagName('script')[0];
    firstScript.parentNode.insertBefore(script, firstScript);

    // Define a factory that generates wrapper methods to push arrays of
    // arguments onto our `analytics` queue, where the first element of the arrays
    // is always the name of the analytics.js method itself (eg. `track`).
    var methodFactory = function (type) {
        return function () {
            analytics.push([type].concat(Array.prototype.slice.call(arguments, 0)));
        };
    };

    // Loop through analytics.js' methods and generate a wrapper method for each.
    var methods = ['identify', 'track', 'trackLink', 'trackForm', 'trackClick',
                   'trackSubmit', 'pageview', 'ab', 'alias', 'ready'];

    for (var i = 0; i < methods.length; i++) {
        analytics[methods[i]] = methodFactory(methods[i]);
    }
};

// Load analytics.js with your API key, which will automatically load all of the
// analytics integrations you've turned on for your account. Boosh!
analytics.load('MYAPIKEY');

It's well commented and I can see what it's doing, but I'm puzzled when it comes to the methodFactory function, which pushes details (method name and arguments) of any method calls made before the main analytics.js script has loaded onto the global analytics array.

This is all well and good, but then if/when the main script does load, it seemingly just overwrites the global analytics variable (see last line here), so all that data will be lost.

I see how this prevents script errors in a web page by stubbing out methods which don't exist yet, but I don't understand why the stubs can't just return an empty function:

var methods = ['identify', 'track', 'trackLink', 'trackForm', 'trackClick',
               'trackSubmit', 'pageview', 'ab', 'alias', 'ready'];

for (var i = 0; i < methods.length; i++) {
    lib[methods[i]] = function () { };
}

What am I missing? Please, help me understand!

Share Improve this question edited May 26, 2015 at 14:44 Matthieu Riegler 54.6k24 gold badges144 silver badges192 bronze badges asked Feb 13, 2013 at 17:11 Mark BellMark Bell 29.7k26 gold badges121 silver badges149 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 29

Ian here, co-founder at Segment.io—I didn't actually write that code, Calvin did, but I can fill you in on what it's doing.

You're right, the methodFactory is stubbing out the methods so that they are available before the script loads, which means people can call analytics.track without wrapping those calls in an if or ready() call.

But the methods are actually better than "dumb" stubs, in that they save the method that was called, so we can replay the actions later. That's this part:

analytics.push([type].concat(Array.prototype.slice.call(arguments, 0)));

To make that more readable:

var methodFactory = function (method) {
    return function () {
        var args = Array.prototype.slice.call(arguments, 0);
        var newArgs = [method].concat(args);
        analytics.push(newArgs);
    };
};

It tacks on the name of the method that was called, which means if I analytics.identify('userId'), our queue actually gets an array that looks like:

['identify', 'userId']

Then, when our library loads in, it unloads all of the queued calls and replays them into the real methods (that are now available) so that all of the data recorded before load is still preserved. That's the key part, because we don't want to just throw away any calls that happen before our library has the chance to load. That looks like this:

// Loop through the interim analytics queue and reapply the calls to their
// proper analytics.js method.
while (window.analytics.length > 0) {
    var item = window.analytics.shift();
    var method = item.shift();
    if (analytics[method]) analytics[method].apply(analytics, item);
}

analytics is a local variable at that point, and after we're done replaying, we replace the global with the local analytics (which is the real deal).

Hope that makes sense. We're actually going to have a series on our blog about all the little tricks for 3rd-party Javascript, so you might dig that soon!

Not very related to the question, but may be useful to those who googled for issue "segment not sends queued events".

In my code I assigned window.analytics to another variable at page loading stage:

let CLIENT = analytics;

Then I used this variable instead of using global analytics:

CLIENT.track();
CLIENT.page();
// etc

But I encountered a problem when sometimes events are sent, and sometimes nothing is being sent. That "sometimes" vary between page reloads. Sometimes it also could ignore all events that fire at page loading, and without page reloading start sending events that are binded after page loading.

Then I debugged and found that CLIENT holds all not sent events in queue. Obviously they were put using methodFactory(). Then I found this SO question. So that's what is happening I think:

CLIENT holds reference to stub analytics object, which calls this methodFactory(). After Segment is fully loaded it replaces window.analytics with actual code while CLIENT still holds reference to old window.analytics. That's why this "sometimes" happens: sometimes window.analytics was replaced by Segment before loading the main script which initializes this CLIENT, and sometimes main script loaded earlier than Segment script.

New code:

let CLIENT = undefined;

if (CLIENT) {
    CLIENT.page();
} else {
    window.analytics.page();
}

I need to have this CLIENT because I'm using same analytics code for web and mobile. On mobile this CLIENT will be initialized separately while on web window.analytics is always available.

本文标签: