grails-powered html rias
DESCRIPTION
Speaker: Brian Kotek Page-based web applications were once the norm, but times have changed. Users increasingly expect rich, desktop-like experiences from their browser-based applications. They demand apps that adhere to common standards, without the need for special plugins. Finally, they want to use them on any device, from smartphone to flat-screen. It's a daunting task, but help is on the way. Grails provides an awesome foundation for HTML-based Rich Internet Applications. In this session, we'll see how Grails can make HTML RIAs a snap. JavaScript is the language of the web, but it has its warts. Grails plugins allow us to easily leverage intermediary languages like CoffeeScript or TypeScript to help create large-scale client-side code. Libraries like ExtJS or Dojo offer expansive UI toolkits that hook perfectly into Grails REST APIs. JSON and GSON are excellent data exchange formats, as long as you properly plan out their structure. And GORM is incredibly powerful, but without careful thought you can inadvertently send huge amounts of unnecessary data to the client. We'll dig into each of these areas, and more. Come see just how much power and productivity Grails brings to the HTML RIA table.TRANSCRIPT
HTML RIAs with Grails
Brian Kotek
Overview
Who am I?
Brian Kotek
Booz Allen Hamilton
Code junkie for 15 years
Twitter: @brian428
compiledammit.com
What Shall We Talk About?
Client Language Options (plugins)
UI Frameworks
Data Format and Structure
Hibernate Issues
Rich Client Issues and Concerns
What is a RIA?
Not just a bit of AJAX
Not page-based
Desktop-like functionality
Stateful client
Strong client-side model
Significant business logic
User Expectations over Time
Client Language Options
Languages: JavaScript
Languages: CoffeeScript
Languages: TypeScript
Languages: Dart
Language Tools
Command Line
Node
Cake
Shell script / batch
Built-in watch args for compilers
IDE Options
IDEA External Tools
IDEA File Watchers
Eclipse Builders
Grails Plugins
Coffeescript-compiler
Typescript
Dart?
Client UI Libraries
Piecemeal Options
jQuery UI (+Extensions)
Bootstrap (+Extensions)
Unified Options
Dojo
Kendo UI
Ext JS / Sencha Touch
Java Options
GWT
SmartClient
ZK
Vaadin
Choosing a UI Library
Free vs. Paid
JavaScript-based vs. Java (+XML)
Complexity of UI
Web vs. Mobile vs. Native App
Data Formats
JSON
render users as JSON
// Done!
XML
render users as XML
// Done!
SOAP
CXF Plugin
Axis Plugin
Metro Plugin
Advice and Common Pitfalls
JSON Structure
Request Structure: Params
GET
user/list?departmentId=5
params.departmentId (or Command object)
POST
new User( params )
user.properties = params
bindData( user, params )
Request Structure: Paging
start=0&limit=100
page=1
Request Structure: Sorting
sort=lastpost&dir=desc
sort=[{"field":"lastpost","dir":"desc"},
{"field":"threadId","dir":"desc"}]
Request Structure: Grouping
group=userId&dir=desc
group=[{"field":"threadId","dir":"desc"},
{"field":"userId","dir":"desc"}]
Request Structure: Filtering
filter=[
{"field":"id", "data":{"type":"numeric",
"comparison":"lt", "value":10}},
{"field":"price", "data":{"type":"numeric",
"comparison":"lt", "value":50}}
]
Response Structure
Content negotiation (withFormat)
Based on Mime type or Accept HTTP header
Response Structure
def list() {
...
withFormat {
html bookList: books // render GSP
js { ... } // build JSON response
xml { ... } // build XML response
}
}
Response Structure
[
{"id":1, "firstName":"Al",
"lastName":"Smith"},
{"id":2, "firstName":"Bill",
"lastName":"Lumberg"}
]
Response Structure
{
"success":true,
"data":[
{"id":1, "firstName":"Al", "lastName":"Smith"},
{"id":2, "firstName":"Bill",
"lastName":"Lumberg"}
]
}
Response Structure
{
"success":true,
"data":[...],
"totalcount":1234
}
Response Structure: Errors
{
"success":false,
"data":[],
"errors": [
"User Name must be unique.",
"User Name must be at least 5 characters."
]
}
Response Structure: Errors
{
"success":false, "data":[],
"errors": [{
"field":"userName",
"messages": [
"User Name must be unique.",
"User Name must be at least 5 characters."
]
}]
}
Takeaways
Use a standard format
Plan Ahead
Consider less-than-obvious cases
Pick a format and stick with it
Domain Model Concerns
Associations
Omit them
Send them all (deep conversion)
Send just the ID (old default converter)
Partial data (situation-dependent)
Serialization and Lazy Loading
Serializes everything
Indiscriminate
Inadvertently send large result data
DTOs to the rescue
Data Transfer Objects
Customized snapshot of object state
No behavior
Can be a realized class
Can be a map
Maps are usually sufficient
Option 1: Manually Building DTOs
Build Map
Recurse associations
Total control
Verbose
Option 2: GSON Plugin
users as GSON
new User( request.GSON )
user.properties = request.GSON
Option 3: Custom JSON Marshaller
JSON.registerObjectMarshaller( User ) { User user ->
Map dto = [:]
// ...build DTO version as a Map...
return dto
}
// To use in Controller:
render userList as JSON
Option 4: Domain Metaclass Methods
Explicitly control what is serialized
(Inspired by Foxgem)
customers.each { thisCustomer ->
// Omit orders and wishlists associations
dto = thisCustomer.asDto(
except: [ "orders", "wishlists" ]
)
result.push( dto )
}
render result as JSON
grailsApplication.domainClasses.each{ domainClass ->
domainClass.metaClass.asDto = { filter ->
def dto = [ id : null ]
if( filter."include" ) {
filter."include".each{ dto[ it ] = delegate."${ it }" }
}
else if( filter."except" ) {
def props= domainClass.persistentProperties.findAll {
!( it.name in filter."except" )
}
props.each{
dto[ it.name ] = delegate."${ it.name }"
}
}
return dto
}
}
Option 5: Assembler Pattern
user = assembler.assemble( params )
assembler.disassemble( user ) as JSON
Option 6: Dozer
Automatic mapping by name
Highly customizable
More complex
DTO plugin
Choosing an Approach
Plan for this on SOME level
Level of control
Level of encapsulation
Complexity of solution
Testing Your JSON API
Especially critical for separate UI and server devs
Can use controller unit tests
Manual confirmation
Compare to baseline
Concurrency
Request Overlap
1. Request 1 (list Users)
2. Request 2 (list Users with filter)
3. Request 2 result
4. Request 1 result
Option 1: UI Locking
Disable controls
Mask UI
Option 2: Synchronous Calls
“Single-thread” calls
Async=false
Option 3: Abort and Replace
Track last request
Abort last and execute new
Reads only?
Data Synchronization
Option 1: Always Reload
Option 2: User-Initiated Reload
Option 3: Client Polling (Comet)
Simple JavaScript setInterval()
XMLHttpRequest long polling
Option 4: Server Push
Websockets
events-push plugin
Can instruct client to refresh
Can push updated data for consumption
Miscellaneous Advice
Client-side MVC
Client logic can be extensive
Avoid spaghetti code
Architect as thoroughly as server-side
Client-side Service Classes
Encapsulate service logic
Isolate data conversions
Built to handle asynchronous processing
Promises
Represents a future value
Provides a stable API
Allows resolve(), reject(), progress(), etc.
Allows chaining, map, reduce, etc.
Mock JSON
Simple config value to switch
Client work can proceed without remote services
Sanity test for proposed JSON structures
Dynamic Tomcat Contexts
// In _Events.groovy:
eventConfigureTomcat = { tomcat ->
def contextRoot = "/myRoot"
File contextPath = new File( "//some/file/path" )
if( contextPath.exists() ) {
def context = tomcat.addWebapp( contextRoot,
contextPath.getAbsolutePath() )
context.reloadable = true
context.loader = new WebappLoader( tomcat.class.classLoader )
}
}
Service API Granularity
vs
Make it Client-Agnostic
Client Testing
Client Tests are Critical
Runtime errors only
RIAs have extensive logic
Match server testing zeal
Common Testing Options
Jasmine
Mocha
Chai
Sinon.js
Many more
WebDriver
Integration tests vs. unit tests
Not granular
Tests wide swathes of client code
Can run with Grails via webdriver plugin
WAR Deployment
Excluding Files
// BuildConfig.groovy:
grails.war.copyToWebApp = { args ->
Environment.executeForCurrentEnvironment {
production {
fileset( dir:"web-app" ) {
exclude( name: "js/test/**" )
exclude( name: "mockdata/**" )
}
}
}
}
Removing Files
// _Events.groovy (invoked just before WAR is zipped)
eventWarStart = { event ->
Environment.executeForCurrentEnvironment {
production {
// Force CoffeeScript recompile and minification
ant.delete(
dir: "${basedir}/web-app/js/app",
includeemptydirs: true
)
}
}
}
Cleaning Up
// _Events.groovy (invoked after WAR is built)
eventWarEnd = { event ->
Environment.executeForCurrentEnvironment {
production {
// Delete minified JS so next dev startup recompiles
ant.delete(
dir: "${basedir}/web-app/js/app",
includeemptydirs: true
)
}
}
}
Conclusion
Recap
Languages and UI Options
Data exchange
Hibernate issues
General RIA challenges
Building and Testing
Questions?
Thanks!
Brian Kotek - @brian428