This tutorial came out of many frustrating google searches trying to find a decent end-to-end boilerplate example of incorporating React as the view component in Backbone. The three components I wanted in a tutorial were:
- Build something more than a single page TODO
- Involved routing and controllers
- Included a build process using a modern build tool
- Optimization and source-mapping
- Done in an organized manner with descriptive naming patterns
What we'll be building in this tutorial is a basic blog viewer. The source can be found here and the finished product can be found here. When complete, we'll be able to write the blog post in markdown in a gist with the title starting with a key word (ex. @post Here's My Post Title!) and display it in all its html glory.
So let's get started.
This tutorial assumes that you have Node.js with npm (preferably with nvm which can be installed via Homebrew on OSX).
If entering which npm
gives you a path, you should be good to go.
Be sure you've installed Gulp globally with the command:
sudo npm install -g gulp
And we'll needing a local server, so go ahead and run sudo npm install -g http-server
. We'll use that later.
Let's set up our environment. In a new directory, create a package.json with these packages:
{
"name": "gistblog",
"version": "0.0.1",
"dependencies": {
"backbone": "^1.2.1",
"backbone-react-component": "^0.8.1",
"backbone.controller": "^0.3.1",
"jquery": "^2.1.4",
"marked": "^0.3.3",
"react": "^0.13.3",
"underscore": "^1.8.3"
},
"devDependencies": {
"browserify": "^10.2.4",
"gulp": "^3.9.0",
"gulp-rename": "^1.2.2",
"gulp-sourcemaps": "^1.5.2",
"gulp-uglify": "^1.2.0",
"gulp-util": "^3.0.6",
"lodash.assign": "^3.2.0",
"reactify": "^1.1.1",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"watchify": "^3.2.3"
}
}
and run npm install
.
Please google all of these packages, as they all do amazing things and are generally well documented.
Let's set up our build system. Create a gulpfile.js and let's add a few things.
// gulpfile.js
'use strict';
var watchify = require('watchify');
var browserify = require('browserify');
var gulp = require('gulp');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var gutil = require('gulp-util');
var sourcemaps = require('gulp-sourcemaps');
var assign = require('lodash.assign');
var reactify = require('reactify');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
// add custom browserify options here
var customOpts = {
entries: ['./app.js'],
debug: true
};
// combine arguments passed in with
// customOpts
var opts = assign({}, watchify.args, customOpts);
// create watchified-browserify object
var b = watchify(browserify(opts));
// transform react syntax
b.transform('reactify');
// so you can run `gulp js` to build the file
gulp.task('js', bundle);
// on any dep update, runs the bundler
b.on('update', bundle);
// output build logs to terminal
b.on('log', gutil.log);
function bundle() {
return b.bundle()
// simply pipes in a filename
.pipe(source('bundle.js'))
// more info here https://goo.gl/htmChu
.pipe(buffer())
// build sourcemaps
// loads map from browserify file
.pipe(sourcemaps.init({loadMaps: true}))
// writes .map file to relative location
// of bundle.js
.pipe(sourcemaps.write('./'))
// destination of bundle.js
.pipe(gulp.dest('./'));
}
// run 'gulp uglify' to produce optimized javascript
gulp.task('uglify', function() {
return gulp.src('bundle.js')
.pipe(uglify())
.pipe(rename('bundle.min.js'))
.pipe(gulp.dest('./'));
});
Let's create our index.html. We'll keep it extremely simple.
<!--index.html-->
<html>
<head>
</head>
<body>
<div id="content"></div>
<script src="./bundle.js"></script>
</body>
</html>
And now we can start on some Javascript. Let's build our entry point into the app. This is what will be tracked by Gulp and Watchify. If anything in this file changes, or any of its required dependencies, or any of the required depenedencies of the required dependencies, etc, gulp should rebuild bundle.js. This will be our app.js:
// app.js
'use strict';
var Backbone = require('backbone');
var PostsController = require('./controllers/PostsController');
var Application = Backbone.Router.extend({
// our router can keep track of our
// instantiated controllers
controllers: {},
initialize: function() {
// instantiate a new PostsController and
// pass in this router
this.controllers.posts = new PostsController({router: this});
// This is to start Backbone listening to
// url hash changes. It will allow us to
// navigate using the back and forward
// buttons on the browser
Backbone.history.start();
}
});
// start the app!
window.app = new Application();
Let's define our models/PostModel.js
// models/PostModel.js
var Backbone = require('backbone');
var marked = require('marked');
module.exports = Backbone.Model.extend({
// here is the api endpoint to receive a
// single gist object
urlRoot: 'https://api.github.com/gists/',
// this attribute is added to the end by
// backbone when making a fetch() call
idAttribute: 'id',
// parse will parse through the raw data
// coming back and let us adjust it to fit
// our needs
parse: function(model)
{
// removes the "@post" from the title
model.description = model.description.replace("@post ", "");
// grabs content from the first file
// included in the gist
var content = model.files[Object.keys(model.files)[0]].content
if (content) {
// if the content exists, run through
// markdown decoder and make it safe
model.content = marked(content, { sanitize: true });
}
// return the adjusted json model for
// backbone.
return model;
}
});
And now for our collections/PostsCollections.js
// collections/PostsCollections.js
var Backbone = require('backbone');
var _ = require('underscore');
var PostModel = require('../models/PostModel');
module.exports = Backbone.Collection.extend({
// here we have our gists endpoint,
// feel free to enter your own github
// username!
url: 'https://api.github.com/users/mistakevin/gists',
model: PostModel,
// let's parse through the returned json
parse: function(collection)
{
// we want only the gists that start
//with '@post '
collection = _.filter(collection, function (model) {
return model.description.indexOf('@post ') > -1;
});
return collection;
}
});
Next, let's create our controllers/PostsController.js
// controllers/PostsController.js
var Backbone = require('backbone');
var React = require('react');
require('backbone.controller');
var PostComponent = require('../components/PostComponent');
var PostsListComponent = require('../components/PostsListComponent');
var PostsCollection = require('../collections/PostsCollection');
var PostModel = require('../models/PostModel');
var $ = require('jquery');
module.exports = Backbone.Controller.extend({
routes: {
'': 'index',
'/': 'index',
'posts/:id': 'show',
'posts': 'index',
},
initialize: function() {
this.collection = new PostsCollection();
},
// this will render the list of posts
index: function() {
// fetch our collection
this.collection.fetch();
// don't forget to pass in your
// collection! At this point, the
// collection probably isn't finished
// fetching, but backebone.react will
// update the the view/component for us
// on a successful sync!
React.render(<PostsListComponent collection={this.collection} />, $('#content')[0]);
},
// this will show the individual post and
// content
show: function(id) {
// if the link is clicked, we'll grab the
// id from the params then grab that
// model out of the already fetched
// collection
var post = this.collection.get(id);
if (!post) {
// if the collection was never fetched,
// as in a page refresh on the
// individual post page, we'll create a
// new model with only the id
// attribute, and that will give us
// enough information to fetch
post = new PostModel({ id: id });
}
// let's fetch our post
post.fetch();
// don't forget to pass in your model!
React.render(<PostComponent model={post} />, $('#content')[0]);
}
});
And now our different compoenents, which will consist of a PostsListComponent, PostItemComponent, and a PostComponent. First in our components/PostsListComponent.js we'll create the list that hold all of the post items.
// components/PostsListComponent
var React = require('react');
var Backbone = require('backbone');
require('backbone-react-component');
module.exports = React.createClass({
mixins: [Backbone.React.Component.mixin],
// every component will have a render()
// function
render: function() {
// the way you access the collection
// passed in is with
// this.state.collection here we are
// mapping through our collection of
// posts, creating a component
// PostItemComponent and passing in
// postItem model as model
var postItems = this.state.collection.map(function(postItem) {
return <PostItemComponent model={postItem} />
});
// we'll return the component, passing in
// the postItems 'mustache style'
return (
<div>
<h1>Posts</h1>
{postItems}
</div>
);
}
});
And for our components/PostItemComponent.js
// components/PostItemComponent.js
var Backbone = require('backbone')
var React = require('react');
require('backbone-react-component');
module.exports = React.createClass({
mixins: [Backbone.React.Component.mixin],
// here we can see that when we pass something
// through a component as a prop, we can access
// it from this.props
render: function () {
return (
<div>
<h3>
<a href={ "#posts/" + this.props.model.id}>
{this.props.model.description}
</a>
</h3>
</div>
);
}
});
From here we can create the components/PostComponent.js, which will be where the item links to so we can read the content of our post!
// components/PostComponent.js
var React = require('react');
var Backbone = require('backbone');
require('backbone-react-component');
module.exports = React.createClass({
mixins: [Backbone.React.Component.mixin],
// the 'dangerouslySetInnerHTML is a way to
// get around the automatic escaping. Since
// we 'sanitized' in our model, we should
// be ok
render: function() {
return (
var content = this.state.model.content;
<div>
<h1>{this.state.model.description}</h1>
<p dangerouslySetInnerHTML={{__html: content}}></p>
</div>
);
}
});
Alright! We should be good to go. Let's run gulp js
and boot up our http-server
in the directory of our project and navigate to localhost:8080 in your browser. Hopefully you'll have your very own blog, ready to deploy to Github Pages.
If you haven't figured it out, this blog you are reading is a stylized verison of the same source on the styled branch!
Please feel free to leave comments and questions below the gist located here.