Статьи

Пение с Синатрой: приложение Recall

Добро пожаловать на второй трек пения с Синатрой . В первой части мы рассмотрели маршруты, как работать с параметрами URI, работать с формами и как Sinatra различает маршруты по HTTP-методу, по которому они были запрошены. Сегодня мы собираемся расширить наши знания о Sinatra, создав небольшое приложение на основе базы данных «Напомним» для ведения заметок / составления списка дел.

Мы будем использовать базу данных SQLite для хранения заметок, и мы будем использовать DataMapper RubyGem для связи с базой данных. Запустите следующее внутри оболочки, чтобы установить соответствующие Gems:

1
gem install sqlite3 datamapper dm-sqlite-adapter

В зависимости от того, как вы настроили RubyGems в вашей системе, вам может потребоваться префикс gem install с помощью sudo .


Давайте сразу же recall.rb к созданию нового каталога для проекта и файла приложения, recall.rb Начните, требуя соответствующие драгоценные камни:

1
2
3
require ‘rubygems’
require ‘sinatra’
require ‘datamapper’

Примечание. Если вы используете Ruby 1.9 (которым вы и должны быть), вы можете удалить строку «require ‘rubygems'», так как Ruby все равно автоматически загружает RubyGems.

И настройте базу данных следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
DataMapper::setup(:default, «sqlite3://#{Dir.pwd}/recall.db»)
 
class Note
  include DataMapper::Resource
  property :id, Serial
  property :content, Text, :required => true
  property :complete, Boolean, :required => true, :default => false
  property :created_at, DateTime
  property :updated_at, DateTime
end
 
DataMapper.finalize.auto_upgrade!

В первой строке мы устанавливаем новую базу данных SQLite3 в текущем каталоге, называемую recall.db Ниже мы на самом деле настраиваем таблицу «Примечания» в базе данных.

Пока мы вызываем класс «Примечание», DataMapper создаст таблицу как «Примечания». Это соответствует соглашению, которому придерживаются Ruby on Rails и другие фреймворки и модули ORM.

Внутри класса мы настраиваем схему базы данных. Таблица «Примечания» будет иметь 5 полей. Поле id которое будет целочисленным первичным ключом и автоинкрементным (это то, что означает «последовательный»). Поле content содержащее текст, логическое complete поле и два поля datetime, created_at и updated_at .

Самая последняя строка указывает DataMapper автоматически обновлять базу данных, чтобы она содержала таблицы и поля, которые мы установили, и делать это снова, если мы вносим какие-либо изменения в схему.


Теперь давайте создадим нашу домашнюю страницу:

Вверху находится форма для добавления новой заметки, а под ней — все заметки в базе данных. Для начала добавьте в файл приложения recall.rb : recall.rb :

1
2
3
4
5
get ‘/’ do
  @notes = Note.all :.order => :id.desc
  @title = ‘All Notes’
  erb :home
end

Важное примечание :.order точку (‘ . ‘) В :.order . (WordPress вмешивается в пример кода.)

Во второй строке вы видите, как мы получаем все заметки из базы данных. Если вы ранее использовали ActiveRecord (ORM, используемый в Rails), синтаксис DataMapper будет очень знакомым. Примечания присваиваются переменной экземпляра @notes . Важно использовать переменные экземпляра (это переменные, начинающиеся с @ ), чтобы они были доступны из файла представления.

Мы устанавливаем переменную экземпляра @title и загружаем файл views/home.erb через анализатор ERB.

Создайте файл views/home.erb и запустите его следующим образом:

1
2
3
4
5
6
7
8
<section id=»add»>
  <form action=»/» method=»post»>
    <textarea name=»content» placeholder=»Your note&hellip;»></textarea>
    <input type=»submit» value=»Take Note!»>
  </form>
</section>
 
<% # display notes %>

У нас есть простая форма, которая помещает POST на домашнюю страницу ( '/' ), и ниже она представляет собой некоторый код ERB, который пока используется в качестве заполнителя.


Многие из стандартов HTML среди вас, возможно, немного пострадали, увидев, что наш домашний файл не содержит тип документа или другие теги HTML. Ну, есть причина для этого. Создайте файл layout.erb в вашем каталоге views/ содержащий следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!doctype html>
<html lang=»en»>
<head>
  <meta charset=»utf8″>
  <title><%= @title + ‘ |
  <link href=»/reset.css» rel=»stylesheet»>
  <link href=»/style.css» rel=»stylesheet»>
</head>
<body>
  <header>
    <hgroup>
      <h1><a href=»/»>Recall</a></h1>
      <h2>’cause you’re too busy to remember</h2>
    </hgroup>
  </header>
 
  <div id=»main»>
    <%= yield %>
  </div>
 
  <footer>
    <p><small>An app for <a href=»http://net.tutsplus.com»>Nettuts+</a>.</small></p>
  </footer>
</body>
</html>

Здесь есть две интересные части: строки 5 и 18. В строке 5 вы видите первое использование тегов <%= … %> ERB. <%= отличается от обычного <% поскольку печатает то, что внутри. Итак, здесь мы отображаем все что есть в переменной экземпляра @title за которой следует | Recall | Recall тег <title> на странице.

В строке 18 указано <%= yield %> . Синатра будет отображать этот файл layout.erb на всех маршрутах. И фактический контент для этого маршрута будет вставлен везде, где есть yield . yield — это термин, который по сути означает «остановись здесь, вставь все, что ждет, затем продолжай».

Запустите сервер с shotgun recall.rb в оболочке и посмотрите на домашнюю страницу в браузере. Вы должны увидеть содержимое из файла макета, а также форму из фактического представления home.erb .


В файл макета мы включили два файла CSS. Sinatra может загружать статические файлы (например, ваш CSS, JS, изображения и т. Д.) Из папки с именем public/ в корневом каталоге. Итак, создайте этот каталог, а внутри него два файла: style.css и style.css . Сброс содержит сброс CSS5 Boilerplate CSS:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/*
    HTML5 ✰ Boilerplate
 
    style.css contains a reset, font normalization and some base styles.
 
    credit is left where credit is due.
    much inspiration was taken from these projects:
        yui.yahooapis.com/2.8.1/build/base/base.css
        camendesign.com/design/
        praegnanz.de/weblog/htmlcssjs-kickstart
*/
 
/*
    html5doctor.com Reset Stylesheet (Eric Meyer’s Reset Reloaded + HTML5 baseline)
    v1.6.1 2010-09-17 |
    html5doctor.com/html-5-reset-stylesheet/
*/
 
html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp,
small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
    margin:0;
    padding:0;
    border:0;
    font-size:100%;
    font: inherit;
    vertical-align:baseline;
}
 
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
        display:block;
}
 
blockquote, q { quotes:none;
 
blockquote:before, blockquote:after,
q:before, q:after { content:»;
 
ins { background-color:#ff9;
 
mark { background-color:#ff9;
 
del { text-decoration: line-through;
 
abbr[title], dfn[title] { border-bottom:1px dotted;
 
table { border-collapse:collapse;
 
hr { display:block;
 
input, select { vertical-align:middle;
 
/* END RESET CSS */
 
/* font normalization inspired by from the YUI Library’s fonts.css: developer.yahoo.com/yui/ */
body { font:13px/1.231 sans-serif;
select, input, textarea, button { font:99% sans-serif;
 
/* normalize monospace sizing
 * en.wikipedia.org/wiki/MediaWiki_talk:Common.css/Archive_11#Teletype_style_fix_for_Chrome */
pre, code, kbd, samp { font-family: monospace, sans-serif;
 
/*
 * minimal base styles
 */
 
body, select, input, textarea {
    /* #444 looks better than black: twitter.com/H_FJ/statuses/11800719859 */
    color: #444;
    /* set your base font here, to apply evenly */
    /* font-family: Georgia, serif;
}
 
/* headers (h1,h2,etc) have no default font-size or margin.
h1,h2,h3,h4,h5,h6 { font-weight: bold;
 
/* always force a scrollbar in non-IE: */
html { overflow-y: scroll;
 
/* accessible focus treatment: people.opera.com/patrickl/experiments/keyboard/test */
a:hover, a:active { outline: none;
 
a, a:active, a:visited { color: #607890;
a:hover { color: #036;
 
ul, ol { margin-left: 2em;
ol { list-style-type: decimal;
 
/* remove margins for navigation lists */
nav ul, nav li { margin: 0;
 
small { font-size: 85%;
strong, th { font-weight: bold;
 
td { vertical-align: top;
 
/* set sub, sup without affecting line-height: gist.github.com/413930 */
sub, sup { font-size: 75%;
sup { top: -0.5em;
sub { bottom: -0.25em;
 
pre {
    /* www.pathf.com/blogs/2008/05/formatting-quoted-code-in-blog-posts-css21-white-space-pre-wrap/ */
    white-space: pre;
    padding: 15px;
}
 
textarea { overflow: auto;
 
.ie6 legend, .ie7 legend { margin-left: -7px;
 
/* align checkboxes, radios, text inputs with their label by: Thierry Koblentz tjkdesign.com/ez-css/css/base.css */
input[type=»radio»] { vertical-align: text-bottom;
input[type=»checkbox»] { vertical-align: bottom;
.ie7 input[type=»checkbox»] { vertical-align: baseline;
.ie6 input { vertical-align: text-bottom;
 
/* hand cursor on clickable input elements */
label, input[type=»button»], input[type=»submit»], input[type=»image»], button { cursor: pointer;
 
/* webkit browsers add a 2px margin outside the chrome of form elements */
button, input, select, textarea { margin: 0;
 
/* colors for form validity */
input:valid, textarea:valid { }
input:invalid, textarea:invalid {
            border-radius: 1px;
}
.no-boxshadow input:invalid, .no-boxshadow textarea:invalid { background-color: #f0dddd;
 
/* These selection declarations have to be separate.
     No text-shadow: twitter.com/miketaylr/status/12228805301
     Also: hot pink.
::-moz-selection{ background: #FF5E99;
::selection { background:#FF5E99;
 
/* j.mp/webkit-tap-highlight-color */
a:link { -webkit-tap-highlight-color: #FF5E99;
 
/* make buttons play nice in IE:
     www.viget.com/inspire/styling-the-button-element-in-internet-explorer/ */
button { width: auto;
 
/* bicubic resizing for non-native sized IMG:
     code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ */
.ie7 img { -ms-interpolation-mode: bicubic;

И style.css содержит некоторые базовые стили, чтобы приложение выглядело красиво:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
body {
    margin: 35px auto;
    width: 640px;
}
 
header {
    text-align: center;
    margin: 0 0 20px;
}
 
header h1 {
    display: inline;
    font-size: 32px;
}
 
header h1 a:link, header h1 a:visited {
    color: #444;
    text-decoration: none;
}
 
header h2 {
    font-size: 16px;
    font-style: italic;
    color: #999;
}
 
#main {
    margin: 0 0 20px;
}
 
#add {
    margin: 0 0 20px;
}
 
#add textarea {
    height: 30px;
    width: 510px;
    padding: 10px;
    border: 1px solid #ddd;
}
 
#add input {
    height: 50px;
    width: 100px;
    margin: -50px 0 0;
    border: 1px solid #ddd;
    background: white;
}
 
#edit textarea {
    height: 30px;
    width: 480px;
    padding: 10px;
    border: 1px solid #ddd;
}
 
#edit input[type=submit] {
    height: 50px;
    width: 100px;
    margin: -50px 0 0;
    border: 1px solid #ddd;
    background: white;
}
 
#edit input[type=checkbox] {
    height: 50px;
    width: 20px;
}
 
article {
    border: 1px solid #eee;
    border-top: none;
    padding: 15px 10px;
}
 
article:first-of-type {
    border: 1px solid #eee;
}
 
article:nth-child(even) {
    background: #fafafa;
}
 
article.complete {
    background: #fedae3;
}
 
article span {
    font-size: 0.8em;
}
 
p {
    margin: 0 0 5px;
}
 
.meta {
    font-size: 0.8em;
    font-style: italic;
    color: #888;
}
 
.links {
    font-size: 1.8em;
    line-height: 0.8em;
    float: right;
    margin: -10px 0 0;
}
 
.links a {
    display: block;
    text-decoration: none;
}

Обновите страницу в вашем браузере, и все должно быть более стилизованным. Не беспокойтесь об этом CSS слишком сильно; это просто заставляет вещи выглядеть немного красивее!


Прямо сейчас, если вы попытаетесь отправить форму на главной странице, вы получите ошибку маршрута. Давайте создадим POST-маршрут для домашней страницы:

1
2
3
4
5
6
7
8
post ‘/’ do
  n = Note.new
  n.content = params[:content]
  n.created_at = Time.now
  n.updated_at = Time.now
  n.save
  redirect ‘/’
end

Поэтому, когда на домашней странице делается запрос на публикацию, мы создаем новый объект Note в n (благодаря DataMapper ORM Note.new представляет новую строку в таблице notes в базе данных). В поле content задаются данные, отправленные из текстовой области, а в полях created_at и updated_at указывается текущая временная метка.

Новая заметка затем сохраняется, и пользователь перенаправляется обратно на домашнюю страницу, где будет отображаться новая заметка.


Итак, мы добавили новую заметку, но пока не видим ее на домашней странице, так как не написали код для нее. В views/home.erb представления views/home.erb строку <%# display notes %> на:

01
02
03
04
05
06
07
08
09
10
11
12
<% @notes.each do |note|
  <article <%= ‘class=»complete»‘ if note.complete %>>
    <p>
      <%= note.content %>
      <span><a href=»/<%= note.id %>»>[edit]</a>
    </p>
    <p class=»links»>
      <a href=»/<%= note.id %>/complete»>&#8623;</a>
    </p>
    <p class=»meta»>Created: <%= note.created_at %></p>
  </article>
<% end %>

В первой строке мы начинаем цикл по каждой из @notes (альтернативно, мы могли бы for note in @notes , но for note in @notes использовать блок, как мы здесь и сейчас). В строке 2 мы даем <article> класс complete если текущая заметка настроена на complete . Остальное должно быть довольно прямым.


Таким образом, мы можем добавлять и просматривать заметки. Теперь нам просто нужна возможность редактировать и удалять их.

Возможно, вы заметили, что в нашем представлении home.erb мы устанавливаем ссылку [edit] для каждой заметки на то, что по сути является /:id , поэтому давайте создадим этот маршрут сейчас:

1
2
3
4
5
get ‘/:id’ do
  @note = Note.get params[:id]
  @title = «Edit note ##{params[:id]}»
  erb :edit
end

Мы извлекаем запрошенную заметку из базы данных, используя предоставленный идентификатор, устанавливаем переменную @title и загружаем файл views/edit.erb через анализатор ERB.

Введите следующее для views/edit.erb view:

01
02
03
04
05
06
07
08
09
10
11
<% if @note %>
  <form action=»/<%= @note.id %>» method=»post» id=»edit»>
    <input type=»hidden» name=»_method» value=»put»>
    <textarea name=»content»><%= @note.content %></textarea>
    <input type=»checkbox» name=»complete» <%= «checked» if @note.complete %>>
    <input type=»submit»>
  </form>
  <p><a href=»/<%= @note.id %>/delete»>Delete</a></p>
<% else %>
  <p>Note not found.</p>
<% end %>

Это довольно простой взгляд. Форма, которая указывает на текущую страницу, текстовое поле, содержащее содержимое заметки, и флажок, который проверяется, если заметка настроена на complete .

Но посмотрите на третью строку. Загадочный. Чтобы объяснить это, нам нужно немного отойти в сторону.

Вы слышали о двух терминах GET и POST.

  • GET: самый распространенный. Это обычно для запроса страницы, и может быть добавлено в закладки.
  • POST: используется для отправки данных и не может быть добавлен в закладки.

Но GET и POST — не единственные «HTTP-глаголы» — есть еще два, о которых вы должны знать: PUT и DELETE.

Технически, POST должен использоваться только для создания чего-то, например, для создания новой заметки в вашем новом веб-приложении.

PUT — глагол для изменения чего-либо. И DELETE, как вы уже догадались, предназначен для удаления чего-то.

Наличие этих четырех глаголов — отличный способ отделить приложение. Это логично К сожалению, веб-браузеры фактически не поддерживают запросы PUT или DELETE, поэтому вы, вероятно, никогда не слышали о них раньше.

Итак, возвращаясь на правильный путь, если мы хотим логически разделить наше приложение (что поощряет Синатра), мы должны подделать эти запросы PUT и DELETE. Вы увидите, что action нашей формы настроено на post . Скрытое _method ввода _method которое мы установили в третьей строке, позволяет Sinatra имитировать этот запрос PUT, фактически используя POST. Rails, помимо других фреймворков, делает вещи похожим образом.


Теперь мы подделали наш запрос PUT и можем создать для него маршрут:

1
2
3
4
5
6
7
8
put ‘/:id’ do
  n = Note.get params[:id]
  n.content = params[:content]
  n.complete = params[:complete] ?
  n.updated_at = Time.now
  n.save
  redirect ‘/’
end

Это все довольно просто. Мы получаем соответствующую заметку, используя идентификатор в URI, устанавливаем поля в новые значения, сохраняем и перенаправляем домой. Обратите внимание, как в четвертой строке мы используем троичный оператор, чтобы установить n.complete в 1 если params[:complete] существует, или 0 противном случае. Это связано с тем, что значение флажка отправляется только с формой, если она отмечена, поэтому мы просто проверяем ее наличие.


В нашем представлении edit.erb мы добавили ссылку «Удалить» к тому, что по сути является путем /:id/delete . Добавьте это в файл приложения:

1
2
3
4
5
get ‘/:id/delete’ do
  @note = Note.get params[:id]
  @title = «Confirm deletion of note ##{params[:id]}»
  erb :delete
end

На этой странице мы получим подтверждение от пользователя, что он действительно хочет удалить эту заметку. Создайте файл представления в views/delete.erb со следующим:

01
02
03
04
05
06
07
08
09
10
<% if @note %>
  <p>Are you sure you want to delete the following note: <em>»<%= @note.content %>»</em>?</p>
  <form action=»/<%= @note.id %>» method=»post»>
    <input type=»hidden» name=»_method» value=»delete»>
    <input type=»submit» value=»Yes, Delete It!»>
    <a href=»/<%= @note.id %>»>Cancel</a>
  </form>
<% else %>
  <p>Note not found.</p>
<% end %>

Обратите внимание, что так же, как мы подделали запрос PUT, установив скрытое _method ввода _method , мы теперь подделываем запрос DELETE.


Я уверен, что вы уже поняли это. Маршрут удаления:

1
2
3
4
5
delete ‘/:id’ do
  n = Note.get params[:id]
  n.destroy
  redirect ‘/’
end

Попробуйте! Теперь вы сможете просматривать, добавлять, редактировать и удалять заметки. Есть еще одна вещь …


Прямо сейчас, если вы хотите сделать заметку complete вам нужно перейти в режим редактирования и установить флажок на этой странице. Давайте сделаем этот процесс немного проще.

Когда мы настраивали главную домашнюю страницу, мы /:id/complete ссылку /:id/complete в каждую заметку. Давайте создадим этот маршрут, который просто сделает заметку завершенной (или неполной, если она уже была завершена):

1
2
3
4
5
6
7
get ‘/:id/complete’ do
  n = Note.get params[:id]
  n.complete = n.complete ?
  n.updated_at = Time.now
  n.save
  redirect ‘/’
end

Ты и Синатра снимаем один потрясающий дуэт! Вы очень быстро написали простое веб-приложение, которое выполняет все операции CRUD, которые вы ожидаете от приложения. Он написан в супер-сексуально чистом Ruby-коде и разделен на логические части.

В заключительной части «Пения с Синатрой», Encore, мы улучшим обработку ошибок, защитим приложение от XSS и создадим RSS-ленту для заметок.

Примечание. Вы можете просмотреть окончательные файлы проекта для этого руководства на GitHub .