[WB//Rails4]

Kilka prostych przykładów

[Frederick P. Brooks, Jr.]

To see what rate of progress one can expect in software technology, let us examine the difficulties of that technology. Following Aristotle, I divide them into essence, the difficulties inherent in the nature of software, and accidents, those difficulties that today attend its production but are not inherent.

I believe the hard part of building software to be the specification, design, and testing of this conceptual construct, not the labor of representing it and testing the fidelity of the representation. We still make syntax errors, to be sure; but they are fuzz compared with the conceptual errors in most systems.

— Frederick P. Brooks, Jr.

Jak wygląda praca z frameworkiem Ruby on Rails pokażemy na przykładzie aplikacji MyGistsMyStaticPages.

Czym jest Ruby on Rails: „Ruby on Rails is an MVC framework for web application development. MVC is sometimes called a design pattern, but thats not technically accurate. It is in fact an amalgamation of design patterns (sometimes referred to as an architecture pattern).”

„Gang of Four” („GoF” = E. Gamma, R. Helm, R. Johnson, J. Vlissides) tak definiuje MVC w książce Wzorce Projektowe): „Model jest obiektem aplikacji. Widok jego ekranową reprezentacją, zaś Koordynator (kontroler) definiuje sposób, w jaki interfejs użytkownika reaguje na operacje wykonywane przez użytkownika. Przed MVC w projektach interfejsu użytkownika te trzy obiekty były na ogół łączone. MVC rozdziela je, aby zwiększyć elastyczność i możliwość wielokrotnego wykorzystywania.

[MVC w Rails]
Schemat aplikacji WWW korzystającej ze wzorca MVC (źródło)

Dlaczego tak postępujemy?
„MVC rozdziela widoki i model, ustanawiając między nimi protokół powiadamiania. Widok musi gwarantować, że jego wygląd odzwierciedla stan modelu. Gdy tylko dane modelu się zmieniają, model powiadamia zależące od niego widoki. Dzięki temu każdy widok ma okazję do uaktualnienia się. To podejście umożliwia podłączenie wielu widoków do jednego modelu w celu zapewnienia różnych prezentacji tych danych. Można także tworzyć nowe widoki dla modelu bez potrzeby modyfikowania go.”

Więcej szczegółów na temat MVC:

Rails Girls

Co dalej?

MyGists

Zaczynamy od wygenerowania rusztowanie aplikacji:

rails new my_gists --skip-bundle

Następnie przechodzimy do katalogu z wygenerowanym kodem:

cd my_gists

gdzie w pliku Gemfile dopisujemy gemy z których będziemy korzystać:

Gemfile
gem 'pygments.rb'
gem 'redcarpet'
gem 'quiet_assets'

Gemy instalujemy za pomocą programu bundle:

bundle install

Jeśli nie mamy uprawnień do instalacji gemów w systemie, instalujemy je gdzieś w swoim katalogu domowym, np. w katalogu ~/.gems:

bundle install --path=$HOME/.gems

Ale jeśli gemy z których korzystamy są już zainstalowane w systemie, to możemy użyć opcji --local w trakcie ich instalacji. Taka instalacja wykonuje się dużo szybciej!

Szablon aplikacji CRUD utworzymy za pomocą generatora kodu o nazwie scaffold:

rails generate scaffold gist snippet:text lang:string description:string

Po wygenerowaniu kodu wykonujemy migrację:

rake db:migrate

Po uruchomieniu prostego serwera HTTP:

rails server --port 3000

Aplikacja jest dotępna z takiego url localhost:3000.

Routing aplikacji sprawdzamy wykonując na konsoli polecenie:

rake routes

lub w przeglądarce localhost:3000/rails/info/routes.

Więcej informacji znajdziemy w sekcji Routing w Rails Guides.

Kolorowanie kodu:

Na koniec podmieniamy zawartośc pliku app/views/gists/show.html.erb na:

<p id="notice"><%= notice %></p>
<p>
  <strong>Lang:</strong>
  <%= @gist.lang %>
</p>
<p>
  <strong>Snippet:</strong>
</p>
<%= raw Pygments.highlight(@gist.snippet, lexer: @gist.lang) %>
<p>
  <strong>Description:</strong>
  <%= @gist.description %>
</p>
<%= link_to 'Edit', edit_gist_path(@gist) %> |
<%= link_to 'Back', gists_path %>

Dodajemy nowy plik app/assets/stylesheets/pygments.css.erb:

<%= Pygments.css(style: "colorful") %>

Lektura dokumentacji funkcji pomocniczej link_to. Co to są assets? a partial templates (szablony częściowe), na przykład _form.html.erb.

MyStaticPages

Jak wyżej, usuwamy niepotrzebne gemy z pliku Gemfile dodajemy gemy z których będziemy korzystać.

Następnie generujemy rusztowanie aplikacji:

rails new my_static_pages --skip-bundle
cd my_static_pages
bundle install --local

W tej aplikacji skorzystamy z generatora kodu o nazwie controller:

rails generate controller pages welcome about

  create  app/controllers/pages_controller.rb
   route  get "pages/about"
   route  get "pages/welcome"
  invoke  erb
  create    app/views/pages
  create    app/views/pages/welcome.html.erb
  create    app/views/pages/about.html.erb

Routing:

rake routes

       Prefix Verb URI Pattern             Controller#Action
pages_welcome GET /pages/welcome(.:format) pages#welcome
  pages_about GET /pages/about(.:format)   pages#about

co oznacza, że te strony będą dostępne z adresów /pages/welcome/pages/about.

Lektura Rails API oraz Rails Guides:

Zrób to sam (kod jest poniżej):

W implementacji metody title skorzystamy z metody provide.

Na każdej stronie wpisujemy jej tytuł w taki sposób:

<% provide :title, 'About Us' %>

W layoucie aplikacji podmieniamy kod znacznika title na:

<title>Moje strony | <%= content_for :title %></title>

I już możemy sprawdzić jak to działa.

A oto inna, bardziej solidna implementacja:

application_helper.rb
def title(content)
  content_for(:title, content)
end

def page_title
  delimiter = "| "
  if content_for?(:title)
    "#{delimiter}#{content_for(:title)}"
  end
end

Przy tej implementacji, w layoucie aplikacji podmieniamy kod znacznika title na:

<title>Moje strony <%= page_title %></title>

Teraz na stronach, które mają tytuł wpisujemy:

<% title 'About Us' %>

Pytanie: Na czym polega „solidność tej implementacji”?

W pliku config/routes.rb wygenerowany kod:

config/routes.rb
get "pages/welcome"
get "pages/about"

wymieniamy na:

config/routes.rb
get "welcome", to: "pages#welcome"
get "about", to: "pages#about"

Przy zmienionym routingu wykonanie polecenia rake routes daje:

 Prefix Verb URI Pattern       Controller#Action
welcome GET /welcome(.:format) pages#welcome
  about GET /about(.:format)   pages#about

co oznacza, że te strony są dostępne z krótszych, niż poprzednio, adresów /welcome/about.

TODO

Wstawić kilka obrazków na stronach welcome i about. Skorzystać z metody pomocniczej image_tag.

MyPlaces

Generujemy szablon aplikacji:

rails new my_places --skip-bundle --skip-active-record

Dopisałem opcję --skip-active-record ponieważ będziemy korzystać z bazy MongoDB i gemu (drivera) Mongoid.

W pliku Gemfile dopisujemy gem Mongoid:

Gemfile
gem 'mongoid', '~> 4.0.2'
# gem 'mongoid', github: 'mongoid/mongoid'

Przy okazji dodamy plik .ruby-gemset:

.ruby-gemset
my_places

Na koniec wygenerujemy plik konfiguracyjny dla MongoDB:

rails g mongoid:config
  create  config/mongoid.yml

Moje lotniska

Tym razem zaczniemy od importu ciekawych miejsc do bazy MongoDB.

Pobieramy plik airports.csv ze strony Our Airports. Pobrane dane zapiszemy w bazie MongoDB w kolekcji airports. za pomocą programu mongoimport:

mongoimport --collection airports  --headerline --type csv airports.csv
  connected to: 127.0.0.1
  2014-02-08T21:44:49.825+0100 check 9 45618
  2014-02-08T21:44:49.897+0100 imported 45617 objects

Oto przykładowy rekord zapisany w kolekcji airports:

{
  "_id": ObjectId("52f6973f4fc0cda07918c564"),
  "id": 6523,
  "ident": "00A",
  "type": "heliport",
  "name": "Total Rf Heliport",
  "latitude_deg": 40.07080078125,
  "longitude_deg": -74.9336013793945,
  "elevation_ft": 11,
  "continent": "NA",
  "iso_country": "US",
  "iso_region": "US-PA",
  "municipality": "Bensalem",
  "scheduled_service": "no",
  "gps_code": "00A",
  "iata_code": "",
  "local_code": "00A",
  "home_link": "",
  "wikipedia_link": "",
  "keywords": ""
}

W kolekcji airports, ciekawe dla mnie miejsca, to dokumenty z informacjami o lotniskach w Polsce. Dokumenty te przekształcimy na takie GeoJSON-y:

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [0, 0]
  },
  "properties": {
    "title": "Konstancin-Jeziorna Airfield",
    "description": "small_airport",
    "marker-size": "medium",
    "marker-symbol": "airport"
  }
}

i w takim formacie zapiszemy je w kolekcji lotniska.

Wszystkie te rzeczy wykonamy na konsoli mongo, korzystając z takiego kodu:

mongo
var cursor = db.airports.find({iso_country: "PL"})
while (cursor.hasNext()) {
  var doc = cursor.next()
  var spec = {
    "type": "Feature",
    "geometry": {
      "type": "Point",
      "coordinates": [doc["longitude_deg"], doc["latitude_deg"]]
    },
    "properties": {
      // a title to show when this item is clicked or hovered over
      "title": doc["name"],
      // a description to show when this item is clicked or hovered over
      "description": doc["type"],
      "marker-size": "medium",
      "marker-symbol": "airport"
    }
  };
  db.lotniska.insert(spec);
}
db.lotniska.count(); #-> 170

W markerach na mapkach (GitHub, Mapbox) wykorzystywany jest zestaw ikon MakiMapbox. W GeoJSON-ach powyżej użyjemy ikony o nazwie airport.

Prosta wizualizacja danych

Zanim zapiszemy dane o lotniskach w bazie i w kolekcji aplikacji MyPlaces przyjrzymy się im bliżej.

W tym celu dokumenty z kolekcji lotniska zapiszemy w pliku w przyjaźniejszym formacie simplestyle spec v1.1.0. Pliki w tym formacie są renderowane w postaci mapek przez serwer GitHub.

Do zmiany formatu użyjemy programów mongoexportjq:

mongoexport -c lotniska | \
jq '{type, geometry, properties}' | \
jq -s . | \
jq '{"type": "FeatureCollection", features: .}' \
> lotniska.geojson

Pytanie: Czy można to zrobić prościej?

Plik lotniska.geojson umieściłem w repozytorium na GitHubie i wyrenderowaną mapkę można obejrzeć tutaj lub poniżej:

Mongoid

Jeśli zajrzymy do pliku config/mongoid.yml:

development:
  sessions:
    default:
      database: my_places_development
      hosts:
        - localhost:27017
      options:
        # Change the default write concern. (default = { w: 1 })
        # write:
        # w: 1
  # Configure Mongoid specific options. (optional)
  options:
    # Includes the root model name in json serialization. (default: false)
    # include_root_in_json: false
    # Include the _type field in serializaion. (default: false)
    # include_type_for_serialization: false

to zobaczymy, że aplikacja MyPlaces oczekuje w trybie development, że dane zostaną zapisane w bazie o nazwie my_places_development.

Teraz wygenerujemy entire resource dla danych:

rails generate scaffold airport type:String geometry:Hash properties:Hash

Ponieważ nazwa resource to airport, dane zapisujemy w kolekcji o nazwie airports:

mongoexport -d test -c lotniska | \
mongoimport -d my_places_development -c airports --drop --type json

Sprawdzamy na konsoli rails, czy stosujemy się do convention over configuration używanych w aplikacjach Rails.

rails console --sandbox
a = Airport.first
  #<Airport _id: 52f92e405debff06c39ea6ee,
   type: "Feature",
   geometry: {"type"=>"Point",
              "coordinates"=>[22.5142993927002, 49.6575012207031]},
   properties: {"title"=>"Arlamów Airport",
                "description"=>"small_airport",
                "marker-size"=>"medium", "marker-symbol"=>"airport"}>
a.type
  # "Feature"
a.geometry["coordinates"].class
  # Array
a.properties["description"] = "big_airport"
a.save

Jest dobrze!

Mapka zamiast tabeli

Po wejściu na stronę airports renderowana jest tabelka. Tabelki nie specjalnie nadają się do prezentacji GeoJSON-ów.

Dlatego kod widoku:

app/views/airports/index.html.erb
<h1>Listing airports</h1>
<table>
  <thead>
    <tr>
      <th>Type <th>Geometry <th>Properties <th> <th> <th>
    </tr>
  </thead>
  <tbody>
    <% @airports.each do |airport| %>
      <tr>
        <td><%= airport.type %></td>
        <td><%= airport.geometry %></td>
        <td><%= airport.properties %></td>
        <td><%= link_to 'Show', airport %></td>
        <td><%= link_to 'Edit', edit_airport_path(airport) %></td>
        <td><%= link_to 'Destroy', airport, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>
<br>
<%= link_to 'New Airport', new_airport_path %>

zastąpimy mapką. Do generowania mapek użyjemy biblioteki Leaflet. Przy okazji, warto wiedzieć, zobacz Mapping geoJSON files on GitHub, że mapki na GitHubie też korzystają z biblioteki Leaflet oraz wtyczki do niej o nazwie Leaflet.markercluster.

Leaflet maps from scratch

Zanim się zabierzemy za dodawanie mapki do widoku index.html.erb przyjrzyjmy się tym przykładom: index, overlays and layers, geojson.

Podmieniamy kod widoku index.html.erb na:

<div id="map"></div>
<%= javascript_include_tag "airports", "data-turbolinks-track" => true %>

Dodajemy plik airports.js:

app/assets/javascripts/airports.js
// dane z OpenStreetMap
var osm = {
  url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
  attribution: '&copy; <a href="http://openstreetmap.org">OpenStreetMap</a> Contributors'
};

// tworzymy mapkę – współrzędne: [ szerokość, długość ]
var map = L.map('map').setView([52.05735, 19.19209], 6);  // center: Łęczyca, zoom: 6
var osmTileLayer = L.tileLayer(osm.url, {attribution: osm.attribution})
osmTileLayer.addTo(map);

// dodajemy markery do mapki
$.getJSON("/airports", function(data) {
  console.log(data.length);

  L.geoJson(data, {
    pointToLayer: function (feature, latlng) {
      return L.marker(latlng, { riseOnHover: true });
    },
    onEachFeature: function(feature, layer) {
      layer.bindPopup(feature.properties.title + '<br>(' + feature.properties.description + ')');
    }
  }).addTo(map);
});

Na koniec dodajemy bibliotekę Leaflet do layoutu aplikacji.

Szablon aplikacji MyPlaces do pobrania z GitHuba.

Leaflet maps via leaflet-rails gem

Czy warto skorzystać z jakiegoś gemu?

TODO

  1. W aplikacji MyPlaces operacje CRUD zaprogramować jako remote (AJAX). Do CREATE i DELETE użyć mapki.
  2. Utworzyć indeks 2dsphere. Natępnie dodać wyszukiwanie lotnisk.
  3. Użyć gemu Geocoder.

Wskazówki:

Podsumowanie

Często używane opcje zapisujemy w pliku ~/.railsrc:

~/.railsrc
--skip-bundle

[nifty secretary]

źródło: Retro Graphics, WordPress Site

Serwery WWW & aplikacje Rails

Każdy serwer ma swoje mocne i słabe strony.

Thin is a Ruby web server that glues together 3 of the best Ruby libraries in web history:

Unicorn is an HTTP server for Rack applications designed to only serve fast clients on low-latency, high-bandwidth connections and take advantage of features in Unix/Unix-like kernels. Slow clients should only be served by placing a reverse proxy capable of fully buffering both the the request and response in between Unicorn and slow clients.

[puma logo]

Puma is built for speed and parallelism.

Rainbows! is an HTTP server for sleepy Rack applications. It is based on Unicorn, but designed to handle applications that expect long request/response times and/or slow clients.