如何进行架构设计

难点不在于架构设计的好不好,而在于细节是否做得妥当,或者叫做你的架构是否基于最佳实践。

依据

  • 用户需求
  • 团队配置
  • 技术成熟度

用户需求

  • 可登陆的增删改查

团队配置

  • 会前后端的全栈工程师

技术成熟度

  • 假设只会 Rails + Vue / React

什么是前后端分离

这里说的前端和后端是指前端代码和后端代码,不指人。
不分离:传统的后端工程师从数据库、Redis读取数据渲染到 HTML 中,HTML 需要引用 js、css ,但是现代前端代码代码都是打包生成的(style.xxx.css、main.xxx.js),所以无法在 HTML 中提前引用这些 js、css,所以只能用 Rails(插件 webpacker) 读取 webpack 的内容,再反向写到 HTML 中。
分离:前端自己负责 HTML,自己搭建静态服务器给用户访问,如:Ngnix 或 Node等等,好处是 HTML 可以直接用 webpack 的插件,直接把打包后生成的文件写到 HTML 中,数据只通过 AJAX 获取。

注意前后端代码不一定要交给两个人写,可以由一个人写

用户需求

视觉稿

用例图

用例图释义:把使子全部画出来

表设计

有哪些表?

  • users / records / tags
  • 还有 taggings 表,表示 records 和 tags 的关联
  • password_reset_requests 记录所有重置请求

每个表有哪些属性

  • 不用一次想好,但命名一定要准确
  • 从简单的需求出发,逐渐迭代

users 表

从需求出发

  • 注册:邮箱、密码、确认密码
  • 数据库需要存密码吗?不需要,只存密文
  • 数据库需要存确认密码吗?不需要
  • 如何加密?使用最佳实践 has_secure_password
  • 密文叫什么?使用最佳实践 password_digest
  • 注册之后需要发欢迎邮件吗? 使用 mailer
  • 需要强制用户验证邮箱吗? 可以不强制,也可以强制

结论

  • users 表含有 email 和 password_digest

开始实现

步骤

  • 创建 model
  • console 操作 user
  • 创建 controller
  • 配置 routes
  • 配置 mailer
  • 创建 mailer
  • 使用 HttpClient
  • 创建 rspec 测试
  • 改代码,测试
  • 改代码,测试
  • 改代码,测试

约定

  • RESTful 接口风格
  • post /users 就是注册接口,不能不加 s
  • 文件目录

    创建数据库 bin/rails db:create

创建 User 表和 Model

1
2
3
bin/rails g model User email:string password_digest:string
bin/rails db:migrate
// bin/rails -T 查看命令

使用控制台增删改查

1
2
3
4
5
6
7
8
9
bin/rails console
# 创建用户
> u = User.new
> u.email = '1.qq.com'
> u.password_digest = 'xxxxxx'
> u.save
# 查
User.all # 全部
User.all[0] # 第一个 或 User.first、User.second(用英文即可)

不用写任何代码,为什么就可以进行增删改查呢?这才是一个成熟的框架应该内置好的。

上述操作有个问题,我们不能直接赋值 password_digest

  • 搜索 rails has_secure_password
  • 找到 Gemfile 打开注释 gem 'bcrypt', '~> 3.1.7'
  • 然后再 User 上加这句话 has_secure_password
  • 安装依赖 bin/bundle install

再次创建

1
2
3
4
5
6
7
bin/rails console
# 创建用户
> u = User.new
> u.email = '2.qq.com'
> u.password = 'xxxxxx'
> u.password_confirmation = 'xxxxxx'
> u.save

使用 http 请求来创建用户

首先在 routes 里添加

1
2
3
4
5
get '/users', to: 'users#index'
get '/users/:id', to: 'users#show'
post '/users', to: 'users#create'
delete '/users/:id', to: 'users#destroy'
patch '/users/:id', to: 'users#update'

这五个增删改查几乎是每个表都要写,既然这么麻烦,于是 rails 就提供了另一个方法,等价于写了上面五句话

1
resources :users

使用 bin/rails routes 即可查看所有 routes 得以验证

创建 controller

1
bin/rails g controller users

然后在 controller 定义一个 create 方法

1
2
3
4
5
6
7
8
9
class UsersController < ApplicationController
def create
user = User.new
user.email = params[:email]
user.password = params[:password]
user.password_confirmation = params[:password_confirmation]
user.save
end
end

但是测试时发现没有传 password_confirmation 竟然也能成功保存,所以我们需要加上非空校验

在 user.rb 中加入

1
2
3
4
5
class User < ApplicationRecord
has_secure_password
validates_presence_of :email
validates_presence_of :password, :password_confirmation, on: [:create] # 只在创建时校验
end

并加上返回响应

1
2
3
4
5
6
7
8
9
10
11
12
13
class UsersController < ApplicationController
def create
user = User.new
user.email = params[:email]
user.password = params[:password]
user.password_confirmation = params[:password_confirmation]
if user.save
render json: user, status: 200
else
render json: user.errors, status: 400
end
end
end

当我们什么都没传时,发现 password 重复报错了两遍,可能是 has_secure_password 也做了校验,所以移除我们自己的

1
2
3
4
5
6
class User < ApplicationRecord
has_secure_password
validates_presence_of :email
validates_uniqueness_of :email
validates_presence_of :password_confirmation, on: [:create] # 只在创建时校验
end

再加上更完善的校验

1
2
3
4
5
6
7
8
class User < ApplicationRecord
has_secure_password
validates_presence_of :email
validates_presence_of :password_confirmation, on: [:create]

validates_format_of :email, with: /.+@.+/
validates_length_of :password, minimum: 6, on: [:create]
end

校验国际化

  • 搜索 rails i18n
  • config/locales/ 下创建文件 zh-CN.yml
  • config/initializers/ 下创建 locale.rb 文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # config/initializers/locale.rb

    # Where the I18n library should search for translation files
    I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')]

    # Permitted locales available for the application
    I18n.available_locales = [:en, 'zh-CN']

    # Set default locale to something other than :en
    I18n.default_locale = 'zh-CN'
  • 根据报错依次追加内容
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    zh-CN:
    activerecord:
    errors:
    models:
    user:
    attributes:
    password:
    blank: 密码不能为空
    too_short: 密码不能少于 %{count} 位
    email:
    blank: 邮箱不能为空
    invalid: 邮箱格式不合法
    password_confirmation:
    blank: 确认密码不能为空

额外,优化一下上面的这部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
class UsersController < ApplicationController
def create
user = User.new
user.email = params[:email]
user.password = params[:password]
user.password_confirmation = params[:password_confirmation]
if user.save
render json: { resource: user }, status: 200
else
render json: { errors: user.errors }, status: 400
end
end
end

第一步

1
2
3
4
5
6
7
8
9
10
class UsersController < ApplicationController
def create
user = User.new({ email: params[:email], password: params[:password], password_confirmation: params[:password_confirmation] })
if user.save
render json: { resource: user }, status: 200
else
render json: { errors: user.errors }, status: 400
end
end
end

第二步使用 permit 方法

1
2
3
4
5
6
7
8
9
10
class UsersController < ApplicationController
def create
user = User.new params.permit(:email, :password, :password_confirmation)
if user.save
render json: { resource: user }, status: 200
else
render json: { errors: user.errors }, status: 400
end
end
end

第三步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class UsersController < ApplicationController
def create
user = User.new create_params
user.save
render_resource user
end

def create_params
params.permit(:email, :password, :password_confirmation)
end

def render_resource(resource)
if resource.valid?
render json: {resource: resource}, status: 200
else
render json: {errors: resource.errors}, status: 400
end
end
end

第四步,使用 create 方法,等价于 先 new 再 save

1
2
3
4
5
6
7
8
9
10
11
class UsersController < ApplicationController
def create
user = User.create create_params
render_resource user
end

def create_params
params.permit(:email, :password, :password_confirmation)
end
end
# render_resource 方法已提升到 ApplicationController 中

第五步,很明显 user 声明了直接使用

1
2
3
4
5
6
7
8
9
class UsersController < ApplicationController
def create
render_resource User.create create_params
end

def create_params
params.permit(:email, :password, :password_confirmation)
end
end

发送邮件

  • bin/rails generate mailer UserMailer
  • app/mailers/user_mailer
    1
    2
    3
    4
    5
    6
    7
    class UserMailer < ApplicationMailer
    def welcome_email(user)
    @user = user
    @url = 'https://www.baidu.com'
    mail(to: @user.email, subject: 'Welcome to My Awesome Site')
    end
    end
  • app/mailers/application_mailer.rb
    1
    2
    3
    4
    class ApplicationMailer < ActionMailer::Base
    default from: '529743595@qq.com'
    layout 'mailer'
    end
  • app/views./user_mailer/welcome_email.html.erb
    1
    2
    3
    4
    5
    6
    7
    8

    <p>
    <%= @user.email %> 您好,
    </p>
    <p>
    欢迎来到 Morney,情记一笔把!
    </p>

  • config/environments/development.rb
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # config.action_mailer.raise_delivery_errors = false
    #
    # config.action_mailer.perform_caching = false
    config.action_mailer.delivery = :smtp
    config.action_mailer.raise_delivery_errors = true
    config.action_mailer.perform_caching = false
    config.action_mailer.smtp_settings = {
    address: ENV['smtp_domain'],
    port: ENV['smtp_port'],
    domain: ENV['smtp_domain'],
    user_name: ENV['smtp_username'],
    password: ENV['smtp_password'],
    authentication: ENV['smtp_authentication'],
    enable_starttls_auto: ENV['smtp_enable_starttls_auto'],
    }
    config.action_mailer.preview_path = "#{Rails.root}/spec/mailers/previews"
  • 用 dotenv 创建环境变量,Gemfile
    1
    gem 'dotenv-rails'
  • config/application.rb
    1
    2
    Bundler.require(*Rails.groups)
    Dotenv::Railtie.load
  • .env
    1
    2
    3
    4
    5
    6
    7
    8
    export smtp_username=''
    export smtp_password=''
    export smtp_domain='smtp.qq.com'
    export smtp_port='587'
    export smtp_authentication='plain'
    export smtp_enable_starttls_auto=true

    export mailer_sender=''
  • .env.local(并添加至 .gitignore)
    1
    2
    export smtp_username='xxxxx'
    export smtp_password='xxxxxx'
  • app/models/user.rb
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class User < ApplicationRecord
    has_secure_password
    validates_presence_of :email
    validates_uniqueness_of :email
    validates_presence_of :password_confirmation, on: [:create]

    validates_format_of :email, with: /.+@.+/, if: :email
    validates_length_of :password, minimum: 6, on: [:create], if: :password

    after_create :send_welcome_email
    def send_welcome_email
    UserMailer.welcome_email(self).deliver_now
    end
    end