TOC
Open TOC
Info
UCB CS169: software engineering
https://learning.edx.org/course/course-v1:BerkeleyX+CS169.1x+1T2022/home
CHIPS - Coding/Hands-on Integrated Projects
https://github.com/saasbook/courseware#chips-codinghands-on-integrated-projects-with-autograding
CHIPS 2.5: Ruby Intro
Info
https://github.com/VGalaxies/hw-ruby-intro
https://www.ruby-lang.org/zh_cn/
https://learnxinyminutes.com/docs/zh-cn/ruby-cn/
Intro
xerolinux 自带了 ruby / gem / rspec
RSpec - 单元测试框架
RubyGems - 包管理器
Bundler - 跟踪并安装所需的特定版本的 gem,以此来为 Ruby 项目提供一致的运行环境
$ gem install bundler
使用 Gemfile
管理 gems
VSCode 插件 - Ruby Solargraph
$ gem install solargraph
需要正确配置路径
gem install xxx
可执行文件路径为 /home/vgalaxy/.local/share/gem/ruby/3.0.0/bin/
注意 bundle 安装 gem 的路径在项目中
$ bundle config set --local path 'vendor/bundle'$ bundle install
使用
$ bundle exec xxx
来运行 gem
Gems
下面介绍一些 gems
- guard
Test Driven Development
http://code.tutsplus.com/tutorials/testing-your-ruby-code-with-guard-rspec-pry—cms-19974
检测文件变化,自动运行测试
通过 guard init
生成 Guardfile
感觉不太灵敏
- byebug
调试器
- pry / irb
提供 REPL 环境
- cucumber
Behaviour Driven Development
- rake
类似 make
也可以通过 archlinux 安装
Rakefile
CHIPS 3.3: HTTP and URIs
https://github.com/saasbook/hw-http-intro
略
CHIPS 3.7: Create and Deploy a Simple SaaS app
Part 0
https://github.com/VGalaxies/simple-saas-app
区分开发和生产环境
https://dev.to/flippedcoding/difference-between-development-stage-and-production-d0p
without bundler
$ gem install rack$ gem install sinatra$ gem install rerun
setup
$ rackup --port 3000$ rerun -- rackup --port 3000
access http://localhost:3000/
deploy to Heroku
https://dashboard.heroku.com/apps
$ yay -S heroku-cli$ heroku login -i
需要额外添加 gem 'puma'
https://akshaykhot.com/couldnt-find-handler-error/
需要生成 Gemfile.lock
似乎提前安装了所需的 gems 后,就不会在项目中安装 gems 了
添加 Procfile
$ web: bundle exec rackup config.ru -p $PORT
部署
$ git push heroku master
Part 1
https://github.com/VGalaxies/hw-sinatra-saas-wordguesser
$ bundle exec autotest$ http http://randomword.saasbook.info/RandomWord
延迟比 guard 低
TDD 体验很好
byebug 报错
LoadError: cannot load such file -- irb Did you mean? erb drb
需要修改 Gemfile
gem 'byebug' #, '5.0.0' gem 'irb' gem 'rdoc'
感觉 Bundler 没有处理好依赖关系
Part 2
介绍了 RESTful API 的设计
Show game state, allow player to enter guess; may redirect to Win or Lose | GET /show |
Display form that can generate POST /create | GET /new |
Start new game; redirects to Show Game after changing state | POST /create |
Process guess; redirects to Show Game after changing state | POST /guess |
Show "you win" page with button to start new game | GET /win |
Show "you lose" page with button to start new game | GET /lose |
两个关注点
- API 之间的解耦
- 如
POST /create
后重定向GET /show
- 如
- 通过
GET /new
生成 form,从而实现人机交互
隐含的 MVC 模型
- model - Part 1
- a class to encapsulate the game itself
- with instance variables that capture the game’s essential state and instance methods that operate on it when the player makes guesses
- controller - Sinatra
- expose operations on the model via RESTful HTTP requests
- view - HTML
- create HTML views and forms to represent the game state
- allow submitting a guess
- allow starting a new game
- display a message when the player wins or loses
Part 3
http://sinatrarb.com/documentation.html
介绍了 app.rb
before / after
- every SaaS request
erb
- 对应 view
- view
- 可以嵌入 ruby 代码
<%= like this %>
- 使用了 CSS 框架 https://themes.getbootstrap.com/
- 可以嵌入 ruby 代码
session[]
- 封装为 cookie
flash[]
- 在 redirect 间传递数据
同时部署到 Heroku 上
https://afternoon-fortress-30623.herokuapp.com/
Part 4
介绍了 cucumber 的使用和 BDD
使用 Capybara 模拟的浏览器的行为,并检查服务器的响应
https://rubydoc.info/github/teamcapybara/capybara#using-capybara-with-cucumber
下面是一个 .feature
文件
Feature: start new game
As a player So I can play Wordguesser I want to start a new game
Scenario: I start a new game
Given I am on the home page And I press "New Game" Then I should see "Guess a letter" And I press "New Game" Then I should see "Guess a letter"
Feature:
为场景说明,不会被 cucumber 执行
Scenario:
描述了每一步的交互,其中
Given / And / Then
为 aliases for the same method- 后面的内容会在
game_steps.rb
中匹配,如
Given /^(?:|I )am on (.+)$/ do |page_name| visit path_to(page_name)end
还有一些值得注意的细节
- 使用
gem - webmock
拦截 HTTP 响应,从而得到确定性的结果 - 使用
params[]
获取提交的 form 信息- the key is the symbolized
name
attribute of the form field
- the key is the symbolized
- Capybara 提供了
save_and_open_page
进行调试- undefined local variable or method
save_and_open_page
- undefined local variable or method
对于 Part 4 而言,只需完成
new.erb
中的<form>
app.rb
中的post '/guess'
即可通过 4 个 features
$ cucumber features/guess.feature
仍然需要手动安装 gems
Part 5
完成 app.rb
中
get '/show'
get '/win'
get '/lose'
即可通过全部 features
需要考虑到直接访问 GET /win
或 GET /lose
的情形
Part 6
new techniques in the future
- Test coverage
- Code metrics
- Refactoring
写完了才发现有 RubyMine
大无语……
Part 7
从本地获取随机单词
class LocalWordGenerator def self.get_word arr = IO.readlines("dict/words") # relative to root return arr[Random.new.rand(0...arr.length)].strip endend
添加一个 GET 即可
get '/local' do @game = WordGuesserGame.new(LocalWordGenerator.get_word) redirect '/show' end
当然只能在本地部署
CHIPS 4.3: ActiveRecord Basics
https://github.com/VGalaxies/hw-activerecord-practice
ActiveRecord 本质上就是一个 ORM 框架
类似 GORM,只不过语法更加简单
相同的特点
- convention over configuration
- focuses on the CRUD actions
- create
- read
- update
- delete
RTFM
- https://guides.rubyonrails.org/active_record_basics.html
- https://guides.rubyonrails.org/active_record_querying.html
- https://ruby-china.github.io/rails-guides/active_record_basics.html
注意对数据库进行修改的测试最后会进行事务的回滚
所以实际上数据并不会被修改
CHIPS 4.5: Rails Routes
https://github.com/VGalaxies/hw-activerecord-practice
RTFM
没有实际的编码练习
可视化 Rails Routes 的结果
在 config/routes.rb
中编写 Rails Routes
例如
resources :photos
会产生如下路由
HTTP Verb | Path | Controller#Action | Used for |
---|---|---|---|
GET | /photos | photos#index | display a list of all photos |
GET | /photos/new | photos#new | return an HTML form for creating a new photo |
POST | /photos | photos#create | create a new photo |
GET | /photos/:id | photos#show | display a specific photo |
GET | /photos/:id/edit | photos#edit | return an HTML form for editing a photo |
PATCH/PUT | /photos/:id | photos#update | update a specific photo |
DELETE | /photos/:id | photos#destroy | delete a specific photo |
CHIPS 4.7: Wordguesser on Rails
https://github.com/VGalaxies/hw-rails-wordguesser
框架从 Sinatra 变成了 Rails
仍然没有实际的编码练习
参考 https://github.com/saasbook/hw-rails-wordguesser/pull/22
重命名 hangperson_game.rb
为 word_guesser_game.rb
$ rails server
下面是对 Code Comprehension Questions 的解答
Q2.1. Where in the Rails app directory structure is the code corresponding to the WordGuesserGame
model?
app/models/word_guesser_game.rb
Q2.2. In what file is the code that most closely corresponds to the logic in the Sinatra apps’ app.rb
file that handles incoming user actions?
app/controllers/game_controller.rb
Q2.3. What class contains that code?
class GameController < ApplicationController
Q2.4. From what other class (which is part of the Rails framework) does that class inherit?
class ApplicationController < ActionController::Base
Q2.5. In what directory is the code corresponding to the Sinatra app’s views (new.erb
, show.erb
, etc.)?
app/views
Q2.6. The filename suffixes for these views are different in Rails than they were in the Sinatra app. What information does the rightmost suffix of the filename (e.g.: in foobar.abc.xyz
, the suffix .xyz
) tell you about the file contents?
ERB
– Ruby Templating
Q2.7. What information does the other suffix tell you about what Rails is being asked to do with the file?
Content-Type
为 text/html
Q2.8. In what file is the information in the Rails app that maps routes (e.g. GET /new
) to controller actions?
config/routes.rb
Q2.9. What is the role of the :as => 'name'
option in the route declarations of config/routes.rb
? (Hint: look at the views.)
一是对应 app/controllers/game_controller.rb
中的 game_path / win_game_path/ lose_game_path
二是对应 app/views
中的 form_tag
,如 guess_path / create_game_path
Q3.1. In the Sinatra version, before do...end
and after do...end
blocks are used for session management. What is the closest equivalent in this Rails app, and in what file do we find the code that does it?
def get_game_from_session @game = WordGuesserGame.new('') if !session[:game].blank? @game = YAML.load(session[:game]) end end
def store_game_in_session session[:game] = @game.to_yaml end
Q3.2. A popular serialization format for exchanging data between Web apps is JSON. Why wouldn’t it work to use JSON instead of YAML? (Hint: try replacing YAML.load()
with JSON.parse()
and .to_yaml
with .to_json
to do this test. You will have to clear out your cookies associated with localhost:3000
, or restart your browser with a new Incognito/Private Browsing window, in order to clear out the session[]
. Based on the error messages you get when trying to use JSON serialization, you should be able to explain why YAML serialization works in this case but JSON doesn’t.)
报错信息如下
undefined method `check_win_or_lose' for {"word"=>"digestion", "guesses"=>"", "wrong_guesses"=>""}:Hash
app/controllers/game_controller.rb:33:in `show'
def show status = @game.check_win_or_lose redirect_to win_game_path if status == :win redirect_to lose_game_path if status == :lose end
只有数据没有方法
Q4.1. In the Sinatra version, each controller action ends with either redirect
(which as you can see becomes redirect_to
in Rails) to redirect the player to another action, or erb
to render a view. Why are there no explicit calls corresponding to erb
in the Rails version? (Hint: Based on the code in the app, can you discern the Convention-over-Configuration rule that is at work here?)
如果没有 redirect_to
,则默认 render 方法名对应的 view
如 show
- show.html.erb
这也解释了 new
方法为空也能工作的原因
def new end
Q4.2. In the Sinatra version, we directly coded an HTML form using the <form>
tag, whereas in the Rails version we are using a Rails method form_tag
, even though it would be perfectly legal to use raw HTML <form>
tags in Rails. Can you think of a reason Rails might introduce this “level of indirection”?
封装,可以参考
- https://guides.rubyonrails.org/form_helpers.html
- https://ruby-china.github.io/rails-guides/form_helpers.html
Q4.3. How are form elements such as text fields and buttons handled in Rails? (Again, raw HTML would be legal, but what’s the motivation behind the way Rails does it?)
submit_tag
- 提交按钮
还有 label_tag
和 text_field_tag
,不过框架代码使用了 raw HTML
Q4.4. In the Sinatra version, the show
, win
and lose
views re-use the code in the new
view that offers a button for starting a new game. What Rails mechanism allows those views to be re-used in the Rails version?
<%= render :template => 'game/new' %>
render
,可以参考
- https://guides.rubyonrails.org/layouts_and_rendering.html
- https://ruby-china.github.io/rails-guides/layouts_and_rendering.html
Q5.1. What is a qualitative explanation for why the Cucumber scenarios and step definitions didn’t need to be modified at all to work equally well with the Sinatra or Rails versions of the app?
$ rake cucumber
因为 API 接口没有变化