Share
Embed
JavaScript application frameworks: the parts
Nuts and bolts of JS application frameworks: how to write tools for messaging, extending objects, and changing state; how to safely integrate DOM or utility libraries and plugins; how to integrate CSS and templates; how to connect all the pieces; how to apply that knowledge to choosing an existing framework.
Garann Means
May 29, 2013
Transcript
91 slides
-
in applications and frameworks, the code is the easy part
the hard part is decision making -
and it is hard to build an app figuring out
what it should do translating requirements to workflows making it all work together (even when you have good tools) -
any framework may need customization a framework is a basis
your app may have slightly different needs different ways of handling the same concept may exist within the same app it may have no opinion on other concepts -
compare to jQuery: tons of functionality some core, some more
esoteric discovering new features leads to better ways of doing things the most discoverable features are those we know should be there -
using a tool well means understanding the decisions it makes,
and making good decisions about how to use it -
we expect framework methods to live on a prototype unlike
a library, whose methods should usually not require an instance allows creation of objects set up to work together instantiation lets us keep related functionality together -
but we also need methods for the app containing the
objects global initialization event broadcasting global state changes stateless utilities -
so it makes sense to start with a global (
f u n c t i o n ( F r m w r k ) { f u n c t i o n _ i n i t ( ) { } F r m w r k . t e s t = f u n c t i o n ( ) { c o n s o l e . l o g ( " y u p " ) ; } ; _ i n i t ( ) ; } ) ( w i n d o w . F r m w r k = { } ) ; F r m w r k . t e s t ( ) ; / / y u p -
we were going to instantiate some objects state data or
both we don't want to have to create too many types -
one object we know we need f u n c
t i o n _ i n i t ( ) { F r m w r k . a p p = n e w F r m w r k . O b j e c t ( ) ; } -
so letG s make that the default F r m
w r k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } r e t u r n t h i s ; } ; -
utility libraries it's also good to understand libraries but this
is not a talk on libraries make a smart choice providing the most you need but no more jQuery, Underscore, Zepto, etc. focus on connecting pieces, not writing essential tools -
including third-party libraries or tools can be appended to your
core file other users don't need to import the additional script difficult to change or upgrade may conflict with other versions in use -
leaving it up to implementers other users need to import
the script separately, in order framework should work with all supported versions may make sense to allow uses of library to be overriden -
in this example, jQuery is used like so: ( f
u n c t i o n ( F r m w r k , $ ) { . . . } ) ( w i n d o w . F r m w r k = { } , w i n d o w . j Q u e r y ) ; -
for a more modular approach, you might use require.js d
e f i n e ( [ " j Q u e r y " ] , f u n c t i o n ( $ ) { . . . } ) ; -
letG s get back to our base object F r
m w r k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } r e t u r n t h i s ; } ; -
what do we expect from a data object a place
to store data the ability to save and update itself notifications if it changes -
setting and changing data ideally, set on object creation but
may need to be set on an empty object (like the app) should be possible to update one or more properties -
the data should be its own object F r m
w r k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " , d a t a : { } } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } r e t u r n t h i s ; } ; -
we want control over how thatG s updated F r
m w r k . O b j e c t . p r o t o t y p e . s e t = _ s e t = f u n c t i o n ( d a t a , v a l ) { i f ( t y p e o f d a t a = = " s t r i n g " ) { t h i s . d a t a [ d a t a ] = v a l ; } e l s e { f o r ( k e y i n d a t a ) { t h i s . d a t a [ k e y ] = d a t a [ k e y ] ; } } r e t u r n t h i s ; } ; -
and a shortcut for the app itself F r m
w r k . s e t = f u n c t i o n ( ) { _ s e t . a p p l y ( F r m w r k . a p p , a r g u m e n t s ) ; } ; -
why not just have the implementer modify ÇobjÓ.data directly this
is a cheap workaround to observing the objects can fire off events, update server allows for a predictable pattern allows us to do a little magic -
the downside is it means we also need a way
to get data F r m w r k . O b j e c t . p r o t o t y p e . g e t = _ g e t = f u n c t i o n ( k e y ) { r e t u r n t h i s . d a t a [ k e y ] ; } ; F r m w r k . g e t = f u n c t i o n ( ) { r e t u r n _ g e t . a p p l y ( F r m w r k . a p p , a r g u m e n t s ) ; } ; -
we could also condense the two, jQuery- style F r
m w r k . O b j e c t . p r o t o t y p e . d a t a = _ d a t a = f u n c t i o n ( d a t a , v a l ) { i f ( a r g u m e n t s . l e n g t h = = 1 & & t y p e o f d a t a = = " s t r i n g " ) { r e t u r n t h i s . d a t a [ k e y ] ; } e l s e { . . . } ; -
once we have data, we have something to store data
persistence tends to be boring to write we need several types of XHRs we want saving and updating to be as invisible as possible -
should we assume REST depends very much on your backend
may be more JavaScripty to do CRUD from all endpoints: data with no ID: create ID only: read data and ID: update ID and string or number: delete but only if that fits the larger architecture -
letG s assume it does F r m w r
k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " , d a t a : { } , e n d p o i n t : n u l l } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } r e t u r n t h i s ; } ; -
we want to sync whenever an update happens F r
m w r k . O b j e c t . p r o t o t y p e . s e t = _ s e t = f u n c t i o n ( d a t a , v a l ) { i f ( t y p e o f d a t a = = " s t r i n g " ) { t h i s . d a t a [ d a t a ] = v a l ; } e l s e { f o r ( k e y i n d a t a ) { t h i s . d a t a [ k e y ] = d a t a [ k e y ] ; } } i f ( t h i s . e n d p o i n t ) { t h i s . s y n c ( ) ; } r e t u r n t h i s ; } ; -
additionally, we want explicit methods to delete or read an
object please use your imagination for those Nremember that thing about CRUD being boring Ñ -
then, of course, we need a function to do the
work F r m w r k . O b j e c t . p r o t o t y p e . s y n c = f u n c t i o n ( d a t a , c m d ) { v a r i d = t y p e o f d a t a = = " n u m b e r " ? d a t a : u n d e f i n e d , t h a t = t h i s ; $ . p o s t ( t h i s . e n d p o i n t , { i d : d a t a . i d | | i d , c o m m a n d : c m d , d a t a : i d ? u n d e f i n e d : d a t a } , f u n c t i o n ( r e s ) { i f ( t y p e o f r e s = = " n u m b e r " ) { t h a t . d a t a . i d = r e s ; } e l s e i f ( r e s . i d ) { f o r ( k e y i n r e s ) { t h a t . d a t a [ k e y ] = d a t a [ k e y ] ; } } -
several decisions reflected there not RESTful, as discussed flexible signature
for a single method without explicit flags most important: happens in the background -
weG ve been talking about objects but we want those
objects to talk to a framework -
it would be nice to have notifications when object data
is set when the data is synced with the server if an error occurs anywhere along the way -
these are all legit. but weG re going to use
the simplest -
we want events to be scoped to objects F r
m w r k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " , d a t a : { } , e n d p o i n t : n u l l , e v e n t s : { } } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } r e t u r n t h i s ; } ; -
weG ll manage that cache with three methods F r
m w r k . O b j e c t . p r o t o t y p e . p u b = _ p u b = f u n c t i o n ( n a m e , a r g s ) { v a r t h a t = t h i s ; i f ( t h i s . e v e n t s [ n a m e ] ) { $ . e a c h ( t h i s . e v e n t s [ n a m e ] , f u n c t i o n ( ) { t h i s . a p p l y ( t h a t , a r g s | | [ ] ) ; } ) ; } } ; F r m w r k . O b j e c t . p r o t o t y p e . s u b = _ s u b = f u n c t i o n ( n a m e , c a l l b a c k ) { i f ( ! t h i s . e v e n t s [ n a m e ] ) { t h i s . e v e n t s [ n a m e ] = [ ] ; } t h i s . e v e n t s [ n a m e ] . p u s h ( c a l l b a c k ) ; r e t u r n [ n a m e , c a l l b a c k ] ; } ; F r m w r k . O b j e c t . p r o t o t y p e . u n s u b = _ u n s u b = f u n c t i o n ( h a n d l e ) { v a r e v t s = t h i s . e v e n t s [ h a n d l e [ 0 ] ] , hat tip to Pete Higgins -
now when something happens, we can publish an event F
r m w r k . O b j e c t . p r o t o t y p e . s e t = _ s e t = f u n c t i o n ( d a t a , v a l ) { i f ( t y p e o f d a t a = = " S t r i n g " ) { t h i s . d a t a [ d a t a ] = v a l ; } e l s e { f o r ( k e y i n d a t a ) { t h i s . d a t a [ k e y ] = d a t a [ k e y ] ; } } t h i s . p u b ( " u p d a t e d " , t h i s . d a t a ) ; i f ( t h i s . e n d p o i n t ) { t h i s . s y n c ( ) ; } r e t u r n t h i s ; } ; -
or if the wrong thing happens, we can notify observers
of the error F r m w r k . O b j e c t . p r o t o t y p e . s y n c = f u n c t i o n ( d a t a , c m d ) { v a r i d = t y p e o f d a t a = = " n u m b e r " ? d a t a : u n d e f i n e d , t h a t = t h i s ; $ . p o s t ( t h i s . e n d p o i n t , { i d : d a t a . i d | | i d | | u n d e f i n e d , c o m m a n d : c m d | | u n d e f i n e d , d a t a : i d ? u n d e f i n e d : d a t a } , f u n c t i o n ( r e s ) { i f ( t y p e o f r e s = = " n u m b e r " ) { t h a t . d a t a . i d = r e s ; } e l s e i f ( r e s . i d ) { f o r ( k e y i n r e s ) { t h a t . d a t a [ k e y ] = d a t a [ k e y ] ; } } -
global events can be scoped to the app F r
m w r k . p u b = f u n c t i o n ( ) { _ p u b . a p p l y ( F r m w r k . a p p , a r g u m e n t s ) ; } ; F r m w r k . s u b = f u n c t i o n ( ) { r e t u r n _ s u b . a p p l y ( F r m w r k . a p p , a r g u m e n t s ) ; } ; F r m w r k . u n s u b = f u n c t i o n ( ) { _ u n s u b . a p p l y ( F r m w r k . a p p , a r g u m e n t s ) ; } ; -
notifications as an implementer controllers, presenters, viewmodels ideally, publishing events
controlled by framework implementations merely listen but we have events we don't control coming from the DOM -
a JS framework should be decoupled from HTML and CSS
maintainability modularity separation of concerns not using JS for things that don't require it -
note: this is JS frameworks client-side frameworks may be more
tightly coupled, and do more view stuff -
elements of the view templates CSS DOM listeners potentially a
data transformation layer -
an object might have more than one view, though for
that we need states -
a new object type F r m w r k
. S t a t e = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { p a r e n t : F r m w r k . a p p , t m p l : n u l l , s e t t i n g s : { } , c a l l b a c k : n u l l } , o b j ) ; r e t u r n t h i s ; } ; -
tied to the generic object F r m w r
k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " , d a t a : { } , e n d p o i n t : n u l l , e v e n t s : { } , s t a t e s : { } } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } r e t u r n t h i s ; } ; -
child objects force us to think about initialization we want
implementers to be able to pass in literals but we want instances that can have prototype methods our options are: 1. force states to be added explicitly 2. filter states supplied on initialization -
this is a place where itG s nice to do
some magic F r m w r k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " , d a t a : { } , e n d p o i n t : n u l l , e v e n t s : { } , s t a t e s : { } } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } f o r ( v a r s i n t h i s . s t a t e s ) { i f ( ! t h i s . s t a t e s [ s ] i n s t a n c e o f F r m w r k . S t a t e ) { t h i s . s t a t e s [ s ] = n e w F r m w r k . S t a t e ( t h i s . s t a t e s [ s ] ) ; } } -
once we have states we can switch between them F
r m w r k . O b j e c t . p r o t o t y p e . s t a t e = _ s t a t e = f u n c t i o n ( n a m e ) { i f ( ! n a m e ) { r e t u r n t h i s . _ c u r r e n t S t a t e = = " _ d e f a u l t " ? n u l l : t h i s . _ c u r r e n t S t a t e } i f ( t h i s . s t a t e s [ n a m e ] ) { t h i s . s t a t e s [ n a m e ] . r e n d e r ( ) ; t h i s . _ c u r r e n t S t a t e = n a m e ; r e t u r n t h i s . s t a t e s [ n a m e ] ; } } ; F r m w r k . s t a t e = f u n c t i o n ( ) { _ s t a t e . a p p l y ( F r m w r k . a p p , a r g u m e n t s ) ; } ; -
and, since theyG re instances, use their methods bringing us
back to templates -
very simple template rendering F r m w r k
. S t a t e . p r o t o t y p e . r e n d e r = f u n c t i o n ( ) { v a r d a t a = $ . e x t e n d ( t r u e , { } , t h i s . p a r e n t . d a t a , t h i s . s e t t i n g s ) ; t h i s . p a r e n t . e l . h t m l ( t h i s . t m p l ( d a t a ) ) ; i f ( t h i s . c a l l b a c k ) { t h i s . c a l l b a c k . a p p l y ( t h i s , a r g u m e n t s ) ; } } ; -
contains several assumptions: t m p l has already been
compiled to a function a state will always replace the object it belongs to any partial templates are already accessible somehow the correct CSS is already loaded -
thatG s a lot can we make some of those
more flexible or automatic -
easy fixes F r m w r k . S
t a t e . p r o t o t y p e . r e n d e r = f u n c t i o n ( ) { v a r d a t a = $ . e x t e n d ( t r u e , { } , t h i s . p a r e n t . d a t a , t h i s . s e t t i n g s ) ; i f ( t h i s . c s s ) { $ ( " h e a d " ) . a p p e n d ( ' < s t y l e c l a s s = " ' + t h i s . _ n a m e + ' " > ' + t h i s . c s s + } t h i s . c o n t a i n e r ? t h i s . c o n t a i n e r . h t m l ( t h i s . t m p l ( d a t a ) ) : t h i s . p a r e n t . e l . h t m l ( t h i s . t m p l ( d a t a ) ) ; i f ( t h i s . c a l l b a c k ) { t h i s . c a l l b a c k . a p p l y ( t h i s , a r g u m e n t s ) ; } } ; -
loading templates and partials is trickier should be able to
take a URL or a template element should know whether the template's already loaded should handle partials differently and know which partials it requires -
we should handle that in our constructor F r m
w r k . S t a t e = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { p a r e n t : F r m w r k . a p p , t m p l : n u l l , s e t t i n g s : { } , c a l l b a c k : n u l l } , o b j ) ; / / t m p l n e e d s t o b e c o m p i l e d i f ( t h i s . t m p l & & t y p e o f t h i s . t m p l ! = " f u n c t i o n " ) { t h i s . t m p l = _ c o m p i l e T m p l ( t h i s . t m p l , t h i s . p a r t i a l s ) ; } r e t u r n t h i s ; } ; -
and do the loading work, if necessary, in a static
function v a r _ t m p l C a c h e = { } ; f u n c t i o n _ c o m p i l e T m p l ( t m p l S t r i n g , p a r t i a l s , n a m e ) { v a r t m p l N a m e = n a m e | | t m p l S t r i n g . r e p l a c e ( " \ / " , " _ " ) . r e p l a c e ( " . " , d e f = { } ; i f ( p a r t i a l s & & t y p e o f p a r t i a l s = = " o b j e c t " & & p a r t i a l s . l e n g t h ) { p a r t i a l s . f o r E a c h ( f u n c t i o n ( p ) { d e f [ p . n a m e ] = _ t m p l C a c h e [ p . n a m e ] ; } ) ; } i f ( _ t m p l C a c h e [ t m p l N a m e ] ) { r e t u r n _ t m p l C a c h e [ t m p l N a m e ] ; } e l s e i f ( $ ( " b o d y " ) . f i n d ( t m p l S t r i n g ) . l e n g t h ) { r e t u r n _ t m p l C a c h e [ t m p l N a m e ] = d o T . t e m p l a t e ( $ ( t m p l S t r i n g ) . h t m l } e l s e { $ . g e t ( t m p l S t r i n g , f u n c t i o n ( t m p l ) { _ t m p l C a c h e [ t m p l N a m e ] = d o T . t e m p l a t e ( t m p l , n u l l , d e f ) ; -
we also need a way to add partials we can't
expect partials to be tied to an object or state we need them to be available when the templates using them are compiled -
thus, the first method belonging exclusively to the application F
r m w r k . a p p . r e g i s t e r T m p l = _ r e g i s t e r T m p l = f u n c t i o n ( n a m e , t m p l S t r i n g ) { _ c o m p i l e T m p l ( t m p l S t r i n g , n u l l , n a m e ) ; } ; -
why not just use _ c o m p i
l e T m p l we could, but itG s nice to have a separate public interface -
it also helps reflect a different use F r m
w r k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " , d a t a : { } , e n d p o i n t : n u l l , e v e n t s : { } , s t a t e s : { } , _ c u r r e n t S t a t e : " _ d e f a u l t " , t m p l s : { } } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } i f ( t h i s . t m p l s ) { f o r ( v a r t i n t h i s . t m p l s ) { _ r e g i s t e r T m p l ( t , t h i s . t m p l s [ t ] ) ; } -
real talk: that could be much better everything's easy until
it's async external resources are likely to require async code this is an ideal place for promises ..which we'd get from yet another external resource -
we want a framework to also help manage dependencies what
we have could work if dependencies are finite if they're generic, we'd be better off with something like Require.js something like that means we have to think of the framework differently -
taking Require as an example.. our object types would be
modules the app would be a specific dependency we could remove any code to load templates or CSS all this code would be required by implementations -
weG ve left a place for some implementation code F
r m w r k . S t a t e = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { p a r e n t : F r m w r k . a p p , t m p l : n u l l , s e t t i n g s : { } , c a l l b a c k : n u l l } , o b j ) ; / / t m p l n e e d s t o b e c o m p i l e d i f ( t h i s . t m p l & & t y p e o f t h i s . t m p l ! = " f u n c t i o n " ) { t h i s . t m p l = _ c o m p i l e T m p l ( t h i s . t m p l , t h i s . p a r t i a l s ) ; } r e t u r n t h i s ; } ; -
some frameworks attempt to contain DOM interaction lists of selectors
and events callbacks belong to the application or a controller this keeps everything nice and clean but doesn't add a whole lot of functionality -
what DOM functionality can a framework add re-rendering attaching DOM
interactions to state changes forwarding DOM events to framework events -
what we need to trigger a state change an element
to delegate to the selector and event the name of the state to switch to -
we want this wired up when our state is created
F r m w r k . S t a t e = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { p a r e n t : F r m w r k . a p p , t m p l : n u l l , s e t t i n g s : { } , c a l l b a c k : n u l l , t r i g g e r : n u l l } , o b j ) ; / / t m p l n e e d s t o b e c o m p i l e d i f ( t h i s . t m p l & & t y p e o f t h i s . t m p l ! = " f u n c t i o n " ) { t h i s . t m p l = _ c o m p i l e T m p l ( t h i s . t m p l , t h i s . p a r t i a l s ) ; } i f ( t h i s . t r i g g e r & & t h i s . t r i g g e r . l e n g t h ) { t h i s . t r i g g e r . f o r E a c h ( f u n c t i o n ( t ) { $ ( t . c o n t a i n e r | | t h i s . p a r e n t . e l ) . o n ( t . e v e n t , t . s e l e c t o r , t h i s } ) ; } -
how about more generic events we may still want to
fire events scoped to an object our publish method is public, so implementers can do that manually for something very small and unopinionated, that's probably enough -
this example is more useful as a complement to something
else lots of control in HTML or CSS a DOM library a datavis library a widget or component framework a client/server framework -
there are a lot of tools but only a handful
of strategies for augmenting them -
with HTML/CSS framework provides initial data and rendering states can
be exclusively data states, not visual state callbacks set up explicit data updates framework exists to store and sync data -
CSS specifically cause thatG s kind of hard CSS interactions
are an alternative to JS we can observe animations (e.g. a n i m a t i o n S t a r t , a n i m a t i o n E n d ) but we can't directly observe something activating : t a r g e t , for example and we can't forward events from CSS to JS the most foolproof way to integrate with CSS is to listen for the same events -
with DOM-heavy libraries or plugins object DOM element is just
the container stores data for child elements that aren't objects child elements created by object's template object's default state callback listens for library events and updates its data accordingly again, storing and syncing -
with components or widgets framework objects could be made less
generic to be aware of library functions or the two can communicate from their controller code components should manage themselves, so framework only needs to worry about public notifications still storing and syncing, but at a higher level of remove -
with frameworks that handle the backend again, we can be
less generic and rely on server- aware objects those may still need rendering and client-only messaging this time, we're observing third-party events coming not from the user but the server or the data handle the manipulation of data, leave the rest to the underlying framework -
in the real world, we usually only think about pieces
of frameworks in conjunction with existing frameworks -
what to do with these ideas don't go write a
new generic do-everything framework think about what pieces your app needs from a framework choose frameworks that are opionated about those things if you can't find a perfect fit, write a small wrapper that adds just what you need