实用技巧

优质
小牛编辑
132浏览
2023-12-01

We’re going to use Sinatra in a similar manner to how we used Express in the last chapter. It will power a RESTful API supporting CRUD operations. Together with a MongoDB data store, this will allow us to easily persist data (todo items) whilst ensuring they are stored in a database. If you’ve read the previous chapter or have gone through any of the Todo examples covered so far, you will find this surprisingly straight-forward.

Remember that the default Todo example included with Backbone.js already persists data, although it does this via a localStorage adapter. Luckily there aren’t a great deal of changes needed to switch over to using our Sinatra-based API. Let’s briefly review the code that will be powering the CRUD operations for this sections practical, as we won’t be starting off with a near-complete base for most of our real world applications.

Installing The Prerequisites

Ruby

If using OSX or Linux, Ruby may be one of a number of open-source packages that come pre-installed and you can skip over to the next paragraph. In case you would like to check if check if you have Ruby installed, open up the terminal prompt and type:

$ ruby -v

The output of this will either be the version of Ruby installed or an error complaining that Ruby wasn’t found.

Should you need to install Ruby manually (e.g for an operating system such as Windows), you can do so by downloading the latest version from http://www.ruby-lang.org/en/downloads/. Alternatively, RVM (Ruby Version Manager) is a command-line tool that allows you to easily install and manage multiple ruby environments with ease.

Ruby Gems

Next, we will need to install Ruby Gems. Gems are a standard way to package programs or libraries written in Ruby and with Ruby Gems it’s possible to install additional dependencies for Ruby applications very easily.

On OSX, Linux or Windows go to http://rubyforge.org/projects/rubygems and download the latest version of Ruby Gems. Once downloaded, open up a terminal, navigate to the folder where this resides and enter:

$> tar xzvf rubygems.tgz
$> cd rubygems
$> sudo ruby setup.rb

There will likely be a version number included in your download and you should make sure to include this when tying the above. Finally, a symlink (symbolic link) to tie everything togther should be run as follows:

$ sudo ln -s /usr/bin/gem1.8.17 /usr/bin/gem

To check that Ruby Gems has been correctly installed, type the following into your terminal:

$ gem -v

Sinatra

With Ruby Gems setup, we can now easily install Sinatra. For Linux or OSX type this in your terminal:

$ sudo gem install sinatra

and if you’re on Windows, enter the following at a command prompt:

c:\> gem install sinatra

Haml

As with other DSLs and frameworks, Sinatra supports a wide range of different templating engines. ERB is the one most often recommended by the Sinatra camp, however as a part of this chapter, we’re going to explore the use of Haml to define our application templates.

Haml stands for HTML Abstractional Markup Language and is a lightweight markup language abstraction that can be used to describe HTML without the need to use traditional markup language semantics (such as opening and closing tags).

Installing Haml can be done in just a line using Ruby Gems as follows:

$ gem install haml

MongoDB

If you haven’t already downloaded and installed MongoDB from an earlier chapter, please do so now. With Ruby Gems, Mongo can be installed in just one line:

$ gem install mongodb

We now require two further steps to get everything up and running.

1.Data directories

MongoDB stores data in the bin/data/db folder but won’t actually create this directory for you. Navigate to where you’ve downloaded and extracted Mongo and run the following from terminal:

sudo mkdir -p /data/db/
sudo chown `id -u` /data/db
2.Running and connecting to your server

Once this is done, open up two terminal windows.

In the first, cd to your MongoDB bin directory or type in the complete path to it. You’ll need to start mongod.

$ ./bin/mongod

Finally, in the second terminal, start the mongo shell which will connect up to localhost by default.

$ ./bin/mongo

MongoDB Ruby Driver

As we’ll be using the MongoDB Ruby Driver, we’ll also require the following gems:

The gem for the driver itself:

$ gem install mongo

and the driver’s other prerequisite, bson:

$ gem install bson_ext

This is basically a collection of extensions used to increase serialization speed.

That’s it for our prerequisites!.

Tutorial

To get started, let’s get a local copy of the practical application working on our system.

Application Files

Clone this repository and navigate to /practicals/stacks/option3. Now run the following lines at the terminal:

ruby app.rb

Finally, navigate to http://localhost:4567/todo to see the application running successfully.

Note: The Haml layout files for Option 3 can be found in the /views folder.

The directory structure for our practical application is as follows:

--public
----css
----img
----js
-----script.js
----test
--views
app.rb

The public directory contains the scripts and stylesheets for our application and uses HTML5 Boilerplate as a base. You can find the Models, Views and Collections for this section within public/js/scripts.js (however, this can of course be expanded into sub-directories for each component if desired).

scripts.js contains the following Backbone component definitions:

--Models
----Todo

--Collections
----TodoList

--Views
---TodoView
---AppView

app.rb is the small Sinatra application that powers our backend API.

Lastly, the views directory hosts the Haml source files for our application’s index and templates, both of which are compiled to standard HTML markup at runtime.

These can be viewed along with other note-worthy snippets of code from the application below.

Backbone

Views

In our main application view (AppView), we want to load any previously stored Todo items in our Mongo database when the view initializes. This is done below with the line Todos.fetch() in the initialize() method where we also bind to the relevant events on the Todos collection for when items are added or changed.

// Our overall **AppView** is the top-level piece of UI.
var AppView = Backbone.View.extend({

    // Instead of generating a new element, bind to the existing skeleton of
    // the App already present in the HTML.
    el: $("#todoapp"),

    // Our template for the line of statistics at the bottom of the app.
    statsTemplate: _.template($('#stats-template').html()),

    // Delegated events for creating new items, and clearing completed ones.
    events: {
      "keypress #new-todo":  "createOnEnter",
      "keyup #new-todo":     "showTooltip",
      "click .todo-clear a": "clearCompleted"
    },

    // At initialization
    initialize: function() {
      this.input    = this.$("#new-todo");

      Todos.on('add',   this.addOne, this);
      Todos.on('reset', this.addAll, this);
      Todos.on('all',   this.render, this);

      Todos.fetch();
    },

    // Re-rendering the App just means refreshing the statistics -- the rest
    // of the app doesn't change.
    render: function() {
      this.$('#todo-stats').html(this.statsTemplate({
        total:      Todos.length,
        done:
 ….

Collections

In the TodoList collection below, we’ve set the url property to point to /api/todos to reference the collection’s location on the server. When we attempt to access this from our Sinatra-backed API, it should return a list of all the Todo items that have been previously stored in Mongo.

For the sake of thoroughness, our API will also support returning the data for a specific Todo item via /api/todos/itemID. We’ll take a look at this again when writing the Ruby code powering our backend.

 // Todo Collection

  var TodoList = Backbone.Collection.extend({

    // Reference to this collection's model.
    model: Todo,

    // Save all of the todo items under the `"todos"` namespace.
    // localStorage: new Store("todos"),
    url: '/api/todos',

    // Filter down the list of all todo items that are finished.
    done: function() {
      return this.filter(function(todo){ return todo.get('done'); });
    },

    // Filter down the list to only todo items that are still not finished.
    remaining: function() {
      return this.without.apply(this, this.done());
    },

    // We keep the Todos in sequential order, despite being saved by unordered
    // GUID in the database. This generates the next order number for new items.
    nextOrder: function() {
      if (!this.length) return 1;
      return this.last().get('order') + 1;
    },

    // Todos are sorted by their original insertion order.
    comparator: function(todo) {
      return todo.get('order');
    }

  });

Model

The model for our Todo application remains largely unchanged from the versions previously covered in this book. It is however worth noting that calling the function model.url() within the below would return the relative URL where a specific Todo item could be located on the server.


  // Our basic **Todo** model has `text`, `order`, and `done` attributes.
  var Todo = Backbone.Model.extend({
    idAttribute: "_id",

    // Default attributes for a todo item.
    defaults: function() {
      return {
        done:  false,
        order: Todos.nextOrder()
      };
    },

    // Toggle the `done` state of this todo item.
    toggle: function() {
      this.save({done: !this.get("done")});
    }
  });

Ruby/Sinatra

Now that we’ve defined our main models, views and collections let’s get the CRUD operations required by our Backbone application supported in our Sinatra API.

We want to make sure that for any operations changing underlying data (create, update, delete) that our Mongo data store correctly reflects these.

app.rb

For app.rb, we first define the dependencies required by our application. These include Sinatra, Ruby Gems, the MongoDB Ruby driver and the JSON gem.

require 'rubygems'
require 'sinatra'
require 'mongo'
require 'json'

Next, we create a new connection to Mongo, specifying any custom configuration desired. If running a multi-threaded application, setting the pool_size allows us to specify a maximum pool size and timeout a maximum timeout for waiting for old connections to be released to the pool.

DB = Mongo::Connection.new.db("mydb", :pool_size => 5, :timeout => 5)

Finally we define the routes to be supported by our API. Note that in the first two blocks - one for our application root (/) and the other for our todo items route /todo - we’re using Haml for template rendering.

class TodoApp < Sinatra::Base

    get '/' do
      haml :index, :attr_wrapper => '"', :locals => {:title => 'hello'}
    end

    get '/todo' do
      haml :todo, :attr_wrapper => '"', :locals => {:title => 'Our Sinatra Todo app'}
    end

haml :index instructs Sinatra to use the views/index.haml for the application index, whilst attr_wrapper is simply defining the values to be used for any local variables defined inside the template. This similarly applies Todo items with the template `views/todo.haml’.

The rest of our routes make use of the params hash and a number of useful helper methods included with the MongoDB Ruby driver. For more details on these, please read the comments I’ve made inline below:

get '/api/:thing' do
  # query a collection :thing, convert the output to an array, map the _id
  # to a string representation of the object's _id and finally output to JSON
  DB.collection(params[:thing]).find.to_a.map{|t| from_bson_id(t)}.to_json
end

get '/api/:thing/:id' do
  # get the first document with the id :id in the collection :thing as a single document (rather
  # than a Cursor, the standard output) using find_one(). Our bson utilities assist with
  # ID conversion and the final output returned is also JSON
  from_bson_id(DB.collection(params[:thing]).find_one(to_bson_id(params[:id]))).to_json
end

post '/api/:thing' do
  # parse the post body of the content being posted, convert to a string, insert into
  # the collection #thing and return the ObjectId as a string for reference
  oid = DB.collection(params[:thing]).insert(JSON.parse(request.body.read.to_s))
  "{\"_id\": \"#{oid.to_s}\"}"
end

delete '/api/:thing/:id' do
  # remove the item with id :id from the collection :thing, based on the bson
  # representation of the object id
  DB.collection(params[:thing]).remove('_id' => to_bson_id(params[:id]))
end

put '/api/:thing/:id' do
  # collection.update() when used with $set (as covered earlier) allows us to set single values
  # in this case, the put request body is converted to a string, rejecting keys with the name '_id' for security purposes
  DB.collection(params[:thing]).update({'_id' => to_bson_id(params[:id])}, {'$set' => JSON.parse(request.body.read.to_s).reject{|k,v| k == '_id'}})
end

# utilities for generating/converting MongoDB ObjectIds
def to_bson_id(id) BSON::ObjectId.from_string(id) end
def from_bson_id(obj) obj.merge({'_id' => obj['_id'].to_s}) end

end

That’s it. The above is extremely lean for an entire API, but does allow us to read and write data to support the functionality required by our client-side application.

For more on what MongoDB and the MongoDB Ruby driver are capable of, please do feel free to read their documentation for more information.

If you’re a developer wishing to take this example further, why not try to add some additional capabilities to the service:

  • Validation: improved validation of data in the API. What more could be done to ensure data sanitization?
  • Search: search or filter down Todo items based on a set of keywords or within a certain date range
  • Pagination: only return the Nth number of Todo items or items from a start and end-point

Haml/Templates

Finally, we move on to the Haml files that define our application index (layout.haml) and the template for a specific Todo item (todo.haml). Both of these are largely self-explanatory, but it’s useful to see the differences between the Jade approach we reviewed in the last chapter vs. using Haml for this implementation.

Note: In our Haml snippets below, the forward slash character is used to indicate a comment. When this character is placed at the beginning of a line, it wraps all of the text after it into a HTML comment. e.g

/ These are templates

compiles to:

<!-- These are templates -->

index.haml

%head
  %meta{'charset' => 'utf-8'}/
  %title=title
  %meta{'name' => 'description', 'content' => ''}/
  %meta{'name' => 'author', 'content' => ''}/
  %meta{'name' => 'viewport', 'content' => 'width=device-width,initial-scale=1'}/

  / CSS concatenated and minified via ant build script
  %link{'rel' => 'stylesheet', 'href' => 'css/style.css'}/
  / end CSS

  %script{'src' => 'js/libs/modernizr.min.js'}
%body
  %div#container
    %header
    %div#main
      = yield
    %footer
  /! end of #container

  %script{'src' => 'http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js'}

  / scripts concatenated and minified via ant build script
  %script{'src' => 'js/mylibs/underscore.js'}
  %script{'src' => 'js/mylibs/backbone.js'}
  %script{'defer' => true, 'src' => 'js/plugins.js'}
  %script{'defer' => true, 'src' => 'js/script.js'}
  / end scripts

todo.haml

%div#todoapp
  %div.title
    %h1
      Todos
      %div.content
        %div#create-todo
          %input#new-todo{"placeholder" => "What needs to be done?", "type" => "text"}/
          %span.ui-tooltip-top{"style" => "display:none;"} Press Enter to save this task
        %div#todos
          %ul#todo-list
        %div#todo-stats

/ Templates

%script#item-template{"type" => "text/template"}
  <div class="todo <%= done ? 'done' : '' %>">
  %div.display
    <input class="check" type="checkbox" <%= done ? 'checked="checked"' : '' %> />
    %div.todo-text
    %span#todo-destroy
  %div.edit
    %input.todo-input{"type" => "text", "value" =>""}/
  </div>

%script#stats-template{"type" => "text/template"}
  <% if (total) { %>
  %span.todo-count
    %span.number <%= remaining %>
    %span.word <%= remaining == 1 ? 'item' : 'items' %>
    left.
  <% } %>
  <% if (done) { %>
  %span.todo-clear
    %a{"href" => "#"}
      Clear
      %span.number-done <%= done %>
      completed
      %span.word-done <%= done == 1 ? 'item' : 'items' %>
  <% } %>

Conclusions

In this chapter, we looked at creating a Backbone application backed by an API powered by Ruby, Sinatra, Haml, MongoDB and the MongoDB driver. I personally found developing APIs with Sinatra a relatively painless experience and one which I felt was on-par with the effort required for the Node/Express implementation of the same application.

This section is by no means the most comprehensive guide on building complex apps using all of the items in this particular stack. I do however hope it was an introduction sufficient enough to help you decide on what stack to try out for your next project.