Source: bigpipe.js

(function () {
    'use strict';

    var debug = false;

    var Utils = {};

    Utils.loadScript = function (url) {
        return new Promise(function (resolve) {
            var script = document.createElement('script');
            script.src = url;
            script.onload = function () {
                if (debug) {
                    console.log('script [' + url + '] loaded');
                }
                resolve();
            };
            var head = document.head || document.getElementsByTagName('head')[0];
            head.appendChild(script);
        });
    };

    Utils.loadStyle = function (url) {
        return new Promise(function (resolve) {
            var stylesheet = document.createElement('link');
            stylesheet.rel = 'stylesheet';
            stylesheet.href = url;
            stylesheet.onload = function () {
                if (debug) {
                    console.log('stylesheet [' + url + '] loaded');
                }
                resolve();
            };
            var head = document.head || document.getElementsByTagName('head')[0];
            head.appendChild(stylesheet);
        });
    };

    Utils.traversePagelet = function (pagelet, fun) {
        fun(pagelet);
        for (var childPageletKey in pagelet.children) {
            Utils.traversePagelet(pagelet.children[childPageletKey], fun);
        }
    };

    // 这里 unique 只需要满足 String 类型的数组元素即可
    // 所以采用了 hashmap 去重
    Utils.unique = function (array) {
        if (!(array instanceof Array)) {
            throw new TypeError(array + ' is not an array');
        }
        var result = [];
        var hashmap = {};
        for (var i = 0, length = array.length; i < length; i++) {
            var item = array[i];
            if (typeof item !== 'string') {
                throw new TypeError('array item ' + item + ' is not a string');
            }
            if (!hashmap[item]) {
                result.push(item);
                hashmap[item] = true;
            }
        }
        return result;
    };

    Utils.filter = function (array, func) {
        var result = [];
        for (var i = 0, length = array.length; i < length; i++) {
            var item = array[i];
            if (func(item, i)) {
                result.push(item);
            }
        }
        return result;
    };

    Utils.each = function (array, func) {
        for (var i = 0, length = array.length; i < length; i++) {
            var item = array[i];
            func(item, i);
        }
        return array;
    };


    /**
     * EventManager - 事件管理类
     *
     * @param  {*} context 对象上下文
     * @return {EventManager}
     */
    var EventManager = function (context) {
        this.context = context;
        this._eventPool = {};
    };

    EventManager.prototype = {
        /**
         * addEventListener - 事件绑定
         *
         * @param  {string} eventName 事件名,支持命名空间如:click.sign
         * @param  {function} handler 事件回调
         * @return {EventManager} this
         */
        addEventListener: function (eventName, handler) {
            var eventObj = this._getEventObject(eventName);
            if (!this._eventPool[eventObj.name]) {
                this._eventPool[eventObj.name] = {};
            }
            if (!this._eventPool[eventObj.name][eventObj.namespace]) {
                this._eventPool[eventObj.name][eventObj.namespace] = [];
            }
            this._eventPool[eventObj.name][eventObj.namespace].push(handler);
            return this;
        },

        /**
         * dispatchEvent - 事件触发
         *
         * @param  {string} eventName 事件名,支持命名空间如:click.sign
         * @param  {*} data 回调参数
         * @param  {*} context 回调上下文
         * @return {EventManager} this
         */
        dispatchEvent: function (eventName, data, context) {
            var callbacks = this._getHandlersByEventName(eventName);
            if (callbacks) {
                for (var i = 0, length = callbacks.length; i < length; i++) {
                    callbacks[i].call(context || this.context, data);
                }
            }
        },

        /**
         * removeEventListener - 事件解绑
         *
         * @param  {string} eventName 事件名,支持命名空间如:click.sign
         * @param  {function} handler 事件回调
         * @return {EventManager} this
         */
        removeEventListener: function (eventName, handler) {
            var eventObj = this._getEventObject(eventName);
            if (this._eventPool[eventObj.name] &&
                this._eventPool[eventObj.name][eventObj.namespace]) {
                if (typeof handler === 'function') {
                    var callbacks = this._eventPool[eventObj.name][eventObj.namespace];
                    var targetIndex;
                    while ((targetIndex = callbacks.indexOf(handler)) != -1) {
                        callbacks.splice(targetIndex, 1);
                    }
                } else {
                    delete this._eventPool[eventObj.name][eventObj.namespace];
                }
            }
            return this;
        },

        /**
         * _getEventObject - 获取事件对象
         *
         * @private
         * @param  {string} eventName 事件名,支持命名空间如:click.sign
         * @return {object} 事件对象,包含name和namespace
         */
        _getEventObject: function (eventName) {
            var arr = eventName.split('.');
            // 默认事件池名字是:__default
            return {
                name: arr[0],
                namespace: arr[1] || '__default'
            };
        },

        /**
         * _getHandlersByEventName - 获取回调函数
         *
         * @private
         * @param  {string} eventName 事件名,支持命名空间如:click.sign
         * @return {array} 回调函数数组
         */
        _getHandlersByEventName: function (eventName) {
            var eventObj = this._getEventObject(eventName);
            if (this._eventPool[eventObj.name]) {
                return this._eventPool[eventObj.name][eventObj.namespace];
            } else {
                return [];
            }
        }
    };

    var styleLoader = {
        _loaders: {}
    };

    styleLoader._getUrl = function (ids) {
        return ids;
    };

    styleLoader.load = function (ids) {
        if (!(ids instanceof Array)) {
            ids = [ids];
        }
        var urls = this._getUrl(ids);
        var loadStyles = [];
        var createLoadStyle = function (url) {
            var loader = styleLoader._loaders[url] || Utils.loadStyle(url);
            styleLoader._loaders[url] = loader;
            loadStyles.push(loader);
        };
        for (var i = 0, length = urls.length; i < length; i++) {
            var url = urls[i];
            createLoadStyle(url);
        }
        return Promise.all(loadStyles);
    };

    styleLoader.config = function (options) {
        if (typeof options !== 'object') {
            return;
        }
        if (typeof options.urlsGenerator === 'function') {
            styleLoader._getUrl = options.urlsGenerator;
        }
    };

    var scriptLoader = {
        _loaders: {}
    };
    scriptLoader._getUrl = function (ids) {
        return ids;
    };
    scriptLoader.load = function (ids) {
        if (!(ids instanceof Array)) {
            ids = [ids];
        }
        var urls = this._getUrl(ids);
        var loadScripts = [];
        var createLoadScript = function (url) {
            var loader = scriptLoader._loaders[url] || Utils.loadScript(url);
            scriptLoader._loaders[url] = loader;
            loadScripts.push(loader);
        };
        for (var i = 0, length = urls.length; i < length; i++) {
            var url = urls[i];
            createLoadScript(url);
        }
        return Promise.all(loadScripts);
    };
    scriptLoader.config = function (options) {
        if (typeof options !== 'object') {
            return;
        }
        if (typeof options.urlsGenerator === 'function') {
            scriptLoader._getUrl = options.urlsGenerator;
        }
    };

    /**
     * 内置资源管理器
     * @param resourceLoader 资源加载器
     * @constructor
     */
    var ResourceManager = function (resourceLoader) {
        var self = this;

        /**
         * 资源加载器
         */
        self.resourceLoader = resourceLoader;

        /**
         * 依赖资源表,未combo
         * @type {Array}
         */
        self.resources = [];

        /**
         * 本地资源表,不依赖外部combo,暂时未使用到,预留给unload机制使用
         * @type {Array}
         */
        self.resourcesLocal = [];

        /**
         * 依赖资源combo结果url,也包含外部依赖的,作为`this.resourceLoader.load`的参数
         * @type {Array}
         */
        self.resourcesComboed = [];
    };

    /**
     * url合并个数上限,超过上限则合并成新的url
     * @type {number}
     */
    ResourceManager.COMBO_SIZE = 25;

    /**
     * 单个资源与combo url的映射表
     * @type {{url: comboUrl}}
     */
    ResourceManager.resourceComboMap = {};

    /**
     * 资源loader池
     * @type {{comboUrl: {count: number, loader: (Promise|*)}}}
     */
    ResourceManager.resourcePool = {};

    /**
     * url合并
     * @param {array} urls
     * @param {number} size
     * @returns {Array}
     */
    ResourceManager.combo = function (urls, size) {
        // 后面对urls进行了splice,这里复制了一次以直接免影响传入值
        urls = urls.concat();
        var comboedUrls = [];
        size = size || ResourceManager.COMBO_SIZE;
        while (urls.length > 0) {
            comboedUrls.push(urls.splice(0, size).join(','));
        }
        return comboedUrls;
    };

    /**
     * 加载资源,返回Promise实例
     * @param ids
     * @returns {Promise}
     */
    ResourceManager.prototype.load = function (ids) {
        this._registerResources(ids);
        if (ids.length > 0) {
            return this.resourceLoader.load(this.resourcesComboed);
        } else {
            return Promise.resolve();
        }
    };

    /**
     * 注册资源,检查是否已合并
     * @param urls
     * @private
     */
    ResourceManager.prototype._registerResources = function (urls) {
        var self = this;
        var resourcesLocal = [];
        // 依赖资源combo结果url
        var resourcesComboed = [];

        // 过滤已combo资源,获取combo url
        resourcesLocal = Utils.filter(urls, function (url) {
            var urlCombed = ResourceManager.resourceComboMap[url];
            if (urlCombed) {
                if (resourcesComboed.indexOf(urlCombed) === -1) {
                    resourcesComboed.push(urlCombed);
                }
                return false;
            } else {
                return true;
            }
        });

        // combo尚未被combo过的资源
        var resourceCombedNew = ResourceManager.combo(resourcesLocal, ResourceManager.COMBO_SIZE);
        // 遍历本次注册中新combo的资源,构建映射
        Utils.each(resourcesLocal, function (url, i) {
            var part = Math.floor(i / ResourceManager.COMBO_SIZE);
            ResourceManager.resourceComboMap[url] = resourceCombedNew[part];
        });

        // 合并本实例所依赖的外部combo资源和内部新增combo资源
        resourcesComboed = resourcesComboed.concat(resourceCombedNew);

        self.resourcesLocal = self.resourcesLocal.concat(resourcesLocal);
        self.resourcesComboed = self.resourcesComboed.concat(resourcesComboed);
    };

    /**
     * 配置
     * @param {object} options
     */
    ResourceManager.config = function (options) {
        if ( typeof options.COMBO_SIZE === 'number') {
            ResourceManager.COMBO_SIZE = options.COMBO_SIZE;
        }
    };

    var pagelets = {};
    // pagelets who need a parent to appendTo
    // organized by parent id
    var orphanPagelets = {};

    var INITIALIZED = 0;
    var LOADED = 1;
    var OUTDATED = -1;

    var ROOT_PAGELET_ID = '__root';

    /**
     * Pagelet
     *
     * @class Pagelet
     * @param  {string} id
     * @param  {object} options description
     */
    function Pagelet(id, options) {
        if (typeof id !== 'string') {
            throw new TypeError('pagelet id must be a String.');
        }
        if (typeof options !== 'object') {
            options = {};
        }
        var self;
        if (pagelets[id]) {
            // 如果 pagelet 已存在
            self = pagelets[id];
            if (self.state !== OUTDATED) {
                // 如果这个 pagelet 不为 OUTDATED 状态,报异常
                throw new Error('pagelet[' + id + '] already loaded.');
            } else {

            }
        } else {
            //
            self = this;

            self.id = id;
            self.children = {};
            self.document = null;
            // 创建一个事件管理类实例维护本实例的事件
            self.eventManager = new EventManager(this);

            self.scripts = [];
            self.styles = [];

            if (id === ROOT_PAGELET_ID) {
                self.state = LOADED;
                pagelets[id] = self;
                return;
            }

            var parentId = options.parent;
            if (!parentId) {
                parentId = ROOT_PAGELET_ID;
            }
            self._parentId = parentId;

        }

        /**
         * @public
         * @type string
         */
        self.content = options.content;
        self.scripts = Utils.unique(self.scripts.concat(options.scripts || []));
        self.styles = Utils.unique(self.styles.concat(options.styles || []));

        self.scriptResourceManager = new ResourceManager(scriptLoader);
        self.styleResourceManager = new ResourceManager(styleLoader);

        self.state = INITIALIZED;

        self.promise = self._init();

        pagelets[id] = self;

        return self;
    }

    Pagelet.prototype = {
        _init: function () {
            var styleLoader = this.styleResourceManager.load(this.styles);
            var scriptLoader = this.scriptResourceManager.load(this.scripts);

            var mounter = new Promise(function (resolve) {
                this.resolveMounter = resolve;
                styleLoader.then(function () {
                    var parentId = this._parentId;
                    if (!(pagelets[parentId] && pagelets[parentId].state >= LOADED)) {
                        // parent not ready yet
                        if (orphanPagelets[parentId]) {
                            orphanPagelets[parentId].push(this);
                        } else {
                            orphanPagelets[parentId] = [this];
                        }
                    } else {
                        this.mount();
                    }
                    // 外部通过此Bigpipe事件給pagelet绑定事件,注意:this.mount()内会清空pagelet下的所有事件,
                    Bigpipe.dispatchEvent('beforepageletload', this);
                    // tell Bigpipe this pagelet's style is loaded
                    Bigpipe.dispatchEvent('pageletstyleloaded', this);
                }.bind(this));
            }.bind(this));

            return Promise.all([mounter, scriptLoader]).then(function () {
                // tell Bigpipe this pagelet's script is loaded
                Bigpipe.dispatchEvent('pageletscriptloaded', this);
            }.bind(this));
        },
        _setParent: function (id) {
            this.parent = pagelets[id];
            this.parent.children[this.id] = this;
            return this;
        },
        _getPlaceholder: function (document) {
            if (!document.getElementById) {
                document = window.document;
            }
            return document.getElementById('pagelet_' + this.id);
        },
        _getHTML: function () {
            if (typeof this.content !== 'undefined') {
                return this.content;
            }
            var htmlContainer = document.getElementById('pagelet_html_' + this.id);
            if (!htmlContainer) {
                return false;
            }
            var result = htmlContainer.innerHTML.trim().match(/^<!--([\s\S]*)-->$/);
            if (result) {
                return result[1];
            } else {
                return '';
            }
        },
        /**
         * mount - 挂载 pagelet
         *
         * @return {Pagelet}  self
         */

        mount: function () {
            // 当这次 mount 是在更新一个过时 pagelet 且时间戳 match
            if (1) {
                // 触发所有子 pagelet 析构
                this.broadcast('destroy');
                // 从 pagelets 中移除所有子 pagelet
                Utils.traversePagelet(this, function (pagelet) {
                    delete pagelets[pagelet.id];
                });
                this.children = {};
                // 自己还是要保留的
                pagelets[this.id] = this;
                // 移除自己身上所有事件,即创建一个新的事件管理类实例
                this.eventManager = new EventManager(this);
            }
            // 下面是正常的挂载逻辑
            this._setParent(this._parentId);
            this._appendTo(this.parent);
            if (orphanPagelets[this.id]) {
                var childrenPagelets = orphanPagelets[this.id];
                for (var i = childrenPagelets.length - 1; i >= 0; i--) {
                    childrenPagelets[i].mount();
                }
            }
            this.state = LOADED;
            delete this.timeStamp;
            this.resolveMounter();
            delete this.resolveMounter;
            return this;
        },
        /**
         * _appendTo - description
         *
         * @param  {Pagelet} pagelet 挂载到的 pagelet
         * @return {Pagelet}         self
         */
        _appendTo: function (pagelet) {
            if (!(pagelet instanceof Pagelet)) {
                throw new TypeError(pagelet + 'is not a pagelet.');
            }
            if (this.state >= LOADED) {
                throw new Error('pagelet[' + this.id + '] is already mounted');
            }
            var html = this._getHTML();
            if (html !== false) {
                if (!pagelet.document) {
                    throw new Error('Cannot append pagelet[' + this.id + '] to documentless pagelet[' + pagelet.id + ']');
                } else {
                    this.document = this._getPlaceholder(pagelet.document);
                    if (!this.document) {
                        throw new Error('Cannot find the placeholder for pagelet[' + this.id + ']');
                    }
                    this.document.innerHTML = html;
                    var htmlContainer = document.getElementById('pagelet_html_' + this.id);
                    if (htmlContainer) {
                        htmlContainer.parentNode.removeChild(htmlContainer);
                    }
                }
            }
            return this;
        },
        remove: function () {
            this.broadcast('destroy');
            this.document.innerHTML = '';
        },
        replaceWith: function (id, options) {

        },
        refresh: function (options) {
            if (typeof options !== 'object') {
                options = {};
            }

            this.broadcast('beforerefresh');

            var timeStamp = new Date().getTime();

            Utils.traversePagelet(this, function (pagelet) {
                pagelet.state = OUTDATED;
                pagelet.timeStamp = timeStamp;
            });

            var url;
            if (typeof options.url === 'string') {
                url = options.url;
            } else if (typeof options.url === 'function') {
                url = options.url(window.location.href);
            } else {
                url = window.location.href;
            }
            // 增加参数,得到单独渲染 pagelet 的 url
            var pageletId = this.id;
            url = URI(url)
                .addQuery('pagelets', pageletId)
                .addQuery('pagelets_stamp', timeStamp)
                .toString();

            return new Promise(function (resolve) {
                // 发请求
                var xhr = new XMLHttpRequest();
                xhr.onload = function () {
                    var results = [];
                    // 将返回内容中的 script 标签的内容提取出来执行
                    var dom = document.createElement('div');
                    dom.innerHTML = xhr.responseText;
                    for (var i = 0, length = dom.childNodes.length; i < length; i++) {
                        var el = dom.childNodes[i];
                        if (el.nodeName !== null && el.nodeName.toUpperCase() === 'SCRIPT' && !el.src) {
                            /*jslint evil: true */
                            results.push(eval(el.innerHTML));
                            /*jslint evil: false */
                        }
                    }
                    Promise.all(results).then(resolve);
                };
                xhr.open('GET', url, true);
                xhr.send();

            });
        },

        /**
         * on - pagelet事件监听,支持事件命名空间
         *
         * @param  {string} name    事件名,支持命名空间如: `click.sign`
         * @param  {function} handler 事件回调
         * @return {Pagelet}         this
         */
        on: function (name, handler) {
            this.eventManager.addEventListener(name, handler);
            return this;
        },

        /**
         * off - 解除事件绑定
         *
         * @param  {string} name  要解除绑定的事件名,支持命名空间
         * @param  {type} handler description
         * @return {type}         description
         */
        off: function (name, handler) {
            this.eventManager.removeEventListener(name, handler);
            return this;
        },
        /**
         * emit - 向上散发事件
         *
         * @param  {string} name 事件名,支持命名控件如: `click.sign`
         * @param  {*} data 回调参数
         * @return {Pagelet}      this
         */
        emit: function (name, data) {
            this.eventManager.dispatchEvent(name, data);
            if (this.parent) {
                this.parent.emit(name, data);
            }
            return this;
        },
        /**
         * broadcast - 向下广播事件
         *
         * @param  {string} name 事件名,支持命名控件如: `click.sign`
         * @param  {*} data 回调参数
         * @return {Pagelet}      this
         */
        broadcast: function (name, data) {
            this.eventManager.dispatchEvent(name, data);
            if (this.children) {
                for (var childPageletId in this.children) {
                    this.children[childPageletId].broadcast(name, data);
                }
            }
            return this;
        }
    };

    var __rootPagelet = new Pagelet(ROOT_PAGELET_ID);
    __rootPagelet.document = document;

    /**
     * @namespace Bigpipe
     */
    var Bigpipe = {};

    /**
     * Bigpipe事件池
     * @type {EventManager}
     * @private
     */
    var _bigpipeEventManager = new EventManager(Bigpipe);

    /**
     * 向所有pagelet广播事件
     * @param name
     * @param data
     */
    Bigpipe.broadcast = function (name, data) {
        __rootPagelet.broadcast(name, data);
        return this;
    };

    /**
     * 在Bigpipe下绑定事件
     * @param name
     * @param handler
     * @returns {Bigpipe}
     */
    Bigpipe.addEventListener = function (name, handler) {
        _bigpipeEventManager.addEventListener(name, handler);
        return this;
    };

    /**
     * 移除Bigpipe下事件
     * @param name
     * @param handler
     * @returns {Bigpipe}
     */
    Bigpipe.removeEventListener = function (name, handler) {
        _bigpipeEventManager.removeEventListener(name, handler);
        return this;
    };

    /**
     * 分发Bigpipe事件
     * @param name
     * @param data
     */
    Bigpipe.dispatchEvent = function (name, data) {
        _bigpipeEventManager.dispatchEvent(name, data);
        return this;
    };

    /**
     * @memberof Bigpipe
     * @param {string} id
     * @param {object} options
     * @return {Promise} Promise resolved with a {@link Pagelet} instance
     */
    Bigpipe.register = function (id, options) {
        var pagelet = new Pagelet(id, options);
        return pagelet.promise.then(function () {
            return pagelet;
        });
    };
    Bigpipe.end = function () {
        // 所有 pagelet 完成加载后广播 pageload 事件
        var loadPagelets = [];
        for (var id in pagelets) {
            loadPagelets.push(pagelets[id].promise);
        }
        Promise.all(loadPagelets).then(function () {
            // tell Bigpipe that the page is loaded
            Bigpipe.dispatchEvent('pageloaded', pagelets);
            // TODO: cleanupOrphanPagelets();
            // 刷新、加载页面部分 pagelets 时可能会留下无用的 orphanPagelets
        });
    };
    /**
     * 配置
     *
     * @memberof Bigpipe
     * @param {string} id
     * @param {object} options
     */
    Bigpipe.config = function (options) {
        if (typeof options.getHTML === 'function') {
            Pagelet.prototype._getHTML = options.getHTML;
        }
        if (typeof options.getPlaceholder === 'function') {
            Pagelet.prototype._getPlaceholder = options.getPlaceholder;
        }
        if (typeof options.getStylesUrl === 'function') {
            styleLoader.config({
                urlsGenerator: options.getStylesUrl
            });
        }
        if (typeof options.getScriptsUrl === 'function') {
            scriptLoader.config({
                urlsGenerator: options.getScriptsUrl
            });
        }
        // 资源管理器可通过外部配置
        if (typeof options.ResourceManager === 'function') {
            ResourceManager = options.ResourceManager;
        }

    };

    Bigpipe.debug = function (value) {
        if (value === true) {
            debug = true;
            Bigpipe.Pagelet = Pagelet;
            Bigpipe.pagelets = pagelets;
            Bigpipe.orphanPagelets = orphanPagelets;
            Bigpipe.styleLoader = styleLoader;
            Bigpipe.scriptLoader = scriptLoader;
            Bigpipe.ResourceManager = ResourceManager;
            Bigpipe.Utils = Utils;
            Bigpipe._bigpipeEventManager = _bigpipeEventManager;
        } else {
            debug = false;
            delete Bigpipe.Pagelet;
            delete Bigpipe.pagelets;
            delete Bigpipe.orphanPagelets;
            delete Bigpipe.styleLoader;
            delete Bigpipe.scriptLoader;
            delete Bigpipe.ResourceManager;
            delete Bigpipe.Utils;
            delete Bigpipe._eventHandlers;
        }
    };

    var global = this;
    global.Bigpipe = Bigpipe;

}).call(this);