Source: App.js

/**
 * @file App
 * @author Leon(leon@outlook.com)
 */

var Promise = require('es6-promise').Promise;
var u = require('underscore');

var invariant = require('./util/invariant');
var locator = require('./locator');
var events = require('./events');
var Router = require('./Router');
var env = require('./env');
var url = require('./url');

/**
 * App
 *
 * @constructor
 * @param {!Object} options 参数
 * @param {Array.Object} options.routes 路由配置
 */
function App(options) {

    invariant(options, 'App need options');
    invariant(options.routes, 'App need routes');

    u.extend(this, options);

    /**
     * 路由器
     *
     * @member {module:Router}
     */
    this.router = new Router(this.routes);

}

/**
 * 启动App
 *
 * 此方法只能在client端使用,并且不会调用Page的`getInitialState()`方法加载资源
 * 因此,只适合同构状态下,有同步的数据支持时使用
 *
 *
 * @public
 *
 * @param {*} initialState 初始状态
 *
 * @fires module:events~app-bootstrap
 * @fires module:events~app-ready
 *
 * @return {Promise}
 */
App.prototype.bootstrap = function (initialState) {

    invariant(env.isClient, 'app-should bootstrap on client only');

    /**
     * @event module:events~app-bootstrap
     */
    events.emit('app-bootstrap');

    // 启动locator侦听浏览器的前进/后退事件
    locator.start().on('redirect', u.bind(this.onLocatorRedirect, this));

    var me = this;

    var request = url.parse(location.href);
    var route = this.route(request);

    return route

        ? Promise.reject({
            status: 404
        })

        : me

            // 加载页面模块
            .loadPage(route.page)

            // 通过初始数据直接启动页面
            .then(function (Page) {

                // 由于这里是客户端的逻辑,那么就记录一下当前页面
                var page = me.page = new Page(initialState);

                page.render(document.getElementById(me.main));

                /**
                 * @event module:events~app-ready
                 */
                events.emit('app-ready');

            });

};

/**
 * 当client端页面地址发生变化时的处理函数
 *
 * @private
 *
 * @param {!string} path  当前页面的path
 * @param {Object}  query query参数
 * @return {Promise}
 */
App.prototype.onLocatorRedirect = function (path, query) {

    var request = {
        path: path,
        query: query
    };

    var me = this;

    // 执行请求处理
    return me
        .execute(request)
        .then(function (result) {

            // 我们不需要因为要考虑页面间替换时,而去复用同一个Page实例
            // 只要直接生成新的页面实例,渲染即可
            // react会帮我们做对同一种ReactComponent渲染优化

            // 如果当前有一个正在展现的页面,
            // 那么把它销毁掉
            if (me.page) {
                me.page.dispose();
            }

            // 由于这里是客户端的逻辑,那么就记录一下当前页面
            var page = me.page = result.page;

            page.render(document.getElementById(me.main));

            /**
             * @event module:events~app-page-switch-succeed
             */
            events.emit('app-page-switch-succeed');

        });

};


/**
 * 处理一个请求
 *
 * @param {!Object}  request      请求
 * @param {?*}       initialState 初始数据状态
 * @return {Promise}
 *
 * @fires module:events~app-request
 * @fires module:events~app-get-initial-state
 * @fires module:events~app-get-initial-state-succeed
 * @fires module:events~app-get-initial-state-failed
 * @fires module:events~app-response-in-json
 * @fires module:events~app-response-in-html
 * @fires module:events~app-page-loaded
 * @fires module:events~app-page-bootstrap
 * @fires module:events~app-page-bootstrap-succeed
 */
App.prototype.execute = function (request) {

    /**
     * @event module:events~app-request
     */
    events.emit('app-request');

    var route = this.route(request);

    if (!route) {
        return Promise.reject({
            status: 404
        });
    }

    var page;

    return this

        // 加载页面模块
        .loadPage(route.page)

        // 加载初始化数据
        .then(function (Page) {

            // 我们不需要因为要考虑页面间替换时,而去复用同一个Page实例
            // 只要直接生成新的页面实例,渲染即可
            // react会帮我们做对同一种ReactComponent渲染优化

            page = new Page();
            return page.getInitialState(request);

        })

        // 渲染视图
        .then(function (state) {

            // 这里看一下是不是server端在处理ajax请求
            // 这种情况应该返回当前的数据即可
            if (env.isServer && request.xhr) {

                /**
                 * @event module:events~app-response-in-json
                 */
                events.emit('app-response-in-json');

                return {
                    state: state,
                    route: route
                };

            }

            /**
             * @event module:events~app-response-in-html
             */
            events.emit('app-response-in-html');

            /**
             * @event module:events~app-page-bootstrap
             */
            events.emit('app-page-bootstrap');

            // 初始化页面,触发页面的第一次数据剪裁
            page.init(state);

            /**
             * @event module:events~app-page-bootstrap-succeed
             */
            events.emit('app-page-bootstrap-succeed');

            return {
                page: page,
                route: route
            };

        })
        ['catch'](function (error) {
            events.emit('app-execute-error', error);
            throw error;
        });

};

/**
 * 根目录路径
 *
 * @public
 *
 * @param {!string} basePath 根目录路径
 *
 * @return {module:App}
 */
App.prototype.setBasePath = function (basePath) {
    this.basePath = basePath;
    return this;
};

/**
 * 加载Page类
 *
 * @protected
 *
 * @param {!string} page 页面模块路径
 *
 * @return {Promise}
 *
 * @fires module:events~app-page-loaded
 * @fires module:events~app-load-page-on-server
 * @fires module:events~app-load-page-on-client
 */
App.prototype.loadPage = function (page) {

    var pool = this.pool;

    if (pool && pool[page]) {

        /**
         * @event module:events~app-page-loaded
         */
        events.emit('app-page-loaded');

        return Promise.resolve(pool[page]);
    }

    return env.isServer ? this.resolveServerModule(page) : this.resolveClientModule(page);

};

/**
 * 服务器端加载Page模块
 *
 * @private
 *
 * @param {string} moduleId Page模块id
 *
 * @return {Promise}
 */
App.prototype.resolveServerModule = function (moduleId) {

    /**
     * @event module:events~app-load-page-on-server
     */
    events.emit('app-load-page-on-server', moduleId);

    var basePath = this.basePath;

    invariant(basePath, 'ei need a basePath to resolve your page');

    var path = basePath + '/' + moduleId;

    var Page = require(path);

    var pool = this.pool;

    if (!pool) {
        pool = this.pool = {};
    }

    pool[moduleId] = Page;

    return Promise.resolve(Page);

};

/**
 * 在客户端上加载Page模块
 *
 * @private
 *
 * @param {string} moduleId Page模块id
 *
 * @return {Promise}
 */
App.prototype.resolveClientModule = function (moduleId) {

    /**
     * @event module:events~app-load-page-on-client
     */
    events.emit('app-load-page-on-client');

    return new Promise(function (resolve, reject) {

        window.require([moduleId], function (Page) {
            resolve(Page);
        });

    });

};

/**
 * 路由
 *
 * @protected
 *
 * @param {!Object} request 请求
 *
 * @return {?Object}
 *
 * @fires module:events~app-route-succeed
 * @fires module:events~app-route-succeed
 * @fires module:events~app-route-failed
 */
App.prototype.route = function (request) {

    /**
     * @event module:events~app-route
     */
    events.emit('app-route');

    var config = this.router.route(request);

    if (config) {

        /**
         * @event module:events~app-route-succeed
         */
        events.emit('app-route-succeed');

    }
    else {

        /**
         * @event module:events~app-route-failed
         */
        events.emit('app-route-failed');
    }

    return config;

};

module.exports = App;