Rails 项目总结
Why ruby
1. 开发效率高,代码可读性强
Ruby 语言语法简洁优雅,非常接近自然语言(比如 if 倒置、unless,until loop,数组的 any? none?,范围 1..10),易于阅读和编写。
数组的加减、访问元素的方式
2. 生态系统成熟
Ruby 社区非常活跃,提供了大量的开源库和工具,可以帮助我们解决各种问题。Rails 框架本身也包含了大量的功能模块,可以满足大部分 Web 应用开发的需求。
3. 像JS
Ruby 是典型的面向对象语言,在 Ruby 中一切皆是对象,包括基础数据类型。很多语言特性非常像 js ,比如数组的方法、字面量创建值,yield
Why Rails
Rails 框架遵循“约定优于配置”的原则,提供了丰富的工具和库,可以快速构建 Web 应用。有经验的 Ruby 开发者使用 Rails 框架进行开发,速度比使用其他语言和框架快 30%-40%。这样可以节省大量时间和精力,让我们可以专注于业务逻辑和用户体验。
面向命令行开发,一切动作都内置了命令行程序
包和源码的组织方案非常成熟
初始化
环境配置
macos
安装 rails
安装数据库
$ brew update
$ brew install postgresql@14
$ brew services start postgresql@14
$ brew services stop postgresql@14
- 安装必要驱动:
pacman -S postgresql-libs
ubuntu(wsl)
安装 postgresql PostgreSQL: Linux downloads (Ubuntu)
安装必要驱动:
sudo apt-get install libpq-dev
数据库配置
macos
# 连接postgresql,不指定用户名和数据库默认是当前登陆系统账号同名的用户与数据库
psql
# 终端执行命令,其作用是创建一个与当前系统登陆用户同名的数据库,目的是为了可以通过这个数据库连接上 postgresql,不执行的话会报错数据库找不到
createdb
psql -U USERNAME -W PASSWORD
# 创建 gsemir 用户
CREATE USER gsemir WITH PASSWORD '123456';
# 删除默认的 postgres 数据库
DROP DATABASE postgres;
# 创建属于 gsemir 用户的数据库
CREATE DATABASE gsemir OWNER gsemir;
# 将所有权限赋给 gsemir 用户
GRANT ALL PRIVILEGES ON DATABASE gsemir to gsemir;
# 给 postgres 用户添加 创建数据库 的属性
ALTER ROLE gsemir CREATEDB;
# 之后就可以使用 gsemir 用户来创建并管理其他数据库了
ubuntu(wsl)
配置同上
注意事项
- 项目代码用户与数据库用户需要统一
- 本项目代码的权限是属于 gsemir 用户,而数据库配置的用户只有 postgres
- 不能使用 gsemir 用户操作 postgres 角色(database.yml)的数据库(运行db create等)
- 因此 gsemir 用户也要在数据库创建同名的角色及同名的数据库,添加 createdb 权限
启动服务
- 启动
sudo service postgresql start
- 状态
sudo service postgresql status
- 启动
docker
docker run -d \
# 容器名称
--name db-for-rails-todo \
# 环境变量
-e POSTGRES_USER=gsemir \
-e POSTGRES_PASSWORD=123456 \
-e POSTGRES_DB=rails-todo-dev \
-e PGDATA=/var/lib/postgresql/data/pgdata \
# 新增数据卷
-v rails-todo-data:/var/lib/postgresql/data \
# 镜像名称 版本14
postgres:14
# 供复制:
docker run -d --name db-for-rails-todo -e POSTGRES_USER=gsemir -e POSTGRES_PASSWORD=123456 -e POSTGRES_DB=rails-todo-dev -e PGDATA=/var/lib/postgresql/data/pgdata -v rails-todo-data:/var/lib/postgresql/data postgres:14
数据库常用命令
\c <database_name>
连接数据库\l
列出全部数据库\dt
显示全部表格
IDE
- 安装vscode扩展
ckolkman.vscode-postgres
项目初始化
ruby 配置
安装 ruby:
rvm install ruby-3.0.0
配置国内源
$ gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/
$ bundle config mirror.https://rubygems.org https://gems.ruby-china.com
安装 rails:
gem install rails -v 7.0.2.3
安装项目依赖:
bundle
数据库配置
开发与测试数据库配置
# config/database.yaml
development:
<<: *default
database: rails_todo_dev
username: gsemir
password: 123456
host: localhost
port: 5432
创建或运行项目
同步数据库及数据表:
bin/rails db:create db:migrate
创建项目
# 仅使用api模式,指定数据库,忽略测试(后面自己配),指定项目名称
rails new --api --database=postgresql --skip-test todo-backend-rails-1
- 启动 rails 项目
bin/rails s -p 3001 -e development
bin/rails s
也行
rspec 配置
- 将
gem 'rspec-rails', '~>5.0.0'
复制到Gemfile
中
group :development, :test do
gem "debug", platforms: %i[ mri mingw x64_mingw ]
gem 'rspec-rails', '~> 5.0.0'
end
运行
bundle
,安装依赖初始化rspec:
bin/rails g rspec:install
配置测试数据库
在 config/database.yml
配置测试数据库,同开发
- 创建数据库,同步数据表:
RAILS_ENV=test bin/rails db:create db:migrate
- 运行单元测试:
$ bundle exe rspec
# 或 rspec
# 执行指定的测试用例
$ rspec -e "获取当前登录用户"
# 也可以指定行数
$ rspec spec/requests/api/v1/me_spec.rb:5``
API 文档配置
- Gemfile 使用本地依赖
gem 'rspec_api_documentation', path: './vendor/rspec_api_documentation'
- 提前解决Api文档中请求体与响应体无法正常显示的问题
- 请求体
# spec/spec_helper.rb
require 'rspec_api_documentation'
RspecApiDocumentation.configure do |config|
# 配置文档中请求体的格式为json
config.request_body_formatter = :json
end
- 响应体
用修复过 bug 的依赖
# 将代码克隆到 vendor 中,别忘删除掉.git
$ git clone git@github.com:GSemir0418/rspec_api_documentation.git vendor/rspec_api_documentation
# 刷新依赖
$ bundle
- 创建 api 文档测试文件夹
mkdir spec/acceptance
API开发
API 开发主要流程及命令
设计 table、api
创建 model
bin/rails g model <Name> field1:type field2:type
同步数据表
bin/rails db:create db:migrate
RAILS_ENV=test bin/rails db:create db:migrate
- 修改表结构
bin/rails g migrate AddDeletedAtToItems deleted_at:datetime
- 回滚命令
bin/rails db:rollback step=1
创建 controller
bin/rails g controller api/v1/<controller_names>
bin/rails g controller Api::V1::<Names>
驼峰和下划线都可, 斜杠也可, 在Rails中,双冒号(::)用于表示命名空间的层级关系,而斜线(/)用于表示路径的层级关系。Rails会将斜线转换为双冒号,并根据命名空间的层级关系创建相应的文件和目录结构。因此,在这两条指令中,最终生成的文件和动作都是相同的。
TDD
- 初始化
bin/rails g rspec:install
- 创建测试文件
bin/rails g rspec:request <controller_names>
- 创建接口测试文档文件
touch /spec/acceptance/<controller_names>_spec.rb
接口文档生成
bin/rake docs:generate
设计 Table
t.string 和 t.text 区别
数据类型: t.string 创建的列会使用数据库中的字符串类型(如 VARCHAR),而 t.text 创建的列会使用数据库中的文本类型(如 TEXT)。
存储空间: 字符串类型 (t.string) 通常用于存储相对较短的文本,例如标题、姓名等,有一个固定的最大长度。而文本类型 (t.text) 通常用于存储更长的文本,如文章内容、评论等,没有固定的最大长度限制。
索引: 由于字符串类型有固定的最大长度,因此可以创建索引以提高搜索和排序的性能。文本类型没有固定的最大长度,所以不能创建普通索引,但可以创建全文索引(Full-Text Index)来支持全文搜索。
Model
执行生成命令后,rails 会帮我们创建数据模型文件
及数据库schema
文件;
模型类默认是单数形式的
ActiveRecord 是 Rails 中的默认 ORM 工具,用于简化与数据库的交互。
模型类通常继承 ApplicationRecord ?? ActiveRecord::Base,以实现与数据库的交互
提供验证方法 validate 支持常规校验与自定义校验,自定义校验除非在self.errors中add error,否则默认返回 nil,表示通过校验
数据范围 default_scope
声明与其他模型之间的关系 belongs_to
定义该数据模型实例的方法,例如User的generate_jwt
支持生命周期函数,例如before_create,after_create,用于数据模型实例在被创建前后执行的逻辑
总结:将数据的校验逻辑及模型相关的工具逻辑抽离到model类中
数据的校验逻辑会在执行 save/create/update 等方法时进行校验
工具逻辑(例如生成 jwt、生成验证码、发送邮件)的执行可以利用 ActiveRecord 提供的生命周期回调(before_create/after_save/before_update
等),决定工具函数的执行时机
而 controller 层仅用于操作数据库,根据不同情况返回不同响应
Migration
生成 model 会生成 migration 文件
执行同步命令后,rails 会主动读取数据库信息,将数据库表结构存入 schema 文件中,因此手动更改这个文件是无效的
数据库表结构只能通过执行一次新的 migration 来修改
Controller
执行生成命令后,rails会帮我们创建控制器文件
及对应路由
专注于请求/响应逻辑,与数据库交互的逻辑等
路由
resource 定义了一个 RESTful 资源,表示该资源具有多个默认的 CRUD(创建、读取、更新、删除)操作。
控制器文件默认都是复数形式的,与我们在路由文件中定义的 resource 的单复数形式无关
关于路由用 resource 还是 resources:
- resource用于当您只有一个模型对象(单数资源)时,例如/profile表示当前登录用户的个人资料。使用resource时,不会创建index路由,而且没有一个路由需要在URL中传递ID参数。
- resources用于当您有多个模型对象(复数资源)时,例如/posts表示所有的文章。使用resources时,会创建index, show, new, edit, create, update, destroy等常用的路由,每个路由都需要在URL中传递ID参数来指定具体的对象。
在 RESTful 设计中,资源的路由通常使用复数形式,rails 也会自动将路由映射为复数形式
路由可以通过定义资源的形式来表示,例如 resources :items, only: [:create, :index, :destroy]
手动指定路由与控制器方法的映射关系:
post :validation_code, to: 'validation_code#create'
only 数组中定义了该资源接收的请求方法,其余请求方法均会返回 404
这些请求方法分别对应 controller 中的方法
其中 index 与 show 方法非常相似,但 index 用于显示资源的集合(列表),可以显示多个资源;而 show 用于显示单个资源的详细信息,主要用于显示单个资源。
这些方法通常与路由一起使用,以便在浏览器中使用相应的 URL 访问它们。例如,index方法可以与 GET /posts 或 /posts?page=1 路由关联,而show方法可以与 GET /posts/:id 路由关联
CRUD Api
在编写 controller 逻辑时,通常需要借助 model 类对数据库进行增删改查的操作。下面列举一些常见 api
new 创建一个数据实例
save 将实例保存至数据库
create 相当于 new + save
find 使用主键查询
find_by 使用传入的值作为查询条件,返回第一个满足条件的数据
update 一般使用find查数据,该数据,再save,相当于update
响应
rails 提供了 render、head、redirect_to等关键字指定响应体内容或重定向操作
render 关键字可以指定要渲染的视图模板、设置响应头、指定响应格式等
render json: {}
:将指定的 JSON 对象作为响应的主体。render plain: 'text'
:将指定的纯文本作为响应的主体。render file: 'path/to/file'
:将指定的文件内容作为响应的主体。render template: 'path/to/template'
:渲染指定的视图模板并作为响应的主体。render action: :new
:渲染指定动作对应的视图模板并作为响应的主体。
head
方法则更适用于简单的状态码响应,不需要具体响应主体内容的情况。
TDD
TDD 测试驱动开发是一种开发策略,即先写单元测试,再以通过测试的目的来写 controller 层逻辑。测试完成后,接口功能基本也实现了
测试分成单元测试和文档测试,单元测试用来测试接口失败的情况,而文档测试用来测试接口成功的情况,成功后会自动生成接口文档
配置rspec,统一文档测试时的请求头
# spec/spec_helper.rb
...
Rspec.configure do |config|
config.before(:each) do |spec|
# 如果是acceptance类型的测试
if spec.metadata[:type].equal? :acceptance
header 'Accept', 'application/json'
header 'Content-Type', 'application/json'
end
end
...
生成文档命令:bin/rake docs:generate
bin/rake 与 bin/rails 的区别:
bin/rake 用于执行 Rake 任务,比如数据库迁移、测试运行、api文档生成等
bin/rails 提供了更高级的命令,比如服务器运行、应用程序生成等
接口文档页面在 doc/api 文件夹下,使用 npx http-server doc/api
来查看
其他
密钥管理
- 生成或编辑 keys
bin/rails credentials:edit
- or 指定 vscode 编辑
EDITOR="code --wait" bin/rails credentials:edit
- 读取 keys
bin/rails c | Rails.application.credentials.secret_key_base
或者Rails.application.credentials.config
- 生产环境 keys
EDITOR="code --wait" rails credentials:edit --environment production
- 需要自定义 secret_key_base
- 读取生产环境 keys
RAILS_ENV=production bin/rails c | Rails.application.credentials.secret_key_base
或者Rails.application.credentials.config
- 生产环境数据库配置
- 剧透:对于本项目来说,只会管理 jwt 密钥、邮箱服务器授权码及数据库密码
- 查看密钥:
bin/rails c | Rails.application.credentials.config
中间件
写中间件 bin/rails middleware 显示全部中间件 touch lib/auto_jwt.rb 在lib下创建中间件文件(lib是自己给自己写的库,vendor是别人给自己写的库)
全局配置中间件
# config/application.rb
...
require_relative "../lib/auto_jwt"
Bundler.require(*Rails.groups)
module RailsTodo1
class Application < Rails::Application
...
config.middleware.use AutoJwt
end
end
改写 mes controller
配置邮件服务器
- 创建 mailer
bin/rails generate mailer User
create app/mailers/user_mailer.rb
invoke erb
create app/views/user_mailer
invoke rspec
create spec/mailers/user_spec.rb
create spec/mailers/previews/user_preview.rb
- Mailer 全局配置
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
# 全局发送邮件地址
default from: "845217811@qq.com"
# html默认布局
layout 'mailer'
end
- 邮件配置
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def welcome_email(code)
# 给模板传递变量
@code = code
mail(to: "845217811@qq.com", subject: 'Welcome to My Awesome Site')
end
end
- 修改邮件内容
<!-- app/views/user_mailer/welcome_email.html.erb -->
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
</head>
<body>
<%# 使用传递的变量 %> hi <%= @code %>
</body>
</html>
- 开启 qq 邮箱的 smtp 服务:登录 qq 邮箱,点击「设置」=>「账户」=>「开启 IMAP/SMTP 服务」=>「短信验证」
- 保存授权码:
EDITOR="code --wait" bin/rails credentials:edit
- 写入 email_pw: ycmbmysnijqcbfgb
- 配置邮件服务:记得生产环境也要配置
# config/environments/development.rb
...
# 是否抛出邮件错误
config.action_mailer.raise_delivery_errors = true
# 是否使用缓存
config.action_mailer.perform_caching = false
#
config.action_mailer.smtp_settings = {
address: 'smtp.qq.com',
port: 587,
domain: 'smtp.qq.com',
user_name: '845217811@qq.com',
password: Rails.application.credentials.email_pw,
authentication: 'plain',
anable_starttls_auto: true,
open_timeout: 10,
read_timeout: 10
}
...
- 控制台测试邮件发送功能
bin/rails console
# 使用UserMailer内置方法即可
UserMailer.welcome_email('123456').deliver
记得生产环境也需要配置授权码
部署
密钥配置
- 保存数据库密码、邮箱权限码以及 jwt 密钥
EDITOR="code --wait" rails credentials:edit --environment production
或者
EDITOR="vi" rails credentials:edit --environment production
可以通过
bin/rails c
进入控制台查询开发环境的密钥(Rails.application.credentials.config)
数据库配置
config/database.yml
production:
<<: *default
database: rails_todo_production
username: ubuntu
# password: <%= ENV["RAILS_TODO_1_DATABASE_PASSWORD"] %>
password: <%= Rails.application.credentials.db_password %>
host: localhost
port: 5432
运行环境配置
- 设置服务器环境变量
vi ~/.zshrc
RAILS_ENV=production
配置 bundle 源
bundle config mirror.https://rubygems.org https://gems.ruby-china.com
安装依赖pg报错
sudo apt install libpq-dev
邮件配置
config/environments/production.rb
脚本环境变量读不出来的bug:脚本开头加上
source "/home/$user/.zshrc"
生产环境使用 puma 作为后端服务器,使用
puma-daemon
后台运行
Gemfile 添加依赖
gem "puma", "~> 5.0"
gem 'puma-daemon', require: false
bundle 安装依赖
修改 puma 配置
require 'puma/daemon'
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count
# Specifies the `worker_timeout` threshold that Puma will use to wait before
# terminating a worker in development environments.
#
worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port ENV.fetch("PORT") { 3000 }
# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch("RAILS_ENV") { "production" }
# Specifies the `pidfile` that Puma will use.
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
# preload_app!
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart
stdout_redirect 'log/access.log', 'log/error.log', true
daemonize
开启后台 puma 服务器 bundle exec puma -C config/puma.rb
终止后台 puma 服务器 bundle exec pumactl stop
部署脚本 bin/deploy.sh
function title {
echo
echo "###############################################################################"
echo "## $1"
echo "###############################################################################"
echo
}
user=ubuntu
ip=gsemir2.tpddns.cn
project_name=todo-backend-rails
time=$(date +'%Y%m%d-%H%M%S')
cache_dir=tmp/deploy_cache
dist=$cache_dir/todo-$time.tar.gz
current_dir=$(dirname $0)
deploy_dir=/home/$user/deploys/backend/$project_name/$time
gemfile=$current_dir/../Gemfile
gemfile_lock=$current_dir/../Gemfile.lock
vendor_dir=$current_dir/../vendor
vendor_api=rspec_api_documentation
mkdir -p $cache_dir
# 打包源代码至 tmp
title '打包源码(缓存与依赖除外)'
tar --exclude="tmp/cache/*" --exclude="tmp/deploy_cache/*" --exclude="vendor/*" -cz -f $dist *
# 打包本地依赖优化项目部署效率
title "打包本地依赖以及${vendor_api}"
bundle cache --quiet
tar -cz -f "$vendor_dir/cache.tar.gz" -C ./vendor cache
tar -cz -f "$vendor_dir/$vendor_api.tar.gz" -C ./vendor $vendor_api
title '创建远程目录'
ssh $user@$ip "mkdir -p $deploy_dir/vendor"
title '上传源代码及依赖'
scp $dist $user@$ip:$deploy_dir/
yes | rm $dist
scp $gemfile $user@$ip:$deploy_dir/
scp $gemfile_lock $user@$ip:$deploy_dir/
# 将cache也上传到部署目录下 -r表示整个路径下的内容
scp -r $vendor_dir/cache.tar.gz $user@$ip:$deploy_dir/vendor/
yes | rm $vendor_dir/cache.tar.gz
scp -r $vendor_dir/$vendor_api.tar.gz $user@$ip:$deploy_dir/vendor/
yes | rm $vendor_dir/$vendor_api.tar.gz
title '上传 setup 脚本'
scp $current_dir/setup.sh $user@$ip:$deploy_dir/
title '执行远程启动脚本'
ssh $user@$ip "export version=$time; /usr/bin/zsh $deploy_dir/setup.sh"
运行脚本 bin/setup.sh
#! /usr/bin/zsh
function title {
echo
echo "###############################################################################"
echo "## $1"
echo "###############################################################################"
echo
}
user=ubuntu
project_name=todo-backend-rails
root=/home/$user/projects/backend/$project_name/$version
deploy_dir=/home/$user/deploys/backend/$project_name/$version
title '初始化zsh'
source "/home/$user/.zshrc"
title '创建项目根目录'
mkdir -p $root/vendor
title '拷贝Gemfile至项目根目录'
cd $deploy_dir
cp Gemfile $root
# 暂时不用 lock,因为服务器架构不同,依赖版本不同
# cp Gemfile.lock $root
title '解压缩依赖包至项目根目录'
tar -xz -f ./vendor/cache.tar.gz -C $root/vendor
tar -xz -f ./vendor/rspec_api_documentation.tar.gz -C $root/vendor
title '本地安装依赖'
cd $root
# bundle config set --local without 'development test'
# bundle install --local
bundle install
title '解压缩源代码'
tar -xz -f $deploy_dir/todo-$version.tar.gz -C ./
# echo "是否要更新数据库?[y/N]"
# read ans
# case $ans in
# y|Y|1 ) echo "yes"; title '更新数据库'; bin/rails db:create db:migrate ;;
# n|N|2 ) echo "no" ;;
# "" ) echo "no" ;;
# esac
# title '启动项目'
# bin/rails s
title '部署完毕, 请手动安装依赖并同步数据库'
title '启动项目 bundle exec puma -C config/puma.rb'
title '终止项目 bundle exec pumactl stop'