Skip to content

Instantly share code, notes, and snippets.

@mridgway
Last active January 11, 2024 06:05
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mridgway/53745e0da019058cc277 to your computer and use it in GitHub Desktop.
Save mridgway/53745e0da019058cc277 to your computer and use it in GitHub Desktop.
Bundle Plugin for Fluxible
/**
* On server, just register all of the resources directly to the plugin. On the client, pass in a loader file that maps bundle
* name to webpack bundle require (a separate entry point). Then from your action, you can call `context.loadBundle` to load
* the bundle resources. On the server, the resources are already available but on the client they will be lazy loaded by
* webpack.
*/
'use strict';
var debug = require('debug')('BundlePlugin');
var PromiseLib = global.Promise || require('es6-promise').Promise;
module.exports = function bundleLoaderFactory(options) {
return new BundleLoader(options);
};
function BundleLoader(options) {
options = options || {};
this._app = options.app;
this._mainBundle = options.mainBundle;
this._enableMainBundle = typeof options.enableMainBundle !== 'undefined' ? options.enableMainBundle : true;
this._bundleLoader = options.bundleLoader;
// Registered later
this._actions = {};
this._components = {};
this._loadedBundles = {};
}
BundleLoader.prototype.name = 'BundlePlugin';
BundleLoader.prototype.plugContext = function () {
var context = new BundleLoaderContext(this);
return {
plugActionContext: function (actionContext) {
actionContext.getAction = context.getAction.bind(context);
actionContext.getComponent = context.getComponent.bind(context);
actionContext.getMainBundle = context.getMainBundle.bind(context);
actionContext.loadBundle = context.loadBundle.bind(context);
actionContext.loadBundles = context.loadBundles.bind(context);
},
plugComponentContext: function (componentContext) {
componentContext.getComponent = context.getComponent.bind(context);
},
dehydrate: function () {
return context.dehydrate();
},
rehydrate: function (state, callback) {
return context.rehydrate(state, callback);
}
};
};
BundleLoader.prototype.getMainBundle = function () {
return this._mainBundle;
};
/**
* Makes an action available for use in page routes
* @method registerAction
* @param {String} name Name of the action
* @param {Function} action action function
*/
BundleLoader.prototype.registerAction = function registerAction(name, action) {
debug(name + ' action registered');
this._actions[name] = action;
};
/**
* Makes a bundle lazy loadable
* @method registerBundle
* @param {String} name Name of the bundle
* @param {Function} bundle bundle object
*/
BundleLoader.prototype.registerBundle = function registerBundle(name, bundle) {
var self = this;
debug(name + ' bundle registered');
self._loadedBundles[name] = bundle;
if (bundle.actions) {
Object.keys(bundle.actions).forEach(function eachAction(actionName) {
self.registerAction(actionName, bundle.actions[actionName]);
});
}
if (bundle.controllerViews) {
Object.keys(bundle.controllerViews).forEach(function eachControllerViews(componentName) {
self.registerComponent(componentName, bundle.controllerViews[componentName]);
});
}
if (bundle.stores) {
Object.keys(bundle.stores).forEach(function eachStore(storeName) {
var store = bundle.stores[storeName];
self._app.registerStore(store);
});
}
};
/**
* Makes a component available for use in composites
* @method registerBundle
* @param {String} name Name of the component
* @param {Function} component React component
*/
BundleLoader.prototype.registerComponent = function registerComponent(name, component) {
debug(name + ' component registered');
this._components[name] = component;
};
/**
* Creates a serializable state of the plugin
* @method dehydrate
* @returns {Object}
*/
BundleLoader.prototype.dehydrate = function dehydrate() {
debug('dehydrate');
return {
mainBundle: this._mainBundle
};
};
/**
* Rehydrates the application and creates a new context with the state from the server
* @method rehydrate
* @param {Object} state
* @async
*/
BundleLoader.prototype.rehydrate = function rehydrate(state) {
debug('rehydrate', state);
var self = this;
self._mainBundle = state.mainBundle;
};
/**
* @class BundleLoaderContext
* @param {object} bundleLoader
* @constructor
*/
function BundleLoaderContext(bundleLoader) {
this.bundleLoader = bundleLoader;
this._bundleLoadPromises = {};
}
BundleLoaderContext.prototype.getMainBundle = function () {
return this.bundleLoader._mainBundle;
};
BundleLoaderContext.prototype.getAction = function (actionName) {
return this.bundleLoader._actions[actionName];
};
BundleLoaderContext.prototype.getComponent = function (componentName) {
return this.bundleLoader._components[componentName];
};
/**
* Loads a bundle rollup
* @method loadBundle
* @param {String} bundleName Bundle name
* @param {Function} [callback] Callback to be called after bundle is loaded
* @return {Promise}
* @async
*/
BundleLoaderContext.prototype.loadBundle = function loadBundle(bundleName, callback) {
debug('load ' + bundleName + ' bundle');
var self = this;
// Make sure we don't try to load the same bundle twice at the same time
if (!self._bundleLoadPromises[bundleName]) {
self._bundleLoadPromises[bundleName] = new PromiseLib(function (resolve, reject) {
// Check if the bundle is already registered first
if (self.bundleLoader._loadedBundles[bundleName]) {
resolve(self.bundleLoader._loadedBundles[bundleName]);
return;
}
// Now attempt to use the loader to lazy load the bundle
if (!self.bundleLoader._bundleLoader) {
reject(new Error('Loader does not exist'));
return;
}
if (!self.bundleLoader._bundleLoader[bundleName]) {
reject(new Error('Bundle ' + bundleName + ' is not available in the loader'));
return;
}
self.bundleLoader._bundleLoader[bundleName](function (err, bundle) {
if (err) {
reject(err);
return;
}
// Register the bundle and its resources
self.bundleLoader.registerBundle(bundleName, bundle);
resolve(bundle);
});
});
}
if (callback) {
self._bundleLoadPromises[bundleName].then(function (result) {
setImmediate(callback, undefined, result);
})['catch'](function (e) {
setImmediate(callback, e);
});
}
return self._bundleLoadPromises[bundleName];
};
/**
* Loads bundles via the loader method
* @method loadBundles
* @param {String|Array} bundleNames Names of bundles to load
* @param {Function} callback Called after all bundles have been loaded
* @async
*/
BundleLoaderContext.prototype.loadBundles = function loadBundles(bundleNames, callback) {
var self = this;
var currentLoadingPromises = [];
if (!Array.isArray(bundleNames)) {
bundleNames = [bundleNames];
}
debug('loading ' + bundleNames.join(', '));
bundleNames.forEach(function (bundleName) {
currentLoadingPromises.push(self.loadBundle(bundleName));
});
PromiseLib.all(currentLoadingPromises).then(function (result) {
debug('loaded ' + bundleNames.join(', '));
setImmediate(callback, null, result);
})['catch'](function (e) {
setImmediate(callback, e);
});
};
/**
* Creates a serializable state of the plugin
* @method dehydrate
* @returns {Object}
*/
BundleLoaderContext.prototype.dehydrate = function dehydrate() {
debug('dehydrate');
var bundlesLoaded = Object.keys(this._bundleLoadPromises);
if (this.bundleLoader._enableMainBundle) {
bundlesLoaded.push(this.bundleLoader._mainBundle); // Always load the main bundle
}
return {
loadedBundles: bundlesLoaded
};
};
/**
* Rehydrates the application and creates a new context with the state from the server
* @method rehydrate
* @param {Object} state
* @param {Function} callback
* @async
*/
BundleLoaderContext.prototype.rehydrate = function rehydrate(state, callback) {
debug('rehydrate', state);
var self = this;
self.loadBundles(state.loadedBundles, function (err) {
if (err) {
callback && callback(err);
return;
}
callback();
});
};
module.exports = function (context, payload, done) {
context.loadBundle('Nest', function () {
//Ensures that Nest components are loaded
context.executeAction(loadPageData, {}, function (err, pageData) {
context.dispatch('NEW_PAGE', pageData);
done();
});
});
}
module.exports = {
actions: {
getAbout: require('./actions/getAbout')
},
controllerViews: {
About: require('./components/About'),
BasicApp: require('./components/Application'),
Followers: require('./components/Followers'),
Home: require('./components/Home')
},
stores: {
AboutStore: require('./stores/AboutStore'),
ApplicationStore: require('./stores/ApplicationStore'),
FollowersStore: require('./stores/FollowersStore'),
TimeStore: require('./stores/TimeStore')
}
};
'use strict';
var debug = require('debug')('Loader');
module.exports = {
'basic': function (callback) {
debug('loading basic');
require('bundle!../bundle.js')(function (bundle) {
debug('loaded basic');
callback(null, bundle);
});
},
'Nest': function (callback) {
debug('loading Nest');
require('bundle!../bundles/Nest/bundle.js')(function (bundle) {
debug('loaded Nest');
callback(null, bundle);
});
}
};
@taylorjames
Copy link

Do you have an example of how this can be implemented with a current fluxible app?

@taylorjames
Copy link

@mridgway is there any way you can show me how I can use this in my app?

@ali1k
Copy link

ali1k commented May 12, 2016

I can't find where example-bundle.js is referred to and how we should setup webpack to use the loader?

@redonkulus
Copy link

@ali1k so we have a Grunt task internally at yahoo that will take example-bundle.js and convert it into example-loader.js. The loader file then gets passed into the bundle plugin and lazy loaded later by webpack.

@ali1k
Copy link

ali1k commented May 18, 2016

@redonkulus do you have any plan to open source your bundling tool? I think it is a crucial feature to build an ecosystem of Fluxible-based components. This example is still incomplete and as others mentioned, without a complete example, users cannot really figure it out how to use dynamic module loading in Fluxible!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment