1
BEYOND FLUX BEYOND FLUX
SCALABLE FRONTEND ARCHITECTURES SCALABLE FRONTEND ARCHITECTURES USING PUBLISH/SUBSCRIBE USING PUBLISH/SUBSCRIBE
Michael Kurze @ goto Amsterdam 2016
BEYOND FLUX BEYOND FLUX SCALABLE FRONTEND ARCHITECTURES SCALABLE - - PowerPoint PPT Presentation
BEYOND FLUX BEYOND FLUX SCALABLE FRONTEND ARCHITECTURES SCALABLE FRONTEND ARCHITECTURES USING PUBLISH/SUBSCRIBE USING PUBLISH/SUBSCRIBE Michael Kurze @ goto Amsterdam 2016 1 WHY ARCHITECTURE? WHY ARCHITECTURE? IT'S ABOUT TIME! IT'S ABOUT
1
Michael Kurze @ goto Amsterdam 2016
2
2012-10-12 2014-05-13 2011-04-01 2016-04-01 100 - 200 - 300 - 400 - 0 - JS Transfer / kB
(Source: httparchive.org) We are building complex sofuware (requires tools), and we are talking about them (requires a language).
3
PLATFORM COMPLEXITY PLATFORM COMPLEXITY DRIVING DRIVING APPLICATION COMPLEXITY APPLICATION COMPLEXITY
4
synchronize state (across devices) provide immediate feedback continuous bi-directional communication (client/server) http://www.reactivemanifesto.org
5
6
7
UI inconsistencies in Facebook chat
8
10
https://github.com/voronianski/flux-comparison
11 . 1
var var React React = = require require( ('react' 'react') ); ; var var FluxibleMixin FluxibleMixin = = require require( ('fluxible' 'fluxible') ). .FluxibleMixin FluxibleMixin; ; var var ProductStore ProductStore = = require require( ('./stores/ProductStore' './stores/ProductStore') ); ; var var addToCart addToCart = = require require( ('./actions/addToCart' './actions/addToCart') ); ; var var Products Products = = React React. .createClass createClass( ({ { mixins mixins: : [ [FluxibleMixin FluxibleMixin] ], , render render: : function function( () ) { { return return < <ul ul> > { {this this. .state state. .products products. .map map( (product product = => > < <li li> > < <img img src src= ={ {product product. .image image} }/> /> { {product product. .title title} }
{product product. .price price} }€ € < <button button
={ {( () ) = => > this this. .addToCart addToCart( (product product) )} } disabled disabled= ={ {product product. .inventory inventory === === 0} }> >add add</ </button button> > </ </li li> > ) )} } </ </ul ul> >; ; } }, , addToCart addToCart: : function function( (product product) ) { { this this. .executeAction executeAction( (addToCart addToCart, , { { product product: : product product } }) ); ; } }, , // ... // ... } }) ); ;
11 . 2
var var React React = = require require( ('react' 'react') ); ; var var FluxibleMixin FluxibleMixin = = require require( ('fluxible' 'fluxible') ). .FluxibleMixin FluxibleMixin; ; var var ProductStore ProductStore = = require require( ('./stores/ProductStore' './stores/ProductStore') ); ; var var addToCart addToCart = = require require( ('./actions/addToCart' './actions/addToCart') ); ; var var Products Products = = React React. .createClass createClass( ({ { // ...render, addToCart... // ...render, addToCart... getInitialState getInitialState: : function function( () ) { { return return this this. ._getStateFromStores _getStateFromStores( () ); ; } }, , _getStateFromStores _getStateFromStores: : function function( () ) { { return return { { products products: : this this. .getStore getStore( (ProductStore ProductStore) ). .getAllProducts getAllProducts( () ) } }; ; } }, , statics statics: : { { storeListeners storeListeners: : { { _onChange _onChange: : [ [ProductStore ProductStore] ] } } } }, , _onChange _onChange: : function function( () ) { { this this. .setState setState( (this this. ._getStateFromStores _getStateFromStores( () )) ); ; } } } }) ); ;
11 . 3
var var React React = = require require( ('react' 'react') ); ; var var FluxibleMixin FluxibleMixin = = require require( ('fluxible' 'fluxible') ). .FluxibleMixin FluxibleMixin; ; var var ProductStore ProductStore = = require require( ('./stores/ProductStore' './stores/ProductStore') ); ; var var addToCart addToCart = = require require( ('./actions/addToCart' './actions/addToCart') ); ; var var Products Products = = React React. .createClass createClass( ({ { /* ... */ /* ... */ } }) ); ; var var ShoppingCart ShoppingCart = = require require( ('./components/CartContainer.jsx' './components/CartContainer.jsx') ); ; var var App App = = React React. .createClass createClass( ({ { mixins mixins: : [ [FluxibleMixin FluxibleMixin] ], , render render: : function function( () ) { { return return < <div div> > < <Products Products /> /> < <ShoppingCart ShoppingCart /> /> </ </div div> >; ; } } } }) ); ; export export function function render render( (context context) ) { { React React. .withContext withContext( ( context context. .getComponentContext getComponentContext( () ), , function function( () ) { { React React. .render render( ( React React. .createElement createElement( (App App) ), , document document. .getElementById getElementById( ('fluxible-app' 'fluxible-app') ) ) ); ; } } ) ); ; } }
12
var var shop shop = = require require( ('../../../api/shop' '../../../api/shop') ); ; // actions/addToCart.js // actions/addToCart.js module module. .exports exports = = function function ( (context context, , payload payload, , done done) ) { { context context. .dispatch dispatch( ('ADD_TO_CART' 'ADD_TO_CART', , { { product product: : payload payload. .product product } }) ); ; done done( () ); ; } }; ; // actions/cartCheckout.js // actions/cartCheckout.js module module. .exports exports = = function function ( (context context, , payload payload, , done done) ) { { var var products products = = payload payload. .products products; ; context context. .dispatch dispatch( ('CART_CHECKOUT' 'CART_CHECKOUT') ); ; shop shop. .buyProducts buyProducts( (products products, , function function ( () ) { { context context. .dispatch dispatch( ('SUCCESS_CHECKOUT' 'SUCCESS_CHECKOUT', , { { products products: : products products } }) ); ; done done( () ); ; } }) ); ; } }; ;
13
var var Fluxible Fluxible = = require require( ('fluxible' 'fluxible') ); ; var var CartStore CartStore = = require require( ('./stores/CartStore' './stores/CartStore') ); ; var var ProductStore ProductStore = = require require( ('./stores/ProductStore' './stores/ProductStore') ); ; var var app app = = new new Fluxible Fluxible( () ); ; app app. .registerStore registerStore( (CartStore CartStore) ); ; app app. .registerStore registerStore( (ProductStore ProductStore) ); ; var var view view = = require require( ('./view' './view') ) var var receiveProducts receiveProducts = = require require( ('./actions/receiveProducts' './actions/receiveProducts') ); ; var var context context = = app app. .createContext createContext( () ); ; context context. .executeAction executeAction( (receiveProducts receiveProducts, , { {} }, , function function ( (err err) ) { { if if ( (err err) ) { { throw throw err err; ; } } return return view view. .render render( (context context) ); ; } }) ); ;
14
var var createStore createStore = = require require( ('fluxible/addons/createStore' 'fluxible/addons/createStore') ); ; var var ProductStore ProductStore = = createStore createStore( ({ { storeName storeName: : 'ProductStore' 'ProductStore', , initialize initialize: : function function ( () ) { { this this. ._products _products = = [ [] ]; ; } }, , handlers handlers: : { { 'ADD_TO_CART' 'ADD_TO_CART': : 'decreaseInventory' 'decreaseInventory', , 'RECEIVE_PRODUCTS' 'RECEIVE_PRODUCTS': : 'handleReceive' 'handleReceive' } }, , handleReceive handleReceive: : function function ( (payload payload) ) { { this this. ._products _products = = payload payload. .products products; ; this this. .emitChange emitChange( () ); ; } }, , decreaseInventory decreaseInventory: : function function ( (payload payload) ) { { this this. .dispatcher dispatcher. .waitFor waitFor( ('CartStore' 'CartStore', , function function( () ) { { var var product product = = payload payload. .product product; ; product product. .inventory inventory = = Math Math. .max max( (product product. .inventory inventory-1
, 0) ); ; this this. .emitChange emitChange( () ); ; } }) ); ; } }, , getAllProducts getAllProducts: : function function ( () ) { { return return this this. ._products _products; ; } } } }) ); ; module module. .exports exports = = ProductStore ProductStore; ;
15
compared to "classic" MVC (AngularJS, Backbone) strict separation: State / UI / Behavior each value stored exactly once clear data flow, good testability server-side rendering relatively simple
16
compared to "classic" MVC (AngularJS, Backbone) each store sees all actions hard-wired store-dependencies stores are (conceptually) singletons anti-pattern: transporting mutable state in actions
17
18
Flux:
19
functional composition snapshot/replay capabilities can yield performance boost (free dirty-checking)
20
must protect against mutating state, using e.g. spread constructor {...oldState, prop: newVal} (ES201?) (test with) recursive Object.freeze() Immutable.js potentially steep learning curve (functional paradigm) container components know the whole state tree
21
an old pattern from the 80ies
22
full decoupling of communicating components: sender does not know about recipient(s) recipient does not know about sender How? Broadcast of events through central bus selection of receivers through topics
23
event bus: connects senders/receivers (asynchronously) view components, like in Flux/Redux (+ create action events) activity components, like Flux stores (+ completion of async actions) unidirectional flow using event patterns
24
topics for resources (state slices) and actions (intents to modify) topics connect components in a configurable way
25 . 1
import import React React from from 'react' 'react'; ; import import patterns patterns from from 'laxar-patterns' 'laxar-patterns'; ; const const injections injections = = [ ['axContext' 'axContext', , 'axReactRender' 'axReactRender'] ]; ; function function create create( (context context, , reactRender reactRender) ) { { patterns patterns. .resources resources. .handlerFor handlerFor( (context context) ) . .registerResourceFromFeature registerResourceFromFeature( ('products' 'products', , render render) ); ; function function render render( () ) { { reactRender reactRender( (< <ul ul> > { {( (context context. .resources resources. .products products || || [ [] ]) ). .map map( (product product = => > < <li li> > < <img img src src= ={ {product product. .image image} }/> /> { {product product. .title title} }
{product product. .price price} }€ € < <button button
={ {( () ) = => > addToCart addToCart( (product product) )} } disabled disabled= ={ {product product. .inventory inventory === === 0} }> >add add</ </button button> > </ </li li> > ) )} } </ </ul ul> >) ); ; } } // ... addToCart ... // ... addToCart ...
25 . 2
import import React React from from 'react' 'react'; ; import import patterns patterns from from 'laxar-patterns' 'laxar-patterns'; ; const const injections injections = = [ ['axContext' 'axContext', , 'axReactRender' 'axReactRender'] ]; ; function function create create( (context context, , reactRender reactRender) ) { { // ... render ... // ... render ... const const addToCartPublisher addToCartPublisher = = patterns patterns. .actions actions. .publisherForFeature publisherForFeature( (context context, , 'addToCart' 'addToCart') ) function function addToCart addToCart( (product product) ) { { if if( (product product. .inventory inventory > > 0) ) { { addToCartPublisher addToCartPublisher( ({ { product product: : product product } }) ); ; } } } } return return { { onDomAvailable
: render render } }; ; } } export export default default { { name name: : 'product-list-view' 'product-list-view', , injections injections: : injections injections, , create create: : create create } }; ;
26 . 1
{ { "widget" "widget": : "product-list-view" "product-list-view", , "features" "features": : { { "addToCart" "addToCart": : { { "action" "action": : "add-to-cart" "add-to-cart" } }, , "products" "products": : { { "resource" "resource": : "product-list" "product-list" } } } } } }, , { { "widget" "widget": : "shopping-cart-view" "shopping-cart-view", , "features" "features": : { { . .. .. . } } } }, , { { "widget" "widget": : "products-activity" "products-activity", , "features" "features": : { { "products" "products": : { { "resource" "resource": : "product-list" "product-list" } }, , "decrementInventory" "decrementInventory": : { { "onActions" "onActions": : [ ["add-to-cart" "add-to-cart"] ] } } } } } }, , { { "widget" "widget": : "shopping-cart-activity" "shopping-cart-activity", , "features" "features": : { { . .. .. . } } } }
27 . 1
import import ax ax from from 'laxar' 'laxar'; ; import import patterns patterns from from 'laxar-patterns' 'laxar-patterns'; ; import import shop shop from from '../../../api' '../../../api'; ; const const injections injections = = [ ['axContext' 'axContext'] ]; ; function function create create( (context context) ) { { let let products products = = [ [] ]; ; const const replaceProducts replaceProducts = = patterns patterns. .resources resources. .replacePublisherForFeature replacePublisherForFeature( (context context, , 'products' 'products') ); ; context context. .eventBus eventBus. .subscribe subscribe( ('beginLifecycleRequest' 'beginLifecycleRequest', , ( () ) = => > { { shop shop. .getProducts getProducts( (list list = => > { { products products = = list list; ; replaceProducts replaceProducts( (products products) ); ; } }) ); ; } }) ); ; // ... action handling (next) ... // ... action handling (next) ... } } export export default default { { name name: : 'products-activity' 'products-activity', , injections injections: : injections injections, , create create: : create create } }; ;
27 . 2
import import ax ax from from 'laxar' 'laxar'; ; import import patterns patterns from from 'laxar-patterns' 'laxar-patterns'; ; import import shop shop from from '../../../api' '../../../api'; ; const const injections injections = = [ ['axContext' 'axContext'] ]; ; function function create create( (context context) ) { { // ... products initialization (previous) ... // ... products initialization (previous) ... patterns patterns. .actions actions. .handlerFor handlerFor( (context context) ) . .registerActionsFromFeature registerActionsFromFeature( ('decrementInventory' 'decrementInventory', , ( ({ { product product } }) ) = => > { { products products . .filter filter( (current current = => > current current. .id id == == product product. .id id) ) . .forEach forEach( (current current = => > { { current current. .inventory inventory = = Math Math. .max max( (0 0, , current current. .inventory inventory -
1) ); ; } }) ); ; replaceProducts replaceProducts( (products products) ); ; } }) ); ; } } export export default default { { name name: : 'products-activity' 'products-activity', , injections injections: : injections injections, , create create: : create create } }; ;
28
FROM EVENT BUS TO FRAMEWORK FROM EVENT BUS TO FRAMEWORK http://www.laxarjs.org component configuration lifecycle: when are event collaborators available? services for view components: where do I render my HTML to? development tools (event log, visual composition structure)
29
decoupling of components visible architecture flexibility of composition (patterns: resources, actions, flags, streams...) freedom of implementation (functional vs. imperative, React vs. AngularJS...)
30
ecosystem small freedom to make mistakes server-side rendering not there yet
31
"I define Flux as 'unidirectional data flow with changes described as plain objects'." ( , Author of Redux) Dan Abramov "It's cool that you are inventing a better Flux by not doing Flux at all." ( , Author of Cycle.js) André Staltz
32
Flux Redux PubSub unidirectional flow + + + complexity handling
+ reactivity
+ scalability (component isolation)
ecosystem
+
snapshot/record/replay
+ * YOUR MILEAGE MAY VARY * YOUR MILEAGE MAY VARY
33
Slides: github.com/x1b/beyond-flux-goto2016 Michael Kurze ( ) @0b11011 laxarjs.org
34
"The ultimate Swiss Army Knife for sale in Interlaken", , Lizenz: , use without modification "1913 photograph Ford company, USA", , Lizenz: "Mark Zuckerberg Facebook SXSWi 2008 Keynote", , Lizenz: , use without modification "Erdmännchen, Zoo, Tier, Sand, Wüste", Pixabay / Didgeman , Lizenz: "Magic Cube, Patience Games, Puzzle", Pixabay / Hans , Lizenz: "Two wedding rings lying on a white surface.", , Lizenz: , use without modification https://www.flickr.com/photos/redjar/113974357 CC BY-SA 2.0 https://en.wikipedia.org/wiki/File:A-line1913.jpg Public Domain https://www.flickr.com/photos/99565773@N00/2323729121 CC BY 2.0 https://pixabay.com/de/erdm%C3%A4nnchen-zoo-tier-sand-w%C3%BCste-363051 CC0 1.0 Public Domain https://pixabay.com/en/magic-cube-patience-games-puzzle-232282 CC0 1.0 Public Domain http://www.picserver.org/w/wedding-rings01.html CC BY-SA 3.0