YOU'VE MADE A BRAVE DECISION, WELCOME.

每一个不曾起舞的日子都是对生命的辜负。

学习笔记:Rails Demo

使用Rails搭建简单的约饭板

一、准备工作

适合Mac OS X和任何 Linux 发行版本。在安装之前,先码几个概念性的词。
RVM 用于帮你安装Ruby环境,帮你管理多个Ruby环境,帮你管理你开发的每个Ruby应用使用机器上哪个Ruby环境。Ruby环境不仅仅是Ruby本身,还包括依赖的第三方Ruby插件。都由RVM管理。

Rails 著名开发框架。

RubyGems 是一个方便而强大的Ruby程序包管理器( package manager),类似RedHat的RPM。它将一个Ruby应用程序打包到一个gem里,作为一个安装单元。无需安装,最新的Ruby版本已经包含RubyGems了。

Gem 是封装起来的Ruby应用程序或代码库。在终端使用的gem命令,是指通过RubyGems管理Gem包。

Gemfile 定义你的应用依赖哪些第三方包,bundle根据该配置去寻找这些包。

Rake 是一门构建语言,和make类似。Rake是用Ruby写的,Rails用rake扩展来完成多种任务,如数据库初始化、更新等。

Rakefile 是由Ruby编写,Rake的命令执行就是由Rakefile文件定义。

Bundle 相当于多个RubyGems批处理运行。在配置文件gemfilel里说明你的应用依赖哪些第三方包,他自动帮你下载安装多个包,并且会下载这些包依赖的包。

  • 安装系统需要的包

    $ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    #安装brew
    $ brew install libxml2 libxslt libiconv
    #安装 Rails 必要的一些三方库
    
  • 安装 RVM

    $ gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
    $ curl -sSL https://get.rvm.io | bash -s stable
    $ curl -L https://raw.githubusercontent.com/wayneeseguin/rvm/master/binscripts/rvm-installer | bash -s stable
    
    $ source ~/.rvm/scripts/rvm
    #载入 RVM 环境(新开的Termal就不用了)
    $ echo "ruby_url=https://cache.ruby-china.org/pub/ruby" > ~/.rvm/user/db
    #修改 RVM 下载 Ruby 的源,到 Ruby China 的镜像
    $ rvm -v
    #检查一下是否安装正确
    
  • 用 RVM 安装 Ruby

    $ rvm list known
    #列出已知的 Ruby 版本
    $ rvm install 2.3.0
    #安装一个 Ruby 版本
    
  • 设置 Ruby 版本

    $ rvm use 2.3.0 --default
    #将指定版本的 Ruby 设置为系统默认版本
    $ ruby -v
    #测试
    $ gem -v
    $ gem sources --add https://gems.ruby-china.org/ --remove https://rubygems.org/
    $ gem install bundler
    
  • 安装 Rails 环境

    $ gem install rails
    $ rails -v
    
  • 安装 MySQL 数据库

    $ sudo apt-get install mysql-server  mysql-client  libmysqlclient-dev
    

二、Hello World

进入某个文件夹,新建一个项目,使用 mysql 作为数据库:rails new showcase -d mysql,showcase就是项目的名字。

现在打开这个项目:cd showcase && rails s

如果遇到Could not find a JavaScript runtime,解决方法$sudo apt-get install -y nodejs
如果遇到Unknown database 'showcase_development',解决方法$rake db:create db:migrate

使用The Rails Way的方式展现hello world。routes -> controller -> view,如果需要调用数据库中的数据,则在controller中调用model。

到 routes.rb 中添加一行代码

root 'page#welcome'

新建 app/controllers/page_controller.rb 文件

class PageController < ApplicationController
  def welcome
  end
end

新建 app/views/page/welome.html 文件

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1>hello world</h1>
</body>
</html>

添加about页面,到 routes.rb 中添加代码

get '/about' => 'page#about'

到page_controller.rb中添加

def about
end

新建 app/views/page/about.html 文件

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1>about</h1>
</body>
</html>

三、静态页面

给welcome页面和about页面加一些内容和简单的样式吧。

首先在app/assets/images下放一张你喜欢的大图,改名为home-banner-bg.jpg(不改也行,不过所有的样式名字都显示不出来,233)。

assets/javascripts下新建文件夹/vendor,进入文件夹后执行$wget https://raw.githubusercontent.com/danmillar/jquery-anystretch/master/jquery.anystretch.min.js,这个插件可以使图片自适应屏幕。

进入到app/assets/javascripts/application.js,添加//= require vendor/jquery.anystretch.min

这样准备工作就做完了。

修改 app/views/page/welcome.html 文件

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Meal</title>
  <%= stylesheet_link_tag "application" %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
</head>
<body>

<div class="navbar clearfix">
  <div class="container">
    <a class="navbar-brand" href="/">
      Meetup
    </a>
    <ul class="nav left">
      <li><a href="about">About</a></li>
      <li><a href="#">placeholder</a></li>
    </ul>
    <ul class="nav right">
      <li><a href="#">login</a></li>
      <li><a href="#">signup</a></li>
    </ul>
  </div>
</div>

<div class="home-banner" data-stretch="<%= image_url "home-banner-bg.jpg" %>"></div>

<div class="footer">
  <div class="container">
    出现吧!黑暗泽克斯原始型二,爆裂吧现实!粉碎吧精神! 放逐这个世界!遵从血的盟/约 我在此召唤汝,夜之女王爆裂吧现实 崩断把神经 消失吧 这个世界!愚蠢的人类啊,我再给你一次选择的机会竟敢与身为黑骑士的我提出挑战,做好觉悟了么!
  </div>
</div>

<script>
  $('.home-banner').anystretch();
</script>
</body>
</html>

修改 app/views/page/about.html 文件

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Meal</title>
  <%= stylesheet_link_tag "application" %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
</head>
<body>

<div class="navbar clearfix">
  <div class="container">
    <a class="navbar-brand" href="/">
      Meetup
    </a>
    <ul class="nav left">
      <li><a href="/about">About</a></li>
      <li><a href="#">placeholder</a></li>
    </ul>
    <ul class="nav right">
      <li><a href="#">login</a></li>
      <li><a href="#">signup</a></li>
    </ul>
  </div>
</div>

<h1>about</h1>

<div class="footer">
  <div class="container">
    出现吧!黑暗泽克斯原始型二,爆裂吧现实!粉碎吧精神! 放逐这个世界!遵从血的盟/约 我在此召唤汝,夜之女王爆裂吧现实 崩断把神经 消失吧 这个世界!愚蠢的人类啊,我再给你一次选择的机会竟敢与身为黑骑士的我提出挑战,做好觉悟了么!
  </div>
</div>

<script>
  $('.home-banner').anystretch();
</script>
</body>
</html>

app/assets/stylesheets下新建index.css

body {
    font-family: sans-serif;
    margin: 0;
    font-size: 14px;
    color: #666;
}

.container {
    width: 1170px;
    margin: 0 auto;
}

*, *:before, *:after {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
}

.clearfix:before,
.clearfix:after {
    content: " ";
    display: table;
}

.clearfix:after {
    clear: both;
}

.clearfix {
    *zoom: 1;
}

ul {
    list-style: none;
    padding: 0;
    margin: 0;
}

.navbar {
    background: #222;
    position: relative;
    z-index: 1000;
}

.navbar a {
    color: #fff;
}

.navbar a:hover {
    color: #c0865e;
}

.navbar-brand {
    float: left;
    padding-left: 0;
    line-height: 80px;
    font-size: 40px;
}

.nav.left {
    float: left;
    margin-left: 40px;
    font-size: 15px;
}

.nav.right {
    float: right;
}

.nav li {
    float: left;
}

.nav li a {
    display: block;
    font-size: 1.1em;
    line-height: 40px;
    padding: 5px 10px;
    margin: 15px 10px;
}

.nav li a:hover {
    color: #000;
    background: #fff;
}

a {
    text-decoration: none;
    color: #c0865e;
}

a:hover {
    color: #845534;
}

.footer {
    border-top: 1px solid #c5c5c5;
    min-height: 200px;
    padding-top: 3em;
    padding-bottom: 3em;
    background: #f8f5f0;
}

.home-banner {
    padding-top: 80px;
    height: 1000px;
}

.index .navbar {
    background: transparent;
}

四、第一次重构

welcome.html.erb和about.html.erb中有很多相同的代码,可以把相同的部分提出来,放在application.html.erb中。移动后的三个文件分别为:

application.html.erb

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Meal</title>
  <%= stylesheet_link_tag "application" %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
</head>
<body>

<div class="navbar clearfix">
  <div class="container">
    <a class="navbar-brand" href="/">
      Meetup
    </a>
    <ul class="nav left">
      <li><a href="about">About</a></li>
      <li><a href="#">placeholder</a></li>
    </ul>
    <ul class="nav right">
      <li><a href="#">login</a></li>
      <li><a href="#">signup</a></li>
    </ul>
  </div>
</div>

<%= yield %>

<div class="footer">
  <div class="container">
    出现吧!黑暗泽克斯原始型二,爆裂吧现实!粉碎吧精神! 放逐这个世界!遵从血的盟/约 我在此召唤汝,夜之女王爆裂吧现实 崩断把神经 消失吧 这个世界!愚蠢的人类啊,我再给你一次选择的机会竟敢与身为黑骑士的我提出挑战,做好觉悟了么!
  </div>
</div>

</body>
</html>

about.html.erb

<h1>about</h1>

welcome.html.erb

<div class="home-banner" data-stretch="<%= image_url "home-banner-bg.jpg" %>"></div>

<script>
  $('.home-banner').anystretch();
</script>

随着html页面结构的调整,layout文件的引入,css文件也需要做相应的修改。新建一个目录sections里面专门放各个页面具体区块的css。把 index.css 移动进去,拆成 layout.css.scss 和 welcome.css.scss 。分别负责 application.html.erb 和 welcome.html.erb 页面的样式。再新建shared,将common.css.scss放入。

cd app/assets/stylesheets
mkdir sections
mkdir shared

五、添加留言板

在welcome页面添加一个可以发布聚餐活动的按钮。在 welcome.html.erb 中, class=”home-banner” 的 div 之内添加

<div class="banner-inner container clearfix">
  <h1>约饭!</h1>
  <p class="subheading">说说你想吃啥呀</p>
  <div class="home-banner-links">
    <%= link_to "发布新聚餐", "#", class: "banner-btn btn" %>
  </div>
</div>

在对应的 css 文件中添加样式

.home-banner {
  margin-top: -80px;
  padding-top: 80px;
  .banner-inner {
    height: 1000px;
    position: relative;
    h1 {
      font-family: "museo-sans-condensed";
      font-size: 88px;
      font-weight: 400;
      letter-spacing: -2px;
      border-radius: 5px;
      margin-top: 60px;
      line-height: 1.2;
      max-width: 50%;
      text-transform: capitalize;
      color: #fff;
      text-shadow: 0 1px 81px rgba(0, 0, 0, 0.3);
    }
    .subheading {
      color: #fff;
      font-weight: 300;
      font-size: 1.3em;
      margin-top: 26px;
      background: rgba(0, 0, 0, 0.5);
      display: block;
      width: 45%;
      padding: 5px 10px;
      line-height: 30px;
      text-align: center;
    }
  }
}

.home-banner-links {
  position: absolute;
  right: 160px;
  top: 194px;
  .banner-btn {
    padding: 15px 15px;
    font-size: 1.2em;
    font-weight: 300;
    font-family: "museo-sans-condensed";
    border-radius: 5px;
    color: #fff;
    background: rgba(0, 0, 0, 0.1);
    margin-left: 8px;
    border: 2px solid;
    position: relative;
    top: 200px;
    left: -700px;
    &:hover {
      border: 1px solid rgba(0, 0, 0, 0.2);
    }
  }
}

在 welcome.html.erb 中添加聚餐列表

<div class="issue-list-header">
  <div class="container clearfix">
    <h1 class="issue-list-heading">聚餐列表</h1>
  </div>
</div>

添加聚餐列表的样式

.issue-list-header {
  border-bottom: 1px solid #ddd;
  padding-bottom: 30px;
  padding-top: 30px;
  margin-top: 0;
  margin-bottom: 35px;
  background: #7EB6AD;
  color: #fff;
  .issue-list-heading {
    font-size: 2em;
    font-weight: normal;
  }
}

在 welcome.html.erb 中添加一个活动

<div class="container clearfix">
  <div class="issue-list">
    <article class="issue clearfix">
      <div class="avatar">
        <a href="#">
          <img src=http://img1.3lian.com/gif/more/11/201301/e47524afabdc211536e595743be46e92.jpg alt="">
        </a>      </div>
      <div class="body">
        <h5 class="title">
          <a href="#">test</a>
        </h5>
        <a class="read-more" href="#">read</a>
        <span class="meta-data">
          <a href="#">avatar</a>
        </span>
      </div>
      <div class="issue-comment-count">
        <a href="#">
          4
        </a>
      </div>
    </article>
  </div>
</div>

对应的样式

.issue-list {
  background: #fff;
  clear: both;
  padding: 0 2em;
  margin-bottom: 1em;
  border: 1px solid #ddd;
  article {
    border-bottom: 1px solid #e3e3e3;
    position: relative;
    margin-top: 0;
    padding: 1em 0;
    .body {
      margin-right: 2em;
      width: 70%;
      float: left;
      .read-more{
        background-color: #316A72;
        color: #fff;
        font-size: 13px;
        border-radius: 4px;
        letter-spacing: -0.3px;
        display: inline-block;
        line-height: 1.7;
        font-weight: 100;
        padding: 0 6px;
      }
      .meta-data {
        color: #999;
        font-size: 12px;
      }

      h5 {
        font-size: 22px;
        line-height: 35px;
        font-weight: normal;
        margin: 0 0 5px;
        a {
          color: #555;
          &:hover {
            color: black;
          }
        }
      }
    }
    .issue-comment-count {
      float: right;
      margin-top: 15px;
      a {
        font-size: 45px;
      }
    }
    .avatar {
      width: 75px;
      float: left;
      img {
        width: 65px;
        padding: 4px;
        border: 1px solid #ddd;
      }
    }
  }
}

修改controller,使数据可以灵活一点地显示。page_controller.rb 中添加

def welcome
@issues  = [ { title: "test1", comments: "4" }, { title: "test2", comments: "5" } ]
end

修改 welcome.html.erb 将 issue-list 中的内容抽出来,新建一个文件 _issue_list.html.erb

<div class="issue-list">
  <% issues.each do |i| %>
      <article class="issue clearfix">
        <div class="avatar">
          <a href="#">
            <img src=http://img1.3lian.com/gif/more/11/201301/e47524afabdc211536e595743be46e92.jpg alt="">
          </a></div>
        <div class="body">
          <h5 class="title">
            <%= link_to i[:title], "#" %>
          </h5>
          <a class="read-more" href="#">read</a>
        <span class="meta-data">
          <a href="#">avatar</a>
        </span>
        </div>
        <div class="issue-comment-count">
          <%= link_to i[:comments], "#" %>
        </div>
      </article>
  <% end %>
</div>

welcome.html.erb 中改为

<%= render partial: 'issue_list', locals: { issues: @issues } %>

六、添加数据库

配置文件 config/database.yml 里面写明了要使用哪个数据库。建立数据表结构

rails g migration CreateIssues

在 db/migrate 会自动生成一个文件,文件名前面是时间戳,里面可以添加需要的字段。

class CreateIssues < ActiveRecord::Migration
  def change
    create_table :issues do |t|
      t.string :title
      t.timestamps
    end
  end
end

运行 rake db:migrate,来把修改内容真正写进 mysql 数据库。会生成 db/schema.rb 文件,该文件显示了当前数据库的表结构。

新建 issue.rb 的 model 文件放在 app/models 下面。

class Issue < ActiveRecord::Base
end

这里的 class 命名是很关键的,如果数据库中的表名是 issues 那这里的 class 名就必须是 Issue,也就是首字母大写,同时变成单数。因为 Rails 可以建立自动的 class 到 table 的映射关系。

执行 rails consolerails c 对表进行操作。

Issue.create(title: "test111")
Issue.create(title: "test111222")
Issue.all

修改 controller 中加载数据的方法

def welcome
  @issues = Issue.all
end

到 _issue_list.html.erb 中,由于数据表中没有添加 commit 个数,所以要将 commit 个数改为固定值

<div class="issue-comment-count">
  <%= link_to i[:comments], "11" %>
</div>

七、CRUD

对 issue 进行 Create Read Update Delete 操作。参考网站:http://railsforzombies.org/levels/1

用 rails generator 来自动生成 controller ,为了避免生成太多用不到的文件,到 application.rb 中添加

config.generators do |g|
    g.assets false
    g.helper false
    g.test_framework false
end

然后执行 $rails g controller issues show,在生成的内容中 routes.rb 中的语句删掉。

修改 issues_controller.rb 中的内容,根据 id 从数据库中读取数据。

def show
  @issue = Issue.find(params[:id])
end

在新生成的 show.html.erb 中添加

<div class="issue-heading">
  <div class="container">
    <%= @issue.title %>
  </div>
</div>
<div class="container">
  <div class="replies">
    <article class="reply clearfix">
      <div class="avatar">
        <img src=http://img1.3lian.com/gif/more/11/201301/e47524afabdc211536e595743be46e92.jpg alt="" class="image-circle">
      </div>
      <div class="body">
        <div class="heading">
          <h5 class="name"><a href="#">avatar</a></h5>
        </div>
        <%= @issue.content %>
      </div>
    </article>
  </div>
</div>

在 sections 中添加相应的样式 issue_show.css.scss

.issue-heading {
  border-bottom: 1px solid #ddd;
  padding-bottom: 30px;
  padding-top: 30px;
  margin-top: 0;
  margin-bottom: 35px;
  background: #7EB6AD;
  color: #fff;
  font-size: 2em;
}

.reply {
  position: relative;
  overflow: hidden;
  margin-bottom: 30px;
  width: 91%;
  .heading {
    margin-bottom: 5px;
    .name {
      font-size: 18px;
      display: inline;
      font-weight: normal;
    }
  }
  .body {
    padding: 15px;
    border-radius: 5px;
    position: relative;
    overflow: visible;
    float: left;
    width: 87%;
    border: 1px solid #ddd;
    line-height: 26px;
    &::before {
      content: "";
      display: block;
      position: absolute;
      top: 21px;
      left: -6px;
      width: 10px;
      height: 10px;
      background: #fff;
      border-left: 1px solid #cad5e0;
      border-top: 1px solid #cad5e0;
      -moz-transform: rotate(-45deg);
      -webkit-transform: rotate(-45deg);
    }
  }
  .avatar {
    float: left;
    margin-right: 29px;
    position: relative;
    overflow: visible;
    text-align: center;
    .image-circle {
      width: 75px;
      border-radius: 50%;
    }
  }
}

在 issues/show.html.erb 中添加一个删除的链接。

<%= link_to 'Destroy', "/issues/#{@issue.id}", method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-primary" %>

在 routes.rb 中添加相应的路由。

delete 'issues/:id' => 'issues#destroy'

到 issues_controller.rb 添加

def destroy
  issue = Issue.find(params[:id])
  issue.destroy
  redirect_to :root
end

到 app/assets/stylesheets/shared/common.css 添加一些按钮样式。

.btn-primary {
  color: white;
  background: #c0865e;
  border-color: #b9784c;
  &:hover {
    background-color: #b9784c;
    border-color: #845534;
  }
}
.btn {
  display: inline-block;
  padding: 6px 12px;
  margin-bottom: 0;
  font-size: 14px;
  font-weight: normal;
  line-height: 1.428571429;
  text-align: center;
  vertical-align: middle;
  cursor: pointer;
  border: 1px solid transparent;
  border-radius: 4px;
  white-space: nowrap;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  -o-user-select: none;
  user-select: none;
}

如果删除失败,则是在操纵数据库过程中触发了rails的保护机制,只需要在 application.html.erb 中添加一句 <%= csrf_meta_tags %> 即可。

想要创建新的聚餐信息,将 welcome.html.erb 的发布按钮的信息改为 <%= link_to "发布新聚餐", "/issues/new", class: "banner-btn btn" %>

在 routes.rb 中添加新的路由

get 'issues' => 'issues#index', :as => 'issues'
get '/issues/new' => "issues#new"
post 'issues' => 'issues#create'

在 issues_controller.rb 中添加

def new
  @issue = Issue.new
end

def create
  Issue.create(issue_params)
  redirect_to :root
end

private
def issue_params
  params.require(:issue).permit(:title, :content)
end

Strong Parameters 是 Rails 为了防止坏人通过表单提交攻击网站,采用的自我保护机制。如果不加说明任何人都可以在 params[:issue] 中加入任何参数,比如 admin: true 这样就可以给他自己管理员权限了,所以就用这种方式进行修改。使用的方式就是 https://github.com/rails/strong_parameters

创建 app/view/issues/new.html.erb 新页面

<div class="new-issue-form-container clearfix">
  <div class="new-issue-form clearfix">
    <%= form_for @issue do |f| %>
      <dl class="form">
        <dt><%= f.label :title %></dt>
        <dd><%= f.text_field :title %></dd>
      </dl>
      <dl class="form">
        <dt><%= f.label :content %></dt>
        <dd><%= f.text_area :content %></dd>
      </dl>
      <p><%= f.submit "发布新活动", :class => "submit-issue-button btn btn-primary" %></p>
    <% end %>
  </div>
</div>

在 common.css.sass 中添加关于 form 的样式

form {
  input {
    font-size: 21px;
    width: 100%;
    padding: 10px 12px;
    color: #555;
  }
  textarea {
    height: 180px;
    font-size: 21px;
    width: 100%;
    padding: 10px 12px;
    color: #555;
  }
  label {
    display: inline-block;
    margin-bottom: 5px;
  }
  dd {
    margin: 0;
  }
}

创建 sections/issue_new.css.scss 添加特有样式

.new-issue-form-container {
  width: 800px;
  background: white;
  margin: 30px auto;
  .new-issue-form {
    width: 600px;
    margin: 10px auto;
  }
}
.submit-issue-button {
  height: 50px;
  width: 100%;
}

到 views/issues/show.html.erb 添加一个 edit 按钮

<%= link_to 'edit', edit_issue_path(@issue), class: "btn btn-primary" %>

到 routes.rb 中,所有的指向 issues_controller.rb 的语句都可以删除,而用一行代替。具体参考文档 http://guides.rubyonrails.org/routing.html

resources :issues

issues_controller.rb 中添加

def edit
  @issue = Issue.find(params[:id])
end

创建 views/issues/edit.html.erb 复制 views/issues/new.html.erb 即可

到 issues_controller.rb 中来添加 update 功能

def update
  i = Issue.find(params[:id])
  i.update_attributes(issue_params)
  redirect_to :root
end

新建一个 views/issues/_form.html.erb,把 issues/new.html.erb 和 issues/edit.html.erb 共用。

<div class="new-issue-form-container clearfix">
  <div class="new-issue-form clearfix">
    <%= form_for issue do |f| %>
      <dl class="form">
        <dt><%= f.label :title %></dt>
        <dd><%= f.text_field :title %></dd>
      </dl>
      <dl class="form">
        <dt><%= f.label :content %></dt>
        <dd><%= f.text_area :content %></dd>
      </dl>
      <p><%= f.submit :class => "submit-issue-button btn btn-primary" %></p>
    <% end %>
  </div>
</div>

把 views/issues/edit.html.erb 和 views/issues/new.html.erb 改为

<%= render partial: 'form', locals: {issue: @issue} %>

八、数据库一对多关系

活动与留言属于一对多的关系,先来创建留言的 model 和 migration 文件。

rails g model comment content:text username:string email:string issue_id:integer

到 views/issues/show.html.erb 中添加

<%= render partial: 'shared/comment_box', locals: {issue: @issue} %>

创建 views/shared/_comment_box.html.erb

<article class="reply clearfix">
  <div class="avatar">
    <%= link_to "#" do %>
      <%= image_tag "default_avatar.png", class: "image-circle" %>
    <% end %>
  </div>
  <div class="body">
  <%= form_tag("/issues/#{issue.id}/comments", method: :post) do  %>
    <%= label_tag :username, "用户名" %>
    <%= text_field_tag(:username) %>
    <%= label_tag :email, "邮箱" %>
    <%= text_field_tag(:email) %>
    <%= text_area_tag(:content) %>
    <%= submit_tag '提交评论', class: 'btn btn-primary btn-submit' %>
  <% end %>
  </div>
</article>

到 routes.rb 添加

post '/issues/:issue_id/comments' => "comments#create"

创建 app/controllers/comments_controller.rb

rails g controller comments

def create
  c = Comment.new
  c.username = params[:username]
  c.email = params[:email]
  c.content = params[:content]
  c.issue_id = params[:issue_id]
  c.save
  issue = Issue.find(params[:issue_id])
  redirect_to issue
end

将 comment 显示出来,到 views/issues/show.html.erb 中添加

<%= render partial: 'shared/comment_list', locals: {comments: @comments} %>

创建 shared/_comment_list.html.erb

<% comments.each do |c| %>
    <div class="reply clearfix">
       <div class="avatar">
         <%= link_to '#' do %>
           <%= image_tag 'default_avatar.png', class: 'image-circle' %>
         <% end %>
       </div>
       <div class="body">
         <div class="heading">
            <h5 class="name"><%= link_to c.username, "#" %></h5>
            <span class="datetime">
              <%= time_ago_in_words c.created_at %>
            </span>
         </div>
         <%= c.content %>
       </div>
    </div>
  <% end %>

在 issues_controller.rb 的 show 方法中添加

def show
   @issue = Issue.find(params[:id])
   @comments = @issue.comments
end

建立一对多的关系,第一步,到 issue.rb 文件中添加:has_many :comments;第二步,到 comment.rb 中添加:belongs_to :issue

修改评论个数,到 views/page/_issue_list.html.erb 中

- <%= link_to '5', "#" %>
+ <%= link_to i.comments.count, i %>

修改用户头像,在 comment.rb 添加

def user_avatar
  gravatar_id = Digest::MD5.hexdigest(self.email.downcase)
  "http://gravatar.com/avatar/#{gravatar_id}.png?s=512&d=retro"
end

到 views/shared/_comment_list.html.erb 修改使用头像的地方 <%= image_tag c.user_avatar, class: 'image-circle' %>


九、用户注册与登录退出

创建 users 表,用户密码一般不明文存储,而是经过加密后,存放在 password_digest 这个字段中。

rails g controller users signup
rails g model user name:string email:string password_digest:string
rake db:migrate

使用 has_secure_password 首先要在 Gemfile 里面添加 Bcrypt,其次在表里有 password_digest 这个字段,然后到 user.rb 中,添加 has_secure_password

添加 /signup 页面,到 application.html.erb 中添加指向 /signup 的链接:<li><a href="/signup">signup</a></li>,然后到 route.rb 中添加:get "signup" => "users#signup", :as => "signup"resources :users

到 users_controller.rb 中

def signup
  @user = User.new
end

def create
  user = User.new(user_params)
  user.save
  redirect_to :root
end

private
  def user_params
    params.require(:user).permit!
  end

在相应的 app/views/users/signup.html.erb 添加

<div class="signup-form-container clearfix">
  <div class="signup-form">
    <%= form_for @user do |f| %>
        <dl class="form">
          <dt><%= f.label :name, "用户名" %></dt>
          <dd><%= f.text_field :name %></dd>
        </dl>
        <dl class="form">
          <dt><%= f.label :email %></dt>
          <dd><%= f.text_field :email %></dd>
        </dl>
        <dl class="form">
          <dt><%= f.label :password, "密码" %></dt>
          <dd><%= f.password_field :password %></dd>
        </dl>
        <dl class="form">
          <dt><%= f.label :password_confirmation, "请再输入一次" %></dt>
          <dd><%= f.password_field :password_confirmation %></dd>
        </dl>
        <p><%= f.submit "注册", :class => "signup-button btn btn-primary" %></p>
    <% end %>
  </div>
</div>

添加对应的样式 app/assets/stylesheets/sections/users.css.scss

.signup-form-container, .login-form-container{
  width: 670px;
  margin: 50px auto;
  border:1px solid #ddd;
  padding: 2em;
  .signup-form, .login-form {
    width: 100%;
  }
  .signup-button, .login-button {
    padding: 13px;
    margin-top: 15px;
    width: 100%;
  }
}

用这个账户来登陆,在 application.html.erb 中来添加 <%= link_to "login", login_path %>

route.rb 中添加get "login" => "users#login", :as => "login"

users_controller.rb 中添加

def login
end

app/views/users/login.html.erb 中添加

<div class="login-form-container clearfix">
  <div class="login-form">
    <%= form_tag "/create_login_session" do %>
      <dl class="form">
        <dt>
          <%= label_tag "用户名" %>
        </dt>
        <dd>
          <%= text_field_tag :name, params[:name] %>
        </dd>
      </dl>
      <dl class="form">
        <dt>
          <%= label_tag "密码" %>
        </dt>
        <dd>
          <%= password_field_tag :password, params[:password] %>
        </dd>
      </dl>
      <p> <%= submit_tag "登录", :class => "login-button btn btn-primary" %> </p>
      <div class="need-signup">
        <%= link_to "没有账号?注册一个吧", signup_path %>
      </div>
    <% end %>
  </div>
</div>

表单中把数据提交到 “/create_login_session” 在 route.rb 中添加 post "create_login_session" => "users#create_login_session"

在 users_controller.rb 中添加

def create_login_session
  user = User.find_by_name(params[:name])
  if user && user.authenticate(params[:password])
    session[:user_id] = user.id
    redirect_to :root
  else
    redirect_to :login
  end
end

到 application_controller.rb 中添加

def current_user
  @current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user

application.html.erb 中添加

<% if current_user %>
  <li><%= link_to current_user.name, "#" %></li>
  <li><%= link_to "logout", "#" %></li>
<% else %>
  <li><%= link_to "login", login_path %></li>
  <li><%= link_to "signup", signup_path %></li>
<% end %>

退出登录,在 application.html.erb 中 logout 对应的这一行改为:<li><%= link_to "logout", logout_path, method: "delete" %></li>

route.rb 中添加:delete "logout" => "users#logout", :as => "logout"

在 users_controller.rb 中修改

def logout
  session[:user_id] = nil
  redirect_to :root
end

此时就可以登录、退出已经注册了,但是测试时需要重新开一个窗口。

记住密码的功能。为了保持登陆状态,引入了 session 方法,但是 session 中存储的数据毕竟是临时性的,虽然目前最新的浏览器有各种保持临时数据的缓存方式,但是基本上可以认为如果网站关闭了或是关机重启了,session 中的数据也就丢失了。

添加 checkbox 的代码,到 login.html.erb 中的提交按钮上方,添加

<dl class="form remember-me">
  <%= check_box_tag :remember_me, 1, params[:remember_me] %>
  <%= label_tag :remember_me %>
</dl>

user.css.scss 添加样式

.remember-me {
  * {
    width: auto;
  }
  #remember_me {
    margin-left: 0;
  }
}

为每一个用户生成一串随机数,用来代表 token。

rails g migration AddAuthTokenToUsers auth_token:string
rake db:migrate

user.rb 中,添加

before_create { generate_token(:auth_token) }

def generate_token(column)
  begin
    self[column] = SecureRandom.urlsafe_base64
  end while User.exists?(column => self[column])
end

users_controller.rb 中的 create_login_session 方法修改为

- session[:user_id] = user.id
+ if params[:remember_me]
+   cookies.permanent[:auth_token] = user.auth_token
+ else
+   cookies[:auth_token] = user.auth_token
+ end

退出登录的代码 logout 修改为

- session[:user_id] = nil
+ cookies.delete(:auth_token)

application_controller.rb 中 current_user 改为

def current_user
  @current_user ||= User.find_by_auth_token!(cookies[:auth_token]) if cookies[:auth_token]
end

需要注册结束后直接跳转,则在 users_controller.rb 的 create 方法中,user.save 语句之后添加 cookies[:auth_token] = user.auth_token


十、数据安全 validation

form validation 表单验证。当用户填写注册表单的时候如果填写的内容有问题,程序能够检查并且报错,避免直接把有问题的内容 直接存入数据库。

user.rb 中添加

validates :name, :email, presence: true
validates :name, :email, uniqueness: { case_sensitive: false }

users_controller.rb 中的 create 方法需要调整,要用实例变量,同时最后不能用 redirect_to 而要用 render

-    user = User.new(user_params)
-    user.save
-    cookies[:auth_token] = user.auth_token
-    redirect_to :root
+    @user = User.new(user_params)
+    if @user.save
+      cookies[:auth_token] = @user.auth_token
+      redirect_to :root
+    else
+      render :signup
+    end

在 signup.html.erb 中适当显示报错信息了

<div class="signup-form-container clearfix">
  <div class="signup-form">
    <%= form_for @user do |f| %>
        <dl class="form">
          <dt><%= f.label :name, "用户名" %></dt>
          <dd><%= f.text_field :name %></dd>
          <% if @user.errors[:name].any? %>
              <dd class="error"><%= @user.errors[:name][0] %></dd>
          <% end %>
        </dl>
        <dl class="form">
          <dt><%= f.label :email %></dt>
          <dd><%= f.text_field :email %></dd>
          <% if @user.errors[:email].any? %>
              <dd class="error"><%= @user.errors[:email][0] %></dd>
          <% end %>
        </dl>
        <dl class="form">
          <dt><%= f.label :password, "密码" %></dt>
          <dd><%= f.password_field :password %></dd>
          <% if @user.errors[:password].any? %>
              <dd class="error"><%= @user.errors[:password][0] %></dd>
          <% end %>
        </dl>
        <dl class="form">
          <dt><%= f.label :password_confirmation, "请再输入一次" %></dt>
          <dd><%= f.password_field :password_confirmation %></dd>
          <% if @user.errors[:password_confirmation].any? %>
              <dd class="error"><%= @user.errors[:password_confirmation][0] %></dd>
          <% end %>
        </dl>
        <p><%= f.submit "注册", :class => "signup-button btn btn-primary" %></p>
    <% end %>
  </div>
</div>

common.css.scss 中 form 里面添加

.error {
  margin: 5px 0 9px 0;
  color: #DB8A14;
}

十一、添加多语言

到 application.rb 中添加 config.i18n.default_locale = 'zh-CN'

添加 config/locals/zh-CN.yml 文件,注意缩进关系,使用两个空格做为一个缩进级别

zh-CN:
navbar:
about: 关于
signup: 注册
logout: 退出
login: 登录

到 application.html.erb 中

<ul class="nav left">
  <li><%= link_to t("navbar.about"), "/about" %></li>
</ul>
<ul class="nav right">
  <% if current_user %>
      <li><%= link_to current_user.name, "#" %></li>
      <li><%= link_to t("navbar.logout"), logout_path, method: "delete" %></li>
  <% else %>
      <li><%= link_to t("navbar.login"), login_path %></li>
      <li><%= link_to t("navbar.signup"), signup_path %></li>
  <% end %>
</ul>

十二、添加简单的权限

登录后才能创建新活动,点击首页的发布新活动按钮,如果用户没有登录,无法创建,需要的代码调整是在 issues_controller.rb 中, new 改成

def new
  if not current_user
    redirect_to :root
    return
  else
    @issue = Issue.new
  end
end

隐藏评论框。退出登录条件下再来访问一个活动的展示页面就会报错。因为在评论框的代码中用到了 current_user 。解决方法是到 issues/show.html.erb 中

+ <% if current_user %>
   <%= render partial: 'shared/comment_box', locals: {issue: @issue} %>
+ <% else %>
+   <%= link_to "登录发评论", login_path %>
+ <% end %>