Home Reference Source

src/IoC.js

/**
 * @file IoC 容器类
 * @author exodia (d_xinxin@163.com)
 */

import Injector from './Injector';
import u from './util';
import Loader from './Loader';
import ImportPlugin from './plugins/ImportPlugin';
import AutoPlugin from './plugins/AutoPlugin';
import PropertyPlugin from './plugins/PropertyPlugin';
import ListPlugin from './plugins/ListPlugin';
import MapPlugin from './plugins/MapPlugin';
import AopPlugin from './plugins/AopPlugin';

const PLUGIN_COLLECTION = Symbol('collection');
const COMPONENTS = Symbol('components');
const CREATE_COMPONENT = Symbol('createComponent');
const CREATE_INSTANCE = Symbol('createInstance');
const LOADER = Symbol('loader');
const INJECTOR = Symbol('injector');
const NULL = {};

/**
 * IoC 容器类
 */
export default class IoC {
    /**
     * 根据配置实例化一个 IoC 容器
     *
     * @param {IoCConfig} [config] ioc 容器配置
     */
    constructor(config = {}) {
        /**
         * @private
         */
        this[COMPONENTS] = Object.create(null);

        /**
         * @private
         */
        this[PLUGIN_COLLECTION] = new PluginCollection([
            new ListPlugin(),
            new MapPlugin(),
            new ImportPlugin(),
            new AopPlugin(),
            new PropertyPlugin(),
            new AutoPlugin()
        ]);
        this[PLUGIN_COLLECTION].addPlugins(config.plugins);

        /**
         * @private
         */
        this[LOADER] = new Loader(this, !!config.skipCheckingCircularDep);

        /**
         * @private
         */
        this[INJECTOR] = new Injector(this);

        config = this[PLUGIN_COLLECTION].onContainerInit(this, config);
        this.initConfig(config);
    }

    /**
     * 初始化配置
     * @param {IoCConfig} iocConfig ioc 配置
     * @protected
     */
    initConfig(iocConfig) {

        if (iocConfig.loader) {
            this.setLoaderFunction(iocConfig.loader);
        }

        this.addComponent(iocConfig.components || {});
    }

    /**
     *
     * 向容器中注册组件
     *
     * @param {string | Object.<string, ComponentConfig>} id 组件 id 或者组件配置集合
     * @param {ComponentConfig} [config] 组件配置, 第一个参数为组件 id 时有效
     * @example
     * ioc.addComponent('list', {
     *     // 构造函数创建构件 new creator, 或者字符串,字符串则为 amd 模块名
     *     creator: require('./List'),
     *     scope: 'transient',
     *     args: [{$ref: 'entityName'}],
     *
     *     // 属性注入, 不设置$setter, 则直接instance.xxx = xxx
     *     properties: {
     *          model: {$ref: 'listModel'},
     *          view: {$ref: 'listView'},
     *          name: 'xxxx' // 未设置$ref/$import操作符,'xxxx' 即为依赖值
     *     }
     * });
     *
     * ioc.addComponent({
     *     listData: {
     *         creator: 'ListData',
     *         scope: 'transient',
     *         properties: {
     *             data: {
     *                 $import: 'requestStrategy', // 创建匿名组件,默认继承 requestStrategy 的配置,
     *                 args: ['list', 'list'] // 重写 requestStrategy 的 args 配置
     *             }
     *         }
     *      }
     * });
     */
    addComponent(id, config) {
        if (typeof id === 'object') {
            for (let k in id) {
                this.addComponent(k, id[k]);
            }
            return;
        }

        if (this.hasComponent(id)) {
            throw new Error(`${String(id)} has been added!`);
        }
        else {
            this[COMPONENTS][id] = this[CREATE_COMPONENT].call(this, id, config);
        }
    }

    /**
     * 获取组件实例
     *
     * @param {string | string[]} id 单个组件 id 字符串或者一系列组件 id 数组
     * @return {Promise<*> | Promise<*[]>} 值为组件实例(传入参数为组件数组时, 值为组件实例数组)的 promise
     */
    getComponent(id) {
        if (id instanceof Array) {
            return Promise.all(id.map(id => this.getComponent(id)));
        }
        let moduleMap = Object.create(null);

        if (!this.hasComponent(id)) {
            id = String(id);
            return Promise.reject(new Error(`\`${id}\` has not been added to the Ioc`));
        }
        else {
            let config = this.getComponentConfig(id);
            this.processConfig(id);
            try {
                moduleMap = this[LOADER].resolveDependentModules(config, moduleMap, config.argDeps);
            }
            catch (e) {
                return Promise.reject(e);
            }
        }

        return this[LOADER].loadModuleMap(moduleMap).then(() => this[CREATE_INSTANCE](id));
    }

    /**
     * 检测是否注册过某个组件
     *
     * @param {string} id 组件 id
     * @return {bool}
     */
    hasComponent(id) {
        return !!this[COMPONENTS][id];
    }

    /**
     * 获取组件配置,不传入则返回所有组件配置
     *
     * @ignore
     * @param {string} [id] 组件id
     * @return {*}
     */
    getComponentConfig(id) {
        return id ? this[COMPONENTS][id] : this[COMPONENTS];
    }

    /**
     * 设置 IoC 的模块加载器
     *
     * @param {Function} amdLoader 符合 AMD 规范的模块加载器
     */
    setLoaderFunction(amdLoader) {
        this[LOADER].setLoaderFunction(amdLoader);
    }

    /**
     * 销毁容器,会遍历容器中的单例,如果有设置 dispose,调用他们的 dispose 方法
     */
    dispose() {
        this[PLUGIN_COLLECTION].onContainerDispose(this);
        this[INJECTOR].dispose();
        this[COMPONENTS] = null;
    }

    /**
     * 在指定位置添加插件
     *
     * @param {ILifeCircleHook[]} plugins 插件数组
     * @param {number} [pos] 插入位置, 默认为当前 ioc 容器插件队列末尾
     */
    addPlugins(plugins, pos) {
        this[PLUGIN_COLLECTION].addPlugins(plugins, pos);
    }

    /**
     * 获取当前实例的插件队列
     *
     * @return {ILifeCircleHook[]}
     */
    getPlugins() {
        return this[PLUGIN_COLLECTION].getPlugins();
    }

    /**
     * 移除指定的插件或指定位置的插件
     *
     * @param {number | ILifeCircleHook} pluginOrPos 插件实例或者插件位置
     * @return {bool} 成功移除返回 true
     */
    removePlugin(pluginOrPos) {
        return this[PLUGIN_COLLECTION].removePlugin(pluginOrPos);
    }

    // todo: to be private
    /**
     * @ignore
     */
    processConfig(id) {
        let config = this.getComponentConfig(id);
        config = this[PLUGIN_COLLECTION].onGetComponent(this, id, config);
        this[COMPONENTS][id] = config;
        if (!config.argDeps) {
            let deps = config.argDeps = [];
            let args = config.args;
            for (let i = args.length - 1; i > -1; --i) {
                u.hasRef(args[i]) && deps.push(args[i].$ref);
            }
        }
    }

    /**
     * @private
     */
    [CREATE_COMPONENT](id, config) {
        config = this[PLUGIN_COLLECTION].onAddComponent(this, id, config);
        return {
            id: id,
            args: [],
            properties: {},
            argDeps: null,
            propDeps: null,
            setterDeps: null,
            scope: 'transient',
            creator: null,
            module: undefined,
            isFactory: false,
            auto: false,
            instance: null,
            ...config
        };
    }

    /**
     * @private
     */
    [CREATE_INSTANCE](id) {
        return this[PLUGIN_COLLECTION].beforeCreateInstance(this, id)
            .then(
                instance => {
                    if (instance === NULL) {
                        let component = this.hasComponent(id) ? this.getComponentConfig(id) : null;
                        return this[LOADER]
                            .wrapCreator(component)
                            .then(component => this[INJECTOR].createInstance(component));
                    }

                    return instance;
                }
            )
            .then(instance => this[PLUGIN_COLLECTION].afterCreateInstance(this, id, instance));
    }
}

const PLUGINS = Symbol('plugins');

class PluginCollection {
    constructor(plugins = []) {
        this[PLUGINS] = plugins;
    }

    onContainerInit(ioc, iocConfig) {
        return this[PLUGINS].reduce(
            (config, plugin) => plugin.onContainerInit(ioc, config),
            iocConfig
        );
    }

    onAddComponent(ioc, componentId, initialComponentConfig) {
        return this[PLUGINS].reduce(
            (componentConfig, plugin) => plugin.onAddComponent(ioc, componentId, componentConfig),
            initialComponentConfig
        );
    }

    onGetComponent(ioc, componentId, initialComponentConfig) {
        return this[PLUGINS].reduce(
            (componentConfig, plugin) => plugin.onGetComponent(ioc, componentId, componentConfig),
            initialComponentConfig
        );
    }

    beforeCreateInstance(ioc, componentId) {
        return this[PLUGINS].reduce(
            (instancePromise, plugin) => instancePromise.then(
                instance => Promise.resolve(plugin.beforeCreateInstance(ioc, componentId, instance))
            ),
            Promise.resolve(NULL)
        );
    }

    afterCreateInstance(ioc, componentId, instance) {
        return this[PLUGINS].reduce(
            (instancePromise, plugin) => instancePromise.then(
                instance => {
                    let result = plugin.afterCreateInstance(ioc, componentId, instance);
                    return u.isPromise(result) ? result : Promise.resolve(instance);
                }
            ),
            Promise.resolve(instance)
        );
    }

    onContainerDispose(ioc) {
        this[PLUGINS].forEach(plugin => plugin.onContainerDispose(ioc));
    }

    addPlugins(plugins = [], pos = this[PLUGINS].length) {
        this[PLUGINS].splice(pos, 0, ...plugins);
    }

    getPlugins() {
        return this[PLUGINS].slice(0);
    }

    removePlugin(pluginOrPos) {
        if (typeof pluginOrPos !== 'number') {
            pluginOrPos = this[PLUGINS].indexOf(pluginOrPos);
            pluginOrPos = pluginOrPos === -1 ? this[PLUGINS].length : pluginOrPos;
        }

        return !!this[PLUGINS].splice(pluginOrPos, 1).length;
    }
}