rails 创建_如何创建基于组件的Rails应用程序

rails 创建

基于组件的Rails应用程序:受控制的大型域(Addison-Wesley Professional Ruby系列)
建议零售价$ 44.99
看见

本文摘自Stephan Hagemann撰写的Pearson Addison-Wesley的“基于组件的Rails应用程序”一书,经Pearson©2018许可在此处转载。有关更多信息,请访问notifyit.com/hagemann/infoworld。

在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'

[编程艺术发展Swift。 InfoWorld可以帮助您导航正在运行的东西和正在运行的东西 | 通过InfoWorld的App Dev Report新闻通讯了解编程方面的热门话题。 ]

代码示例: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将安装链轮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拾取,我们需要删除它们。 如果您在gemspecGem::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 皮尔森·艾迪生·韦斯利

图1.您的第一个CBRA运行的网页

将组件与容器应用程序分离也使我们能够绘制组件图(参见图2)。 此图使用宝石名称在中间显示了我们的组件。 应用程序名称周围的框表示我们的代码所包含的Rails应用程序。 箭头指示容器应用程序直接依赖于app_component (正如我们刚刚看到的, AppComponent直接添加到Sportsball的Gemfile中)。

cbra fig02 皮尔森·艾迪生·韦斯利

图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调用而在./不起作用时,它将起作用。 对于主应用程序,它不会失败。 它只是什么都不做。

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这样做的。 ActiveAdminDevise等使用最广泛的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将安装应用程序中所有引擎的所有新迁移。 如果在安装程序运行后添加了迁移,则需要再次运行它以确保所有迁移都存在。

与我们习惯使用的迁移相比,每次将迁移添加到引擎时都必须安装迁移。 而且,如前所述,在许多情况下(需要独立于应用程序开发宝石)在我们的情况下并不成立。 事实证明,我们可以更改引擎以使主机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:setuprake db:rollbackrake db:migration:redo停止使用此方法。 我一直无法确认这一点,它肯定可以像我们到目前为止创建的在Sportsball应用中正常工作。

命名引擎问题

在添加引擎迁移的代码片段中,您可能已经注意到了奇怪的行, 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 ,则前者与后者匹配,并且不会加载其迁移。 应该使用启发式的更强大的版本来防止此问题。

结论

我们为TeamGame添加了模型,并确保可以在主应用程序中运行迁移。 如果还没有这样做,请启动服务器。

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 皮尔森·艾迪生·韦斯利

图3.已添加两个团队的团队列表

cbra fig04 皮尔森·艾迪生·韦斯利

图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 。 相反,仅需要列出直接依赖项。

例如,假设在以前的Gemfilecomponent_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"

此行应添加到AppComponentgemspec文件中,以要求使用slim-rails 。 在主应用程序的根文件夹中运行bundle ,我们应该看到安装了slim-railsslim 。 记下已安装的slim-rails的确切版本。 在撰写本文时,它是3.1.3。

要使用我们的新gem,让我们以当前的欢迎页面为例,并将其转换为slim 。 实际上,当前的欢迎页面仍包含该默认的自动生成的文本。 我们将借此机会为页面提供更多有意义的内容。 因此,让我们删除./components/app_component/app/views/app_component/welcome/index.html.erb并在同一文件夹中创建index.html.slim 。 新页面链接到TeamGame的管理页面。

./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 | &nbsp;|&nbsp;
6 = link_to "Manage Games", games_path

但是,当我们启动服务器并尝试加载应用程序的新主页而不是页面时,会出现图5所示的“模板丢失”错误。

cbra图05 皮尔森·艾迪生·韦斯利

图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 皮尔森·艾迪生·韦斯利

图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或更高版本)。(有关版本限制规范的完整说明,请查看gemfileBundler页面)。关于模式RubyGems指南 。)我们添加了没有任何版本限制的slim-rails

使自定义生成器与引擎一起使用

我们要求slim-rails能够直接在slim生成视图代码。 但是,就像宝石本身一样,生成器本身的可用性不足以使组件开始使用它。 如果我们要重复生成TeamGame支架,我们将再次获得.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添加到源代码控制中以锁定所有依赖项的版本。 这样可以确保当代码在不同的环境中或由不同的人运行时,其行为相同。

所以。 我们正在构建一个应用程序 ,但使用的是gems 。 我们应该采取哪种策略? 我们应该采用宽松的政策还是严格的版本政策? 好吧,我将组件中的所有运行时依赖项锁定为确切的版本,如下所示:

./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 | &nbsp;|&nbsp;
6 = link_to "Manage Games", games_path
7 | &nbsp;|&nbsp;
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 皮尔森·艾迪生·韦斯利

The Figure 7. Sportsball homepage with link to predictions

cbra fig08 皮尔森·艾迪生·韦斯利

Figure 8. Requesting the prediction of a game

cbra fig09 皮尔森·艾迪生·韦斯利

Figure 9. Showing the prediction result

Sportsball is now fully functional! (At least for the purposes of this chapter.)

翻译自: https://www.infoworld.com/article/3306258/how-to-create-a-component-based-rails-application.html

rails 创建

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值