获取完整的书
![](https://i-blog.csdnimg.cn/blog_migrate/3f7caaa6a689a44f1225bc0a0eba5372.png)
在Rails中使用组件方法可以提高可维护性,降低复杂性并加速在大型Rails应用程序中的测试。
在准备创建第一个基于组件的Rails应用程序(CBRA)应用程序时,应确保您的系统关于Ruby和Rails是最新的。 我建议使用Ruby和Rails gem的最新发布版本。 我使用Ruby Version Manager(RVM)进行系统设置的方式类似于以下内容:
Install rvm, bundler, and rails.
Execute anywhere $ rvm get stable
$ rvm install 2.4.2
$ gem install bundler -v '1.15.4'
$ gem install rails -v '5.1.4'
代码示例:Sportsball应用
在本书中,我均以Sportsball应用为例。 显然,从体育领域来说,这个应用程序将使您可以存储和管理团队和比赛,以及分析性能。 该应用程序将被证明很简单。 很简单,它不会以特定领域的知识淹没本书的解释。 有时感觉太简单了,因为组件化的开销有时会远远超过您在这么小的域中所能获得的收益。 在这些情况下,我将与相应主题一起讨论。
对目录的所有引用均相对于Sportsball应用程序的根目录。 本着这种精神,。 ./
也指代应用程序的根目录。
要键入的命令以$
符号为前缀。 相应的代码清单指定在哪个目录中执行命令。 在本章中,只有要执行的命令和源代码更改将详尽无遗,这意味着您只需要这本书。 在随后的章节中执行相同的操作会淹没实际的主题,而带来太多的干扰。 对于所有章节,请参考本书的源代码 。 源代码是按章组织的,并包含命令和更改的完整列表。
组件内的整个应用程序
在本部分中,我们将创建一个Rails应用程序,该app
不直接在app
文件夹中包含任何手写代码,而是将引擎内部的所有代码安装到该应用程序中。
创建CBRA应用程序的第一步与创建任何新的Rails应用程序相同: rails new
!
Generate Sportsball app. Execute where you like to
store your Rails projects $ rails new sportsball
$ cd sportsball
我经常将此Rails应用称为主应用或容器应用 。 但是,我们不会费心将应用程序的任何代码写入新创建的文件夹结构中。 取而代之的是,我们将所有内容都写在一个组件中。 该组件将通过Rails引擎实现。 实际上,让我们强迫自己这样做并删除app
文件夹。
Delete the app folder. Execute in ./ $ rm -rf app
为什么要删除app
文件夹
此时可以选择删除app
文件夹。 我建议这样做有两个原因。 首先,它向所有在代码库上工作的人发出明确的消息,说明正在发生某些特殊情况。 另外,当前不需要app
文件夹,将来可能不再需要。 因此,将其保留在当前位置将很麻烦。 并且如果需要,可以随时将其添加回去。
链轮4强制存在app
文件夹
如sprockets-rails
gem问题369中所述 ,Sprockets版本4强制在app/assets/config/manifest.js
存在配置文件。 该文件是Sprockets的新机制,用于配置要在预编译中包括的资产。 自Rails 3以来,此更改的引入是首次对Rails应用程序强制app
文件夹的存在。
一些发行的候选版本的Rails 5.1(特别是Rails 5.1.0.rc1)安装了链轮4.0.0.beta4,而Rails 5.1.0.rc2安装了旧版本的Sprockets 3.7.1。 在撰写本文时,Rails 5.1.4安装了Sprockets 3.7.1。 我们希望,在Rails永久升级到新版本之前,预编译配置的可配置性会回到Sprockets。
关于在创建的文件夹结构中应在哪里添加应用程序组件,没有明显的选择。 所有文件夹都有其特定的用途,我们的组件实际上并不适合其中的任何一个。 因此,我认为组件不应进入创建的文件夹结构,而应在根目录下的新目录中找到它们的位置。 我喜欢使用components
作为文件夹名称。
Create the components folder. Execute in ./
$ mkdir components
为了创建我们的第一个组件,我们将使用rails plugin new
命令创建一个Rails引擎。 由于缺少更好的名称,我们将其称为第一个组件app_component
。
选择好的组件名称!
组件名称应仔细选择。
第一条技术准则是,特定名称要比非特定名称更好。 如果一侧出错,则过度指定名称也出错。 这是因为从更具体的名称到不太具体的名称进行重构通常很容易,而反之则不成立。
第二个技术准则是避免使用通用名称(同名)和在Rails应用程序中通常使用的名称来命名冲突。 即使为基础概念指定了特定的名称,如果创建了同音异义词,也不是一个好选择。 由于这些原因,下面列出了一些常用但特别糟糕的名称:
- 应用程式
- 零件
- 发动机
- 共同
- 一般
- 基础
- 核心
- 杂项
- 语境
这些名称过于具体,范围太广。 另外,在任何情况下都不会说:“哦,这个对象不应该存在于此组件中,它甚至不符合其名称所描述的概念!” 这是因为这些名称不会为其内容创建有意义的上下文。 因此,最后一个(可能是最重要的)查找正确名称的准则是表达组件在应用程序中做出的特定贡献。 以下是一些未遵循本指南的名称示例。
- 基本组件
- 核心宝石
- <您的公司/部门/集团名称>
- <您的公司/部门/集团名称的缩写>
最初,我将使用app_component
作为组件名称。 尽管我之前说过关于组件的命名,但是这还是没有的。 请阅读以下内容进行辩护。 但首先,使用以下命令生成此组件:
Generate the app_component gem. Execute in ./
$ rails plugin new components/app_component --full --mountable
AppComponent
不是一个好的组件名称-为什么使用它?
让我们根据命名准则检查AppComponent
。 首先,它是特定的,并且与通用的Rails术语不冲突。 因此它通过了技术质量标准。 但是,它显然没有上下文设置功能:它仅告诉我们它是应用程序的组成部分 。 从上下文中可以明显看出这两个方面。 此名称未添加任何内容。 AppComponent
没有通过最重要的命名质量标准。
我将使用AppComponent
作为该组件的名称正是因为它具有这些属性! 从技术上讲,它是可以的,并且对它应该(或应该包含)的内容没有期望。 因为从技术上讲还可以,所以我们将能够正确地重构事物,并且其模棱两可避免我们陷入名称的任何特定含义中。 这很重要,因为我们将把整个应用程序转储到单个组件中。 虽然这是一个好的开始,但这不是我们想要结束的地方! 在第2章和第3章中,我们将与AppComponent
一起工作。这些章使我们准备AppComponent
第4章中最终摆脱这个单一的名称(不好称),我们所有的组件都将遵循先前列出的所有命名准则。 可以说,在这些重构的最后,剩下的一小部分AppComponent
将重命名为WebUI
并且仅包含整个应用程序共享的用户界面的各个方面。
命令行参数--full
和--mountable
使插件生成器创建一个gem,该gem加载将自身与Rails应用程序隔离开的Rails::Engine
。 它带有一个测试工具应用程序和基于Test::Unit
蓝图。
现在,我们可以将cd
放入./components/app_ component
的component文件夹并执行bundle
。 根据我们的bundler
版本,将出现关于我们的gemspec
无效的警告(错误):
app_component gem error during bundle.
Execute in ./components/app_component
$ bundle
The latest bundler is1.16.0.pre.3, but you are currently
running 1.15.4.
To update, run `gem install bundler --pre`
You have one or more invalid gemspecs that need to be fixed.
The gemspec at ./components/app_component/app_component.gemspec is not valid.
Please fix this gemspec.
The validation error was '"FIXME" or "TODO" is not
a description'
该警告是因为gemspec
中有一堆TODO
条目被bundler
选中,我们需要删除它们。 如果在gemspec
的Gem::Specification
块中用TODO
填写所有字段,则该警告错误将消失。 但是,不需要电子邮件,主页,描述和许可证,如果删除这些,则只需要填写作者和摘要即可摆脱警告。
从生成的gemspec
删除所有TODO
会导致以下文件,该文件不会引发错误:
./components/app_component/app_component.gemspec
1 $:.push File.expand_path("../lib", FILE)
2
3 # Maintain your gems version:
4 require "app_component/version"
5
6 # Describe your gem and declare its dependencies:
7 Gem::Specification.new do |s|
8 s.name = "app_component"
9 s.version = AppComponent::VERSION
10 s.authors = ["Stephan Hagemann"]
11 s.summary = "Summary of AppComponent."
12 s.license = "MIT"
13
14 s.files = Dir["{app,config,db,lib}/**/*",
"MIT-LICENSE", "Rakefile", "README.md"]
15
16 s.add_dependency"rails", "~> 5.1.0"
17
18 s.add_development_dependency"sqlite3"
19 end
现在,如果我们回到Sportsball的根文件夹并在其中调用bundle
,您会注意到正在使用的许多gem中有一个新组件app_component
。
Seeing AppComponent being used in the app (some results omitted).
Execute in ./
$ bundle
The latest bundler is 1.16.0.pre.3, but you are currently
running 1.15.4.
To update, run `gem install bundler --pre`
Resolving dependencies...
Using rake 12.2.1>
...
Using rails 5.1.4
Using sass-rails 5.0.6
Using app_component 0.1.0 from source at `components/app_component`
Bundle complete! 17 Gemfile dependencies,73 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
这是因为rails plugin new
不仅会创建一个新的gem,还会在Gemfile
该应用的上下文中自动将对此gem的引用添加到应用的Gemfile
中。 也就是说,因为我们从Sportsball应用程序的根目录执行了rails plugin new
,所以我们还在Gemfile
获得了一个新条目。
./Gemfile showing reference to AppComponent gem (some lines omitted)
1 source 'https://rubygems.org'
2
3 ...
4
5 gem 'tzinfo-data', platforms:[:mingw, :mswin, :x64_mingw,:jruby]
6 gem 'app_component', path:'components/app_component'
在早期版本的Rails中,这不会自动发生。 如果您处于这种情况,则在捆绑应用程序时,将前一片段的最后一行添加到您的Gemfile
以得到相同的结果。 请注意,gem引用使用path
选项指定可以在何处找到该gem(即本地文件系统)。 通常, path
仅用于开发中的gem,但是您将看到,它在CBRA应用程序中工作得很好。
返回AppComponent
。 现在,它已连接到Sportsball应用程序中,但实际上尚未执行任何操作。 接下来让我们修复该问题。 我们将创建一个登录页面,并将引擎的根目录路由到该页面。
Generate a controller for the component. Execute in ./ $ cd components/app_component $ rails g controller welcome index
通过移入gem的文件夹,我们正在使用引擎的Rails设置。 因此,由于我们使它成为可安装的引擎,因此在AppComponent
命名空间内创建了欢迎控制器。
我们如下更改组件的路由,以将新控制器挂接到引擎路由的根目录:
./components/app_component/config/routes.rb
1 AppComponent::Engine.routes.draw do
2 root to:"welcome#index"
3 end
最后,通过将主引擎安装到应用程序的路由中,使主应用程序了解引擎的路由。 在这里,我们将引擎安装到应用程序的根目录,因为没有其他引擎,并且主应用程序将永远没有任何自己的路由。
./config/routes.rb
1 Rails.application.routes.draw do
2 mount AppComponent::Engine, at: "/"
3 end
就这样。 让我们启动服务器!
Start up the Rails server. Execute in ./
$ rails s
=> Booting Puma
=> Rails 5.1.4 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.10.0 (ruby 2.4.2-p198), codename: Russell's Teapot
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
现在,当使用浏览器打开http:// localhost:3000时,您应该看到(如图1所示)新的welcome控制器的索引页面。 还不错吧?
![cbra fig01](https://i-blog.csdnimg.cn/blog_migrate/d45ab1a1296f0257c699eb2ed33ddddd.png)
图1.您的第一个CBRA运行的网页
将组件从容器应用程序中分离出来,还使我们能够绘制组件图(参见图2)。 此图使用宝石名称在中间显示了我们的组件。 应用程序名称周围的框表示我们的代码所包含的Rails应用程序。 箭头指示容器应用程序直接依赖于app_component
(正如我们刚刚看到的, AppComponent
直接添加到Sportsball的Gemfile
中)。
![cbra fig02](https://i-blog.csdnimg.cn/blog_migrate/fa1d3e9eb0a16f14fd2696ea98d44ef7.png)
图2.第一个组件图
在应用程序的此阶段自己生成此图有些棘手。 这是我们目前在Gemfile
中引用组件的方式。 如果您仍然想这样做,则涉及几个步骤。
首先,您需要安装cobradeps
gem ,这是我为允许生成Rails组件图而编写的。 该工具依赖于graphviz
(一种开源的图形可视化软件)。 您可以按照以下步骤安装这两个应用程序(假设您在MacOS上并且是homebrew
,我建议这样做)。
Install graphviz and cobradeps. Execute anywhere
$ brew install graphviz
$ gem install cobradeps
为了减轻我们提到的宝石引用问题,改变AppComponent
在你行Gemfile
这样的:
Reference to app_component in ./Gemfile allowing
for graph generation
1 gem 'app_component',
2 path: 'components/app_component',
3 group: [:default, :direct]
group: [:default, :direct]
在bundler
正常运行,并被cobradeps
用来确定gem确实是直接依赖项(您将在后面看到为什么这样做的必要性)。 现在,您可以通过执行以下语句来生成组件图。 结果将输出到./component_diagram.png
。
Generate a component graph for Sportsball. Execute in ./
$ cobradeps -g component_diagram .
而已! 您的第一个CBRA应用已准备就绪,可以认真对待。 现在,您可以继续components/app_component
内部的所有功能开发。
附录中更深入地介绍了我们在本节中忽略的某些方面。 有关Rails功能的各种引擎的详细信息,请参阅附录A;有关引擎布线和安装的介绍,请参阅附录B。
ActiveRecord和处理组件中的迁移
让我们为当前准系统应用程序添加一些实际功能。 我们要关注的第一个功能是该应用程序能够根据过去的表现来预测未来游戏的结果。 为此,我们将团队和游戏作为模型添加到AppComponent
。 我们将为团队和比赛创建一个管理界面,它将为我们提供足够的数据来尝试预测一些比赛。
请记住要在AppComponent
的上下文中(即在./components/app_component
下)执行这些命令。
Scaffolding Team and Game. Execute in ./components/app_component
$ rails g scaffold team name:string
$ rails g scaffold game date:datetime \
location:string \
first_team_id:integer
second_team_id:integer \
winning_team:integer \
first_team_score:integer \
second_team_score:integer
为什么我在示例中继续使用脚手架
当我每天在工作中使用Rails时,我几乎从不使用Rails生成支架的功能。 这样做的原因有很多:它们并不是真正需要构建的东西,它们需要进行调整才能正常工作,它们不受用户故事的驱动,也不受测试驱动。
但是,有关大规模模式和体系结构的书必须处理大量代码。 否则,将冒着模式不明显或看起来荒谬的风险。 为此,脚手架是一种交流大量代码而无需展示大量代码的好方法。
下一步是运行rake db:migrate
在数据库中创建适当的表。 有趣的是,这在./components/app_component
调用时./components/app_component
,但在./
中则./components/app_component
。 对于主应用程序,它不会失败。 它什么也没做。
Scaffolding Team and Game. Execute in ./components/app_component
$ rake db:migrate
== 20171029235211 CreateAppComponentTeams:migrating ================
-- create_table(:app_component_teams)
-> 0.0005s
== 20171029235211 CreateAppComponentTeams: migrated (0.0006s) ======
== 20171029235221 CreateAppComponentGames:migrating ================
-- create_table(:app_component_games)
-> 0.0007s
== 20171029235221 CreateAppComponentGames: migrated (0.0007s) ======
$ cd ../..
$ rake db:migrate
Running via Spring preloader in process 58196
在此之前,我们需要使主应用程序意识到引擎提供的迁移。
使用rake
安装引擎迁移
常见的解决方案是使用rake app_component:install:migrations
将引擎的迁移安装到主应用中。 这会将在引擎的db/migrate
文件夹中找到的所有db/migrate
复制到主应用程序中。
有一些宝石可以使用此功能。 例如,宝石Commontator
执行此操作。 ActiveAdmin
和Devise
等使用最广泛的gem在主机应用程序中生成更复杂的迁移。 它们实际上并不带有自己的迁移,而是使用生成器基于用户指定的配置选项来创建它们。
如果要在Sportsball应用程序中运行rake app_component:install:migrations
,您将在引擎和主应用程序中获得以下内容:
Install engine migrations into the main app. Execute in ./
$ rake app_component:install:migrations
Running via Spring preloader in process 58464
Copied migration 20171030000159_create_app_component_teams.\
app_component.rb from app_component
Copied migration 20171030000160_create_app_component_games.\
app_component.rb from app_component
AppComponent engine migration
$ tree ./components/app_component/db/migrate
components/app_component/db/migrate
|—20171029235211_create_app_component_teams.rb
|—20171029235221_create_app_component_games.rb
Sportsball application migrations $ tree ./db/migrate
./db/migrate
|—20170507205125_create_app_component_teams.app_component.rb
|—20170507205126_create_app_component_games.app_component.rb
尽管引擎中仍存在原始迁移,但rake
任务已将其复制到主应用程序中。 为此,它重命名了它们并将其日期更改为当前时间。 这样可以确保引擎的迁移作为应用程序中的最后一个迁移(在将它们安装到应用程序中时为“最后”)运行。
对于打算分发的引擎(如前面提到的那些引擎),将迁移迁移到当前时间是非常重要的,因为尚不清楚何时将宝石及其迁移添加到应用程序中。 如果日期没有更改,将无法控制它们在整个迁移顺序中的哪个位置。 但是,由于必须在应用程序开发(或应用程序的相关部分)开始之前发布该gem,因此它们很可能会在很早之前运行。 反过来,这很可能导致运行该应用程序的任何系统(甚至是生产系统)上的整体迁移状态无效:尽管运行了较新的迁移,但缺少了较旧的迁移。
请注意, rake railties:install:migrations
migrations从应用程序中的所有引擎安装所有新的迁移。 如果在安装程序运行后添加了迁移,则将需要再次运行它以确保存在所有迁移。
与我们习惯使用的迁移相比,每次将迁移添加到引擎时都必须安装迁移。 而且,如前所述,在许多情况下(需要独立于应用程序开发宝石)在我们的情况下并不成立。 事实证明,我们可以更改引擎以使主机Rails应用程序自动查找并使用其迁移。
就地加载引擎迁移
我们可以确保主应用程序可以通过在组件的engine.rb
添加几行代码来找到它们,而不是从组件复制迁移到主应用程序。 本技术最早由本·史密斯提出 。
./components/app_component/lib/app_component/engine.rb –
Engine migrations configuration
1 moduleAppComponent
2 class Engine< ::Rails::Engine
3 isolate_namespace AppComponent
4>
5 initializer:append_migrations do |app|
6 unlessapp.root.to_s.match root.to_s+File::SEPARATOR
7 app.config.paths["db/migrate"].concat(
config.paths["db/migrate"].expanded)
8 end
9 end
10 end
11 end
现在rake db:migrate
将找到并正确运行引擎的迁移。
重要的是,不要将迁移安装rake
任务与该技术结合使用,并删除在遵循上一节的过程中可能添加的内容。 这将导致两次加载迁移的问题,这与我们接下来将要看到的类似。
已知的问题
使用这种方法时可能会遇到一些障碍,应该避免。
在rake
任务中链接db:migrate
无法添加
由于仍然未知的原因, rake db:drop db:create db:migrate
对于正常的Rails应用程序工作正常,但是当引擎迁移加载到位时无法添加迁移。 解决此问题的最简单方法是,通过运行rake db:drop db:create && rake db:migrate
将该命令分为两部分。 这会影响性能,因为rake
现在必须加载两次。
本·史密斯(Ben Smith)针对此问题提出了几种不同的修复方法,最简洁的方法是要求在db:load_config
之前通过添加以下文件db.rake
来加载Rails环境:
./components/app_component/lib/tasks/db.rake
1 Rake::Task["db:load_config"].enhance [:environment]
这具有在运行任何数据库任务之前始终加载环境(即,Rails应用程序的内容)的副作用。 这不仅对性能有影响,而且还会影响每个环境。 尽管模式rake db:drop db:create db:migrate
在开发中很常见,但在生产环境中永远不会调用它。 我认为,以未知的方式影响生产以实现发展的小收益是一个不好的权衡。
总之,我建议解决此问题,而不要使用尚未完全理解的解决方案。 应该通过了解和解决Rails运行迁移所做的真正原因来解决此问题。 对于找到它的人,这也会向Rails发出很好的请求!
报告其他rake
任务不起作用
据报道,其他与数据库相关的任务,例如rake db:setup
, rake db:rollback
和rake db:migration:redo
停止使用此方法。 我一直无法确认这一点,并且它肯定可以在Sportball应用程序中正常工作,因为到目前为止我们已经创建了它。
命名引擎问题
在添加引擎迁移的代码片段中,您可能已经注意到了奇怪的行, unless app.root.to_s.match root.to_ s+File::SEPARATOR
。 它可以防止在引擎本身内部运行rake app_ component:db:migrate
出现问题,否则会引发以下错误:
Engine migrations loaded twice through dummy app. Executed in ./components/app_ component (if the check is removed)
$ rake app_component:db:migrate
rake aborted!
ActiveRecord::DuplicateMigrationNameError:
Multiple migrations have the name CreateAppComponentTeams
匹配是一种启发式方法,用于确定引擎当前是否正在其自己的虚拟应用程序中加载。 在这种情况下,初始化器不应添加路由,因为它们是自动添加的。 如果引擎的虚拟应用程序不在其自己的目录结构之外,则启发式测试将失败,这永远不会发生。
此启发式方法的功能较弱的版本评估app.root.to_s. match root.to_s
app.root.to_s. match root.to_s
,如果与两个引擎(其中包含引擎的名称以包含引擎的名称开头,并且它们位于同一目录中)一起使用,则失败。 例如,如果/components/users_ server
依赖于/components/users
,则前者与后者匹配,并且不会加载其迁移。 应该使用启发式的更强大的版本来防止此问题。
结论
我们为Team
和Game
添加了模型,并确保可以在主应用程序中运行迁移。 如果还没有这样做,请启动服务器。
Run migrations and start the server. Executed in ./
$ rake db:migrate
Running via Spring preloader in process 58740
== 20171029235211 CreateAppComponentTeams:migrating =================
-- create_table(:app_component_teams)
-> 0.0010s
== 20171029235211 CreateAppComponentTeams: migrated (0.0010s) =======
== 20171029235221 CreateAppComponentGames:migrating =================
-- create_table(:app_component_games)
-> 0.0007s
== 20171029235221 CreateAppComponentGames: migrated (0.0007s) =======
$ rails s
=> Booting Puma
=> Rails 5.1.4 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.10.0 (ruby 2.4.2-p198), codename: Russell's Teapot
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
这样,您可以分别导航到http:// localhost:3000 / teams和http:// localhost:3000 / games来访问团队和游戏的UI。 在这里,您会看到标准的脚手架管理页面外观,如图3和图4所示。
![cbra fig03](https://i-blog.csdnimg.cn/blog_migrate/d526fc5a39fcf45f82e4986ec20acc70.png)
图3.已添加两个团队的团队列表
![cbra fig04](https://i-blog.csdnimg.cn/blog_migrate/787fbbba9676c763243a5d0e991e6d85.png)
图4.添加一个新团队
处理组件中的依赖项
随着Sportsball现在具有存储团队和游戏的能力,我们可以转向如何根据过去的表现预测游戏结果的问题。 为此,我们想添加一个页面,让我们选择两个团队。 点击标有“预测获胜者!”的按钮。 并查看应用程序对谁有可能最终获胜的预测。
使用path
块指定CBRA依赖项
添加组件时,我们使用以下格式在应用程序的Gemfile
声明此依赖关系,如下所示:
Sample Gemfile reference using path option
1 gem 'app_component', path:'components/app_component'
还有另一种使用块语法来声明此依赖性的方法,如下所示:
Same Gemfile reference using path block
1 path "components" do
2 gem "app_component"
3 end
第一个明显的区别是,当依赖项列表增加时,将要编写的代码更少。 也就是说,当然,仅当我们将将来的组件放入相同的components
文件夹中时。 只需将其他组件添加到块中:
Gemfile reference with multiple gems in path block
1 path "components" do
2 gem "app_component"
3 gem "component_a"
4 gem "component_b"
5 end
path
选项和块语法之间还有另一个区别。 如Enrico Teotti所述,块语法使用bundler
中的一项功能,该功能可确保在指定的path
文件夹中查找AppComponent
可传递依赖项。 这意味着不必在Gemfile
明确声明每个可传递的CBRA依赖Gemfile
。 相反,仅需要列出直接依赖项。
例如,假设在以前的Gemfile
, component_a
取决于component_c
。 如果没有path
块语法,则需要在我们的Gemfile
添加gem "component_c", path: "components/component_c"
。 使用path
块语法,我们不必这样做。 我们已经免费声明了直接依赖component_a
已列出,因此我们免费获得它。
因此,在使用cobradeps
生成组件图时,不再需要为直接依赖关系指定特殊的组。 cobradeps
只是假设所有陈述的依赖关系都是直接依赖关系。
添加常规宝石: slim
不同的模板
在到达计算可能结果的部分之前,我们需要添加新页面。 我发现ERB
不必要冗长,请尽可能避免使用。 幸运的是,那里有很多替代方案,我们可以将第一个依赖项添加到我们的组件中,而不是Rails。
我喜欢slim
因为与ERB
相比,它大大减少了我必须编写的代码量。 特别是,人字形符号(在HTML中很常见的<和>符号)的数量大大减少了,对此我非常喜欢。 除了添加slim
宝石外 ,我们将添加slim-rails
,这又将需要slim
,但是另外添加了可以以slim
语法创建视图的Rails生成器。
./components/app_component/app_component.gemspec Add slim dependency
1 s.add_dependency "slim-rails"
此行应添加到AppComponent
的gemspec
文件中,以要求使用slim-rails
。 在主应用程序的根文件夹中运行bundle
,我们应该看到安装了slim-rails
和slim
。 记下已安装的slim-rails
的确切版本。 在撰写本文时,它是3.1.3。
要使用我们的新gem,让我们以当前的欢迎页面为例,并将其转换为slim
。 实际上,当前的欢迎页面仍然包含默认的自动生成的文本。 我们将借此机会为页面提供更多有意义的内容。 因此,让我们删除./components/app_component/app/views/app_component/welcome/index.html.erb
并在同一文件夹中创建index.html.slim
。 新页面链接到Team
和Game
的管理页面。
./components/app_component/app/views/app_component/welcome/index.html.slim
1 h1 Welcome to Sportsball!
2 p Predicting the outcome of matches since 2015.
3
4 = link_to "Manage Teams", teams_path
5 | |
6 = link_to "Manage Games", games_path
但是,当我们启动服务器并尝试加载应用程序的新主页而不是页面时,会出现图5所示的“模板丢失”错误。
![cbra图05](https://i-blog.csdnimg.cn/blog_migrate/ded64f776470ba7435c6ffc68a02d327.png)
图5.切换到slim
后出现“模板丢失”错误
原因是,与Rails 应用程序不同,Rails 应用程序自动需要它们直接依赖的所有gem,而Rails 引擎则不需要。 查看有关该问题的Jonathan Rochkind的博客文章 。 我们从来没有要求slim
我们的引擎,它表明,由于Rails仅报告标配模板处理程序: :handlers=>[:erb, :builder, :raw, :ruby]
,但不是:slim
如你所愿。
要解决此问题,我们必须在AppComponent
组件中明确要求slim-rails
,如下所示。 请注意,我将require "app_component/engine"
移到了AppComponent
模块的范围内。 并没有程序上的需要,但是我喜欢让gems用这种方式来表示,它要求是本地的 (即,在gem内)与外部的 (即,外部gem的依赖项)。
./components/app_component/lib/app_component.rb Require slim
1 require "slim-rails"
2
3 module AppComponent
4 require "app_component/engine"
5 end
我们重新启动Rails以使其拾取新需要的gem,并在重新加载主页后,我们得到了期望的结果,如图6所示。
![cbra fig06](https://i-blog.csdnimg.cn/blog_migrate/144b917990d8ff9192ba295b50b42aa9.png)
图6.用slim
编写的新欢迎页面
锁定宝石版本
让我们再仔细看看AppComponent gemspec
现在存在的运行时依赖AppComponent gemspec
。
./components/app_component/app_component.gemspec Production dependencies
1s.add_dependency "rails","~> 5.1.4"
2s.add_dependency "slim-rails"
Rails依赖项的生成方式为~> 5.1.4
,允许使用所有版本的Rails 5.1.*
(其中*
为4或更高版本)。(有关版本限制规范的完整说明,请查看gemfiles的Bundler
页面,以及( 关于模式的RubyGems指南 。)我们添加了没有任何版本限制的slim-rails
。
使自定义生成器与引擎一起使用
我们要求slim-rails
能够直接在slim
生成视图代码。 但是,就像宝石本身一样,生成器本身的可用性不足以使组件开始使用它。 如果我们要重复生成Team
和Game
支架,我们将再次获得.erb
文件。
为了使引擎的生成器可以slim
用作模板库,需要按以下方式配置AppComponent::Engine
生成器定义。 请注意,我们还可以为对象关系映射(ORM)(已经是ActiveRecord
并且我们现在不希望对其进行更改)和测试框架设置生成器,我们将回到下面。 现在重要的是将template_engine
生成器切换为:slim
。 此后,将使用slim
模板生成所有视图。
./components/app_component/lib/app_component/engine.rb Setting up the engine for different generators
1 module AppComponent
2 class Engine <::Rails::Engine
3 isolate_namespace AppComponent
4
5 initializer:append_migrations do |app|
6 unless app.root.to_s.match root.to_s+File::SEPARATOR
7 app.config.paths["db/migrate"].concat(
8 config.paths["db/migrate"].expanded)
9 end
10 end
11
12 config.generatorsdo |g|
13 g.orm :active_record
14 g.template_engine :slim
15 g.test_framework :rspec
16 end
17 end
18 end
通常,在开发宝石时,作者努力使所需宝石的可接受版本范围尽可能广泛。 这是为了排除使用gem的最少数量的可能处于不同情况和不同更新路径的开发人员。 仅对于不兼容的差异(这会阻止宝石正常工作),通常会添加限制。
与此相反,在Rails应用程序中,将Gemfile.lock
添加到源代码控制中以锁定所有依赖项的版本。 这样可以确保在不同环境中或由不同人员运行代码时,它们的行为相同。
所以。 我们正在构建一个应用程序 ,但正在使用gem 。 我们应该采取哪种策略? 我们应该制定宽松的政策还是严格的版本政策? 好吧,我将组件中的所有运行时依赖项锁定为确切的版本,如下所示:
./components/app_component/app_component.gemspec Production dependencies locked down
1 s.add_dependency "rails", "5.1.4"
2 s.add_dependency "slim-rails", "3.1.3"
版本锁定的原因与组件的测试有关,并且基于两个假设。 我假设你写:
- 自动测试代码
- 各种测试,例如单元测试,功能测试,集成测试和功能测试
- 以最低级别进行测试
如果这些假设对您来说都是正确的,则您将尝试验证组件本身中组件的所有内部结构。 这也意味着您将不会测试组件外部的内部结构。 也就是说,在完成的Rails应用程序的上下文中。 在这种情况下,如果组件的依赖关系版本与Rails应用程序中使用的依赖关系版本有所不同,会发生什么情况? 那将是生产中未经测试的依赖关系:将根据生产中未使用的依赖项版本来验证组件的功能。 版本锁定强制所有由Rails应用程序绑定在一起的组件一起运行,并针对相同版本的依赖项进行测试。 测试组件是下一部分的主题。
对于本部分,足以说有必要在应用程序的所有部分之间保持依赖项的版本同步。 在运行的应用程序中,每个依赖项的一个版本将被加载; 我们不妨对它的工作原理不感到惊讶。
添加宝石的开发版本:Trueskill-评级计算库
现在我们可以转向对未来比赛结果的预测。 如果我们不想搞清楚该怎么做,那么最好找到一个可以为我们做这样的计算的宝石。 幸运的是,有很多理论可供我们借鉴,例如排名算法,评分算法或贝叶斯网络 。 我从Wikipedia上的FIFA世界排名页面开始寻找合适的宝石,该页面虽然没有解释官方排名的计算方式,但提到了另一种Elo评级系统。 Elo是为在国际象棋中使用而创建的,但现在已用于许多竞争对手与竞争对手的游戏中。 对Elo的改进是Glicko评分系统 ,Microsoft又将其扩展到Trueskill ,该系统适用于多人游戏。 对于所有这些(Elo,Glicko和Trueskill),我们都可以在Rubygems上找到相应的宝石。 对于以下内容,我们将使用trueskill
gem 。 在考虑球员实力的同时评估球队实力的想法不仅吸引人,而且宝石还提出了一个小问题:完全过时了。 在撰写本文时,gem的最新版本于2011年发布。但是,直到2014年末,代码才被添加到原始项目的分支中。
我们要用于trueskill
的代码版本是在benjaminleesmith
fork上提交e404f45af5 。 (我知道此分叉能够按预期工作,因为我以前曾在Boulder Pivotal Labs办公室将其用于对桌上足球球员进行排名的应用程序: true-foos-skills 。)
问题在于我们只能指定gem依赖于其他gem的发布版本。 我们无法基于提交SHA设置限制。 对于打算发布的gem,这是有道理的:它们不应该依赖于未同时作为gem发布和分发的代码。
要解决此问题,我们必须同时使用gem的gemspec
及其Gemfile
。
./components/app_component/Gemfile
1 source "https://rubygems.org"
2
3 gemspec
4
5 gem "trueskill",
6 git: "https://github.com/benjaminleesmith/trueskill",
7 ref: "e404f45af5b3fb86982881ce064a9c764cc6a901"
./components/app_component/app_component.gemspec – Dependencies
1 s.add_dependency "rails","5.1.4"
2 s.add_dependency "slim-rails","3.1.3"
3 s.add_dependency "trueskill"
该Gemfile
在宝石的目录宝石的开发过程中使用,就像Gemfile
的Rails应用程序。 在此目录中调用bundle
,它将安装那里列出的所有gem依赖项。 特殊行gemspec
告诉bundler
程序在当前目录中查找gemspec
文件,并将在那里指定的所有依赖项添加到当前bundle
。 In our case, the gemspec
states that AppComponent
has a runtime dependency on trueskill
and the Gemfile
restricts this to be from the specified git URL at the given SHA.
Bundle AppComponent with trueskill (some results omitted).
Execute in ./components/app_component
$ bundle
The latest bundler is 1.16.0.pre.3, but you are currently running 1.15.4.
To update, run `gem install bundler --pre`
Fetching https://github.com/benjaminleesmith/trueskill
Fetching gem metadata from https://rubygems.org/..........
Fetching version metadata from https://rubygems.org/..
Fetching dependency metadata from https://rubygems.org/.
Resolving dependencies...
...
Using trueskill 1.0.0 from https://github.com/benjaminleesmith/\
trueskill (at e404f45@e404f45)
...
Bundle complete! 3 Gemfile dependencies,46 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
When bundling the component, we see Git checking out the repository specified and using the correct SHA. However, bundling the main app will reveal that it does not take into account the restriction posed by the Gemfile
. That is because the Gemfile
of any gem is ignored by other gems or apps depending on it (again, due to fact that the common expectation is for a gem to be published).
To work around this, there is no other way than to ensure that the version of the dependency is enforced by the app itself. That leads to an exact duplicate of the trueskill
line from AppComponent
's Gemfile
in the main app's Gemfile
.
New lines in ./Gemfile
1 gem "trueskill",
2 git: "https://github.com/benjaminleesmith/trueskill",
3 ref: "e404f45af5b3fb86982881ce064a9c764cc6a901"
And just like with slim-rails
, we need to explicitly require the trueskill
gem in AppComponent
to make sure it is loaded.
./components/app_component/lib/app_component.rb Requiring the trueskill
dependency
1 require"slim-rails"
2 require"saulabs/trueskill"
3
4 module AppComponent
5 require"app_component/engine"
6 end
Adding predictions to the app
With models, scaffolds for administration, and the rating calculation library in place, we can turn to implementing the first iteration of game prediction.
Let's create a cursory sketch of how our models might interact to generate a prediction. A predictor
object might get a collection of all the games it should consider. As we are using an external library, we don't really know what is going on. The best way we can describe it is that the predictor learns (about the teams or the games). Because of this, we will make learn
the first method of the public interface of the class.
After the predictor has learned the strengths of teams it can, given two teams, predict the outcome of their next match. predict
becomes the second method of the public interface.
./components/app_component/app/models/app_component/predictor.rb
1 moduleAppComponent
2 class Predictor
3 def initialize(teams)
4 @teams_lookup = teams.inject({}) do |memo, team|
5 memo[team.id] = {
6 team: team,
7 rating:[Saulabs::TrueSkill::Rating.new(
8 1500.0, 1000.0,1.0)]
9 }
10 memo
11 end
12 end
13
14 def learn(games)
15 games.each do |game|
16 first_team_rating =
17 @teams_lookup[game.first_team_id][:rating]
18 second_team_rating =
19 @teams_lookup[game.second_team_id][:rating]
20 game_result = game.winning_team == 1 ?
21 [first_team_rating, second_team_rating] :
22 [second_team_rating, first_team_rating]
23 Saulabs::TrueSkill::FactorGraph.new(
24 game_result, [1, 2]).update_skills
25 end
26 end
27
28 def predict(first_team, second_team)
29 team1 = @teams_lookup[first_team.id][:team]
30 team2 = @teams_lookup[second_team.id][:team]
31 winner = higher_mean_team(first_team, second_team) ?
32 team1 : team2
33 AppComponent::Prediction.new(team1, team2, winner)
34 end
35
36 def higher_mean_team(first_team, second_team)
37 @teams_lookup[first_team.id][:rating].first.mean >
38 @teams_lookup[second_team.id][:rating].first.mean
39 end
40 end
41 end
To start, initialize
creates a lookup hash from all the teams it is handed that allows the Predictor
class to efficiently access teams and their ratings by a team's ID.
Inside of learn
, the predictor loops over all the games that were given. It looks up the ratings of the two teams playing each game. The teams' ratings are passed into an object from trueskill
called FactorGraph
in the order “winner first, loser second” so that the update_skills
method can update the ratings of both teams.
predict
simply compares the mean rating values of the two teams and “predicts” that the stronger team will win. It returns a Prediction
object, which we will look at next.
There is not much going on in the Prediction
class. It is simply a data object that holds on to the teams participating in the prediction, as well as the winning team.
./components/app_component/app/models/app_component/prediction.rb
1 moduleAppComponent
2 class Prediction
3 attr_reader :first_team, :second_team, :winner
4
5 def initialize(first_team, second_team, winner)
6 @first_team = first_team
7 @second_team= second_team
8 @winner= winner
9 end
10 end
11 end
The PredictionsController
has two actions: new
and create
. The first, new
, loads all teams so they are available for the selection of the game to be predicted. create
creates a new Predictor
and then calls learn
and predict
in sequence to generate a prediction.
./components/app_component/app/controllers/app_component/
predictions_controller.rb
1 require_dependency "app_component/application_controller"
2 module AppComponent
3 class PredictionsController< ApplicationController
4 def new
5 @teams= AppComponent::Team.all
6 end
7
8 def create
9 predictor= Predictor.new(AppComponent::Team.all)
10 predictor.learn(AppComponent::Game.all)
11 @prediction = predictor.predict(
12 AppComponent::Team.find(params["first_team"]["id"]),
13 AppComponent::Team.find(params["second_team"]["id"]))
14 end
15 end
16 end
For completeness, we list the two views of the prediction interface as well as a helper that is used to generate the prediction result that will be displayed as a result.
./components/app_component/app/views/app_component/predictions/
new.html.slim
1
2
3 = form_tag prediction_path, method: "post" do |f|
4 .field
5 = label_tag :first_team_id
6 = collection_select(:first_team,:id, @teams,:id, :name)
7
8 .field
9 = label_tag :second_team_id
10 = collection_select(:second_team, :id,@teams, :id,:name)
11 .actions = submit_tag"What is it going to be?", class: "button"
./components/app_component/app/views/app_component/predictions/
create.html.slim
1 h1 Prediction
2
3 =prediction_text @prediction.first_team,
@prediction.second_team,
@prediction.winner
4
5 .actions
6 = link_to "Try again!", new_prediction_path, class: "button"
./components/app_component/app/helpers/app_component/
predictions_ helper.rb
1 moduleAppComponent
2 modulePredictionsHelper
3 def prediction_text(team1, team2, winner)
4 "In the game between #{team1.name} and #{team2.name} " +
5 "the winner will be #{winner.name}"
6 end
7 end
8 end
Finally, we can add a link to the prediction to the homepage to complete this feature.
./components/app_component/app/views/app_component/welcome/index.html.slim
1 h1 Welcome to Sportsball!
2 p Predicting the outcome of matches since 2015.
3
4 = link_to "Manage Teams", teams_path
5 | |
6 = link_to "Manage Games", games_path
7 | |
8 = link_to "Predict an outcome!", new_prediction_path
With the changes from this section in place, we can navigate to http:// localhost:3000/ to see a new homepage (see Figure 7) from which you can navigate to our new prediction section. Figure 8 shows how you can request a new prediction. Finally, in Figure 9, you see the result of a successful prediction.
![cbra fig07](https://i-blog.csdnimg.cn/blog_migrate/a4dec35d01ae74aa707aefde3e42d973.png)
The Figure 7. Sportsball homepage with link to predictions
![cbra fig08](https://i-blog.csdnimg.cn/blog_migrate/b56b4b1af75926ec79dfdfb13036e7e1.png)
Figure 8. Requesting the prediction of a game
![cbra fig09](https://i-blog.csdnimg.cn/blog_migrate/06f8660fee2dad5e59f0bc8ccc92d1db.png)
Figure 9. Showing the prediction result
Sportsball is now fully functional! (At least for the purposes of this chapter.)
From: https://www.infoworld.com/article/3306258/how-to-create-a-component-based-rails-application.html