Rails笔记

前言

Rails是用ruby语言写的,方便开发Web应用的框架.
采用了许多约定俗成的规则来实现MVC结构.(但也导致代码不好关联理解)
如果说还有一个更为简单的,约定俗成特例,
那就是为个人博客而构建的hexo,
同样是使用命令来产生文件,
也同样是用命令来开启服务器等.

从ORM的操作上可以看到laravel的影子.

似乎是有着较为完备的测试代码文档,
同时教程建议写代码以测试为导向.使重构变得自信一些.

安装rails

rails是ruby的一个包,使用ruby的包管理器 gem 安装,可以指定版本.

1
2
printf "install: --no-rdoc --no-ri\nupdate:  --no-rdoc --no-ri\n" >> ~/.gemrc
gem install rails -v 5.1.6

当然arch系统不屑于使用ruby自带包管理器安装

需要了解的ruby的一些写法

ruby这个语言对于一种想法有多种表达,
因此才有了这些屁事.

各种各样的类语言化函数,以及各种不知道在哪看到就拿来用假装有逼格的表达

1
2
3
4
5
6
7
8
9
10
11
a.first
a.second
a.last
5.megabytes # 需要特定gem支持
2.hours
a.empty? # 问号表示函数的返回值是boolean型,不过不强制
a.blank?
arr.include?(b)
a.sort # 输出a排序后的结果
a.sort! # 排序a后输出结果
arr << 6 # 等于 arr.push(6)

一行中表达为

1
(1..5).each { |i| puts 2 * i }

多行中表达为

1
2
3
(1..5).each do |i|
puts 2 * i
end

把{}替换为了do…end

散列(hash)和符号(symbol)

散列的举例和简写

1
2
3
{"last_name" => "Hartl", "first_name" => "Michael"}
{:last_name => "Hartl", :first_name => "Michael"}
{last_name: "Hartl", first_name: "Michael"}

其中的:last~name和~:first~name是符号~,个人认为本质是 字符串作为变量名时的简写
比如 "last_name" 可以写作 :last_name
调用时可以更明显, user["last_name"] 等于 user[:last_name]
同时散列中还可以简写一步, :last_name => 简写作 last_name: (注意冒号必须紧贴)
我猜可能和日语键盘不好打引号但容易打冒号有极大关系

实参不带括号

1
2
3
4
5
6
7
8
arr.include?(b)
arr.include? b

new user(user_params())
new user(user_params)

render json: {}, status: 400
render({json: {}, status: 400})

符号的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
:name           # 冒号多表示symbol或表示与=>相同的含义
{name: "1"}

name.empty? # 问号表示该方法的返回值是boolean型,但不强制

email.downcase! # ruby的感叹号表示修改对象本身
email = self.email.downcase

a # 变量
$a # 全局变量
@a # 实例变量(对象的成员变量)
@@a # 类变量(类成员变量中的静态变量)

map(&: downcase)# &专用于表示映射,相当于lambda函数的简写
map { |char| char.downcase }

其他简写

字符串数组方面去引号的表达

1
2
3
4
:name
"name"
%w[A B C]
["A", "B", "C"]

事实上还有许多其他的%用法,见参考1.

hello world

初始化项目

1
rails _5.1.6_ new hello_app

产生了固定结构的文件夹:

├── app
│  ├── assets(css,js)
│  ├── channels(略)
│  ├── jobs(定时任务等)
│  ├── mailers(邮件相关)
│  ├── helpers(通用函数)
│  ├── controllers
│  ├── models
│  └── views
├── bin(rails自己需要的一些东西)
├── config
│  ├── application.rb
│  ├── database.yml
│  ├── locales
│  ├── routes.rb
│  └── ...
├── db
│  └── seeds.rb
├── Gemfile
├── Gemfile.lock
├── lib
│  ├── assets
│  └── tasks
├── log
├── package.json
├── public
│  ├── 404.html
│  ├── 422.html
│  ├── 500.html
│  ├── apple-touch-icon-precomposed.png
│  ├── apple-touch-icon.png
│  ├── favicon.ico
│  └── robots.txt
├── README.md
├── storage
├── test
│  ├── application_system_test_case.rb
│  ├── controllers
│  ├── fixtures
│  ├── helpers
│  ├── integration
│  ├── mailers
│  ├── models
│  ├── system
│  └── test_helper.rb
├── tmp
│  ├── cache
│  └── storage
└── vendor

自动运行了 bundle install 命令,安装了需要安装的库.
与python不同的是,ruby的可以安装在非全局的位置中,
但此处貌似不能选择安装在本地.

引入更多外部的库

更改 hello_app/Gemile,
安装一些包,指定版本等,举例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
source 'https://rubygems.org'

gem 'rails', '5.1.6'
gem 'puma', '3.9.1'
gem 'sass-rails', '5.0.6'
gem 'uglifier', '3.2.0'
gem 'coffee-rails', '4.2.2'
gem 'jquery-rails', '4.3.1'
gem 'turbolinks', '5.0.1'
gem 'jbuilder', '2.7.0'

group :development, :test do
gem 'sqlite3', '1.3.13'
gem 'byebug', '9.0.6', platform: :mri
end

group :development do
gem 'web-console', '3.5.1'
gem 'listen', '3.1.5'
gem 'spring', '2.0.2'
gem 'spring-watcher-listen', '2.0.1'
end

group :production do
gem 'pg', '0.20.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

学习这里的 group 表达.
不想安装生产环境才需要的gem可以使用如下命令.

1
bundle install --without production

如果没有特别需求可以不带参数.

初始的一些文件

请求先抵达路由

1
2
3
4
5
# config/routes.rb

Rails.application.routes.draw do
root 'application#hello'
end
  • root 即为网址根地址(比如 http://localhost:3000/)的别名
  • application 是controller的名字
  • hello 是controller中定义的方法

在简单的结构中,请求经过路由直接发送给后台controller.
这样后端会直接拿到前端的数据,无论是get还是post.

1
2
3
4
5
6
7
8
9
# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
protect_from_forgery with: :exception

def hello
render html: "hello, world!"
end
end

这里的controller直接渲染了html文件
正常的话还会传给view层去渲染html文件.

运行

1
rails server

一般是运行在 http://localost:3000/
访问之即可发现 hello, world!

部署

基本操作兼名词解释

大多数的操作使用了rails作为命令,
rails命令的特点之一是参数可以使用简称.
比如 rails server 可写作 rails s.
和hexo非常相似.
唯一麻烦一些的是版本指定的方格并不好.
以下相同含义命令的多种写法之间不再备注或者.

新建项目

1
2
rails new app-name
rails _5.1.6_ new hello_app

安装需要的gem

gem负责安装单个包,
bundle是包依赖管理器,在安装包的同时会安装有依赖关系的包.
在Gemfile中声明项目需要使用的包,然后安装

1
2
3
4
5
6
# 包安装在系统中
bundle install
# 包安装在项目中
bundle install --path vendor/bundle
# 不包括生产环境
bundle install --without production

启动服务器

1
2
3
4
rails s
rails server
# 指定环境,决定了rails服务器使用哪个数据库等等
rails server --environment production

生成各种文件

使用 rails generate 文件类别 文件名 参数 生成需要的文件
使用 rails destroy 文件类别 文件名 撤销操作

  1. 利用脚手架直接产生所有文件

    包括以下文件

    • active~record相关文件~
      • 定义数据库结构的文件
      • model文件
    • 模型测试相关
      • 模型测试文件
      • 数据注入文件(也称为固件)
    • 资源文件(大致包含了resource概念的内容)
      • 路由中的定义
      • controller文件
      • controller的测试文件
      • helper文件(希望controller中只有业务代码,其他放在helper中)
      • view层的各种文件
        • html.erb
        • scss
        • coffee

    activeRecord是rails的数据库工具,将数据库定义延伸使用到模型中.
    (比如user对象有哪些成员不是看代码而是看数据库的定义.)
    同时也有许多laravel风格的模型关联方法.

    此处以User模型举例,有两个字段分别是string类型的

    1
    rails g scaffold User name:string email:string
  2. 生成数据库迁移文件

    1
    2
    rails g migration add_index_to_users_email
    rails g migration add_password_digest_to_users password_digest:string
  3. 生成controller

    1
    2
    rails g controller StaticPages home help
    rails g controller PasswordResets new edit --no-test-framework
    • 此处生成了名为 StaticPages的controller,其中有home和help两个方法
      • 一般还会同时生成controller测试文件,除非声明不需要
    • rails对命名有要求
      • 命令行中使用大驼峰,文件名使用蛇形
      • 在引用时有不写全名的习惯,比如在route中的controller名
      • 对一些局部视图,默认文件名开头带下划线
      • model使用单数(如 User.rb),而controller和数据库等使用复数(users table, users~controller~)
  4. 生成model

    1
    2
    3
    4
    rails g model User name:string email:string

    # 撤销
    rails destroy model User
  5. 生成测试

    1
    rails g integration_test site_layout

    测试有一般有三种

    • models
    • controller
    • integration

    前两个一般随着主要文件的产生而产生,
    可以视为unit测试

  6. 生成邮件程序

    1
    rails g mailer UserMailer account_actiivation password_reset

    名为UserMailer的mailer有两个方法分别是account~activation和passwordreset~

  7. 生成图像上传程序

    1
    rails g uploader Picture

    需要两个gem

    • carrierwave
    • mini~magick~

    会生成文件

    • app/uploaders/picture_uploader.rb

数据库操作

  1. 迁移数据库

    使用了生成的db/migrate/datestring~name~.rb文件

    1
    2
    3
    4
    5
    6
    7
    8
    rails db:migrate

    # 指定环境
    rails db:migrate RAILS_ENV=production

    # 貌似支持管道风的使用方法,此处在迁移数据库结构后重置数据库内容
    rails db:migrate:reset

  2. 撤销迁移操作

    1
    2
    3
    4
    rails db:rollback

    # 回到特定版本
    rails db:migrate VERSION=0
  3. 植入数据

    使用的是 db/seeds.rb 文件

    1
    rails db:seed

打开控制台

常常用于练习active record的使用

1
2
3
4
5
6
rails c
rails console
# 结束后恢复数据库状态
rails console --sandbox
# 使用test环境(意味着数据库中存储特定的数据)
rails console test

开始测试

1
2
3
4
5
6
7
8
9
10
11
12
13
rails t
rails test

# 指定不同测试对象
rails test:models
rails test:controller
rails test:integration

# 直接指定文件
rails test test/integration/users_login_test.rb

# 测试邮件发送程序
rails test:mailers

查看当前的路由

1
rails routes

其他名词解释

Rails的简单处理流程

  1. 浏览器发送请求
  2. 经过routes.rb
  3. 根据路由在controller中寻找合适的函数
  4. controller调用模型的方法
  5. 模型通过继承activeRecord类获得了和数据库交流的方法,
    比如查询数据等等
  6. controller得到结果,更改变量的值,设定变量的值,
    然后根据controller的设置决定是否继续渲染对应的html页面
    1. rails可恶在渲染的页面不是手动指定的,只能看文件名
  7. 视图文件html.erb中可以使用controller中的变量,渲染html
  8. 用户看到结果

路由

路由的写法

  1. 简写案例1:

    get ‘static~pages~/home’
    get ‘/static~pages~/home’, to: ‘static~pages~#home’ (全称)
    get ‘/home’, to: ‘static~pages~#home’ (更符合常理的url)
    以get请求请求地址’static~pages~/home’,
    使用 static_pages_controllerhome 方法处理,
    最后返回 app/views/static_pages/home.html.erb 中内容.

  2. 简写案例2

    root ‘static~pages~#home’
    root, to: ‘static~pages~#home’
    get ‘/’, to: ‘static~pages~#home’ (全称)

具名路由

在get后写的路径,比如’/home’,可以在其他地方写成home~path来代替~.
另外还有一种写法是url,path和url的区别如下

root_path -> '/'
root_url  -> 'http://www.example.com/'

‘static~pages~/home’,貌似只能使用static~pageshomeurl来指代~,
因home~path表示~‘/home’

带参数的具名路由

常用在测试中

1
get edit_user_path(@user)
  • 这里使用user~controller的edit方法处理~

不过有时也正常使用

1
redirect_to @user

会自动调用user~controller的show方法~

resource和REST

如果路由中写

1
resource :users

则下列一系列的网址均可以使用

请求 URL 动作 作用
GET /users index 列出所有用户
GET /users/1 show 显示id为1的用户
GET /users/new new 显示创建新用户的页面
POST /users create 创建新用户
GET /users/edit edit 显示 ID 为 1 的用户的编辑页面
PATCH /users/1 update 更新 ID 为 1 的用户
DELETE /users/1 destroy 删除 ID 为 1 的用户

如果只想使用其中某些路由,则可以使用only

1
resources :invoices, only: [:create, :show, :update, :destroy]

REST的覆盖

有些地址,比如 /users/new,
可能开发者更喜欢使用其他的名称,
比如 signup,则
可能路由会写成
get ‘signup’, to: ‘users#new’

controller

各种过滤器

rails自带的好像是before~action~,
还有一些过滤器需要特定的gem才能工作

1
2
3
4
5
6
7
8
# 使用destroy函数前,先调用admin_user函数获
before_action :admin_user, only: :destroy

# 常用技巧,在使用show, edit等函数时,先使用set_user统一确定被编辑的对象
before_action :set_user, only: [:show, :edit, :update, :destroy]

# 在非登陆情况下访问该路径时,返回错误
protect_from_forgery with: :exception

include用法

当某个controller想要使用某模块中定义的函数时,
可以使用

1
include SessionsHelper

等形式引入该模块

render方法

rails的函数可以有多种返回值类型

  • html
  • json

其中html还可以根据语境使用对应的模板.
以下为举例

1
2
3
4
5
6
render html: "hello, world!"
render html: 'new'
render html: :new

render json: {}
render json: @user, status: 200
  • 若以上代码属于users~controller~.
    则渲染的new都是 app/view/users/new.html.erb

private方法

使用private关键字声明,
而后这些方法只能内部使用,
不能使用路由调用.
习惯上为了使private更明显,会将之后的函数全部再缩进一层

1
2
3
4
5
private
# Never trust parameters from the scary internet, only allow the white list through.
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end

model

模型验证

  1. 使用validates添加标准验证

    1
    2
    3
    validates :email, presence: true, length: { maximum: 255},
    format: { with: VALID_EMAIL_REGEX},
    uniqueness: { case_sensitive: false }

    email 属性进行验证

    • 不能为空或empty
    • 长度最大255
    • 需要符合特定格式([email protected])
    • 在内存中需要唯一(无视大小写)
  2. 使用validate添加自定义验证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    validate  :picture_size

    private

    # 验证上传的图像大小
    def picture_size
    if picture.size > 5.megabytes
    errors.add(:picture, "should be less than 5MB")
    end
    end

    其中 picture 为要验证的属性,全称为 self.picture,
    验证函数中可以使用该属性的各种信息,
    如果验证出错,最后会在模型验证后的信息中添加 'picture should be less than 5MB’的信息

  3. 模型错误信息

    user.valid? 返回true或false
    user.errors 错误对象.
    user.errors.messages 常用的错误信息样式,模型的字段为单位,每个后面跟着字符串数组
    user.errors.details 类似message,不过每个字段后跟着的是字典组成的数组,不常用
    user.errors.full~messages~ 也可能会比较常用,只是一个字符串数组,其中每句话都可以被自然理解

  4. 翻译文件及可以引用的变量

    模型出错后在报错时,可以使用本地化文件渲染翻译.
    翻译文件在 src/config/locales/xx.yml
    在报错信息中可以含有变量,
    %{value}表示目前变量的值,
    %{count}表示变量的目标值.
    %{model}可能表示当前的model,
    %{attribute}表示出问题的字段.

模型关联

现假设有users表,microposts表,和relationships表
一个user可以有多个microposts
一个user可以有多个active~relationships~(关注了其他人),
也因此,该user的following(被该user关注的人)有多个

1
2
3
4
5
6
7
8
# 一个用户有许多微博,当删除该用户时,一并删除所有关联的微博
has_many :microposts, dependent: :destroy
# user通过relationships表中的follower_id关联到users表自身
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
# users表自身的别名,通过relationships表建立了联系
has_many :following, through: :active_relationships, source: :followed

多个microposts可能只有一个user,

1
belongs_to :user

虚拟属性

一般模型中的字段与数据库中的字段是对应的,
有时当数据库中并不存在但为了使用方便需要为模型添加虚拟的字段.
比如当数据库中只存了xx~digest~(加密后的token)时,却想使用xx~token~,
可以使用以下方法.

1
2
3
4
5
6
7
attr_accessor :remember_token, :activation_token, :reset_token

# 为了持久保存会话,在数据库中记住用户
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end

过滤器

1
2
3
4
5
6
7
8
9
10
11
12
# 在调用模型的save方法前执行的动作
before_save { email.downcase! }
before_save { self.email = email.downcase }
before_save { self.email = self.email.downcase }

# 在调用模型的create方法前执行的动作
before_create :create_activation_digest

# scope方法表示将某个常用的查询写成一个方法,在执行数据库操作时一并执行
# 如果希望一个操作在每次查询数据库时都执行,则可以将该操作防止default_scope中
# -> 称为Proc(procedure,过程),或lambda,接收一个代码块(匿名函数)作为参数
default_scope -> { order(created_at: :desc)}

其他放在前面的内容

1
2
3
4
5
6
7
# carrierwave插件的方法,将图像和模型关联起来
# 数据库中以picture字段存储图像的路径
# 真正的图像使用PictureUploader上传到服务器并保存在约定的位置
mount_uploader :picture, PictureUploader

# 详见密码
has_secure_password

view

erb

  1. erb简介

    嵌入式Ruby(简称ERb),可以在html代码中嵌入ruby代码,有点像jsp.
    多数变量使用时会使用

    1
    <%= xxx %>
  2. 在erb中可使用的表达

    1. yield

      原理上不是很清楚,但目前使用的地方有两个

      1. 使用变量

        1
        <title><%= yield(:title) %></title>

        将页面的title设置为controller中设置的 title 变量的值

      2. 用作占位符

        1
        2
        3
        <body>
        <%= yield %>
        </body>

        yield处会填充各个访问的页面,比如home.html.erb等,
        这些放在application.html.erb下级的内容.

    2. content~tag~

      将某数据使用某tag表达出来

      1
      2
      <%= content_tag(:div, message, class: "alert alert-#{message_type}") %>
      <div class="alert alert-<%= message_type %>"><%= message %></div>
    3. link~to~

      用于表达一个链接,
      link~to的写法可能是因为Rails更喜欢使用具名路由而不是写死的编码~,
      这样可以更改url而无需更改对应的视图文件的文件名.

      1
      2
      <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
      <a href="/signup" class="btn btn-lg btn-primary">Sign up now!</a>
    4. render

      1
      2
      3
      4
      5
      <!-- 直接指定要渲染的模板 -->
      <%= render 'layouts/header' %>

      <!-- 指定一个复数的模型,ruby会寻找单数模型的模板,然后循环渲染 -->
      <%= render @users %>
    5. form~for~

      Rails自带的一个生成表单的方法,比原生表单方便一些
      传入一个ActiveRecord对象,则可以生成一个form.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <%= form_for(@user) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>
      <div class="form-group">
      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>
      </div>

      <div class="form-group">
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>
      </div>

      <%= f.submit yield(:button_text), class: "btn btn-primary" %>
      <% end %>

      但也不一定传入ActiveRecord对象,
      没有model文件的resource对象或者controller名也是可以的.
      使用url可以指定按下按钮后提交的路径.
      按说只要提交的参数与后台接收的参数能匹配即可.

      1
      2
      3
      4
      5
      6
      <%= form_for(:password_reset, url: password_resets_path) do |f| %>
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.submit "Submit", class: "btn btn-primary" %>
      <% end %>
    6. flash

      这是Rails一个很方便的组件,
      效果是在页面上出现一个几秒钟的提示文本.
      html,css,js等等全部写好了,只需要调用.

      比如提示成功可以使用success.
      此外还可以使用danger,应该是和bootstrap的主题联系起来的.
      还有一个flash是flash.now,用于在跳转后的页面中显示闪现消息.

      1
      2
      flash[:success] = "Welcome to the Sample App!"
      flash.now[:danger] = 'Invalid email/password combination'
  3. 视图

    视图的文件名一般是 app/view/<model>s/xx.html.erb
    通常调用可以是

    • controller中使用了对应方法xx后,自动使用对应的xx视图
    • controller中的其他函数主动跳转到该视图
      • 没登录时跳转到首页
      • 改密码的update方法报错时使用edit的视图
      • 两个有关联的功能没有各自的视图,合用一个,均使用render渲染同一个视图
  4. 局部视图

    一般使用下划线作文件名开头,
    比如 app/views/layouts/_header.html.erb
    调用时方法基本如下

    1
    <%= render 'layouts/header' %>
    • 省略了下划线
    • 常常用于重复使用的场景
      • header
      • footer
      • errors

Sass

Sass是一种编写CSS的语言,从多方面增强了CSS的功能

  • 嵌套
  • 变量
  • 混入

Sass支持一种名为scss的格式,scss是css的超集,
没有引入新的语法,一个css就相当于一个没有变量的scss.
Rails使用Asset Pipeline的方式管理css和js文件,
默认使用sass语言编译scss文件到css文件.
然后在生产环境中,会将所有css合并到application.css,所有js合并到application.js.
如果项目使用了bootstrap(使用Less语言编写css),
则需要先借助 bootstrap-sass gem先将Less转换成Sass才可以.

  1. 嵌套

    1
    2
    3
    4
    5
    6
    7
    .center {
    text-align: center;
    }

    .center h1 {
    margin-bottom: 10px;
    }

    写成

    1
    2
    3
    4
    5
    6
    .center {
    text-align: center;
    h1 {
    margin-bottom: 10px;
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #logo {
    float: left;
    margin-right: 10px;
    font-size: 1.7em;
    color: #fff;
    text-transform: uppercase;
    letter-spacing: -1px;
    padding-top: 9px;
    font-weight: bold;
    }

    #logo:hover {
    color: #fff;
    text-decoration: none;
    }

    写成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #logo {
    float: left;
    margin-right: 10px;
    font-size: 1.7em;
    color: #fff;
    text-transform: uppercase;
    letter-spacing: -1px;
    padding-top: 9px;
    font-weight: bold;
    &:hover {
    color: #fff;
    text-decoration: none;
    }
    }
  2. 变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    h2 {
    ...
    color: #777;
    }

    footer {
    ...
    color: #777;
    }

    写成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    $light-gray: #777;

    h2 {
    ...
    color: $light-gray;
    }

    footer {
    ...
    color: $light-gray;
    }

    如果使用了 bootstrap-sass gem,
    则可以直接使用其中定义好的变量,无需自行定义.

  3. 混入(mixin)

    更像是一段宏

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @mixin box_sizing {
    -moz-box-sizing: border-box;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    }

    .debug_dump {
    ...
    @include box_sizing;
    }

测试及测试导向

测试的分类

  1. 一般分3类

    • models测试
      • 命名上一般使用模型名(单数)~test~.rb,比如 user_test.rb
    • controllers测试
      • 命名上使用controller名~test~.rb,比如 users_controller_test.rb
    • integration测试
      • 命名上使用自定义测试名称~test~.rb,比如 users_login_test.rb
  2. 其余还有功能上的一些测试比如

    • helpers
      • 用于测试放在helper文件中的通用函数
    • mailers
      • 用于测试邮件发送程序
  3. 固件

    还有注意 test/fixtures/models.yml 文件

    • 用于在测试前给测试用数据库填充数据,支持使用ruby的循环
    • 命名上使用复数(可以理解为同数据库中表名)

测试驱动开发的介绍和优点

  1. 介绍

    测试驱动开发(Test-Driven Development,简称TDD)
    过程为

    1. 编写测试(此时测试是失败的)
    2. 编写代码让测试通过
    3. 重构时更改代码,保持测试还是通过的
  2. 优点

    1. 避免回归问题(之前能用的功能改代码后不能用了)
    2. 重构有自信
    3. 一定程度上让开发者明确目标
  3. 建议

    1. 需求不明确可以先写代码再写测试
    2. 安全相关功能先写测试
    3. 明确知道要改的先不写测试(比如html的细节)
    4. 重构前写测试

大致的测试流程

  1. 使用test环境,产生 log/test.log
  2. 依照 test/fixtures/models.yml 中内容为测试数据库填充数据
  3. 开始跑测试文件
    1. 先跑setup函数,再跑某一个测试项目
    2. 先跑setup函数,再跑下一个测试项目

常用的测试语句

  1. 动作

    1. 请求地址

      get static~pageshomeurl~
      get login~path~
      post login~path~, params: { test: content }
      delete logout~path~
      路由相关需要看路由

    2. 自行编写方法

      在test~helper~.rb中自行编写函数,并用于测试
      log~inas~(@user, remember~me~: ‘1’)

  2. assert

    1. 用于判断真假值

      assert flash.empty?
      assert~not~ flash.empty?

    2. 用于判断是否为空

      assert~empty~ cookies[‘remember~token~’]
      assert~notempty~ cookies[‘remember~token~’]

    3. 用于判断是否相等

      assert~equal~ 1, ActionMailer::Base.deliveries.size

    4. 用于判断返回的状况

      assert~response~ :success
      判断response的header是否为2xx

      assert~template~ ‘sessions/new’
      assert~match~ content, response.body
      判断response的body是否与template相同或是否与某字段匹配

      assert~select~ “a[href=?]”, login~path~, count: 2
      判断得到的页面上是否有2个类似<x href=“/login”>的元素

      assert~redirectedto~ @user
      判断是否跳转到某个页面

    5. 多用于测试数据库中

      assert~nodifference~ ‘Micropost.count’ do
      post microposts~path~, params: { micropost: { content: “” } }
      end
      判断执行post动作后Micropost.count的结果没有改变

      assert~difference~ ‘Micropost.count’, 1 do
      post microposts~path~, params: { micropost: { content: content } }
      end
      判断执行post动作后Micropost.count的结果改变了

测试美化

使用 minitest-reporters gem
并在 test/test_helper.rb 中添加如下

1
2
3
4
5
6
7
8
9
10
11
12
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
+ require "minitest/reporters"
+ Minitest::Reporters.use!

class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all

# Add more helper methods to be used by all tests here...
end

然后即可在使用 rails test 命令后以红绿色等更好看的格式显示结果

自动化测试

使用 guard gem
使用以下命令生成guard配置文件 Guardfile

1
bundle exec guard init

在配置文件中写

1
guard :minitest, spring: "bin/rails test", all_on_start: false do

表示让Guard使用Rails提供的Spring服务器,从而减少加载时间,而且启动时不运行整个测试组件
同时需要把应该把 spring/ 目录添加到 .gitignore 文件中,
防止sping和git产生冲突.
然后使用以下命令开始

1
bundle exec guard

书上说会监视文件的变化来自动测试,
但实测没有工作.

测试诊断

若为failed可以查看测试输出
若为error建议查看test.log
有时候某些测试失败,需要重启rails服务器才会成功
而还有一些时候出现错误,需要改项目的文件夹名才会成功

密码

简介

Rails默认使用的密码套件是 bcrypt,
功能包括

  • 加密字符串
  • 为user模型添加虚拟属性
  • 提供验证的函数

使用方法

  1. 安装bcrypt gem

  2. 在user model中使用 has_secure_password

    • 为user增加了两个虚拟属性,password和password~confirmation~
    • 为user增加了从password到password~digest的加密和保存方法~
  3. 编辑数据库

    1. 先生成迁移文件

      1
      2
      3
      4
      5
      class AddPasswordDigestToUsers < ActiveRecord::Migration[5.2]
      def change
      add_column :users, :password_digest, :string
      end
      end
    2. 然后使用 rails db:migrate 迁移数据库

  4. 自动加密并存储密码

    1
    2
    3
    user = User.new(name: "Example User", email: "[email protected]",
    password: "foobar", password_confirmation: "foobar")
    user.save # 自动加密password并在数据库中存储为password_digest

bcrypt的其他功能

  1. 验证是否正确

    1
    BCrypt::Password.new(digest).is_password?(token)
  2. 有需要时加密字符串

    1
    2
    3
    4
    5
    def digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
    BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
    end

登陆

基于session的登陆

  • 准备session控制器
  • 准备好session/new,或者说login页面对应的html文件
  • 验证成功后操作session变量即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module SessionsHelper

# 登入指定的用户
def log_in(user)
session[:user_id] = user.id
end

# 返回当前登录的用户(如果有的话)
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
end

# 退出当前用户
def log_out
session.delete(:user_id)
@current_user = nil
end
end
  • current~user方法定义并返回~ @current_user 变量的值.
    即current~user~()的值同 @current_user,
    因此可能会遇到current~user与~@current~user混用的情况~.

基于cookie的登陆

  1. 方案

    使用以下方案实现持久会话

    1. 数据库中添加remember~digest~,用于保存令牌的摘要
    2. 生成随机字符串,用作记忆令牌
    3. cookie中存入
      • 加密的用户id
      • 记忆令牌
      • 过期时间设置为未来的某个日期
    4. 数据库中存储加密后的令牌(或称令牌的摘要)
    5. 如果cookie中有用户的ID,就用这个ID在数据库中查找用户,
      并且检查cookie中的记忆令牌和数据库中的哈希摘要是否匹配.
  2. 具体代码

    数据库(模型)中操作
    app/models/user.rb

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 为了持久保存会话,在数据库中记住用户
    def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
    end

    # 忘记用户
    def forget
    update_attribute(:remember_digest, nil)
    end

    关于cookie的操作
    app/helpers/sessions_helper.rb

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 在持久会话中记住用户
    def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
    end

    # 忘记持久会话
    def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
    end

    在登陆或登出的同时一并进行记住不记住的问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    # app/controllers/sessions_controller.rb
    # 处理登陆的函数
    def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
    log_in user
    + params[:session][:remember_me] == '1' ? remember(user) : forget(user)
    redirect_to user
    else
    flash.now[:danger] = 'Invalid email/password combination'
    render 'new'
    end
    end

    # app/helpers/sessions_helper.rb
    # 退出当前用户
    def log_out
    + forget(current_user)
    session.delete(:user_id)
    @current_user = nil
    end

    # 处理登出
    # app/controllers/sessions_controller.rb

    def destroy
    + log_out if logged_in?
    redirect_to root_url
    end

    注意

    • 已经登录的才能退出

admin权限管理

使用以下方案实现自制的权限管理

  1. 在数据库中添加admin字段,boolean型,默认false(activeRecord会自动生成User.admin?方法)

  2. 在执行高危动作时先行判断: current~user~.admin?
    或者使用

    1
    2
    3
    4
    5
    6
    7
    8
    before_action :admin_user,     only: :destroy

    private

    # 确保是管理员
    def admin_user
    redirect_to(root_url) unless current_user.admin?
    end

    等形式进行限制

发送邮件

邮件也是系统的一个延伸,也需要有MVC结构

视图

纯文本版(app/views/user~mailer~/account~activation~.text.erb)

1
2
3
  UserMailer#account_activation

<%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erb

html版(app/views/user~mailer~/account~activation~.html.erb)

1
2
3
4
5
<h1>UserMailer#account_activation</h1>

<p>
<%= @greeting %>, find me in app/views/user_mailer/account_activation.html.erb
</p>

总的配置

1
2
3
4
5
6
7
8
9
Rails.application.configure do

config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :test
host = 'localhost:3000'

config.action_mailer.default_url_options = { host: host, protocol: 'http' }

end

定义发件人

1
2
3
4
class ApplicationMailer < ActionMailer::Base
default from: "[email protected]"
layout 'mailer'
end

定义发送动作

1
2
3
4
5
6
7
8
9
10
11
12
13
class UserMailer < ApplicationMailer

def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end

def password_reset
@greeting = "Hi"

mail to: "[email protected]"
end
end

邮件的预览

test/mailers/previews/user_mailer_preview.rb 文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview

# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/account_activation
def account_activation
user = User.first
user.activation_token = User.new_token
UserMailer.account_activation(user)
end

# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset
UserMailer.password_reset
end
en

激活功能

方案

  1. 数据库中添加相关字段
    1. activation~digest~
    2. activated
    3. activated~at~
  2. 模型中添加 activation_token 虚拟属性
  3. 注册时生成user记录,生成令牌并加密后存入数据库
  4. 向用户发送未加密的秘钥
  5. account_activations_controller 中添加验证并修改model的代码

实现代码

首先是user自身在创建前需要创建令牌
app/models/user.rb

1
2
3
4
5
6
7
before_create :create_activation_digest

# 创建并赋值激活令牌和摘要
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end

然后是邮件的内容和发送
app/views/user_mailer/account_activation.text.erb

1
2
3
4
5
Hi <%= @user.name %>,

Welcome to the Sample App! Click on the link below to activate your account:

<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>

邮件中包含了需要访问的网址和生成的令牌.

1
2
3
4
5
6
7
8
9
10
11
12
13
# app/mailers/user_mailer.rb
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end

# app/models/user.rb
# 发送激活邮件
def send_activation_email
UserMailer.account_activation(self).deliver_now
end

# 在注册的处理中执行send_activation_email即可

然后是处理函数
app/controllers/account_activations_controller.rb

1
2
3
4
5
6
7
8
9
10
11
12
def edit
user = User.find_by(email: params[:email])
if user && !user.activated? && user.authenticated?(:activation, params[:id])
user.activate
log_in user
flash[:success] = "Account activated!"
redirect_to user
else
flash[:danger] = "Invalid activation link"
redirect_to root_url
end
end

修改密码的功能

方案

  1. 数据库中添加字段
    1. reset~digest~
    2. reset~sendat~(主要用于30分钟失效)
  2. 模型中添加 reset_token 虚拟属性
  3. 当用户输入邮箱,验证正确后,生成令牌并加密后存入数据库
  4. 想用户发送未加密的秘钥
  5. password_resets_controller 中添加验证并修改密码的代码

实现代码

类似激活

关于动态流

应该是一种视觉效果,
一个随时更新的列表,
就像微博.
通常为user类定义feed方法.
如果用户不关注任何人,
user.feed和user.microposts效果相同.

分页

使用 will_paginate gem.
在页面上使用

1
<%= will_paginate @users %>

在controller中使用

1
2
3
def index
@users = User.where(activated: true).paginate(page: params[:page])
end

参考

  1. 奇怪符号