The State of Front-end At CrowdTwist
- 2. Different apps with lots of code...
☞ FanCenter: 22k (SLOC)
☞ Control Center: 12k
☞ Widgets: 9k
- 3. ...written in various libraries and frameworks...
☞ FanCenter: Backbone.js, Marionette.js, Geppetto,
RequireJS
☞ Control Center & Widgets: AngularJS
- 4. ...using different tools.
☞ FanCenter: Crusher (in-house build tool), Mocha
☞ Control Center & Widgets: npm, Bower, Grunt, Express,
Karma
- 7. Growing pains
As a new member of the team, I would like to learn
CoffeeScript, Jade, Sass, as well as Backbone, Marionette,
Geppetto, Angular, Grunt, and Crusher, so that I can develop
new features.
Some of these may seem relatively simple to pick up and start
using, but it becomes hard really fast to write scalable and
maintainable code across the different apps and frameworks.
Think bloated views and spaghetti events in Backbone and custom
Angular directives with crazy $scope magic and deep equality
checks.
- 8. ❝Just because you're using
Backbone and Angular doesn't
mean your code has to suck.❞
—a reasonable developer
- 9. To which I respond...
True! But... it makes it harder to grow an idiomatic codebase.
Each framework has its own special sauce and set of rules
you need to understand and follow if you want to write
maintainable code.
- 10. Unless, of course, every developer is a
rockstar and their thought process is
identical to mine!
That's how it works, right?
Right!?
- 12. How did we get here?
☞ Magical frameworks
☞ False separation of concerns
☞ Nascent tools and rapidly evolving ecosystem
- 14. Frameworks
Frameworks make certain assumptions about how an
application will function, providing tools out of the box to
greatly simplify the development process as a whole.
Tend to stress thinking less and doing more, faster.
For certain use cases, this is a good solution, eg. basic CRUD
interfaces, smaller single-page apps.
- 15. Frameworks
When dealing with larger applications and growing pains
described earlier, it is valuable to ask the following question:
What are the costs / consequences, if any, of coupling your
application to a framework?
- 17. Frameworks
Consider a form view leveraging a custom directive to render a
dynamic list of inputs:
<div ng-controller="SurveyCtrl">
<form name="surveyForm">
<question-list ng-model="survey.questions" />
</form>
</div>
Now ask yourself, who owns the view(s)?
- 20. Separation of concerns
Separating concerns, such as making requests, validating forms,
and rendering dynamic inputs, into separate components makes
sense.
Directives make sense, as do route controllers and form validators.
The problem is how these components communicate, the implicit
dependencies between them, and being able to answer the original
question of who owns the view?
- 21. Separation of concerns
If Robert Plant comes along and wants to add a new feature to the
existing functionality, he would first need to:
☞ Understand how nested object properties affect a child's $scope
☞ (Kinda) understand the differences between scope, child scope,
isolate scope, transculsion
☞ The evils of ngInit and its partner in crime $scope.$watch
☞ $modelValue → $formatters → $viewValue → $render
$modelValue ← $parsers ← $viewValue ← $setViewValue
- 22. Separation of concerns
This magic makes AngularJS a simplicity trap.
Angular.js is attractive when coming from Backbone.js where you
have to do everything yourself (not an ideal situation either).
The problem is you relinquish a non-trivial amount of ownership
and control over your application in hopes of the magic paying off
and a false sense of simplicity.
When dealing with large applications that consistently grow in
features with an increasing amount of developers, ownership and
true simplicity become even more important.
- 25. No, seriously, what's up with that?
☞ The API is radically different
☞ It's essentially a new framework
☞ No support for IE 8 (no one wants to, but we still need to)
☞ Did I mention it's a completely new framework?
We just started building our Angular apps last year!!
- 27. Change is good
That's how the radical notion of front-end development came
to be, as well as amazing tools like jQuery, Backbone and
Angular.
- 28. Stability is better
Is the tool simple, small and predictable enough to use now
and still support next year?
Turns out that was a hard question to answer last year.
And it's still hard to answer today!
- 32. The language is maturing
This means we can simplify our
toolchain and remove additional
languages like CoffeeScript.
We don't need CoffeeScript, it just
makes JavaScript better.
But now, JavaScript is making
JavaScript better!
- 34. The tooling is maturing
import {get} from "common/request.js";
class MyComponent {...};
export default MyComponent;
- 36. ❝JavaScript is great and all, but
I need to write an app and I'm
not gonna start rolling my own
router, MVC library and
templating solution❞
—a reasonable developer
- 41. Back to basics
Instead of building controllers to manage views that depend
on other controllers that manage views that manage
templates that manage DOM, just build self-contained
Components.
- 42. import React from "react";
import Question from "views/question";
let QuestionList = React.createClass({
render() {
let questions = this.props.questions.map(question => {
return <Question data={question} />;
});
return (
<div class="questions">
{questions}
</div>
);
}
});
export default QuestionList;
- 43. ❝Dude, is that HTML in your
business logic? WAT?? You know
you're not supposed to...❞
—a reasonable developer
- 45. Not quite.
That HTML is actually JSX, a syntax that is essentially XML and
allows for the DOM to be composed alongside your logic.
If you think that's breaking all the rules, newsflash. You're doing it
already.
- 46. unwatch = $scope.$watch('survey', function(survey) {
if (survey) {
unwatch();
$scope.survey.hasQuestions = survey.questions.length > 0 ? true : false;
}
});
<div ng-controller="SurveyCtrl">
<form name="surveyForm">
<input type="text" ng-init="survey.title = survey.title || 'Default'" ng-model="survey.title">
<input type="checkbox" ng-model="survey.hasQuestions">
</form>
</div>
It may not be as obvious as JSX, but the coupling is still there!
- 47. With React, you at least get all the dependencies isolated to a
single component in a single file.
That is ownership.
- 48. If you still oppose the idea of JSX, you can use React without JSX:
React.createElement(Question, {data: question}, "innerText");
- 49. Composition
Your component can render other components!
<QuestionList>
<Question />
<Question />
</QuestionList>
(without the complexity of isolate scopes and transclusion)
- 50. Speed
You may be thinking the (re)rendering of these components is
slow, but React is smart.
React operates on the DOM in-memory before making the
smallest set of changes to the browser's document.
This is facilitated by a straightforward definition of state.
- 51. State
A component's data is managed by 2 objects, props and state.
props is the configuration of your component passed in as
attributes. It is immutable.
state is private to that component and defines the mutations
a component undergoes, typically causing re-render.
- 52. State
let QuestionList = React.createClass({
getInitialState: {
hasAnswer: false
},
processAnswer(answer) {
this.setState({
hasAnswer: true
});
}
});
<QuestionList title="Answer these!" />
☞ title is a prop that is passed into the component
☞ hasAnswer is part of the state of the component
- 53. State
With React, you know the exact state a component is in at any
given point in time, and the UI will reflect that consistently.
With Angular, it is a lot harder to tell what the state of a particular
$scope is, depending on ongoing $digest loops, $watch calls, and
other magical constructs.
- 57. Quick look at FanCenter
White-label web app for user engagement with brands
Users complete various activities to earn rewards
- 61. From a dev perspective
☞ Heavy data-driven customization of UI
☞ Show this view if this client
☞ Rounded corners for that client
☞ Big views with dynamic subviews
☞ Composition?
- 63. We chose Marionette.js
Boilerplate views on top of Backbone to simplify common use
cases, eg. ItemView, CollectionView, Layout.
Keep in mind it is late 2012, Backbone.js is the cool kid on the block
It will be at least a year until anyone knows what Angular is
- 64. Marionette.js
class HomePage extends Marionette.Layout
template: HomePageJadeTemplate
# Marionette.Layout container for subviews
regions:
dashboardRegion: '#dashboard-region'
...
_initDashboard: =>
@dashboardView = new DashboardView({...});
@listenTo this, 'show', =>
@dashboardRegion.show @dashboardView
- 65. Backbone / Marionette
☞ Many moving parts
☞ Poor layout/view lifecycle control
☞ Spaghetti events around DOM
presence
☞ False separation of concerns
☞ Simple routing of urls to function
calls make managing state, nested
views, and re-renders a very
manual and tedious process.
- 66. Custom build tool, a.k.a Crusher
☞ In-house build tool
☞ Developed to facilitate control of custom client
implementations on the front-end (white-label)
☞ Each client has its own app.js and app.css files
☞ Built dynamically from a directory structure organized by
client id
☞ Leverages Sass $variables to override defaults with
custom configuration
- 67. Custom build tool, a.k.a Crusher
config.scss
clients/
├── 1
└── stylesheets
├── config.scss
- 68. Pros
☞ Front-end owns the view (kinda)
☞ Fits well into model of Cascading Style Sheets
☞ Sharing code with minimal duplication
- 69. Cons
☞ Maintenance costs of custom build tool
☞ Very (very) slow build time
☞ False separation of concerns
☞ Database still requires knowledge of specific client
configuration
☞ Back-end already provides view-driven data
- 70. What if we move all client configuration to the server and use
React to build components that manage their own data, styling,
and templates?
- 71. let HomePage = React.createClass({
mixins: [TextKeyMixin, StyleMixin],
statics: {
fetchData(params) {
return get({
page_data: {
text: ['title.home']
},
model_data: {
activities: ['id', 'title']
},
style_data: {
attributes: ['borderRadius', 'fontSize', 'backgroundColor']
}
});
}
},
render() {
return (
<div id="home-page" className={this.styleData}>
<h1>{this.getTextKey('title.home')}</h1>
<Activities activities={this.getModelData('activities')} />
</div>
);
}
});
- 72. Testing components is simple too..
import SampleComponent from "src/common/sample_component.jsx";
let {TestUtils} = React.addons;
describe('Sample Component', function() {
it('should render with no props', function() {
let sampleComponent = TestUtils.renderIntoDocument(
<SampleComponent />
);
let heading = TestUtils.findRenderedDOMComponentWithTag(
sampleComponent, 'h1');
expect(heading.getDOMNode().textContent).to.be.empty();
});
});
- 74. To conclude
☞ The problem of scaling large applications with magical
frameworks is real
☞ If you think you separated your concerns, think again
☞ The web is changing, keeping up is nearly impossible
☞ Ownership is important, how would you define simplicity?
☞ JavaScript is slowly solving its own problems
☞ There is no silver bullet
- 75. To conclude
☞ React's approach is to keep it small and contained
☞ It's just a library for building UI using JavaScript
Suddenly, it's no longer 2006.