/** * @class kit * @property {kit.bsCore} * @property {kit.toolkit} */ window.kit = window.kit || {}; (function(global){ var stateList = { undefined: 0, registered: 1, loaded: 3, installed: 4, }; var moduleTypeList = { loader: 'loader', import: 'import', }; var PluginManager = function(global, modules = {}){ this.plugins = {}; this.global = global; this.registerPlugins(modules); }; var __proto = PluginManager.prototype; __proto.__manual = function(){ return { config: { type: { import: 'Значение по умолчанию. Нативный загрузчик модулей. Файл модуля содержит ключевые фразы "export" и "export default"', loader: 'Файл модуля содержит функцию kit.PluginManager.register(pluginId, function(module, exports, require){})', }, alias: 'тип Array|string. Список ключей, которые могут быть использованы вместо pluginId, как и в require() так и в global', import: 'тип string. Ссылка на файл с модулем', loader: 'Используется для загрузки модулей типа loader', deps: 'тип Array|string. Зависимости, Id плагинов.', css: 'тип Array|string. Массив путей к стилям. Стили грузятся синхронно при загрузке плагина. Без событий', js: 'тип Array|string. Массив путей к скриптам. Загрузка идет по цепочке. Для синхронной загрузки скрипты ложатся в массив. пр: [1.js, [1.1.js, 1.2.js]]', loadJsAfterModule: 'определяет порядок загрузки скриптов и модуля', onInstall: 'функция выполняемая после установки плагина (однократно)', onLoad: 'функция выполняемая после загрузки плагина (однократно)', path: 'префикс пути для модуля, скриптов и стилей', modulePath: 'префикс пути для модуля. если не указан, то берется config.path', }, } }(); __proto.__checkState = function(pluginId, state){ return this.plugins.hasOwnProperty(pluginId) && this.plugins[pluginId].state >= state; }; /** * @param plugins * @return {PluginManager} */ __proto.registerPlugins = function (plugins) { if(Array.isArray(plugins)) { plugins = Object.assign({}, plugins); } for (var pluginId in plugins) { if(plugins.hasOwnProperty(pluginId)) { this.register(pluginId, plugins[pluginId]); } } return this; }; __proto.register = function (pluginId, config = {}) { //console.warn('PluginManager.register: plugin ' + pluginId + ' ('+(this.plugins[pluginId]?this.plugins[pluginId].state:'0')+')'); var defaultPlugin = { module: { exports: {}, }, config: { }, promise: new Promise(function(resolve, reject){resolve()}), id: pluginId, state: stateList.undefined, stateInit: { loading: false, installing: false, }, }; var plugin = this.__getPlugin(pluginId); if(plugin) { pluginId = plugin.id; } else { plugin = this.plugins[pluginId] = defaultPlugin; } if( this.__checkState(pluginId, stateList.installed) || ( (plugin.stateInit.loading || this.__checkState(pluginId, stateList.loaded)) && typeof config === 'object' ) ) { console.warn('PluginManager.register: plugin ' + pluginId + ' уже невозможно конфигурировать ('+plugin.state+')'); return false; } var loader; if(typeof config === 'function') { loader = config; config = plugin.config; config.loader = loader; config.type = 'loader'; } if(typeof config === 'object') { if(typeof config.onLoad !== 'function') { config.onLoad = function(){}; } if(typeof config.onInstall !== 'function') { config.onInstall = function(){}; } if(typeof config.onRegister !== 'function') { config.onRegister = function(){}; } if(typeof config.path !== 'string') { config.path = ''; } if(!config.hasOwnProperty('modulePath')) { config.modulePath = config.path; } else if(typeof config.modulePath !== 'string') { config.modulePath = config.path; } if(typeof config.js === 'string') { config.js = [config.js]; } else if(!Array.isArray(config.js)) { config.js = []; } if(typeof config.css === 'string') { config.css = [config.css]; } else if(!Array.isArray(config.css)) { config.css = []; } if(typeof config.alias === 'string') { config.alias = [config.alias]; } else if(!Array.isArray(config.alias)) { config.alias = []; } config.loadJsAfterModule = !!config.loadJsAfterModule; config.type = config.type || 'import'; //loader if(typeof config.deps === 'string') { config.deps = [config.deps]; } else if(!Array.isArray(config.deps)) { config.deps = []; } } else { console.warn('PluginManager.register: plugin ' + pluginId + ' is not a valid format'); return false; } Object.assign(plugin.config, config); if(!this.__checkState(pluginId, stateList.registered)) { plugin.state = stateList.registered; plugin.config.onRegister(plugin); } return true; }; /** * @param pluginId * @param callback * @return {PluginManager} */ __proto.load = function (pluginId, callback) { //console.log('PluginManager.load: plugin ' + pluginId + ' ('+(this.plugins[pluginId]?this.plugins[pluginId].state:'0')+')'); if(typeof callback !== 'function') { callback = function(){}; } var plugin = this.__getPlugin(pluginId); if(plugin) { pluginId = plugin.id; } if(!this.__checkState(pluginId, stateList.registered)) { console.warn('kit.load: plugin ' + pluginId + ' is not register'); return this; } if(this.__checkState(pluginId, stateList.loaded)) { //console.warn('PluginManager.load: plugin ' + pluginId + ' уже загружен'); callback(); return this; } if(plugin.stateInit.loading) { //console.log('PluginManager.install: plugin ' + pluginId + ' уже грузится'); this.__exec(pluginId, callback); return this; } plugin.stateInit.loading = true; var configModule = plugin.config; var stackPush = function (callbackPromise, argArr = []){ if(typeof plugin.promise === 'undefined') { plugin.promise = callbackPromise.apply(this, argArr); } else { plugin.promise = plugin.promise.then(function(m){ return callbackPromise.apply(this, argArr); }); } }.bind(this); var getModulePromise = (function () { function loadScript(src, resolveCallback, rejectCallback) { if(typeof resolveCallback !== 'function'){ resolveCallback = function(){}; } if(typeof rejectCallback !== 'function'){ rejectCallback = function(res){ console.warn(res.message); }; } return new Promise(function(resolve, reject) { var script = document.querySelector('script[src^="'+src+'"]'); if (!script) { script = document.createElement('script'); script.src = '' + src + '?__pm' + (new Date()).getTime(); script.onload = () => resolve(script); script.onerror = () => reject(new Error(`Ошибка загрузки скрипта ${src}`)); document.head.append(script); } else { global.config.debug && console.warn(`Скрип ${src} уже загружен`); resolve(script); } }).then(resolveCallback, rejectCallback); } function syncLoadScriptList(list) { return new Promise(function(resolve, reject) { var i = 0; var status = true; var count = list.length; var checkResolve = function(src, cond){ if(!cond) { console.error(`Ошибка загрузки скрипта ${src}`) } status = status && cond; i++; if(i === count) { resolve(); } }; var syncPromise = new Promise(function(_resolve, _reject){_resolve();}); list.forEach(function(item, i, arr){ syncPromise.then(function(){ var src = configModule.path + item; var script = document.querySelector('script[src^="'+src+'"]'); if (!script) { script = document.createElement('script'); script.src = '' + src + '?__pm' + (new Date()).getTime(); script.onload = () => checkResolve(src, true); script.onerror = () => checkResolve(src, false); document.head.append(script); } else { checkResolve(src, true); } }); }); }) } function loadStyle(src) { return new Promise(function(resolve, reject) { var link = document.querySelector('link[href^="'+src+'"]'); if (!link) { link = document.createElement('link'); link.rel = 'stylesheet'; link.type = 'text/css'; link.href = src + '?__pm' + (new Date()).getTime(); link.media = 'all'; document.head.append(link); resolve(link); } else { global.config.debug && console.warn(`Скрип ${src} уже загружен`); resolve(link); } }); } var getImportPromise = function (){ if(configModule.module && configModule.type === 'import') { return import(configModule.modulePath + configModule.module).then(function(m) { m.__esModule = true; plugin.module.exports = m; }.bind(this), function(r){ console.error(r); }); } else if(configModule.module && configModule.type === 'loader') { return loadScript(configModule.modulePath + configModule.module).then(function() { }.bind(this)); } return new Promise(function(resolve, reject){resolve();}); }.bind(this); var getResPromise = function(){ var p = new Promise(function(resolve, reject){resolve();}); configModule.js.forEach(function(item, i, array){ if(Array.isArray(item)) { p = p.then(function () { return syncLoadScriptList(item); }); } else { p = p.then(function () { return loadScript(configModule.path + item); }); } //loadScript(configModule.path + item); }); return p; }.bind(this); var setSkipLoadPromise = function (depsPromise) { var noJsPromise, noImportPromise; if(configModule.js.length === 0) { noJsPromise = true; } if((!configModule.module && configModule.type === 'import') || (configModule.type === 'loader' && typeof configModule.loader === 'function')) { noImportPromise = true; } plugin.skipLoadPromise = noImportPromise && noJsPromise && !depsPromise; return plugin.skipLoadPromise; }.bind(this); configModule.css.forEach(function(item, i, array){ loadStyle(configModule.path + item); }); var depsPromise = this.__getDepsPromise(configModule.deps); setSkipLoadPromise(depsPromise); if(depsPromise) { stackPush(function () { return depsPromise; }); } if(configModule.loadJsAfterModule) { stackPush(getImportPromise); stackPush(getResPromise); } else { stackPush(getResPromise); stackPush(getImportPromise); } this.__exec(pluginId, function(){ plugin.state = stateList.loaded; plugin.skipLoadPromise = true; configModule.onLoad.call(plugin.module.exports); callback(); }.bind(this)); return plugin.promise; }).bind(this); getModulePromise(); return this; }; /** * @param pluginId * @param callback * @return {PluginManager} */ __proto.install = function(pluginId, callback){ //console.warn('PluginManager.install: plugin ' + pluginId + ' ('+(this.plugins[pluginId]?this.plugins[pluginId].state:'0')+')'); if(typeof callback !== 'function') { callback = function(){}; } var plugin = this.__getPlugin(pluginId); if(plugin) { pluginId = plugin.id; } var module = plugin.module; if(!this.__checkState(pluginId, stateList.registered)) { console.warn('PluginManager.install: plugin ' + pluginId + ' is not register'); return this; } if(this.__checkState(pluginId, stateList.installed)) { callback(); return this; } if(plugin.stateInit.installing) { //console.log('PluginManager.install: plugin ' + pluginId + ' уже устанавливается'); this.__exec(pluginId, callback); return this; } plugin.stateInit.installing = true; this.load(pluginId); var installer = function(){ var setGlobalExtension = function (key, aliasList, data) { if(data === undefined) { return; } if(!this.global.hasOwnProperty(key)) { this.global[key] = data; } else { console.warn('PluginManager.install: plugin ' + pluginId + ' already exist in kit ('+plugin.state+')'); } if(Array.isArray(aliasList) && aliasList.length) { aliasList.forEach(function (alias) { setGlobalExtension(alias, [], data) }.bind(this)) } }.bind(this); if(plugin.config.type === moduleTypeList.loader && typeof plugin.config.loader === 'function') { var returnData = plugin.config.loader.bind(module.exports).call(module.exports, module, module.exports, this.require.bind(this), global); setGlobalExtension(pluginId, plugin.config.alias, returnData); } else if(plugin.config.type === moduleTypeList.import && typeof plugin.config.module === 'string') { setGlobalExtension(pluginId, plugin.config.alias, (typeof module.exports === 'object' && module.exports.default !== undefined)?module.exports.default:module.exports); } console.log('%cPluginManager.install: plugin ' + pluginId + ' установлен', "color: green;"); plugin.state = stateList.installed; }.bind(this); this.__exec(pluginId, function(){ installer(); plugin.config.onInstall.call(module.exports, plugin); callback(); }.bind(this)); return this; }; __proto.require = function (pluginId, callback) { //console.warn('PluginManager.require: plugin ' + pluginId + ' ('+(this.plugins[pluginId]?this.plugins[pluginId].state:'0')+')'); var plugin = this.__getPlugin(pluginId); if(plugin) { pluginId = plugin.id; } var module = plugin.module; if(!this.__checkState(pluginId, stateList.registered)) { console.warn('PluginManager.require: plugin ' + pluginId + ' is not register'); return false; } if(typeof callback !== 'function') { callback = function(){}; } this.install(pluginId, callback); return module.exports; }; __proto.exec = function (deps, callback, argArr = [], context = undefined) { if(typeof deps === 'function') { context = argArr; argArr = callback; callback = deps; deps = []; } if(context === undefined) { context = this; } if(!Array.isArray(deps) ) { deps = [deps]; } if(typeof argArr === 'undefined') { argArr = []; } deps = Array.from(new Set(deps.concat(global.config.requiredDependencies))); if(typeof callback !== 'function') { return undefined; } var depsPromise = this.__getDepsPromise(deps); var getArgArr = function () { if(argArr.length === 0) { deps.forEach(function (pluginId) { argArr.push(this.require(pluginId)); }.bind(this)); return argArr; } return argArr; }.bind(this); if(depsPromise) { depsPromise.then(function(){ callback.apply(context, getArgArr()); }); } else { callback.apply(context, getArgArr()) } }; __proto.__exec = function (pluginId, callback, argArr = []) { var module = this.plugins[pluginId]; var exec = function(_callback, argArr){ if(module.skipLoadPromise) { _callback.apply(this, argArr); } else { module.promise = module.promise.then(function(){ _callback.apply(this, argArr); }.bind(this)); } }.bind(this); exec(callback, argArr); }; __proto.__getDepsPromise = function (pluginList) { var skipPromise = true; var promise = new Promise((resolve, reject)=>{ var i = 0; var checkModule = function(){ if(++i === pluginList.length + 1) { resolve(); } }; checkModule(); pluginList.forEach(function(pluginId, i, array){ if(!this.__checkState(pluginId, stateList.registered)) { checkModule(); return false; } this.install(pluginId, checkModule); skipPromise = skipPromise && this.plugins[pluginId].skipLoadPromise; }.bind(this)); }); return !skipPromise?promise:undefined; }; __proto.ready = function(deps, callback) { if(typeof deps === 'function') { callback = deps; deps = []; } if (/complete|interactive|loaded/.test(document.readyState)) { // In case the document has finished parsing, document's readyState will // be one of "complete", "interactive" or (non-standard) "loaded". this.exec(deps, callback); } else { // The document is not ready yet, so wait for the DOMContentLoaded event document.addEventListener("DOMContentLoaded", function () { this.exec(deps, callback); }.bind(this)); } }; __proto.__getPlugin = function (pluginId) { if(this.plugins.hasOwnProperty(pluginId)) { return this.plugins[pluginId]; } for (var _pluginId in this.plugins) { if(this.plugins.hasOwnProperty(_pluginId) && this.plugins[_pluginId].config.alias.indexOf(pluginId) !== -1) { return this.plugins[_pluginId]; } } return undefined; }; var defaultConfig = { defaultPath: '', requiredDependencies: [], debug: false, }; var manager = new PluginManager(global); manager.register('PluginManager', { alias: 'pm', type: moduleTypeList.loader, loader: function(module, exports, require) { return module.exports = manager; } }); manager.install('PluginManager'); manager.register('config', { alias: 'pm', type: moduleTypeList.loader, loader: function(module, exports, require, global) { global.config = global.config || {}; global.config = Object.assign(defaultConfig, global.config); } }); manager.install('config'); global.PluginManager = manager; global.ready = manager.ready.bind(manager); global.exec = manager.exec.bind(manager); global.require = manager.require.bind(manager); })(kit || window);