├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── book.adoc ├── glossary.md ├── images ├── article_with_comments.png ├── belongs_to.png ├── challenge.png ├── confirm_dialog.png ├── csrf.png ├── demo_html_safe.png ├── demo_localized_pirate.png ├── demo_translated_en.png ├── demo_translated_pirate.png ├── demo_translation_missing.png ├── demo_untranslated.png ├── forbidden_attributes_for_new_article.png ├── form_with_errors.png ├── habtm.png ├── has_many.png ├── has_many_through.png ├── has_one.png ├── has_one_through.png ├── index_action_with_edit_link.png ├── new_article.png ├── polymorphic.png ├── rails_welcome.png ├── routing_error_no_controller.png ├── ruby-china-logo.jpg ├── session_fixation.png ├── show_action_for_articles.png ├── template_is_missing_articles_new.png ├── unknown_action_create_for_articles.png └── unknown_action_new_for_articles.png ├── manuscript ├── 5_0_release_notes.adoc ├── 5_1_release_notes.adoc ├── action_cable_overview.adoc ├── action_controller_overview.adoc ├── action_mailer_basics.adoc ├── action_view_overview.adoc ├── active_job_basics.adoc ├── active_model_basics.adoc ├── active_record_basics.adoc ├── active_record_callbacks.adoc ├── active_record_migrations.adoc ├── active_record_querying.adoc ├── active_record_validations.adoc ├── active_support_core_extensions.adoc ├── active_support_instrumentation.adoc ├── api_app.adoc ├── api_documentation_guidelines.adoc ├── asset_pipeline.adoc ├── association_basics.adoc ├── autoloading_and_reloading_constants.adoc ├── caching_with_rails.adoc ├── command_line.adoc ├── configuring.adoc ├── contributing.adoc ├── contributing_to_ruby_on_rails.adoc ├── controllers.adoc ├── debugging_rails_applications.adoc ├── development_dependencies_install.adoc ├── digging_depper.adoc ├── engines.adoc ├── extending_rails.adoc ├── foreword.adoc ├── form_helpers.adoc ├── generators.adoc ├── getting_started.adoc ├── i18n.adoc ├── initialization.adoc ├── layouts_and_rendering.adoc ├── maintenance.adoc ├── maintenance_policy.adoc ├── models.adoc ├── plugins.adoc ├── rails_application_templates.adoc ├── rails_on_rack.adoc ├── release_notes.adoc ├── routing.adoc ├── ruby_on_rails_guides_guidelines.adoc ├── security.adoc ├── start_here.adoc ├── supplement.adoc ├── testing.adoc ├── upgrading_ruby_on_rails.adoc ├── views.adoc └── working_with_javascript_in_rails.adoc ├── md_tpl ├── block_admonition.html.erb ├── block_dlist.html.erb ├── block_image.html.erb ├── block_listing.html.erb ├── block_olist.html.erb ├── block_open.html.erb ├── block_paragraph.html.erb ├── block_quote.html.erb ├── block_table.html.erb ├── block_ulist.html.erb ├── document.html.erb ├── embedded.html.erb ├── helpers.rb ├── inline_anchor.html.erb ├── inline_footnote.html.erb ├── inline_quoted.html.erb └── section.html.erb ├── plugins ├── after_build_site.rb └── term_macro.rb ├── release_notes.md └── themes ├── epub ├── epub.css └── rails-guides-cover.jpg ├── html └── multiple.html.liquid └── pdf ├── part-bg.jpg ├── pdf.css ├── rails-guides-cover.jpg └── sample-cover.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | _old/_site 2 | builds/ 3 | tmp/ 4 | *.html 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### master 2 | 3 | ### v5.1.4.p1,2017-12-28 4 | 5 | - 更新到 Rails 5.1.4 6 | 7 | ### v5.1.3.p1,2017-8-8 8 | 9 | - 更新到 Rails 5.1.3 10 | 11 | ### v5.1.2.1,2017-7-7 12 | 13 | - 更新到 Rails 5.1.2 14 | - 修正错别字 15 | 16 | ### v5.1.1.1, 2017-5-20 17 | 18 | - 更新到 Rails 5.1.1 19 | - PDF 格式中的目录分两栏显示 20 | - 标出术语、专有名词 21 | 22 | ### v5.0.2.1, 2017-3-20 23 | 24 | - 更新到 Rails 5.0.2 25 | - 增加“Rails 初始化过程” 26 | - 增加“Asset Pipeline” 27 | - 增加“引擎入门” 28 | 29 | ### v5.0.1.2, 2017-2-28 30 | 31 | - 修正错别字 32 | - 增加“Ruby on Rails 安全指南” 33 | - 增加“Action Cable 概览” 34 | 35 | ### v5.0.1.1, 2017-2-8 36 | 37 | - 首次发布 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献说明 2 | 3 | 我们欢迎任何人为本仓库做贡献,力求为读者提供更好的 Rails 参考资料。在您准备为本仓库做贡献之前,请认真阅读下述说明。在本仓库问题追踪系统中发布的工单,默认都同意这里的说明。 4 | 5 | 1. 你的贡献可能被纳入译稿中,而我们会将其用在电子书中销售,由此得到的收入不会分给你。 6 | 2. 安道和 chinakr 负责审核,未通过审核的修改不予合并。 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'persie', path: '~/Projects/persie' 4 | gem 'persie_theme_rails_guides', path: '~/Projects/persie_theme_rails_guides' 5 | gem 'rake' 6 | gem 'tilt', '2.0.7' # For using erb template to convert to Markdown 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../../Projects/persie_theme_rails_guides 3 | specs: 4 | persie_theme_rails_guides (0.0.1) 5 | 6 | PATH 7 | remote: ../../Projects/persie 8 | specs: 9 | persie (0.0.3.beta.4) 10 | asciidoctor (= 1.5.5) 11 | colorize (= 0.8.1) 12 | gepub (= 0.6.9.2) 13 | liquid (= 4.0.0) 14 | nokogiri (= 1.6.8.1) 15 | rouge (= 2.0.7) 16 | thor (= 0.19.4) 17 | thread_safe (= 0.3.5) 18 | uuid (= 2.3.8) 19 | 20 | GEM 21 | remote: https://rubygems.org/ 22 | specs: 23 | asciidoctor (1.5.5) 24 | colorize (0.8.1) 25 | gepub (0.6.9.2) 26 | nokogiri (~> 1.6.1) 27 | rubyzip (>= 1.1.1) 28 | liquid (4.0.0) 29 | macaddr (1.7.1) 30 | systemu (~> 2.6.2) 31 | mini_portile2 (2.1.0) 32 | nokogiri (1.6.8.1) 33 | mini_portile2 (~> 2.1.0) 34 | rake (12.0.0) 35 | rouge (2.0.7) 36 | rubyzip (1.2.1) 37 | systemu (2.6.5) 38 | thor (0.19.4) 39 | thread_safe (0.3.5) 40 | tilt (2.0.7) 41 | uuid (2.3.8) 42 | macaddr (~> 1.0) 43 | 44 | PLATFORMS 45 | ruby 46 | 47 | DEPENDENCIES 48 | persie! 49 | persie_theme_rails_guides! 50 | rake 51 | tilt (= 2.0.7) 52 | 53 | BUNDLED WITH 54 | 1.14.6 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rails 指南 2 | 3 | [Rails Guides](http://guides.rubyonrails.org/) 简体中文翻译。 4 | 5 | ## 做贡献 6 | 7 | 我们欢迎你为 Rails 指南添砖加瓦,你可以做的有: 8 | 9 | - 纠正翻译错误 10 | - 提出建议 11 | 12 | 注意:做贡献之前,请先阅读[说明](https://github.com/AndorChen/rails-guides/blob/master/CONTRIBUTING.md)。我们将假定你已经阅读并同意其中的条款。 13 | 14 | ## 译者 15 | 16 | - [安道](http://about.ac) 17 | - [chinakr](https://github.com/chinakr) 18 | 19 | ## 赞助商 20 | 21 | - [Ruby China](https://ruby-china.org) 22 | 23 | ## 许可证 24 | 25 | 简体中文版与英文原版一样,基于 [CC BY-SA 4.0 协议](https://creativecommons.org/licenses/by-sa/4.0/deed.zh)。 26 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'yaml' 4 | require 'asciidoctor' 5 | 6 | desc 'Build pdf format' 7 | task :pdf do 8 | system 'bundle exec persie build pdf' 9 | end 10 | 11 | desc 'Build pdf sample' 12 | task :sample do 13 | require 'colorize' 14 | require 'persie/dependency' 15 | 16 | unless Persie::Dependency.which 'cpdf' 17 | puts 'You must install "cpdf"(https://github.com/coherentgraphics/cpdf-binaries) first' 18 | end 19 | 20 | puts '=== Build sample ' << '=' * 55 21 | puts 'Generating sample...' 22 | 23 | info = File.read('./book.adoc') 24 | info.match /:revnumber:\s*(\d+\.?\d*\.?\d*\.?\w*)/ 25 | rev = $1 26 | info.match /:slug:\s*(\w+)/ 27 | slug = $1 28 | 29 | page_range = '2-76' # EDIT this 30 | 31 | system "cpdf ./themes/pdf/sample-cover.pdf ./builds/pdf/#{slug}-#{rev}.pdf #{page_range} -o ./builds/pdf/#{slug}-sample.pdf" 32 | 33 | puts ' Done'.colorize(:green) 34 | puts " Location: ./builds/pdf/#{slug}-sample.pdf" 35 | puts '=' * 72 36 | end 37 | 38 | desc 'Build epub format' 39 | task :epub do 40 | system 'bundle exec persie build epub -c' 41 | end 42 | 43 | desc 'Build mobi format' 44 | task :mobi do 45 | system 'bundle exec persie build mobi' 46 | end 47 | 48 | desc 'Build site format' 49 | task :site do 50 | system 'bundle exec persie build html -m' 51 | end 52 | 53 | desc 'Convert to Markdown format' 54 | task :md do 55 | FileUtils.mkdir_p 'builds/markdown' 56 | FileUtils.rm Dir['builds/markdown/*'] 57 | FileUtils.mkdir_p 'tmp/markdown' 58 | 59 | bottle = {} 60 | puts "Collecting refids" 61 | Dir.glob("manuscript/*.adoc") do |f| 62 | doc = Asciidoctor.load_file(f) 63 | refids = doc.references[:ids] 64 | 65 | refids.each do |_, v| 66 | v.gsub!(/|<\/code>/, '`') 67 | end 68 | 69 | bottle.merge! refids 70 | end 71 | File.open('tmp/markdown/refs.yml', 'w') { |f| f.write(bottle.to_yaml) } 72 | 73 | excludes = [ 74 | 'contributing', 75 | 'controllers', 76 | 'digging_depper', 77 | 'extending_rails', 78 | 'foreword', 79 | 'maintenance', 80 | 'models', 81 | 'release_notes', 82 | 'start_here', 83 | 'supplement', 84 | 'views' 85 | ] 86 | 87 | files = FileList.new('manuscript/*.adoc') 88 | files.exclude(excludes.map { |n| "manuscript/#{n}.adoc"}) 89 | files.each do |f| 90 | puts "Converting #{File.basename(f)}" 91 | asciidoc = File.open(f).read 92 | 93 | imagesdir = "images" 94 | imagesdir << "/#{File.basename(f, '.adoc')}" if ['getting_started', 'i18n'].include?(File.basename(f, '.adoc')) 95 | markdown = Asciidoctor.convert( asciidoc, 96 | template_dir: 'md_tpl', 97 | template_engine_options: { erb: { trim: '-' } }, 98 | attributes: { 99 | 'imagesdir' => imagesdir 100 | } 101 | ) 102 | 103 | # weird, you cannot use `**' etc directly in templates 104 | markdown.gsub!('', '_') 105 | markdown.gsub!('', '**') 106 | markdown.gsub!('', '`') 107 | 108 | filename = "#{File.basename(f,'.*')}.md" 109 | File.open("builds/markdown/#{filename}", 'w') { |f| f.write(markdown) } 110 | end 111 | 112 | end 113 | 114 | desc 'Preview site' 115 | task :preview do 116 | system 'bundle exec persie preview multiple' 117 | end 118 | -------------------------------------------------------------------------------- /book.adoc: -------------------------------------------------------------------------------- 1 | = Rails 指南 2 | :slug: railsguides 3 | :translator: 安道/chinakr 4 | :translator-email: andor.chen.27@email.com 5 | :revnumber: 5.1.4.p1 6 | :revdate: 2017-12-28T20:00:00+08:00 7 | :lang: zh-CN 8 | :description: Rails 全方位解读。 9 | :keywords: rails, guide, guides, 指南 10 | :uuid: urn:uuid:bcb9fde0-5d33-0134-172d-482a140fb2a7 11 | :author-label: {author} 著 12 | :translator-label: {translator} 译 13 | :version-label: rev 14 | :toc: 15 | :toclevels: 2 16 | :sectnumlevels: 5 17 | :toc-title: 目录 18 | :admonition-use-caption: 19 | :caution-caption: 警告 20 | :important-caption: 重要 21 | :note-caption: 注意 22 | :tip-caption: 提示 23 | :warning-caption: 提醒 24 | :listing-caption: 代码清单 %NUM%-%SUBNUM% 25 | :image-caption: 图 %NUM%-%SUBNUM% 26 | :table-caption: 表 %NUM%-%SUBNUM% 27 | :table-caption-continued: (续) 28 | :sidebar-caption: 旁注 %NUM%-%SUBNUM% 29 | :caption-trailing: : 30 | :chapter-caption: 第 %NUM% 章 31 | :section-caption: %NUM% 节 32 | :appendix-caption: 附录 %NUM% 33 | :epub-identifier-scheme: uuid 34 | :highlight: 35 | :pdf-cover-image: rails-guides-cover.jpg 36 | :epub-cover-image: rails-guides-cover.jpg 37 | :cover-page-title: 封面 38 | :ebook-theme: rails_guides 39 | :rails-version: 5.1.4 40 | 41 | ifeval::["{ebook-format}" != "html"] 42 | include::manuscript/foreword.adoc[] 43 | endif::[] 44 | 45 | :numbered: 46 | 47 | include::manuscript/start_here.adoc[] 48 | 49 | include::manuscript/getting_started.adoc[] 50 | 51 | include::manuscript/models.adoc[] 52 | 53 | include::manuscript/active_record_basics.adoc[] 54 | 55 | include::manuscript/active_record_migrations.adoc[] 56 | 57 | include::manuscript/active_record_validations.adoc[] 58 | 59 | include::manuscript/active_record_callbacks.adoc[] 60 | 61 | include::manuscript/association_basics.adoc[] 62 | 63 | include::manuscript/active_record_querying.adoc[] 64 | 65 | include::manuscript/active_model_basics.adoc[] 66 | 67 | include::manuscript/views.adoc[] 68 | 69 | include::manuscript/action_view_overview.adoc[] 70 | 71 | include::manuscript/layouts_and_rendering.adoc[] 72 | 73 | include::manuscript/form_helpers.adoc[] 74 | 75 | include::manuscript/controllers.adoc[] 76 | 77 | include::manuscript/action_controller_overview.adoc[] 78 | 79 | include::manuscript/routing.adoc[] 80 | 81 | include::manuscript/digging_depper.adoc[] 82 | 83 | include::manuscript/active_support_core_extensions.adoc[] 84 | 85 | include::manuscript/i18n.adoc[] 86 | 87 | include::manuscript/action_mailer_basics.adoc[] 88 | 89 | include::manuscript/active_job_basics.adoc[] 90 | 91 | include::manuscript/testing.adoc[] 92 | 93 | include::manuscript/security.adoc[] 94 | 95 | include::manuscript/debugging_rails_applications.adoc[] 96 | 97 | include::manuscript/configuring.adoc[] 98 | 99 | include::manuscript/command_line.adoc[] 100 | 101 | include::manuscript/asset_pipeline.adoc[] 102 | 103 | include::manuscript/working_with_javascript_in_rails.adoc[] 104 | 105 | include::manuscript/initialization.adoc[] 106 | 107 | include::manuscript/autoloading_and_reloading_constants.adoc[] 108 | 109 | include::manuscript/caching_with_rails.adoc[] 110 | 111 | include::manuscript/active_support_instrumentation.adoc[] 112 | 113 | include::manuscript/api_app.adoc[] 114 | 115 | include::manuscript/action_cable_overview.adoc[] 116 | 117 | include::manuscript/extending_rails.adoc[] 118 | 119 | include::manuscript/plugins.adoc[] 120 | 121 | include::manuscript/rails_on_rack.adoc[] 122 | 123 | include::manuscript/generators.adoc[] 124 | 125 | include::manuscript/engines.adoc[] 126 | 127 | include::manuscript/contributing.adoc[] 128 | 129 | include::manuscript/contributing_to_ruby_on_rails.adoc[] 130 | 131 | include::manuscript/api_documentation_guidelines.adoc[] 132 | 133 | include::manuscript/ruby_on_rails_guides_guidelines.adoc[] 134 | 135 | include::manuscript/maintenance.adoc[] 136 | 137 | include::manuscript/maintenance_policy.adoc[] 138 | 139 | include::manuscript/release_notes.adoc[] 140 | 141 | include::manuscript/upgrading_ruby_on_rails.adoc[] 142 | 143 | include::manuscript/5_1_release_notes.adoc[] 144 | 145 | include::manuscript/5_0_release_notes.adoc[] 146 | 147 | include::manuscript/supplement.adoc[] 148 | 149 | include::manuscript/rails_application_templates.adoc[] 150 | 151 | include::manuscript/development_dependencies_install.adoc[] 152 | -------------------------------------------------------------------------------- /glossary.md: -------------------------------------------------------------------------------- 1 | | 英文 | 中文 | 备注 | 2 | |------|-----|------| 3 | | application | 应用 | | 4 | | action | (控制器)动作 | | 5 | | helper | 辅助方法 | | 6 | | validation | 数据验证 | | 7 | | eager load | 及早加载 | | 8 | | sanitizer | 净化程序 | | 9 | | fixture | (测试)固件 | | 10 | | mass assignment | 批量赋值 | | 11 | | redirect | 重定向 | | 12 | | authentication | 身份验证 | | 13 | | header | 首部 | | 14 | | initializer | 初始化脚本 | | 15 | -------------------------------------------------------------------------------- /images/article_with_comments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/article_with_comments.png -------------------------------------------------------------------------------- /images/belongs_to.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/belongs_to.png -------------------------------------------------------------------------------- /images/challenge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/challenge.png -------------------------------------------------------------------------------- /images/confirm_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/confirm_dialog.png -------------------------------------------------------------------------------- /images/csrf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/csrf.png -------------------------------------------------------------------------------- /images/demo_html_safe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/demo_html_safe.png -------------------------------------------------------------------------------- /images/demo_localized_pirate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/demo_localized_pirate.png -------------------------------------------------------------------------------- /images/demo_translated_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/demo_translated_en.png -------------------------------------------------------------------------------- /images/demo_translated_pirate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/demo_translated_pirate.png -------------------------------------------------------------------------------- /images/demo_translation_missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/demo_translation_missing.png -------------------------------------------------------------------------------- /images/demo_untranslated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/demo_untranslated.png -------------------------------------------------------------------------------- /images/forbidden_attributes_for_new_article.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/forbidden_attributes_for_new_article.png -------------------------------------------------------------------------------- /images/form_with_errors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/form_with_errors.png -------------------------------------------------------------------------------- /images/habtm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/habtm.png -------------------------------------------------------------------------------- /images/has_many.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/has_many.png -------------------------------------------------------------------------------- /images/has_many_through.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/has_many_through.png -------------------------------------------------------------------------------- /images/has_one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/has_one.png -------------------------------------------------------------------------------- /images/has_one_through.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/has_one_through.png -------------------------------------------------------------------------------- /images/index_action_with_edit_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/index_action_with_edit_link.png -------------------------------------------------------------------------------- /images/new_article.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/new_article.png -------------------------------------------------------------------------------- /images/polymorphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/polymorphic.png -------------------------------------------------------------------------------- /images/rails_welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/rails_welcome.png -------------------------------------------------------------------------------- /images/routing_error_no_controller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/routing_error_no_controller.png -------------------------------------------------------------------------------- /images/ruby-china-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/ruby-china-logo.jpg -------------------------------------------------------------------------------- /images/session_fixation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/session_fixation.png -------------------------------------------------------------------------------- /images/show_action_for_articles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/show_action_for_articles.png -------------------------------------------------------------------------------- /images/template_is_missing_articles_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/template_is_missing_articles_new.png -------------------------------------------------------------------------------- /images/unknown_action_create_for_articles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/unknown_action_create_for_articles.png -------------------------------------------------------------------------------- /images/unknown_action_new_for_articles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/images/unknown_action_new_for_articles.png -------------------------------------------------------------------------------- /manuscript/action_cable_overview.adoc: -------------------------------------------------------------------------------- 1 | [[action-cable-overview]] 2 | == Action Cable 概览 3 | 4 | // chinakr 翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 本文介绍 Action Cable 的工作原理,以及在 Rails 应用中如何通过 WebSocket 实现实时功能。 9 | 10 | 读完本文后,您将学到: 11 | 12 | * Action Cable 是什么,以及对前后端的集成; 13 | * 如何设置 Action Cable; 14 | * 如何设置频道(channel); 15 | * Action Cable 的部署和架构设置。 16 | -- 17 | 18 | [[introduction]] 19 | === 简介 20 | 21 | Action Cable 将 https://en.wikipedia.org/wiki/WebSocket[WebSocket] 与 Rails 应用的其余部分无缝集成。有了 Action Cable,我们就可以用 Ruby 语言,以 Rails 风格实现实时功能,并且保持高性能和可扩展性。Action Cable 为此提供了全栈支持,包括客户端 JavaScript 框架和服务器端 Ruby 框架。同时,我们也能够通过 Action Cable 访问使用 Active Record 或其他 ORM 编写的所有模型。 22 | 23 | [[what-is-pub-sub]] 24 | === Pub/Sub 是什么 25 | 26 | link:https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern[Pub/Sub],也就是发布/订阅,是指在消息队列中,信息发送者(发布者)把数据发送给某一类接收者(订阅者),而不必单独指定接收者。Action Cable 通过发布/订阅的方式在服务器和多个客户端之间通信。 27 | 28 | [[server-side-components]] 29 | === 服务器端组件 30 | 31 | [[server-side-connections]] 32 | ==== 连接 33 | 34 | 连接是客户端-服务器通信的基础。每当服务器接受一个 WebSocket,就会实例化一个连接对象。所有频道订阅(channel subscription)都是在继承连接对象的基础上创建的。连接本身并不处理身份验证和授权之外的任何应用逻辑。WebSocket 连接的客户端被称为连接用户(connection consumer)。每当用户新打开一个浏览器标签、窗口或设备,对应地都会新建一个用户-连接对(consumer-connection pair)。 35 | 36 | 连接是 `ApplicationCable::Connection` 类的实例。对连接的授权就是在这个类中完成的,对于能够识别的用户,才会继续建立连接。 37 | 38 | [[connection-setup]] 39 | ===== 连接设置 40 | 41 | [source,ruby] 42 | ---- 43 | # app/channels/application_cable/connection.rb 44 | module ApplicationCable 45 | class Connection < ActionCable::Connection::Base 46 | identified_by :current_user 47 | 48 | def connect 49 | self.current_user = find_verified_user 50 | end 51 | 52 | private 53 | def find_verified_user 54 | if current_user = User.find_by(id: cookies.signed[:user_id]) 55 | current_user 56 | else 57 | reject_unauthorized_connection 58 | end 59 | end 60 | end 61 | end 62 | ---- 63 | 64 | 其中 `identified_by` 用于声明连接标识符,连接标识符稍后将用于查找指定连接。注意,在声明连接标识符的同时,在基于连接创建的频道实例上,会自动创建同名委托(delegate)。 65 | 66 | 上述例子假设我们已经在应用的其他部分完成了用户身份验证,并且在验证成功后设置了经过用户 ID 签名的 cookie。 67 | 68 | 尝试建立新连接时,会自动把 cookie 发送给连接实例,用于设置 `current_user`。通过使用 `current_user` 标识连接,我们稍后就能够检索指定用户打开的所有连接(如果删除用户或取消对用户的授权,该用户打开的所有连接都会断开)。 69 | 70 | [[channels]] 71 | ==== 频道 72 | 73 | 和常规 MVC 中的控制器类似,频道用于封装逻辑工作单元。默认情况下,Rails 会把 `ApplicationCable::Channel` 类作为频道的父类,用于封装频道之间共享的逻辑。 74 | 75 | [[parent-channel-setup]] 76 | ===== 父频道设置 77 | 78 | [source,ruby] 79 | ---- 80 | # app/channels/application_cable/channel.rb 81 | module ApplicationCable 82 | class Channel < ActionCable::Channel::Base 83 | end 84 | end 85 | ---- 86 | 87 | 接下来我们要创建自己的频道类。例如,可以创建 `ChatChannel` 和 `AppearanceChannel` 类: 88 | 89 | [source,ruby] 90 | ---- 91 | # app/channels/chat_channel.rb 92 | class ChatChannel < ApplicationCable::Channel 93 | end 94 | 95 | # app/channels/appearance_channel.rb 96 | class AppearanceChannel < ApplicationCable::Channel 97 | end 98 | ---- 99 | 100 | 这样用户就可以订阅频道了,订阅一个或两个都行。 101 | 102 | [[subscriptions]] 103 | ===== 订阅 104 | 105 | 订阅频道的用户称为订阅者。用户创建的连接称为(频道)订阅。订阅基于连接用户(订阅者)发送的标识符创建,生成的消息将发送到这些订阅。 106 | 107 | [source,ruby] 108 | ---- 109 | # app/channels/chat_channel.rb 110 | class ChatChannel < ApplicationCable::Channel 111 | # 当用户成为此频道的订阅者时调用 112 | def subscribed 113 | end 114 | end 115 | ---- 116 | 117 | [[client-side-components]] 118 | === 客户端组件 119 | 120 | [[client-side-connections]] 121 | ==== 连接 122 | 123 | 用户需要在客户端创建连接实例。下面这段由 Rails 默认生成的 JavaScript 代码,正是用于在客户端创建连接实例: 124 | 125 | [[connect-consumer]] 126 | ===== 连接用户 127 | 128 | [source,js] 129 | ---- 130 | // app/assets/javascripts/cable.js 131 | //= require action_cable 132 | //= require_self 133 | //= require_tree ./channels 134 | 135 | (function() { 136 | this.App || (this.App = {}); 137 | 138 | App.cable = ActionCable.createConsumer(); 139 | }).call(this); 140 | ---- 141 | 142 | 上述代码会创建连接用户,并将通过默认的 `/cable` 地址和服务器建立连接。我们还需要从现有订阅中至少选择一个感兴趣的订阅,否则将无法建立连接。 143 | 144 | [[subscriber]] 145 | ===== 订阅者 146 | 147 | 一旦订阅了某个频道,用户也就成为了订阅者: 148 | 149 | [source,ruby] 150 | ---- 151 | # app/assets/javascripts/cable/subscriptions/chat.coffee 152 | App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" } 153 | 154 | # app/assets/javascripts/cable/subscriptions/appearance.coffee 155 | App.cable.subscriptions.create { channel: "AppearanceChannel" } 156 | ---- 157 | 158 | 上述代码创建了订阅,稍后我们还要描述如何处理接收到的数据。 159 | 160 | 作为订阅者,用户可以多次订阅同一个频道。例如,用户可以同时订阅多个聊天室: 161 | 162 | [source,ruby] 163 | ---- 164 | App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" } 165 | App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" } 166 | ---- 167 | 168 | [[client-server-interactions]] 169 | === 客户端-服务器的交互 170 | 171 | [[streams]] 172 | ==== 流(stream) 173 | 174 | 频道把已发布内容(即广播)发送给订阅者,是通过所谓的“流”机制实现的。 175 | 176 | [source,ruby] 177 | ---- 178 | # app/channels/chat_channel.rb 179 | class ChatChannel < ApplicationCable::Channel 180 | def subscribed 181 | stream_from "chat_#{params[:room]}" 182 | end 183 | end 184 | ---- 185 | 186 | 有了和模型关联的流,就可以从模型和频道生成所需的广播。下面的例子用于订阅评论频道,以接收 `Z2lkOi8vVGVzdEFwcC9Qb3N0LzE` 这样的广播: 187 | 188 | [source,ruby] 189 | ---- 190 | class CommentsChannel < ApplicationCable::Channel 191 | def subscribed 192 | post = Post.find(params[:id]) 193 | stream_for post 194 | end 195 | end 196 | ---- 197 | 198 | 向评论频道发送广播的方式如下: 199 | 200 | [source,ruby] 201 | ---- 202 | CommentsChannel.broadcast_to(@post, @comment) 203 | ---- 204 | 205 | [[broadcasting]] 206 | ==== 广播 207 | 208 | 广播是指发布/订阅的链接,也就是说,当频道订阅者使用流接收某个广播时,发布者发布的内容会被直接发送给订阅者。 209 | 210 | 广播也是时间相关的在线队列。如果用户未使用流(即未订阅频道),稍后就无法接收到广播。 211 | 212 | 在 Rails 应用的其他部分也可以发送广播: 213 | 214 | [source,ruby] 215 | ---- 216 | WebNotificationsChannel.broadcast_to( 217 | current_user, 218 | title: 'New things!', 219 | body: 'All the news fit to print' 220 | ) 221 | ---- 222 | 223 | 调用 `WebNotificationsChannel.broadcast_to` 将向当前订阅适配器(生产环境默认为 `redis`,开发和测试环境默认为 `async`)的发布/订阅队列推送一条消息,并为每个用户设置不同的广播名。对于 ID 为 1 的用户,广播名是 `web_notifications:1`。 224 | 225 | 通过调用 `received` 回调方法,频道会使用流把到达 `web_notifications:1` 的消息直接发送给客户端。 226 | 227 | [[client-server-interactions-subscriptions]] 228 | ==== 订阅 229 | 230 | 订阅频道的用户,称为订阅者。用户创建的连接称为(频道)订阅。订阅基于连接用户(订阅者)发送的标识符创建,收到的消息将被发送到这些订阅。 231 | 232 | [source,coffee] 233 | ---- 234 | # app/assets/javascripts/cable/subscriptions/chat.coffee 235 | # 假设我们已经获得了发送 Web 通知的权限 236 | App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, 237 | received: (data) -> 238 | @appendLine(data) 239 | 240 | appendLine: (data) -> 241 | html = @createLine(data) 242 | $("[data-chat-room='Best Room']").append(html) 243 | 244 | createLine: (data) -> 245 | """ 246 |
247 | #{data["sent_by"]} 248 | #{data["body"]} 249 |
250 | """ 251 | ---- 252 | 253 | [[passing-parameters-to-channels]] 254 | ==== 向频道传递参数 255 | 256 | 创建订阅时,可以从客户端向服务器端传递参数。例如: 257 | 258 | [source,ruby] 259 | ---- 260 | # app/channels/chat_channel.rb 261 | class ChatChannel < ApplicationCable::Channel 262 | def subscribed 263 | stream_from "chat_#{params[:room]}" 264 | end 265 | end 266 | ---- 267 | 268 | 传递给 `subscriptions.create` 方法并作为第一个参数的对象,将成为频道的参数散列。其中必需包含 `channel` 关键字: 269 | 270 | [source,coffee] 271 | ---- 272 | # app/assets/javascripts/cable/subscriptions/chat.coffee 273 | App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, 274 | received: (data) -> 275 | @appendLine(data) 276 | 277 | appendLine: (data) -> 278 | html = @createLine(data) 279 | $("[data-chat-room='Best Room']").append(html) 280 | 281 | createLine: (data) -> 282 | """ 283 |
284 | #{data["sent_by"]} 285 | #{data["body"]} 286 |
287 | """ 288 | ---- 289 | 290 | [source,ruby] 291 | ---- 292 | # 在应用的某个部分中调用,例如 NewCommentJob 293 | ActionCable.server.broadcast( 294 | "chat_#{room}", 295 | sent_by: 'Paul', 296 | body: 'This is a cool chat app.' 297 | ) 298 | ---- 299 | 300 | [[rebroadcasting-a-message]] 301 | ==== 消息重播 302 | 303 | 一个客户端向其他已连接客户端重播自己收到的消息,是一种常见用法。 304 | 305 | [source,ruby] 306 | ---- 307 | # app/channels/chat_channel.rb 308 | class ChatChannel < ApplicationCable::Channel 309 | def subscribed 310 | stream_from "chat_#{params[:room]}" 311 | end 312 | 313 | def receive(data) 314 | ActionCable.server.broadcast("chat_#{params[:room]}", data) 315 | end 316 | end 317 | ---- 318 | 319 | [source,coffee] 320 | ---- 321 | # app/assets/javascripts/cable/subscriptions/chat.coffee 322 | App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, 323 | received: (data) -> 324 | # data => { sent_by: "Paul", body: "This is a cool chat app." } 325 | 326 | App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." }) 327 | ---- 328 | 329 | 所有已连接的客户端,包括发送消息的客户端在内,都将收到重播的消息。注意,重播时使用的参数与订阅频道时使用的参数相同。 330 | 331 | [[full-stack-examples]] 332 | === 全栈示例 333 | 334 | 本节的两个例子都需要进行下列设置: 335 | 336 | 1. 设置连接; 337 | 2. 设置父频道; 338 | 3. 连接用户。 339 | 340 | [[example-one-user-appearances]] 341 | ==== 例 1:用户在线状态(user appearance) 342 | 343 | 下面是一个关于频道的简单例子,用于跟踪用户是否在线,以及用户所在的页面。(常用于显示用户在线状态,例如当用户在线时,在用户名旁边显示绿色小圆点。) 344 | 345 | 在服务器端创建在线状态频道(appearance channel): 346 | 347 | [source,ruby] 348 | ---- 349 | # app/channels/appearance_channel.rb 350 | class AppearanceChannel < ApplicationCable::Channel 351 | def subscribed 352 | current_user.appear 353 | end 354 | 355 | def unsubscribed 356 | current_user.disappear 357 | end 358 | 359 | def appear(data) 360 | current_user.appear(on: data['appearing_on']) 361 | end 362 | 363 | def away 364 | current_user.away 365 | end 366 | end 367 | ---- 368 | 369 | 订阅创建后,会触发 `subscribed` 回调方法,这时可以提示说“当前用户上线了”。上线/下线 API 的后端可以是 Redis、数据库或其他解决方案。 370 | 371 | 在客户端创建在线状态频道订阅: 372 | 373 | [source,coffee] 374 | ---- 375 | # app/assets/javascripts/cable/subscriptions/appearance.coffee 376 | App.cable.subscriptions.create "AppearanceChannel", 377 | # 当服务器上的订阅可用时调用 378 | connected: -> 379 | @install() 380 | @appear() 381 | 382 | # 当 WebSocket 连接关闭时调用 383 | disconnected: -> 384 | @uninstall() 385 | 386 | # 当服务器拒绝订阅时调用 387 | rejected: -> 388 | @uninstall() 389 | 390 | appear: -> 391 | # 在服务器上调用 `AppearanceChannel#appear(data)` 392 | @perform("appear", appearing_on: $("main").data("appearing-on")) 393 | 394 | away: -> 395 | # 在服务器上调用 `AppearanceChannel#away` 396 | @perform("away") 397 | 398 | 399 | buttonSelector = "[data-behavior~=appear_away]" 400 | 401 | install: -> 402 | $(document).on "turbolinks:load.appearance", => 403 | @appear() 404 | 405 | $(document).on "click.appearance", buttonSelector, => 406 | @away() 407 | false 408 | 409 | $(buttonSelector).show() 410 | 411 | uninstall: -> 412 | $(document).off(".appearance") 413 | $(buttonSelector).hide() 414 | ---- 415 | 416 | [[client-server-interaction]] 417 | ===== 客户端-服务器交互 418 | 419 | 1. **客户端**通过 `App.cable = ActionCable.createConsumer("ws://cable.example.com")`(位于 `cable.js` 文件中)连接到**服务器**。**服务器**通过 `current_user` 标识此连接。 420 | 421 | 2. **客户端**通过 `App.cable.subscriptions.create(channel: "AppearanceChannel")`(位于 `appearance.coffee` 文件中)订阅在线状态频道。 422 | 423 | 3. **服务器**发现在线状态频道创建了一个新订阅,于是调用 `subscribed` 回调方法,也即在 `current_user` 对象上调用 `appear` 方法。 424 | 425 | 4. **客户端**发现订阅创建成功,于是调用 `connected` 方法(位于 `appearance.coffee` 文件中),也即依次调用 `@install` 和 `@appear`。`@appear` 会调用服务器上的 `AppearanceChannel#appear(data)` 方法,同时提供 `{ appearing_on: $("main").data("appearing-on") }` 数据散列。之所以能够这样做,是因为服务器端的频道实例会自动暴露类上声明的所有公共方法(回调除外),从而使远程过程能够通过订阅的 `perform` 方法调用它们。 426 | 427 | 5. **服务器**接收向在线状态频道的 `appear` 动作发起的请求,此频道基于连接创建,连接由 `current_user`(位于 `appearance_channel.rb` 文件中)标识。**服务器**通过 `:appearing_on` 键从数据散列中检索数据,将其设置为 `:on` 键的值并传递给 `current_user.appear`。 428 | 429 | [[example-two-receiving-new-web-notifications]] 430 | ==== 例 2:接收新的 Web 通知 431 | 432 | 上一节中在线状态的例子,演示了如何把服务器功能暴露给客户端,以便在客户端通过 WebSocket 连接调用这些功能。但是 WebSocket 的伟大之处在于,它是一条双向通道。因此,在本节的例子中,我们要看一看服务器如何调用客户端上的动作。 433 | 434 | 本节所举的例子是一个 Web 通知频道(Web notification channel),允许我们在广播到正确的流时触发客户端 Web 通知。 435 | 436 | 创建服务器端 Web 通知频道: 437 | 438 | [source,ruby] 439 | ---- 440 | # app/channels/web_notifications_channel.rb 441 | class WebNotificationsChannel < ApplicationCable::Channel 442 | def subscribed 443 | stream_for current_user 444 | end 445 | end 446 | ---- 447 | 448 | 创建客户端 Web 通知频道订阅: 449 | 450 | [source,coffee] 451 | ---- 452 | # app/assets/javascripts/cable/subscriptions/web_notifications.coffee 453 | # 客户端假设我们已经获得了发送 Web 通知的权限 454 | App.cable.subscriptions.create "WebNotificationsChannel", 455 | received: (data) -> 456 | new Notification data["title"], body: data["body"] 457 | ---- 458 | 459 | 在应用的其他部分向 Web 通知频道实例发送内容广播: 460 | 461 | [source,ruby] 462 | ---- 463 | # 在应用的某个部分中调用,例如 NewCommentJob 464 | WebNotificationsChannel.broadcast_to( 465 | current_user, 466 | title: 'New things!', 467 | body: 'All the news fit to print' 468 | ) 469 | ---- 470 | 471 | 调用 `WebNotificationsChannel.broadcast_to` 将向当前订阅适配器的发布/订阅队列推送一条消息,并为每个用户设置不同的广播名。对于 ID 为 1 的用户,广播名是 `web_notifications:1`。 472 | 473 | 通过调用 `received` 回调方法,频道会用流把到达 `web_notifications:1` 的消息直接发送给客户端。作为参数传递的数据散列,将作为第二个参数传递给服务器端的广播调用,数据在传输前使用 JSON 进行编码,到达服务器后由 `received` 解码。 474 | 475 | [[more-complete-examples]] 476 | ==== 更完整的例子 477 | 478 | 关于在 Rails 应用中设置 Action Cable 并添加频道的完整例子,参见 link:https://github.com/rails/actioncable-examples[rails/actioncable-examples] 仓库。 479 | 480 | [[configuration]] 481 | === 配置 482 | 483 | 使用 Action Cable 时,有两个选项必需配置:订阅适配器和允许的请求来源。 484 | 485 | [[subscription-adapter]] 486 | ==== 订阅适配器 487 | 488 | 默认情况下,Action Cable 会查找 `config/cable.yml` 这个配置文件。该文件必须为每个 Rails 环境指定适配器和 URL 地址。关于适配器的更多介绍,请参阅 <>。 489 | 490 | [source,yml] 491 | ---- 492 | development: 493 | adapter: async 494 | 495 | test: 496 | adapter: async 497 | 498 | production: 499 | adapter: redis 500 | url: redis://10.10.3.153:6381 501 | channel_prefix: appname_production 502 | ---- 503 | 504 | [[adapter-configuration]] 505 | ===== 配置适配器 506 | 507 | 下面是终端用户可用的订阅适配器。 508 | 509 | [[async-adapter]] 510 | ====== async 适配器 511 | 512 | async 适配器只适用于开发和测试环境,不应该在生产环境使用。 513 | 514 | [[redis-adapter]] 515 | ====== Redis 适配器 516 | 517 | Action Cable 包含两个 Redis 适配器:常规的 Redis 和事件型 Redis。这两个适配器都要求用户提供指向 Redis 服务器的 URL。此外,多个应用使用同一个 Redis 服务器时,可以设定 `channel_prefix`,以免名称冲突。详情参见 https://redis.io/topics/pubsub#database-amp-scoping[Redis PubSub 文档]。 518 | 519 | [[postgresql-adapter]] 520 | ====== PostgreSQL 适配器 521 | 522 | PostgreSQL 适配器使用 Active Record 的连接池,因此使用应用的 `config/database.yml` 数据库配置连接。以后可能会变。link:https://github.com/rails/rails/issues/27214[#27214] 523 | 524 | [[allowed-request-origins]] 525 | ==== 允许的请求来源 526 | 527 | Action Cable 仅接受来自指定来源的请求。这些来源是在服务器配置文件中以数组的形式设置的,每个来源既可以是字符串,也可以是正则表达式。对于每个请求,都要对其来源进行检查,看是否和允许的请求来源相匹配。 528 | 529 | [source,ruby] 530 | ---- 531 | config.action_cable.allowed_request_origins = ['http://rubyonrails.com', %r{http://ruby.*}] 532 | ---- 533 | 534 | 若想禁用来源检查,允许任何来源的请求: 535 | 536 | [source,ruby] 537 | ---- 538 | config.action_cable.disable_request_forgery_protection = true 539 | ---- 540 | 541 | 在开发环境中,Action Cable 默认允许来自 pass:[localhost:3000] 的所有请求。 542 | 543 | [[consumer-configuration]] 544 | ==== 用户配置 545 | 546 | 要想配置 URL 地址,可以在 HTML 布局文件的 `` 元素中添加 `action_cable_meta_tag` 标签。这个标签会使用环境配置文件中 `config.action_cable.url` 选项设置的 URL 地址或路径。 547 | 548 | [[other-configurations]] 549 | ==== 其他配置 550 | 551 | 另一个常见的配置选项,是应用于每个连接记录器的日志标签。下述示例在有用户账户时使用账户 ID,没有时则标记为“no-account”: 552 | 553 | [source,ruby] 554 | ---- 555 | config.action_cable.log_tags = [ 556 | -> request { request.env['user_account_id'] || "no-account" }, 557 | :action_cable, 558 | -> request { request.uuid } 559 | ] 560 | ---- 561 | 562 | 关于所有配置选项的完整列表,请参阅 `ActionCable::Server::Configuration` 类的 API 文档。 563 | 564 | 还要注意,服务器提供的数据库连接在数量上至少应该和职程(worker)相等。职程池的默认大小为 100,也就是说数据库连接数量至少为 4。职程池的大小可以通过 `config/database.yml` 文件中的 `pool` 属性设置。 565 | 566 | [[running-standalone-cable-servers]] 567 | === 运行独立的 Cable 服务器 568 | 569 | [[in-app]] 570 | ==== 和应用一起运行 571 | 572 | Action Cable 可以和 Rails 应用一起运行。例如,要想监听 `/websocket` 上的 WebSocket 请求,可以通过 `config.action_cable.mount_path` 选项指定监听路径: 573 | 574 | [source,ruby] 575 | ---- 576 | # config/application.rb 577 | class Application < Rails::Application 578 | config.action_cable.mount_path = '/websocket' 579 | end 580 | ---- 581 | 582 | 在布局文件中调用 `action_cable_meta_tag` 后,就可以使用 `App.cable = ActionCable.createConsumer()` 连接到 Cable 服务器。可以通过 `createConsumer` 方法的第一个参数指定自定义路径(例如,`App.cable = 583 | ActionCable.createConsumer("/websocket")`)。 584 | 585 | 对于我们创建的每个服务器实例,以及由服务器派生的每个职程,都会新建对应的 Action Cable 实例,通过 Redis 可以在不同连接之间保持消息同步。 586 | 587 | [[standalone]] 588 | ==== 独立运行 589 | 590 | Cable 服务器可以和普通应用服务器分离。此时,Cable 服务器仍然是 Rack 应用,只不过是单独的 Rack 应用罢了。推荐的基本设置如下: 591 | 592 | [source,ruby] 593 | ---- 594 | # cable/config.ru 595 | require_relative '../config/environment' 596 | Rails.application.eager_load! 597 | 598 | run ActionCable.server 599 | ---- 600 | 601 | 然后用 `bin/cable` 中的一个 binstub 命令启动服务器: 602 | 603 | [source,shell] 604 | ---- 605 | #!/bin/bash 606 | bundle exec puma -p 28080 cable/config.ru 607 | ---- 608 | 609 | 上述代码在 28080 端口上启动 Cable 服务器。 610 | 611 | [[notes]] 612 | ==== 注意事项 613 | 614 | WebSocket 服务器没有访问会话的权限,但可以访问 cookie,而在处理身份验证时需要用到 cookie。link:http://www.rubytutorial.io/actioncable-devise-authentication[这篇文章]介绍了如何使用 Devise 验证身份。 615 | 616 | [[action-cable-overview-dependencies]] 617 | === 依赖关系 618 | 619 | Action Cable 提供了用于处理发布/订阅内部逻辑的订阅适配器接口,默认包含异步、内联、PostgreSQL、事件 Redis 和非事件 Redis 适配器。新建 Rails 应用的默认适配器是异步(async)适配器。 620 | 621 | 对 Ruby gem 的依赖包括 link:https://github.com/faye/websocket-driver-ruby[websocket-driver]、link:https://github.com/celluloid/nio4r[nio4r] 和 link:https://github.com/ruby-concurrency/concurrent-ruby[concurrent-ruby]。 622 | 623 | [[deployment]] 624 | === 部署 625 | 626 | Action Cable 由 WebSocket 和线程组成。其中框架管道和用户指定频道的职程,都是通过 Ruby 提供的原生线程支持来处理的。这意味着,只要不涉及线程安全问题,我们就可以使用常规 Rails 线程模型的所有功能。 627 | 628 | Action Cable 服务器实现了Rack 套接字劫持 API(Rack socket hijacking API),因此无论应用服务器是否是多线程的,都能够通过多线程模式管理内部连接。 629 | 630 | 因此,Action Cable 可以和流行的应用服务器一起使用,例如 Unicorn、Puma 和 Passenger。 631 | -------------------------------------------------------------------------------- /manuscript/active_job_basics.adoc: -------------------------------------------------------------------------------- 1 | [[active-jobs-basics]] 2 | == Active Job 基础 3 | 4 | // 安道翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 本文全面说明创建、入队和执行后台作业的基础知识。 9 | 10 | 读完本文后,您将学到: 11 | 12 | - 如何创建作业; 13 | - 如何入队作业; 14 | - 如何在后台运行作业; 15 | - 如何在应用中异步发送电子邮件。 16 | -- 17 | 18 | [[introduction]] 19 | === 简介 20 | 21 | Active Job 框架负责声明作业,在各种队列后端中运行。作业各种各样,可以是定期清理、账单支付和寄信。其实,任何可以分解且并行运行的工作都可以。 22 | 23 | [[the-purpose-of-active-job]] 24 | === Active Job 的作用 25 | 26 | 主要作用是确保所有 Rails 应用都有作业基础设施。这样便可以在此基础上构建各种功能和其他 gem,而无需担心不同作业运行程序(如 Delayed Job 和 Resque)的 API 之间的差异。此外,选用哪个队列后端只是战术问题。而且,切换队列后端也不用重写作业。 27 | 28 | NOTE: Rails 自带了一个在进程内线程池中执行作业的异步队列。这些作业虽然是异步执行的,但是重启后队列中的作业就会丢失。 29 | 30 | [[creating-a-job]] 31 | === 创建作业 32 | 33 | 本节逐步说明创建和入队作业的过程。 34 | 35 | [[create-the-job]] 36 | ==== 创建作业 37 | 38 | Active Job 提供了一个 Rails 生成器,用于创建作业。下述命令在 `app/jobs` 目录中创建一个作业(还在 `test/jobs` 目录中创建相关的测试用例): 39 | 40 | [source,sh] 41 | ---- 42 | $ bin/rails generate job guests_cleanup 43 | invoke test_unit 44 | create test/jobs/guests_cleanup_job_test.rb 45 | create app/jobs/guests_cleanup_job.rb 46 | ---- 47 | 48 | 还可以创建在指定队列中运行的作业: 49 | 50 | [source,sh] 51 | ---- 52 | $ bin/rails generate job guests_cleanup --queue urgent 53 | ---- 54 | 55 | 如果不想使用生成器,可以自己动手在 `app/jobs` 目录中新建文件,不过要确保继承自 `ApplicationJob`。 56 | 57 | 看一下作业: 58 | 59 | [source,ruby] 60 | ---- 61 | class GuestsCleanupJob < ApplicationJob 62 | queue_as :default 63 | 64 | def perform(*guests) 65 | # 稍后做些事情 66 | end 67 | end 68 | ---- 69 | 70 | 注意,`perform` 方法的参数是任意个。 71 | 72 | [[enqueue-the-job]] 73 | ==== 入队作业 74 | 75 | 像下面这样入队作业: 76 | 77 | [source,ruby] 78 | ---- 79 | # 入队作业,作业在队列系统空闲时立即执行 80 | GuestsCleanupJob.perform_later guest 81 | ---- 82 | 83 | [source,ruby] 84 | ---- 85 | # 入队作业,在明天中午执行 86 | GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest) 87 | ---- 88 | 89 | [source,ruby] 90 | ---- 91 | # 入队作业,在一周以后执行 92 | GuestsCleanupJob.set(wait: 1.week).perform_later(guest) 93 | ---- 94 | 95 | [source,ruby] 96 | ---- 97 | # `perform_now` 和 `perform_later` 会在幕后调用 `perform` 98 | # 因此可以传入任意个参数 99 | GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter') 100 | ---- 101 | 102 | 就这么简单! 103 | 104 | [[job-execution]] 105 | === 执行作业 106 | 107 | 在生产环境中入队和执行作业需要使用队列后端,即要为 Rails 提供一个第三方队列库。Rails 本身只提供了一个进程内队列系统,把作业存储在 RAM 中。如果进程崩溃,或者设备重启了,默认的异步后端会丢失所有作业。这对小型应用或不重要的作业来说没什么,但是生产环境中的多数应用应该挑选一个持久后端。 108 | 109 | [[backends]] 110 | ==== 后端 111 | 112 | Active Job 为多种队列后端(Sidekiq、Resque、Delayed Job,等等)内置了适配器。最新的适配器列表参见 http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html[`ActiveJob::QueueAdapters` 的 API 文档]。 113 | 114 | [[setting-the-backend]] 115 | ==== 设置后端 116 | 117 | 队列后端易于设置: 118 | 119 | [source,ruby] 120 | ---- 121 | # config/application.rb 122 | module YourApp 123 | class Application < Rails::Application 124 | # 要把适配器的 gem 写入 Gemfile 125 | # 请参照适配器的具体安装和部署说明 126 | config.active_job.queue_adapter = :sidekiq 127 | end 128 | end 129 | ---- 130 | 131 | 也可以在各个作业中配置后端: 132 | 133 | [source,ruby] 134 | ---- 135 | class GuestsCleanupJob < ApplicationJob 136 | self.queue_adapter = :resque 137 | #.... 138 | end 139 | 140 | # 现在,这个作业使用 `resque` 作为后端队列适配器 141 | # 把 `config.active_job.queue_adapter` 配置覆盖了 142 | ---- 143 | 144 | [[starting-the-backend]] 145 | ==== 启动后端 146 | 147 | Rails 应用中的作业并行运行,因此多数队列库要求为自己启动专用的队列服务(与启动 Rails 应用的服务不同)。启动队列后端的说明参见各个库的文档。 148 | 149 | 下面列出部分文档: 150 | 151 | - https://github.com/mperham/sidekiq/wiki/Active-Job[Sidekiq] 152 | - https://github.com/resque/resque/wiki/ActiveJob[Resque] 153 | - https://github.com/brandonhilkert/sucker_punch#active-job[Sucker Punch] 154 | - https://github.com/QueueClassic/queue_classic#active-job[Queue Classic] 155 | 156 | [[queues]] 157 | === 队列 158 | 159 | 多数适配器支持多个队列。Active Job 允许把作业调度到具体的队列中: 160 | 161 | [source,ruby] 162 | ---- 163 | class GuestsCleanupJob < ApplicationJob 164 | queue_as :low_priority 165 | #.... 166 | end 167 | ---- 168 | 169 | 队列名称可以使用 `application.rb` 文件中的 `config.active_job.queue_name_prefix` 选项配置前缀: 170 | 171 | [source,ruby] 172 | ---- 173 | # config/application.rb 174 | module YourApp 175 | class Application < Rails::Application 176 | config.active_job.queue_name_prefix = Rails.env 177 | end 178 | end 179 | 180 | # app/jobs/guests_cleanup_job.rb 181 | class GuestsCleanupJob < ApplicationJob 182 | queue_as :low_priority 183 | #.... 184 | end 185 | 186 | # 在生产环境中,作业在 production_low_priority 队列中运行 187 | # 在交付准备环境中,作业在 staging_low_priority 队列中运行 188 | ---- 189 | 190 | 默认的队列名称前缀分隔符是 `'_'`。这个值可以使用 `application.rb` 文件中的 `config.active_job.queue_name_delimiter` 选项修改: 191 | 192 | [source,ruby] 193 | ---- 194 | # config/application.rb 195 | module YourApp 196 | class Application < Rails::Application 197 | config.active_job.queue_name_prefix = Rails.env 198 | config.active_job.queue_name_delimiter = '.' 199 | end 200 | end 201 | 202 | # app/jobs/guests_cleanup_job.rb 203 | class GuestsCleanupJob < ApplicationJob 204 | queue_as :low_priority 205 | #.... 206 | end 207 | 208 | # 在生产环境中,作业在 production.low_priority 队列中运行 209 | # 在交付准备环境中,作业在 staging.low_priority 队列中运行 210 | ---- 211 | 212 | 如果想更进一步控制作业在哪个队列中运行,可以把 `:queue` 选项传给 `#set` 方法: 213 | 214 | [source,ruby] 215 | ---- 216 | MyJob.set(queue: :another_queue).perform_later(record) 217 | ---- 218 | 219 | 如果想在作业层控制队列,可以把一个块传给 `#queue_as` 方法。那个块在作业的上下文中执行(因此可以访问 `self.arguments`),必须返回队列的名称: 220 | 221 | [source,ruby] 222 | ---- 223 | class ProcessVideoJob < ApplicationJob 224 | queue_as do 225 | video = self.arguments.first 226 | if video.owner.premium? 227 | :premium_videojobs 228 | else 229 | :videojobs 230 | end 231 | end 232 | 233 | def perform(video) 234 | # 处理视频 235 | end 236 | end 237 | 238 | ProcessVideoJob.perform_later(Video.last) 239 | ---- 240 | 241 | NOTE: 确保队列后端“监听”着队列名称。某些后端要求指定要监听的队列。 242 | 243 | [[callbacks]] 244 | === 回调 245 | 246 | Active Job 在作业的生命周期内提供了多个钩子。回调用于在作业的生命周期内触发逻辑。 247 | 248 | [[available-callbacks]] 249 | ==== 可用的回调 250 | 251 | - `before_enqueue` 252 | - `around_enqueue` 253 | - `after_enqueue` 254 | - `before_perform` 255 | - `around_perform` 256 | - `after_perform` 257 | 258 | [[usage]] 259 | ==== 用法 260 | 261 | [source,ruby] 262 | ---- 263 | class GuestsCleanupJob < ApplicationJob 264 | queue_as :default 265 | 266 | before_enqueue do |job| 267 | # 对作业实例做些事情 268 | end 269 | 270 | around_perform do |job, block| 271 | # 在执行之前做些事情 272 | block.call 273 | # 在执行之后做些事情 274 | end 275 | 276 | def perform 277 | # 稍后做些事情 278 | end 279 | end 280 | ---- 281 | 282 | [[action-mailer]] 283 | === Action Mailer 284 | 285 | 对现代的 Web 应用来说,最常见的作业是在请求-响应循环之外发送电子邮件,这样用户无需等待。Active Job 与 Action Mailer 是集成的,因此可以轻易异步发送电子邮件: 286 | 287 | [source,ruby] 288 | ---- 289 | # 如需想现在发送电子邮件,使用 #deliver_now 290 | UserMailer.welcome(@user).deliver_now 291 | 292 | # 如果想通过 Active Job 发送电子邮件,使用 #deliver_later 293 | UserMailer.welcome(@user).deliver_later 294 | ---- 295 | 296 | [[internationalization]] 297 | === 国际化 298 | 299 | 创建作业时,使用 `I18n.locale` 设置。如果异步发送电子邮件,可能用得到: 300 | 301 | [source,ruby] 302 | ---- 303 | I18n.locale = :eo 304 | 305 | UserMailer.welcome(@user).deliver_later # 使用世界语本地化电子邮件 306 | ---- 307 | 308 | [[globalid]] 309 | === GlobalID 310 | 311 | Active Job 支持参数使用 GlobalID。这样便可以把 Active Record 对象传给作业,而不用传递类和 ID,再自己反序列化。以前,要这么定义作业: 312 | 313 | [source,ruby] 314 | ---- 315 | class TrashableCleanupJob < ApplicationJob 316 | def perform(trashable_class, trashable_id, depth) 317 | trashable = trashable_class.constantize.find(trashable_id) 318 | trashable.cleanup(depth) 319 | end 320 | end 321 | ---- 322 | 323 | 现在可以简化成这样: 324 | 325 | [source,ruby] 326 | ---- 327 | class TrashableCleanupJob < ApplicationJob 328 | def perform(trashable, depth) 329 | trashable.cleanup(depth) 330 | end 331 | end 332 | ---- 333 | 334 | 为此,模型类要混入 `GlobalID::Identification`。Active Record 模型类默认都混入了。 335 | 336 | [[exceptions]] 337 | === 异常 338 | 339 | Active Job 允许捕获执行作业过程中抛出的异常: 340 | 341 | [source,ruby] 342 | ---- 343 | class GuestsCleanupJob < ApplicationJob 344 | queue_as :default 345 | 346 | rescue_from(ActiveRecord::RecordNotFound) do |exception| 347 | # 处理异常 348 | end 349 | 350 | def perform 351 | # 稍后做些事情 352 | end 353 | end 354 | ---- 355 | 356 | [[deserialization]] 357 | ==== 反序列化 358 | 359 | 有了 GlobalID,可以序列化传给 `#perform` 方法的整个 Active Record 对象。 360 | 361 | 如果在作业入队之后、调用 `#perform` 方法之前删除了传入的记录,Active Job 会抛出 `ActiveJob::DeserializationError` 异常。 362 | 363 | [[job-testing]] 364 | === 测试作业 365 | 366 | 测试作业的详细说明参见 <>。 367 | -------------------------------------------------------------------------------- /manuscript/active_model_basics.adoc: -------------------------------------------------------------------------------- 1 | [[active-model-basics]] 2 | == Active Model 基础 3 | 4 | // 安道翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 本文简述模型类。Active Model 允许使用 Action Pack 辅助方法与普通的 Ruby 类交互。Active Model 还协助构建自定义的 ORM,可在 Rails 框架外部使用。 9 | 10 | 读完本文后,您将学到: 11 | 12 | - Active Record 模型的行为; 13 | - 回调和数据验证的工作方式; 14 | - 序列化程序的工作方式; 15 | - Active Model 与 Rails 国际化(i18n)框架的集成。 16 | -- 17 | 18 | NOTE: 本文原文尚未完工! 19 | 20 | [[active-model-basics-introduction]] 21 | === 简介 22 | 23 | Active Model 库包含很多模块,用于开发要在 Active Record 中存储的类。下面说明其中部分模块。 24 | 25 | [[attribute-methods]] 26 | ==== 属性方法 27 | 28 | `ActiveModel::AttributeMethods` 模块可以为类中的方法添加自定义的前缀和后缀。它用于定义前缀和后缀,对象中的方法将使用它们。 29 | 30 | [source,ruby] 31 | ---- 32 | class Person 33 | include ActiveModel::AttributeMethods 34 | 35 | attribute_method_prefix 'reset_' 36 | attribute_method_suffix '_highest?' 37 | define_attribute_methods 'age' 38 | 39 | attr_accessor :age 40 | 41 | private 42 | def reset_attribute(attribute) 43 | send("#{attribute}=", 0) 44 | end 45 | 46 | def attribute_highest?(attribute) 47 | send(attribute) > 100 48 | end 49 | end 50 | 51 | person = Person.new 52 | person.age = 110 53 | person.age_highest? # => true 54 | person.reset_age # => 0 55 | person.age_highest? # => false 56 | ---- 57 | 58 | [[callbacks]] 59 | ==== 回调 60 | 61 | `ActiveModel::Callbacks` 模块为 Active Record 提供回调,在某个时刻运行。定义回调之后,可以使用前置、后置和环绕方法包装。 62 | 63 | [source,ruby] 64 | ---- 65 | class Person 66 | extend ActiveModel::Callbacks 67 | 68 | define_model_callbacks :update 69 | 70 | before_update :reset_me 71 | 72 | def update 73 | run_callbacks(:update) do 74 | # 在对象上调用 update 时执行这个方法 75 | end 76 | end 77 | 78 | def reset_me 79 | # 在对象上调用 update 方法时执行这个方法 80 | # 因为把它定义为 before_update 回调了 81 | end 82 | end 83 | ---- 84 | 85 | [[conversion]] 86 | ==== 转换 87 | 88 | 如果一个类定义了 `persisted?` 和 `id` 方法,可以在那个类中引入 `ActiveModel::Conversion` 模块,这样便能在类的对象上调用 Rails 提供的转换方法。 89 | 90 | [source,ruby] 91 | ---- 92 | class Person 93 | include ActiveModel::Conversion 94 | 95 | def persisted? 96 | false 97 | end 98 | 99 | def id 100 | nil 101 | end 102 | end 103 | 104 | person = Person.new 105 | person.to_model == person # => true 106 | person.to_key # => nil 107 | person.to_param # => nil 108 | ---- 109 | 110 | [[dirty]] 111 | ==== 弄脏 112 | 113 | 如果修改了对象的一个或多个属性,但是没有保存,此时就把对象弄脏了。`ActiveModel::Dirty` 模块提供检查对象是否被修改的功能。它还提供了基于属性的存取方法。假如有个 `Person` 类,它有两个属性,`first_name` 和 `last_name`: 114 | 115 | [source,ruby] 116 | ---- 117 | class Person 118 | include ActiveModel::Dirty 119 | define_attribute_methods :first_name, :last_name 120 | 121 | def first_name 122 | @first_name 123 | end 124 | 125 | def first_name=(value) 126 | first_name_will_change! 127 | @first_name = value 128 | end 129 | 130 | def last_name 131 | @last_name 132 | end 133 | 134 | def last_name=(value) 135 | last_name_will_change! 136 | @last_name = value 137 | end 138 | 139 | def save 140 | # 执行保存操作…… 141 | changes_applied 142 | end 143 | end 144 | ---- 145 | 146 | [[querying-object-directly-for-its-list-of-all-changed-attributes]] 147 | ===== 直接查询对象,获取所有被修改的属性列表 148 | 149 | [source,ruby] 150 | ---- 151 | person = Person.new 152 | person.changed? # => false 153 | 154 | person.first_name = "First Name" 155 | person.first_name # => "First Name" 156 | 157 | # 如果修改属性后未保存,返回 true 158 | person.changed? # => true 159 | 160 | # 返回修改之后没有保存的属性列表 161 | person.changed # => ["first_name"] 162 | 163 | # 返回一个属性散列,指明原来的值 164 | person.changed_attributes # => {"first_name"=>nil} 165 | 166 | # 返回一个散列,键为修改的属性名,值是一个数组,包含旧值和新值 167 | person.changes # => {"first_name"=>[nil, "First Name"]} 168 | ---- 169 | 170 | [[attribute-based-accessor-methods]] 171 | ===== 基于属性的存取方法 172 | 173 | 判断具体的属性是否被修改了: 174 | 175 | [source,ruby] 176 | ---- 177 | # attr_name_changed? 178 | person.first_name # => "First Name" 179 | person.first_name_changed? # => true 180 | ---- 181 | 182 | 查看属性之前的值: 183 | 184 | [source,ruby] 185 | ---- 186 | person.first_name_was # => nil 187 | ---- 188 | 189 | 查看属性修改前后的值。如果修改了,返回一个数组,否则返回 `nil`: 190 | 191 | [source,ruby] 192 | ---- 193 | person.first_name_change # => [nil, "First Name"] 194 | person.last_name_change # => nil 195 | ---- 196 | 197 | [[validations]] 198 | ==== 数据验证 199 | 200 | `ActiveModel::Validations` 模块提供数据验证功能,这与 Active Record 中的类似。 201 | 202 | [source,ruby] 203 | ---- 204 | class Person 205 | include ActiveModel::Validations 206 | 207 | attr_accessor :name, :email, :token 208 | 209 | validates :name, presence: true 210 | validates_format_of :email, with: /\A([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})\z/i 211 | validates! :token, presence: true 212 | end 213 | 214 | person = Person.new 215 | person.token = "2b1f325" 216 | person.valid? # => false 217 | person.name = 'vishnu' 218 | person.email = 'me' 219 | person.valid? # => false 220 | person.email = 'me@vishnuatrai.com' 221 | person.valid? # => true 222 | person.token = nil 223 | person.valid? # => raises ActiveModel::StrictValidationFailed 224 | ---- 225 | 226 | [[naming]] 227 | ==== 命名 228 | 229 | `ActiveModel::Naming` 添加一些类方法,便于管理命名和路由。这个模块定义了 `model_name` 类方法,它使用 `ActiveSupport::Inflector` 中的一些方法定义一些存取方法。 230 | 231 | [source,ruby] 232 | ---- 233 | class Person 234 | extend ActiveModel::Naming 235 | end 236 | 237 | Person.model_name.name # => "Person" 238 | Person.model_name.singular # => "person" 239 | Person.model_name.plural # => "people" 240 | Person.model_name.element # => "person" 241 | Person.model_name.human # => "Person" 242 | Person.model_name.collection # => "people" 243 | Person.model_name.param_key # => "person" 244 | Person.model_name.i18n_key # => :person 245 | Person.model_name.route_key # => "people" 246 | Person.model_name.singular_route_key # => "person" 247 | ---- 248 | 249 | [[model]] 250 | ==== 模型 251 | 252 | `ActiveModel::Model` 模块能让一个类立即能与 Action Pack 和 Action View 集成。 253 | 254 | [source,ruby] 255 | ---- 256 | class EmailContact 257 | include ActiveModel::Model 258 | 259 | attr_accessor :name, :email, :message 260 | validates :name, :email, :message, presence: true 261 | 262 | def deliver 263 | if valid? 264 | # 发送电子邮件 265 | end 266 | end 267 | end 268 | ---- 269 | 270 | 引入 `ActiveModel::Model` 后,将获得以下功能: 271 | 272 | - 模型名称内省 273 | - 转换 274 | - 翻译 275 | - 数据验证 276 | 277 | 还能像 Active Record 对象那样使用散列指定属性,初始化对象。 278 | 279 | [source,ruby] 280 | ---- 281 | email_contact = EmailContact.new(name: 'David', 282 | email: 'david@example.com', 283 | message: 'Hello World') 284 | email_contact.name # => 'David' 285 | email_contact.email # => 'david@example.com' 286 | email_contact.valid? # => true 287 | email_contact.persisted? # => false 288 | ---- 289 | 290 | 只要一个类引入了 `ActiveModel::Model`,它就能像 Active Record 对象那样使用 `form_for`、`render` 和任何 Action View 辅助方法。 291 | 292 | [[serialization]] 293 | ==== 序列化 294 | 295 | `ActiveModel::Serialization` 模块为对象提供基本的序列化支持。你要定义一个属性散列,包含想序列化的属性。属性名必须使用字符串,不能使用符号。 296 | 297 | [source,ruby] 298 | ---- 299 | class Person 300 | include ActiveModel::Serialization 301 | 302 | attr_accessor :name 303 | 304 | def attributes 305 | {'name' => nil} 306 | end 307 | end 308 | ---- 309 | 310 | 这样就可以使用 `serializable_hash` 方法访问对象的序列化散列: 311 | 312 | [source,ruby] 313 | ---- 314 | person = Person.new 315 | person.serializable_hash # => {"name"=>nil} 316 | person.name = "Bob" 317 | person.serializable_hash # => {"name"=>"Bob"} 318 | ---- 319 | 320 | [[activemodel-serializers]] 321 | ===== `ActiveModel::Serializers` 322 | 323 | Rails 还提供了用于序列化和反序列化 JSON 的 `ActiveModel::Serializers::JSON`。这个模块自动引入前文介绍过的 `ActiveModel::Serialization` 模块。 324 | 325 | [[activemodel-serializers-json]] 326 | ====== `ActiveModel::Serializers::JSON` 327 | 328 | 若想使用 `ActiveModel::Serializers::JSON`,只需把 `ActiveModel::Serialization` 换成 `ActiveModel::Serializers::JSON`。 329 | 330 | [source,ruby] 331 | ---- 332 | class Person 333 | include ActiveModel::Serializers::JSON 334 | 335 | attr_accessor :name 336 | 337 | def attributes 338 | {'name' => nil} 339 | end 340 | end 341 | ---- 342 | 343 | `as_json` 方法与 `serializable_hash` 方法相似,用于提供模型的散列表示形式。 344 | 345 | [source,ruby] 346 | ---- 347 | person = Person.new 348 | person.as_json # => {"name"=>nil} 349 | person.name = "Bob" 350 | person.as_json # => {"name"=>"Bob"} 351 | ---- 352 | 353 | 还可以使用 JSON 字符串定义模型的属性。然后,要在类中定义 `attributes=` 方法: 354 | 355 | [source,ruby] 356 | ---- 357 | class Person 358 | include ActiveModel::Serializers::JSON 359 | 360 | attr_accessor :name 361 | 362 | def attributes=(hash) 363 | hash.each do |key, value| 364 | send("#{key}=", value) 365 | end 366 | end 367 | 368 | def attributes 369 | {'name' => nil} 370 | end 371 | end 372 | ---- 373 | 374 | 现在,可以使用 `from_json` 方法创建 `Person` 实例,并且设定属性: 375 | 376 | [source,ruby] 377 | ---- 378 | json = { name: 'Bob' }.to_json 379 | person = Person.new 380 | person.from_json(json) # => # 381 | person.name # => "Bob" 382 | ---- 383 | 384 | [[translation]] 385 | ==== 翻译 386 | 387 | `ActiveModel::Translation` 模块把对象与 Rails 国际化(i18n)框架集成起来。 388 | 389 | [source,ruby] 390 | ---- 391 | class Person 392 | extend ActiveModel::Translation 393 | end 394 | ---- 395 | 396 | 使用 `human_attribute_name` 方法可以把属性名称变成对人类友好的格式。对人类友好的格式在本地化文件中定义。 397 | 398 | - `config/locales/app.pt-BR.yml` 399 | + 400 | [source,ruby] 401 | ---- 402 | pt-BR: 403 | activemodel: 404 | attributes: 405 | person: 406 | name: 'Nome' 407 | ---- 408 | 409 | [source,ruby] 410 | ---- 411 | Person.human_attribute_name('name') # => "Nome" 412 | ---- 413 | 414 | [[lint-tests]] 415 | ==== lint 测试 416 | 417 | `ActiveModel::Lint::Tests` 模块测试对象是否符合 Active Model API。 418 | 419 | - `app/models/person.rb` 420 | + 421 | [source,ruby] 422 | ---- 423 | class Person 424 | include ActiveModel::Model 425 | end 426 | ---- 427 | 428 | - `test/models/person_test.rb` 429 | + 430 | [source,ruby] 431 | ---- 432 | require 'test_helper' 433 | 434 | class PersonTest < ActiveSupport::TestCase 435 | include ActiveModel::Lint::Tests 436 | 437 | setup do 438 | @model = Person.new 439 | end 440 | end 441 | ---- 442 | 443 | [source,sh] 444 | ---- 445 | $ rails test 446 | 447 | Run options: --seed 14596 448 | 449 | # Running: 450 | 451 | ...... 452 | 453 | Finished in 0.024899s, 240.9735 runs/s, 1204.8677 assertions/s. 454 | 455 | 6 runs, 30 assertions, 0 failures, 0 errors, 0 skips 456 | ---- 457 | 458 | 为了使用 Action Pack,对象无需实现所有 API。这个模块只是提供一种指导,以防你需要全部功能。 459 | 460 | [[securepassword]] 461 | ==== 安全密码 462 | 463 | `ActiveModel::SecurePassword` 提供安全加密密码的功能。这个模块提供了 `has_secure_password` 类方法,它定义了一个名为 `password` 的存取方法,而且有相应的数据验证。 464 | 465 | [[requirements]] 466 | ===== 要求 467 | 468 | `ActiveModel::SecurePassword` 依赖 https://github.com/codahale/bcrypt-ruby[bcrypt],因此要在 `Gemfile` 中加入这个 gem,`ActiveModel::SecurePassword` 才能正确运行。为了使用安全密码,模型中必须定义一个名为 `password_digest` 的存取方法。`has_secure_password` 类方法会为 `password` 存取方法添加下述数据验证: 469 | 470 | 1. 密码应该存在 471 | 2. 密码应该等于密码确认(前提是有密码确认) 472 | 3. 密码的最大长度为 72(`ActiveModel::SecurePassword` 依赖的 `bcrypt` 的要求) 473 | 474 | [[examples]] 475 | ===== 示例 476 | 477 | [source,ruby] 478 | ---- 479 | class Person 480 | include ActiveModel::SecurePassword 481 | has_secure_password 482 | attr_accessor :password_digest 483 | end 484 | 485 | person = Person.new 486 | 487 | # 密码为空时 488 | person.valid? # => false 489 | 490 | # 密码确认与密码不匹配时 491 | person.password = 'aditya' 492 | person.password_confirmation = 'nomatch' 493 | person.valid? # => false 494 | 495 | # 密码长度超过 72 时 496 | person.password = person.password_confirmation = 'a' * 100 497 | person.valid? # => false 498 | 499 | # 只有密码,没有密码确认 500 | person.password = 'aditya' 501 | person.valid? # => true 502 | 503 | # 所有数据验证都通过时 504 | person.password = person.password_confirmation = 'aditya' 505 | person.valid? # => true 506 | ---- 507 | -------------------------------------------------------------------------------- /manuscript/active_record_basics.adoc: -------------------------------------------------------------------------------- 1 | :sample: 2 | 3 | [[active-record-basics]] 4 | == Active Record 基础 5 | 6 | // 安道翻译 7 | 8 | [.chapter-abstract] 9 | -- 10 | 本文简介 Active Record。 11 | 12 | 读完本文后,您将学到: 13 | 14 | * 对象关系映射(Object Relational Mapping,ORM)和 Active Record 是什么,以及如何在 Rails 中使用; 15 | * Active Record 在 MVC 中的作用; 16 | * 如何使用 Active Record 模型处理保存在关系型数据库中的数据; 17 | * Active Record 模式(schema)的命名约定; 18 | * 数据库迁移,数据验证和回调。 19 | -- 20 | 21 | [[what-is-active-record]] 22 | === Active Record 是什么? 23 | 24 | Active Record 是 http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller[MVC] 中的 M(模型),负责处理数据和业务逻辑。Active Record 负责创建和使用需要持久存入数据库中的数据。Active Record 实现了 Active Record 模式,是一种对象关系映射系统。 25 | 26 | [[the-active-record-pattern]] 27 | ==== Active Record 模式 28 | 29 | http://www.martinfowler.com/eaaCatalog/activeRecord.html[Active Record 模式]出自 Martin Fowler 写的《link:https://book.douban.com/subject/4826290/[企业应用架构模式]》一书。在 Active Record 模式中,对象中既有持久存储的数据,也有针对数据的操作。Active Record 模式把数据存取逻辑作为对象的一部分,处理对象的用户知道如何把数据写入数据库,还知道如何从数据库中读出数据。 30 | 31 | [[object-relational-mapping]] 32 | ==== 对象关系映射 33 | 34 | 对象关系映射(ORM)是一种技术手段,把应用中的对象和关系型数据库中的数据表连接起来。使用 ORM,应用中对象的属性和对象之间的关系可以通过一种简单的方法从数据库中获取,无需直接编写 SQL 语句,也不过度依赖特定的数据库种类。 35 | 36 | [[active-record-as-an-orm-framework]] 37 | ==== 用作 ORM 框架的 Active Record 38 | 39 | Active Record 提供了很多功能,其中最重要的几个如下: 40 | 41 | * 表示模型和其中的数据; 42 | * 表示模型之间的关系; 43 | * 通过相关联的模型表示继承层次结构; 44 | * 持久存入数据库之前,验证模型; 45 | * 以面向对象的方式处理数据库操作。 46 | 47 | [[convention-over-configuration-in-active-record]] 48 | === Active Record 中的“多约定少配置”原则 49 | 50 | 使用其他编程语言或框架开发应用时,可能必须要编写很多配置代码。大多数 ORM 框架都是这样。但是,如果遵循 Rails 的约定,创建 Active Record 模型时不用做多少配置(有时甚至完全不用配置)。Rails 的理念是,如果大多数情况下都要使用相同的方式配置应用,那么就应该把这定为默认的方式。所以,只有约定无法满足要求时,才要额外配置。 51 | 52 | [[naming-conventions]] 53 | ==== 命名约定 54 | 55 | 默认情况下,Active Record 使用一些命名约定,查找模型和数据库表之间的映射关系。Rails 把模型的类名转换成复数,然后查找对应的数据表。例如,模型类名为 `Book`,数据表就是 `books`。Rails 提供的单复数转换功能很强大,常见和不常见的转换方式都能处理。如果类名由多个单词组成,应该按照 Ruby 的约定,使用驼峰式命名法,这时对应的数据库表将使用下划线分隔各单词。因此: 56 | 57 | * 数据库表名:复数,下划线分隔单词(例如 `book_clubs`) 58 | * 模型类名:单数,每个单词的首字母大写(例如 `BookClub`) 59 | 60 | |=== 61 | | 模型/类 | 表/模式 62 | 63 | | `Article` | `articles` 64 | | `LineItem` | `line_items` 65 | | `Deer` | `deers` 66 | | `Mouse` | `mice` 67 | | `Person` | `people` 68 | |=== 69 | 70 | [[schema-conventions]] 71 | ==== 模式约定 72 | 73 | 根据字段的作用不同,Active Record 对数据库表中的字段命名也做了相应的约定: 74 | 75 | - *外键*:使用 `singularized_table_name_id` 形式命名,例如 `item_id`,`order_id`。创建模型关联后,Active Record 会查找这个字段; 76 | - *主键*:默认情况下,Active Record 使用整数字段 `id` 作为表的主键。使用 <>创建数据库表时,会自动创建这个字段; 77 | 78 | 还有一些可选的字段,能为 Active Record 实例添加更多的功能: 79 | 80 | * `created_at`:创建记录时,自动设为当前的日期和时间; 81 | * `updated_at`:更新记录时,自动设为当前的日期和时间; 82 | * `lock_version`:在模型中添加link:http://api.rubyonrails.org/classes/ActiveRecord/Locking.html[乐观锁]; 83 | * `type`:让模型使用link:http://api.rubyonrails.org/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance[单表继承]; 84 | * `(association_name)_type`:存储<>的类型; 85 | * `(table_name)_count`:缓存所关联对象的数量。比如说,一个 `Article` 有多个 `Comment`,那么 `comments_count` 列存储各篇文章现有的评论数量; 86 | 87 | [NOTE] 88 | ==== 89 | 虽然这些字段是可选的,但在 Active Record 中是被保留的。如果想使用相应的功能,就不要把这些保留字段用作其他用途。例如,`type` 这个保留字段是用来指定数据库表使用单表继承(Single Table Inheritance,STI)的。如果不用单表继承,请使用其他的名称,例如“context”,这也能表明数据的作用。 90 | ==== 91 | 92 | [[creating-active-record-models]] 93 | === 创建 Active Record 模型 94 | 95 | 创建 Active Record 模型的过程很简单,只要继承 `ApplicationRecord` 类就行了: 96 | 97 | [source,ruby] 98 | ---- 99 | class Product < ApplicationRecord 100 | end 101 | ---- 102 | 103 | 上面的代码会创建 `Product` 模型,对应于数据库中的 `products` 表。同时,`products` 表中的字段也映射到 `Product` 模型实例的属性上。假如 `products` 表由下面的 SQL 语句创建: 104 | 105 | [source,sql] 106 | ---- 107 | CREATE TABLE products ( 108 | id int(11) NOT NULL auto_increment, 109 | name varchar(255), 110 | PRIMARY KEY (id) 111 | ); 112 | ---- 113 | 114 | 按照这样的数据表结构,可以编写下面的代码: 115 | 116 | [source,ruby] 117 | ---- 118 | p = Product.new 119 | p.name = "Some Book" 120 | puts p.name # "Some Book" 121 | ---- 122 | 123 | [[overriding-the-naming-conventions]] 124 | === 覆盖命名约定 125 | 126 | 如果想使用其他的命名约定,或者在 Rails 应用中使用即有的数据库可以吗?没问题,默认的约定能轻易覆盖。 127 | 128 | `ApplicationRecord` 继承自 `ActiveRecord::Base`,后者定义了一系列有用的方法。使用 `ActiveRecord::Base.table_name=` 方法可以指定要使用的表名: 129 | 130 | [source,ruby] 131 | ---- 132 | class Product < ApplicationRecord 133 | self.table_name = "my_products" 134 | end 135 | ---- 136 | 137 | 如果这么做,还要调用 `set_fixture_class` 方法,手动指定固件(my_products.yml)的类名: 138 | 139 | [source,ruby] 140 | ---- 141 | class ProductTest < ActiveSupport::TestCase 142 | set_fixture_class my_products: Product 143 | fixtures :my_products 144 | ... 145 | end 146 | ---- 147 | 148 | 还可以使用 `ActiveRecord::Base.primary_key=` 方法指定表的主键: 149 | 150 | [source,ruby] 151 | ---- 152 | class Product < ApplicationRecord 153 | self.primary_key = "product_id" 154 | end 155 | ---- 156 | 157 | [[crud-reading-and-writing-data]] 158 | === CRUD:读写数据 159 | 160 | CURD 是四种数据操作的简称:C 表示创建,R 表示读取,U 表示更新,D 表示删除。Active Record 自动创建了处理数据表中数据的方法。 161 | 162 | [[create]] 163 | ==== 创建 164 | 165 | Active Record 对象可以使用散列创建,在块中创建,或者创建后手动设置属性。`new` 方法创建一个新对象,`create` 方法创建新对象,并将其存入数据库。 166 | 167 | 例如,`User` 模型中有两个属性,`name` 和 `occupation`。调用 `create` 方法会创建一个新记录,并将其存入数据库: 168 | 169 | [source,ruby] 170 | ---- 171 | user = User.create(name: "David", occupation: "Code Artist") 172 | ---- 173 | 174 | `new` 方法实例化一个新对象,但不保存: 175 | 176 | [source,ruby] 177 | ---- 178 | user = User.new 179 | user.name = "David" 180 | user.occupation = "Code Artist" 181 | ---- 182 | 183 | 调用 `user.save` 可以把记录存入数据库。 184 | 185 | 最后,如果在 `create` 和 `new` 方法中使用块,会把新创建的对象拉入块中,初始化对象: 186 | 187 | [source,ruby] 188 | ---- 189 | user = User.new do |u| 190 | u.name = "David" 191 | u.occupation = "Code Artist" 192 | end 193 | ---- 194 | 195 | [[read]] 196 | ==== 读取 197 | 198 | Active Record 为读取数据库中的数据提供了丰富的 API。下面举例说明。 199 | 200 | [source,ruby] 201 | ---- 202 | # 返回所有用户组成的集合 203 | users = User.all 204 | ---- 205 | 206 | [source,ruby] 207 | ---- 208 | # 返回第一个用户 209 | user = User.first 210 | ---- 211 | 212 | [source,ruby] 213 | ---- 214 | # 返回第一个名为 David 的用户 215 | david = User.find_by(name: 'David') 216 | ---- 217 | 218 | [source,ruby] 219 | ---- 220 | # 查找所有名为 David,职业为 Code Artists 的用户,而且按照 created_at 反向排列 221 | users = User.where(name: 'David', occupation: 'Code Artist').order(created_at: :desc) 222 | ---- 223 | 224 | <>会详细介绍查询 Active Record 模型的方法。 225 | 226 | [[update]] 227 | ==== 更新 228 | 229 | 检索到 Active Record 对象后,可以修改其属性,然后再将其存入数据库。 230 | 231 | [source,ruby] 232 | ---- 233 | user = User.find_by(name: 'David') 234 | user.name = 'Dave' 235 | user.save 236 | ---- 237 | 238 | 还有种使用散列的简写方式,指定属性名和属性值,例如: 239 | 240 | [source,ruby] 241 | ---- 242 | user = User.find_by(name: 'David') 243 | user.update(name: 'Dave') 244 | ---- 245 | 246 | 一次更新多个属性时使用这种方法最方便。如果想批量更新多个记录,可以使用类方法 `update_all`: 247 | 248 | [source,ruby] 249 | ---- 250 | User.update_all "max_login_attempts = 3, must_change_password = 'true'" 251 | ---- 252 | 253 | [[delete]] 254 | ==== 删除 255 | 256 | 类似地,检索到 Active Record 对象后还可以将其销毁,从数据库中删除。 257 | 258 | [source,ruby] 259 | ---- 260 | user = User.find_by(name: 'David') 261 | user.destroy 262 | ---- 263 | 264 | [[validations]] 265 | === 数据验证 266 | 267 | 在存入数据库之前,Active Record 还可以验证模型。模型验证有很多方法,可以检查属性值是否不为空,是否是唯一的、没有在数据库中出现过,等等。 268 | 269 | 把数据存入数据库之前进行验证是十分重要的步骤,所以调用 `save` 和 `update` 方法时会做数据验证。验证失败时返回 `false`,此时不会对数据库做任何操作。这两个方法都有对应的爆炸方法(`save!` 和 `update!`)。爆炸方法要严格一些,如果验证失败,抛出 `ActiveRecord::RecordInvalid` 异常。下面举个简单的例子: 270 | 271 | [source,ruby] 272 | ---- 273 | class User < ApplicationRecord 274 | validates :name, presence: true 275 | end 276 | 277 | user = User.new 278 | user.save # => false 279 | user.save! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank 280 | ---- 281 | 282 | <>会详细介绍数据验证。 283 | 284 | [[callbacks]] 285 | === 回调 286 | 287 | Active Record 回调用于在模型生命周期的特定事件上绑定代码,相应的事件发生时,执行绑定的代码。例如创建新纪录时、更新记录时、删除记录时,等等。<>会详细介绍回调。 288 | 289 | [[migrations]] 290 | === 迁移 291 | 292 | Rails 提供了一个 DSL(Domain-Specific Language)用来处理数据库模式,叫做“迁移”。迁移的代码存储在特定的文件中,通过 `rails` 命令执行,可以用在 Active Record 支持的所有数据库上。下面这个迁移新建一个表: 293 | 294 | [source,ruby] 295 | ---- 296 | class CreatePublications < ActiveRecord::Migration[5.0] 297 | def change 298 | create_table :publications do |t| 299 | t.string :title 300 | t.text :description 301 | t.references :publication_type 302 | t.integer :publisher_id 303 | t.string :publisher_type 304 | t.boolean :single_issue 305 | 306 | t.timestamps 307 | end 308 | add_index :publications, :publication_type_id 309 | end 310 | end 311 | ---- 312 | 313 | Rails 会跟踪哪些迁移已经应用到数据库上,还提供了回滚功能。为了创建表,要执行 `rails db:migrate` 命令。如果想回滚,则执行 `rails db:rollback` 命令。 314 | 315 | 注意,上面的代码与具体的数据库种类无关,可用于 MySQL、PostgreSQL、Oracle 等数据库。关于迁移的详细介绍,参阅<>。 316 | -------------------------------------------------------------------------------- /manuscript/active_record_callbacks.adoc: -------------------------------------------------------------------------------- 1 | [[active-record-callbacks]] 2 | == Active Record 回调 3 | 4 | // chinakr 翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 本文介绍如何介入 Active Record 对象的生命周期。 9 | 10 | 读完本文后,您将学到: 11 | 12 | * Active Record 对象的生命周期; 13 | * 如何创建用于响应对象生命周期内事件的回调方法; 14 | * 如何把常用的回调封装到特殊的类中。 15 | -- 16 | 17 | [[the-object-life-cycle]] 18 | === 对象的生命周期 19 | 20 | 在 Rails 应用正常运作期间,对象可以被创建、更新或删除。Active Record 为对象的生命周期提供了钩子,使我们可以控制应用及其数据。 21 | 22 | 回调使我们可以在对象状态更改之前或之后触发逻辑。 23 | 24 | [[callbacks-overview]] 25 | === 回调概述 26 | 27 | 回调是在对象生命周期的某些时刻被调用的方法。通过回调,我们可以编写在创建、保存、更新、删除、验证或从数据库中加载 Active Record 对象时执行的代码。 28 | 29 | [[callback-registration]] 30 | ==== 注册回调 31 | 32 | 回调在使用之前需要注册。我们可以先把回调定义为普通方法,然后使用宏式类方法把这些普通方法注册为回调: 33 | 34 | [source,ruby] 35 | ---- 36 | class User < ApplicationRecord 37 | validates :login, :email, presence: true 38 | 39 | before_validation :ensure_login_has_a_value 40 | 41 | private 42 | def ensure_login_has_a_value 43 | if login.nil? 44 | self.login = email unless email.blank? 45 | end 46 | end 47 | end 48 | ---- 49 | 50 | 宏式类方法也接受块。如果块中的代码短到可以放在一行里,可以考虑使用这种编程风格: 51 | 52 | [source,ruby] 53 | ---- 54 | class User < ApplicationRecord 55 | validates :login, :email, presence: true 56 | 57 | before_create do 58 | self.name = login.capitalize if name.blank? 59 | end 60 | end 61 | ---- 62 | 63 | 回调也可以注册为仅被某些生命周期事件触发: 64 | 65 | [source,ruby] 66 | ---- 67 | class User < ApplicationRecord 68 | before_validation :normalize_name, on: :create 69 | 70 | # :on 选项的值也可以是数组 71 | after_validation :set_location, on: [ :create, :update ] 72 | 73 | private 74 | def normalize_name 75 | self.name = name.downcase.titleize 76 | end 77 | 78 | def set_location 79 | self.location = LocationService.query(self) 80 | end 81 | end 82 | ---- 83 | 84 | 通常应该把回调定义为私有方法。如果把回调定义为公共方法,就可以从模型外部调用回调,这样做违反了对象封装原则。 85 | 86 | [[available-callbacks]] 87 | === 可用的回调 88 | 89 | 下面按照回调在 Rails 应用正常运作期间被调用的顺序,列出所有可用的 Active Record 回调。 90 | 91 | [[creating-an-object]] 92 | ==== 创建对象 93 | 94 | * `before_validation` 95 | * `after_validation` 96 | * `before_save` 97 | * `around_save` 98 | * `before_create` 99 | * `around_create` 100 | * `after_create` 101 | * `after_save` 102 | * `after_commit/after_rollback` 103 | 104 | [[updating-an-object]] 105 | ==== 更新对象 106 | 107 | * `before_validation` 108 | * `after_validation` 109 | * `before_save` 110 | * `around_save` 111 | * `before_update` 112 | * `around_update` 113 | * `after_update` 114 | * `after_save` 115 | * `after_commit/after_rollback` 116 | 117 | [[destroying-an-object]] 118 | ==== 删除对象 119 | 120 | * `before_destroy` 121 | * `around_destroy` 122 | * `after_destroy` 123 | * `after_commit/after_rollback` 124 | 125 | WARNING: 无论按什么顺序注册回调,在创建和更新对象时,`after_save` 回调总是在更明确的 `after_create` 和 `after_update` 回调之后被调用。 126 | 127 | [[after-initialize-and-after-find]] 128 | ==== `after_initialize` 和 `after_find` 回调 129 | 130 | 当 Active Record 对象被实例化时,不管是通过直接使用 `new` 方法还是从数据库加载记录,都会调用 `after_initialize` 回调。使用这个回调可以避免直接覆盖 Active Record 的 `initialize` 方法。 131 | 132 | 当 Active Record 从数据库中加载记录时,会调用 `after_find` 回调。如果同时定义了 `after_initialize` 和 `after_find` 回调,会先调用 `after_find` 回调。 133 | 134 | `after_initialize` 和 `after_find` 回调没有对应的 `before_*` 回调,这两个回调的注册方式和其他 Active Record 回调一样。 135 | 136 | [source,ruby] 137 | ---- 138 | class User < ApplicationRecord 139 | after_initialize do |user| 140 | puts "You have initialized an object!" 141 | end 142 | 143 | after_find do |user| 144 | puts "You have found an object!" 145 | end 146 | end 147 | ---- 148 | 149 | [source,irb] 150 | ---- 151 | >> User.new 152 | You have initialized an object! 153 | => # 154 | 155 | >> User.first 156 | You have found an object! 157 | You have initialized an object! 158 | => # 159 | ---- 160 | 161 | [[after-touch]] 162 | ==== `after_touch` 回调 163 | 164 | 当我们在 Active Record 对象上调用 `touch` 方法时,会调用 `after_touch` 回调。 165 | 166 | [source,ruby] 167 | ---- 168 | class User < ApplicationRecord 169 | after_touch do |user| 170 | puts "You have touched an object" 171 | end 172 | end 173 | ---- 174 | 175 | [source,irb] 176 | ---- 177 | >> u = User.create(name: 'Kuldeep') 178 | => # 179 | 180 | >> u.touch 181 | You have touched an object 182 | => true 183 | ---- 184 | 185 | `after_touch` 回调可以和 `belongs_to` 一起使用: 186 | 187 | [source,ruby] 188 | ---- 189 | class Employee < ApplicationRecord 190 | belongs_to :company, touch: true 191 | after_touch do 192 | puts 'An Employee was touched' 193 | end 194 | end 195 | 196 | class Company < ApplicationRecord 197 | has_many :employees 198 | after_touch :log_when_employees_or_company_touched 199 | 200 | private 201 | def log_when_employees_or_company_touched 202 | puts 'Employee/Company was touched' 203 | end 204 | end 205 | ---- 206 | 207 | [source,irb] 208 | ---- 209 | >> @employee = Employee.last 210 | => # 211 | 212 | # triggers @employee.company.touch 213 | >> @employee.touch 214 | Employee/Company was touched 215 | An Employee was touched 216 | => true 217 | ---- 218 | 219 | [[running-callbacks]] 220 | === 调用回调 221 | 222 | 下面这些方法会触发回调: 223 | 224 | * `create` 225 | * `create!` 226 | * `decrement!` 227 | * `destroy` 228 | * `destroy!` 229 | * `destroy_all` 230 | * `increment!` 231 | * `save` 232 | * `save!` 233 | * `save(validate: false)` 234 | * `toggle!` 235 | * `update_attribute` 236 | * `update` 237 | * `update!` 238 | * `valid?` 239 | 240 | 此外,下面这些查找方法会触发 `after_find` 回调: 241 | 242 | * `all` 243 | * `first` 244 | * `find` 245 | * `find_by` 246 | * `find_by_*` 247 | * `find_by_*!` 248 | * `find_by_sql` 249 | * `last` 250 | 251 | 每次初始化类的新对象时都会触发 `after_initialize` 回调。 252 | 253 | NOTE: `find_by_*` 和 `find_by_*!` 方法是为每个属性自动生成的动态查找方法。关于动态查找方法的更多介绍,请参阅 <>。 254 | 255 | [[skipping-callbacks]] 256 | === 跳过回调 257 | 258 | 和验证一样,我们可以跳过回调。使用下面这些方法可以跳过回调: 259 | 260 | * `decrement` 261 | * `decrement_counter` 262 | * `delete` 263 | * `delete_all` 264 | * `increment` 265 | * `increment_counter` 266 | * `toggle` 267 | * `touch` 268 | * `update_column` 269 | * `update_columns` 270 | * `update_all` 271 | * `update_counters` 272 | 273 | 请慎重地使用这些方法,因为有些回调包含了重要的业务规则和应用逻辑,在不了解潜在影响的情况下就跳过回调,可能导致无效数据。 274 | 275 | [[halting-execution]] 276 | === 停止执行 277 | 278 | 回调在模型中注册后,将被加入队列等待执行。这个队列包含了所有模型的验证、已注册的回调和将要执行的数据库操作。 279 | 280 | 整个回调链包装在一个事务中。只要有回调抛出异常,回调链随即停止,并且发出 `ROLLBACK` 消息。如果想故意停止回调链,可以这么做: 281 | 282 | [source,ruby] 283 | ---- 284 | throw :abort 285 | ---- 286 | 287 | WARNING: 当回调链停止后,Rails 会重新抛出除了 `ActiveRecord::Rollback` 和 `ActiveRecord::RecordInvalid` 之外的其他异常。这可能导致那些预期 `save` 和 `update_attributes` 等方法(通常返回 `true` 或 `false` )不会引发异常的代码出错。 288 | 289 | [[relational-callbacks]] 290 | === 关联回调 291 | 292 | 回调不仅可以在模型关联中使用,还可以通过模型关联定义。假设有一个用户在博客中发表了多篇文章,现在我们要删除这个用户,那么这个用户的所有文章也应该删除,为此我们通过 `Article` 模型和 `User` 模型的关联来给 `User` 模型添加一个 `after_destroy` 回调: 293 | 294 | [source,ruby] 295 | ---- 296 | class User < ApplicationRecord 297 | has_many :articles, dependent: :destroy 298 | end 299 | 300 | class Article < ApplicationRecord 301 | after_destroy :log_destroy_action 302 | 303 | def log_destroy_action 304 | puts 'Article destroyed' 305 | end 306 | end 307 | ---- 308 | 309 | [source,irb] 310 | ---- 311 | >> user = User.first 312 | => # 313 | >> user.articles.create! 314 | => #
315 | >> user.destroy 316 | Article destroyed 317 | => # 318 | ---- 319 | 320 | [[conditional-callbacks]] 321 | === 条件回调 322 | 323 | 和验证一样,我们可以在满足指定条件时再调用回调方法。为此,我们可以使用 `:if` 和 `:unless` 选项,选项的值可以是符号、`Proc` 或数组。要想指定在哪些条件下调用回调,可以使用 `:if` 选项。要想指定在哪些条件下不调用回调,可以使用 `:unless` 选项。 324 | 325 | [[using-if-and-unless-with-a-symbol]] 326 | ==== 使用符号作为 `:if` 和 `:unless` 选项的值 327 | 328 | 可以使用符号作为 `:if` 和 `:unless` 选项的值,这个符号用于表示先于回调调用的断言方法。当使用 `:if` 选项时,如果断言方法返回 `false` 就不会调用回调;当使用 `:unless` 选项时,如果断言方法返回 `true` 就不会调用回调。使用符号作为 `:if` 和 `:unless` 选项的值是最常见的方式。在使用这种方式注册回调时,我们可以同时使用几个不同的断言,用于检查是否应该调用回调。 329 | 330 | [source,ruby] 331 | ---- 332 | class Order < ApplicationRecord 333 | before_save :normalize_card_number, if: :paid_with_card? 334 | end 335 | ---- 336 | 337 | [[using-if-and-unless-with-a-proc]] 338 | ==== 使用 Proc 作为 `:if` 和 `:unless` 选项的值 339 | 340 | 最后,可以使用 Proc 作为 `:if` 和 `:unless` 选项的值。在验证方法非常短时最适合使用这种方式,这类验证方法通常只有一行代码: 341 | 342 | [source,ruby] 343 | ---- 344 | class Order < ApplicationRecord 345 | before_save :normalize_card_number, 346 | if: Proc.new { |order| order.paid_with_card? } 347 | end 348 | ---- 349 | 350 | [[multiple-conditions-for-callbacks]] 351 | ==== 在条件回调中使用多个条件 352 | 353 | 在编写条件回调时,我们可以在同一个回调声明中混合使用 `:if` 和 `:unless` 选项: 354 | 355 | [source,ruby] 356 | ---- 357 | class Comment < ApplicationRecord 358 | after_create :send_email_to_author, if: :author_wants_emails?, 359 | unless: Proc.new { |comment| comment.article.ignore_comments? } 360 | end 361 | ---- 362 | 363 | [[callback-classes]] 364 | === 回调类 365 | 366 | 有时需要在其他模型中重用已有的回调方法,为了解决这个问题,Active Record 允许我们用类来封装回调方法。有了回调类,回调方法的重用就变得非常容易。 367 | 368 | 在下面的例子中,我们为 `PictureFile` 模型创建了 `PictureFileCallbacks` 回调类,在这个回调类中包含了 `after_destroy` 回调方法: 369 | 370 | [source,ruby] 371 | ---- 372 | class PictureFileCallbacks 373 | def after_destroy(picture_file) 374 | if File.exist?(picture_file.filepath) 375 | File.delete(picture_file.filepath) 376 | end 377 | end 378 | end 379 | ---- 380 | 381 | 在上面的代码中我们可以看到,当在回调类中声明回调方法时,回调方法接受模型对象作为参数。回调类定义之后就可以在模型中使用了: 382 | 383 | [source,ruby] 384 | ---- 385 | class PictureFile < ApplicationRecord 386 | after_destroy PictureFileCallbacks.new 387 | end 388 | ---- 389 | 390 | 请注意,上面我们把回调声明为实例方法,因此需要实例化新的 `PictureFileCallbacks` 对象。当回调想要使用实例化的对象的状态时,这种声明方式特别有用。尽管如此,一般我们会把回调声明为类方法: 391 | 392 | [source,ruby] 393 | ---- 394 | class PictureFileCallbacks 395 | def self.after_destroy(picture_file) 396 | if File.exist?(picture_file.filepath) 397 | File.delete(picture_file.filepath) 398 | end 399 | end 400 | end 401 | ---- 402 | 403 | 如果把回调声明为类方法,就不需要实例化新的 `PictureFileCallbacks` 对象。 404 | 405 | [source,ruby] 406 | ---- 407 | class PictureFile < ApplicationRecord 408 | after_destroy PictureFileCallbacks 409 | end 410 | ---- 411 | 412 | 我们可以根据需要在回调类中声明任意多个回调。 413 | 414 | [[transaction-callbacks]] 415 | === 事务回调 416 | 417 | `after_commit` 和 `after_rollback` 这两个回调会在数据库事务完成时触发。它们和 `after_save` 回调非常相似,区别在于它们在数据库变更已经提交或回滚后才会执行,常用于 Active Record 模型需要和数据库事务之外的系统交互的场景。 418 | 419 | 例如,在前面的例子中,`PictureFile` 模型中的记录删除后,还要删除相应的文件。如果 `after_destroy` 回调执行后应用引发异常,事务就会回滚,文件会被删除,模型会保持不一致的状态。例如,假设在下面的代码中,`picture_file_2` 对象是无效的,那么调用 `save!` 方法会引发错误: 420 | 421 | [source,ruby] 422 | ---- 423 | PictureFile.transaction do 424 | picture_file_1.destroy 425 | picture_file_2.save! 426 | end 427 | ---- 428 | 429 | 通过使用 `after_commit` 回调,我们可以解决这个问题: 430 | 431 | [source,ruby] 432 | ---- 433 | class PictureFile < ApplicationRecord 434 | after_commit :delete_picture_file_from_disk, on: :destroy 435 | 436 | def delete_picture_file_from_disk 437 | if File.exist?(filepath) 438 | File.delete(filepath) 439 | end 440 | end 441 | end 442 | ---- 443 | 444 | NOTE: `:on` 选项说明什么时候触发回调。如果不提供 `:on` 选项,那么每个动作都会触发回调。 445 | 446 | 由于只在执行创建、更新或删除动作时触发 `after_commit` 回调是很常见的,这些操作都拥有别名: 447 | 448 | * `after_create_commit` 449 | * `after_update_commit` 450 | * `after_destroy_commit` 451 | 452 | [source,ruby] 453 | ---- 454 | class PictureFile < ApplicationRecord 455 | after_destroy_commit :delete_picture_file_from_disk 456 | 457 | def delete_picture_file_from_disk 458 | if File.exist?(filepath) 459 | File.delete(filepath) 460 | end 461 | end 462 | end 463 | ---- 464 | 465 | WARNING: 在事务中创建、更新或删除模型时会调用 `after_commit` 和 `after_rollback` 回调。然而,如果其中有一个回调引发异常,异常会向上冒泡,后续 `after_commit` 和 `after_rollback` 回调不再执行。因此,如果回调代码可能引发异常,就需要在回调中救援并进行适当处理,以便让其他回调继续运行。 466 | -------------------------------------------------------------------------------- /manuscript/active_support_instrumentation.adoc: -------------------------------------------------------------------------------- 1 | [[active-support-instrumentation]] 2 | == Active Support 监测程序 3 | 4 | // 安道翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | Active Support 是 Rails 核心的一部分,提供 Ruby 语言扩展、实用方法等。其中包括一份监测 API,在应用中可以用它测度 Ruby 代码(如 Rails 应用或框架自身)中的特定操作。不过,这个 API 不限于只能在 Rails 中使用,如果愿意,也可以在其他 Ruby 脚本中使用。 9 | 10 | 本文教你如何使用 Active Support 中的监测 API 测度 Rails 和其他 Ruby 代码中的事件。 11 | 12 | 读完本文后,您将学到: 13 | 14 | - 使用监测程序能做什么; 15 | - Rails 框架为监测提供的钩子; 16 | - 订阅钩子; 17 | - 自定义监测点。 18 | -- 19 | 20 | NOTE: 本文原文尚未完工! 21 | 22 | [[introduction-to-instrumentation]] 23 | === 监测程序简介 24 | 25 | Active Support 提供的监测 API 允许开发者提供钩子,供其他开发者订阅。在 Rails 框架中,有很多。通过这个 API,开发者可以选择在应用或其他 Ruby 代码中发生特定事件时接收通知。 26 | 27 | 例如,Active Record 中有一个钩子,在每次使用 SQL 查询数据库时调用。开发者可以订阅这个钩子,记录特定操作执行的查询次数。还有一个钩子在控制器的动作执行前后调用,记录动作的执行时间。 28 | 29 | 在应用中甚至还可以自己创建事件,然后订阅。 30 | 31 | [[rails-framework-hooks]] 32 | === Rails 框架中的钩子 33 | 34 | Ruby on Rails 框架为很多常见的事件提供了钩子。下面详述。 35 | 36 | [[action-controller]] 37 | === Action Controller 38 | 39 | [[write-fragment-action-controller]] 40 | ==== write_fragment.action_controller 41 | 42 | |=== 43 | | 键 | 值 44 | 45 | | `:key` | 完整的键 46 | |=== 47 | 48 | [source,ruby] 49 | ---- 50 | { 51 | key: 'posts/1-dashboard-view' 52 | } 53 | ---- 54 | 55 | [[read-fragment-action-controller]] 56 | ==== read_fragment.action_controller 57 | 58 | |=== 59 | | 键 | 值 60 | 61 | | `:key` | 完整的键 62 | |=== 63 | 64 | [source,ruby] 65 | ---- 66 | { 67 | key: 'posts/1-dashboard-view' 68 | } 69 | ---- 70 | 71 | [[expire-fragment-action-controller]] 72 | ==== expire_fragment.action_controller 73 | 74 | |=== 75 | | 键 | 值 76 | 77 | | `:key` | 完整的键 78 | |=== 79 | 80 | [source,ruby] 81 | ---- 82 | { 83 | key: 'posts/1-dashboard-view' 84 | } 85 | ---- 86 | 87 | [[exist-fragment-questionmark-action-controller]] 88 | ==== exist_fragment?.action_controller 89 | 90 | |=== 91 | | 键 | 值 92 | 93 | | `:key` | 完整的键 94 | |=== 95 | 96 | [source,ruby] 97 | ---- 98 | { 99 | key: 'posts/1-dashboard-view' 100 | } 101 | ---- 102 | 103 | [[write-page-action-controller]] 104 | ==== write_page.action_controller 105 | 106 | |=== 107 | | 键 | 值 108 | 109 | | `:path` | 完整的路径 110 | |=== 111 | 112 | [source,ruby] 113 | ---- 114 | { 115 | path: '/users/1' 116 | } 117 | ---- 118 | 119 | [[expire-page-action-controller]] 120 | ==== expire_page.action_controller 121 | 122 | |=== 123 | | 键 | 值 124 | 125 | | `:path` | 完整的路径 126 | |=== 127 | 128 | [source,ruby] 129 | ---- 130 | { 131 | path: '/users/1' 132 | } 133 | ---- 134 | 135 | [[start-processing-action-controller]] 136 | ==== start_processing.action_controller 137 | 138 | |=== 139 | | 键 | 值 140 | 141 | | `:controller` | 控制器名 142 | | `:action` | 动作名 143 | | `:params` | 请求参数散列,不过滤 144 | | `:headers` | 请求首部 145 | | `:format` | html、js、json、xml 等 146 | | `:method` | HTTP 请求方法 147 | | `:path` | 请求路径 148 | |=== 149 | 150 | [source,ruby] 151 | ---- 152 | { 153 | controller: "PostsController", 154 | action: "new", 155 | params: { "action" => "new", "controller" => "posts" }, 156 | headers: #, 157 | format: :html, 158 | method: "GET", 159 | path: "/posts/new" 160 | } 161 | ---- 162 | 163 | [[process-action-action-controller]] 164 | ==== process_action.action_controller 165 | 166 | |=== 167 | | 键 | 值 168 | 169 | | `:controller` | 控制器名 170 | | `:action` | 动作名 171 | | `:params` | 请求参数散列,不过滤 172 | | `:headers` | 请求首部 173 | | `:format` | html、js、json、xml 等 174 | | `:method` | HTTP 请求方法 175 | | `:path` | 请求路径 176 | | `:status` | HTTP 状态码 177 | | `:view_runtime` | 花在视图上的时间量(ms) 178 | | `:db_runtime` | 执行数据库查询的时间量(ms) 179 | |=== 180 | 181 | [source,ruby] 182 | ---- 183 | { 184 | controller: "PostsController", 185 | action: "index", 186 | params: {"action" => "index", "controller" => "posts"}, 187 | headers: #, 188 | format: :html, 189 | method: "GET", 190 | path: "/posts", 191 | status: 200, 192 | view_runtime: 46.848, 193 | db_runtime: 0.157 194 | } 195 | ---- 196 | 197 | [[send-file-action-controller]] 198 | ==== send_file.action_controller 199 | 200 | |=== 201 | | 键 | 值 202 | 203 | | `:path` | 文件的完整路径 204 | |=== 205 | 206 | [TIP] 207 | ==== 208 | 调用方可以添加额外的键。 209 | ==== 210 | 211 | [[send-data-action-controller]] 212 | ==== send_data.action_controller 213 | 214 | `ActionController` 在载荷(payload)中没有任何特定的信息。所有选项都传到载荷中。 215 | 216 | [[redirect-to-action-controller]] 217 | ==== redirect_to.action_controller 218 | 219 | |=== 220 | | 键 | 值 221 | 222 | | `:status` | HTTP 响应码 223 | | `:location` | 重定向的 URL 224 | |=== 225 | 226 | [source,ruby] 227 | ---- 228 | { 229 | status: 302, 230 | location: "http://localhost:3000/posts/new" 231 | } 232 | ---- 233 | 234 | [[halted-callback-action-controller]] 235 | ==== halted_callback.action_controller 236 | 237 | |=== 238 | | 键 | 值 239 | 240 | | `:filter` | 过滤暂停的动作 241 | |=== 242 | 243 | [source,ruby] 244 | ---- 245 | { 246 | filter: ":halting_filter" 247 | } 248 | ---- 249 | 250 | [[action-view]] 251 | === Action View 252 | 253 | [[render-template-action-view]] 254 | ==== render_template.action_view 255 | 256 | |=== 257 | | 键 | 值 258 | 259 | | `:identifier` | 模板的完整路径 260 | | `:layout` | 使用的布局 261 | |=== 262 | 263 | [source,ruby] 264 | ---- 265 | { 266 | identifier: "/Users/adam/projects/notifications/app/views/posts/index.html.erb", 267 | layout: "layouts/application" 268 | } 269 | ---- 270 | 271 | [[render-partial-action-view]] 272 | ==== render-partial-action-view 273 | 274 | |=== 275 | | 键 | 值 276 | 277 | | `:identifier` | 模板的完整路径 278 | |=== 279 | 280 | [source,ruby] 281 | ---- 282 | { 283 | identifier: "/Users/adam/projects/notifications/app/views/posts/_form.html.erb" 284 | } 285 | ---- 286 | 287 | [[render-collection-action-view]] 288 | ==== render_collection.action_view 289 | 290 | |=== 291 | | 键 | 值 292 | 293 | | `:identifier` | 模板的完整路径 294 | | `:count` | 集合的大小 295 | | `:cache_hits` | 从缓存中获取的局部视图数量 296 | |=== 297 | 298 | 仅当渲染集合时设定了 `cached: true` 选项,才有 `:cache_hits` 键。 299 | 300 | [source,ruby] 301 | ---- 302 | { 303 | identifier: "/Users/adam/projects/notifications/app/views/posts/_post.html.erb", 304 | count: 3, 305 | cache_hits: 0 306 | } 307 | ---- 308 | 309 | [[active-record]] 310 | === Active Record 311 | 312 | [[sql-active-record]] 313 | ==== sql.active_record 314 | 315 | |=== 316 | | 键 | 值 317 | 318 | | `:sql` | SQL 语句 319 | | `:name` | 操作的名称 320 | | `:connection_id` | `self.object_id` 321 | | `:binds` | 绑定的参数 322 | | `:cached` | 使用缓存的查询时为 `true` 323 | |=== 324 | 325 | [TIP] 326 | ==== 327 | 适配器也会添加数据。 328 | ==== 329 | 330 | [source,ruby] 331 | ---- 332 | { 333 | sql: "SELECT \"posts\".* FROM \"posts\" ", 334 | name: "Post Load", 335 | connection_id: 70307250813140, 336 | binds: [] 337 | } 338 | ---- 339 | 340 | [[instantiation-active-record]] 341 | ==== instantiation.active_record 342 | 343 | |=== 344 | | 键 | 值 345 | 346 | | `:record_count` | 实例化记录的数量 347 | | `:class_name` | 记录所属的类 348 | |=== 349 | 350 | [source,ruby] 351 | ---- 352 | { 353 | record_count: 1, 354 | class_name: "User" 355 | } 356 | ---- 357 | 358 | [[action-mailer]] 359 | === Action Mailer 360 | 361 | [[receive-action-mailer]] 362 | ==== receive.action_mailer 363 | 364 | |=== 365 | | 键 | 值 366 | 367 | | `:mailer` | 邮件程序类的名称 368 | | `:message_id` | 邮件的 ID,由 Mail gem 生成 369 | | `:subject` | 邮件的主题 370 | | `:to` | 邮件的收件地址 371 | | `:from` | 邮件的发件地址 372 | | `:bcc` | 邮件的密送地址 373 | | `:cc` | 邮件的抄送地址 374 | | `:date` | 发送邮件的日期 375 | | `:mail` | 邮件的编码形式 376 | |=== 377 | 378 | [source,ruby] 379 | ---- 380 | 381 | { 382 | mailer: "Notification", 383 | message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail", 384 | subject: "Rails Guides", 385 | to: ["users@rails.com", "ddh@rails.com"], 386 | from: ["me@rails.com"], 387 | date: Sat, 10 Mar 2012 14:18:09 +0100, 388 | mail: "..." # 为了节省空间,省略 389 | } 390 | ---- 391 | 392 | [[deliver-action-mailer]] 393 | ==== deliver.action_mailer 394 | 395 | |=== 396 | | 键 | 值 397 | 398 | | `:mailer` | 邮件程序类的名称 399 | | `:message_id` | 邮件的 ID,由 Mail gem 生成 400 | | `:subject` | 邮件的主题 401 | | `:to` | 邮件的收件地址 402 | | `:from` | 邮件的发件地址 403 | | `:bcc` | 邮件的密送地址 404 | | `:cc` | 邮件的抄送地址 405 | | `:date` | 发送邮件的日期 406 | | `:mail` | 邮件的编码形式 407 | |=== 408 | 409 | [source,ruby] 410 | ---- 411 | { 412 | mailer: "Notification", 413 | message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail", 414 | subject: "Rails Guides", 415 | to: ["users@rails.com", "ddh@rails.com"], 416 | from: ["me@rails.com"], 417 | date: Sat, 10 Mar 2012 14:18:09 +0100, 418 | mail: "..." # 为了节省空间,省略 419 | } 420 | ---- 421 | 422 | [[active-support]] 423 | === Active Support 424 | 425 | [[cache-read-active-support]] 426 | ==== cache_read.active_support 427 | 428 | |=== 429 | | 键 | 值 430 | 431 | | `:key` | 存储器中使用的键 432 | | `:hit` | 是否读取了缓存 433 | | `:super_operation` | 如果使用 `#fetch` 读取了,添加 `:fetch` 434 | |=== 435 | 436 | [[cache-generate-active-support]] 437 | ==== cache_generate.active_support 438 | 439 | 仅当使用块调用 `#fetch` 时使用这个事件。 440 | 441 | |=== 442 | | 键 | 值 443 | 444 | | `:key` | 存储器中使用的键 445 | |=== 446 | 447 | [TIP] 448 | ==== 449 | 写入存储器时,传给 `fetch` 的选项会合并到载荷中。 450 | ==== 451 | 452 | [source,ruby] 453 | ---- 454 | { 455 | key: 'name-of-complicated-computation' 456 | } 457 | ---- 458 | 459 | [[cache-fetch-hit-active-support]] 460 | ==== cache_fetch_hit.active_support 461 | 462 | 仅当使用块调用 `#fetch` 时使用这个事件。 463 | 464 | |=== 465 | | 键 | 值 466 | 467 | | `:key` | 存储器中使用的键 468 | |=== 469 | 470 | [TIP] 471 | ==== 472 | 传给 `fetch` 的选项会合并到载荷中。 473 | ==== 474 | 475 | [source,ruby] 476 | ---- 477 | { 478 | key: 'name-of-complicated-computation' 479 | } 480 | ---- 481 | 482 | [[cache-write-active-support]] 483 | ==== cache_write.active_support 484 | 485 | |=== 486 | | 键 | 值 487 | 488 | | `:key` | 存储器中使用的键 489 | |=== 490 | 491 | [TIP] 492 | ==== 493 | 缓存存储器可能会添加其他键。 494 | ==== 495 | 496 | [source,ruby] 497 | ---- 498 | { 499 | key: 'name-of-complicated-computation' 500 | } 501 | ---- 502 | 503 | [[cache-delete-active-support]] 504 | ==== cache_delete.active_support 505 | 506 | |=== 507 | | 键 | 值 508 | 509 | | `:key` | 存储器中使用的键 510 | |=== 511 | 512 | [source,ruby] 513 | ---- 514 | { 515 | key: 'name-of-complicated-computation' 516 | } 517 | ---- 518 | 519 | [[cache-exist-questionmark-active-support]] 520 | ==== cache_exist?.active_support 521 | 522 | |=== 523 | | 键 | 值 524 | 525 | | `:key` | 存储器中使用的键 526 | |=== 527 | 528 | [source,ruby] 529 | ---- 530 | { 531 | key: 'name-of-complicated-computation' 532 | } 533 | ---- 534 | 535 | [[active-job]] 536 | === Active Job 537 | 538 | [[enqueue-at-active-job]] 539 | ==== enqueue_at.active_job 540 | 541 | |=== 542 | | 键 | 值 543 | 544 | | `:adapter` | 处理作业的 `QueueAdapter` 对象 545 | | `:job` | 作业对象 546 | |=== 547 | 548 | [[enqueue-active-job]] 549 | ==== enqueue.active_job 550 | 551 | |=== 552 | | 键 | 值 553 | 554 | | `:adapter` | 处理作业的 `QueueAdapter` 对象 555 | | `:job` | 作业对象 556 | |=== 557 | 558 | [[perform-start-active-job]] 559 | ==== perform_start.active_job 560 | 561 | |=== 562 | | 键 | 值 563 | 564 | | `:adapter` | 处理作业的 `QueueAdapter` 对象 565 | | `:job` | 作业对象 566 | |=== 567 | 568 | [[perform-active-job]] 569 | ==== perform.active_job 570 | 571 | |=== 572 | | 键 | 值 573 | 574 | | `:adapter` | 处理作业的 `QueueAdapter` 对象 575 | | `:job` | 作业对象 576 | |=== 577 | 578 | [[railties]] 579 | === Railties 580 | 581 | [[load-config-initializer-railties]] 582 | ==== load_config_initializer.railties 583 | 584 | |=== 585 | | 键 | 值 586 | 587 | | `:initializer` | 从 `config/initializers` 中加载的初始化脚本的路径 588 | |=== 589 | 590 | [[rails]] 591 | === Rails 592 | 593 | [[deprecation-rails]] 594 | ==== deprecation.rails 595 | 596 | |=== 597 | | 键 | 值 598 | 599 | | `:message` | 弃用提醒 600 | | `:callstack` | 弃用的位置 601 | |=== 602 | 603 | [[subscribing-to-an-event]] 604 | === 订阅事件 605 | 606 | 订阅事件是件简单的事,在 `ActiveSupport::Notifications.subscribe` 的块中监听通知即可。 607 | 608 | 这个块接收下述参数: 609 | 610 | - 事件的名称 611 | - 开始时间 612 | - 结束时间 613 | - 事件的唯一 ID 614 | - 载荷(参见前述各节) 615 | 616 | [source,ruby] 617 | ---- 618 | ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, started, finished, unique_id, data| 619 | # 自己编写的其他代码 620 | Rails.logger.info "#{name} Received!" 621 | end 622 | ---- 623 | 624 | 每次都定义这些块参数很麻烦,我们可以使用 `ActiveSupport::Notifications::Event` 创建块参数,如下: 625 | 626 | [source,ruby] 627 | ---- 628 | ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args| 629 | event = ActiveSupport::Notifications::Event.new *args 630 | 631 | event.name # => "process_action.action_controller" 632 | event.duration # => 10 (in milliseconds) 633 | event.payload # => {:extra=>information} 634 | 635 | Rails.logger.info "#{event} Received!" 636 | end 637 | ---- 638 | 639 | 多数时候,我们只关注数据本身。下面是只获取数据的简洁方式: 640 | 641 | [source,ruby] 642 | ---- 643 | ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args| 644 | data = args.extract_options! 645 | data # { extra: :information } 646 | end 647 | ---- 648 | 649 | 此外,还可以订阅匹配正则表达式的事件。这样可以一次订阅多个事件。下面是订阅 `ActionController` 中所有事件的方式: 650 | 651 | [source,ruby] 652 | ---- 653 | ActiveSupport::Notifications.subscribe /action_controller/ do |*args| 654 | # 审查所有 ActionController 事件 655 | end 656 | ---- 657 | 658 | [[creating-custom-events]] 659 | === 自定义事件 660 | 661 | 自己添加事件也很简单,繁重的工作都由 `ActiveSupport::Notifications` 代劳,我们只需调用 `instrument`,并传入 `name`、`payload` 和一个块。通知在块返回后发送。`ActiveSupport` 会生成起始时间和唯一的 ID。传给 `instrument` 调用的所有数据都会放入载荷中。 662 | 663 | 下面举个例子: 664 | 665 | [source,ruby] 666 | ---- 667 | ActiveSupport::Notifications.instrument "my.custom.event", this: :data do 668 | # 自己编写的其他代码 669 | end 670 | ---- 671 | 672 | 然后可以使用下述代码监听这个事件: 673 | 674 | [source,ruby] 675 | ---- 676 | ActiveSupport::Notifications.subscribe "my.custom.event" do |name, started, finished, unique_id, data| 677 | puts data.inspect # {:this=>:data} 678 | end 679 | ---- 680 | 681 | 自己定义事件时,应该遵守 Rails 的约定。事件名称的格式是 `event.library`。如果应用发送推文,应该把事件命名为 `tweet.twitter`。 682 | -------------------------------------------------------------------------------- /manuscript/api_app.adoc: -------------------------------------------------------------------------------- 1 | [[using-rails-for-api-only-applications]] 2 | == 使用 Rails 开发只提供 API 的应用 3 | 4 | // 安道翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 在本文中您将学到: 9 | 10 | - Rails 对只提供 API 的应用的支持; 11 | - 如何配置 Rails,不使用任何针对浏览器的功能; 12 | - 如何决定使用哪些中间件; 13 | - 如何决定在控制器中使用哪些模块。 14 | -- 15 | 16 | [[what-is-an-api-application-questionmark]] 17 | === 什么是 API 应用? 18 | 19 | 人们说把 Rails 用作“API”,通常指的是在 Web 应用之外提供一份可通过编程方式访问的 API。例如,GitHub 提供了 http://developer.github.com/[API],供你在自己的客户端中使用。 20 | 21 | 随着客户端框架的出现,越来越多的开发者使用 Rails 构建后端,在 Web 应用和其他原生应用之间共享。 22 | 23 | 例如,Twitter 使用自己的link:https://dev.twitter.com/[公开 API] 构建 Web 应用,而文档网站是一个静态网站,消费 JSON 资源。 24 | 25 | 很多人不再使用 Rails 生成 HTML,通过表单和链接与服务器通信,而是把 Web 应用当做 API 客户端,分发包含 JavaScript 的 HTML,消费 JSON API。 26 | 27 | 本文说明如何构建伺服 JSON 资源的 Rails 应用,供 API 客户端(包括客户端框架)使用。 28 | 29 | [[why-use-rails-for-json-apis-questionmark]] 30 | === 为什么使用 Rails 构建 JSON API? 31 | 32 | 提到使用 Rails 构建 JSON API,多数人想到的第一个问题是:“使用 Rails 生成 JSON 是不是有点大材小用了?使用 Sinatra 这样的框架是不是更好?” 33 | 34 | 对特别简单的 API 来说,确实如此。然而,对大量使用 HTML 的应用来说,应用的逻辑大都在视图层之外。 35 | 36 | 多数人使用 Rails 的原因是,Rails 提供了一系列默认值,开发者能快速上手,而不用做些琐碎的决定。 37 | 38 | 下面是 Rails 提供的一些开箱即用的功能,这些功能在 API 应用中也适用。 39 | 40 | 在中间件层处理的功能: 41 | 42 | - 重新加载:Rails 应用支持简单明了的重新加载机制。即使应用变大,每次请求都重启服务器变得不切实际,这一机制依然适用。 43 | - 开发模式:Rails 应用自带智能的开发默认值,使得开发过程很愉快,而且不会破坏生产环境的效率。 44 | - 测试模式:同开发模式。 45 | - 日志:Rails 应用会在日志中记录每次请求,而且为不同环境设定了合适的详细等级。在开发环境中,Rails 记录的信息包括请求环境、数据库查询和基本的性能信息。 46 | - 安全性:Rails 能检测并防范 https://en.wikipedia.org/wiki/IP_address_spoofing[IP 欺骗攻击],还能处理link:http://en.wikipedia.org/wiki/Timing_attack[时序攻击]中的加密签名。不知道 IP 欺骗攻击和时序攻击是什么?这就对了。 47 | - 参数解析:想以 JSON 的形式指定参数,而不是 URL 编码字符串形式?没问题。Rails 会代为解码 JSON,存入 `params` 中。想使用嵌套的 URL 编码参数?也没问题。 48 | - 条件 GET 请求:Rails 能处理条件 `GET` 请求相关的首部(`ETag` 和 `Last-Modified`),然后返回正确的响应首部和状态码。你只需在控制器中使用 http://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-stale-3F[`stale?`] 做检查,剩下的 HTTP 细节都由 Rails 处理。 49 | - HEAD 请求:Rails 会把 `HEAD` 请求转换成 `GET` 请求,只返回首部。这样 `HEAD` 请求在所有 Rails API 中都可靠。 50 | 51 | 虽然这些功能可以使用 Rack 中间件实现,但是上述列表的目的是说明 Rails 默认提供的中间件栈提供了大量有价值的功能,即便“只是生成 JSON”也用得到。 52 | 53 | 在 Action Pack 层处理的功能: 54 | 55 | - 资源式路由:如果构建的是 REST 式 JSON API,你会想用 Rails 路由器的。按照约定以简明的方式把 HTTP 映射到控制器上能节省很多时间,不用再从 HTTP 方面思考如何建模 API。 56 | - URL 生成:路由的另一面是 URL 生成。基于 HTTP 的优秀 API 包含 URL(比如 http://developer.github.com/v3/gists/[GitHub Gist API])。 57 | - 首部和重定向响应:`head :no_content` 和 `redirect_to user_url(current_user)` 用着很方便。当然,你可以自己动手添加相应的响应首部,但是为什么要费这事呢? 58 | - 缓存:Rails 提供了页面缓存、动作缓存和片段缓存。构建嵌套的 JSON 对象时,片段缓存特别有用。 59 | - 基本身份验证、摘要身份验证和令牌身份验证:Rails 默认支持三种 HTTP 身份验证。 60 | - 监测程序:Rails 提供了监测 API,在众多事件发生时触发注册的处理程序,例如处理动作、发送文件或数据、重定向和数据库查询。各个事件的载荷中包含相关的信息(对动作处理事件来说,载荷中包括控制器、动作、参数、请求格式、请求方法和完整的请求路径)。 61 | - 生成器:通常生成一个资源就能把模型、控制器、测试桩件和路由在一个命令中通通创建出来,然后再做调整。迁移等也有生成器。 62 | - 插件:有很多第三方库支持 Rails,这样不必或很少需要花时间设置及把库与 Web 框架连接起来。插件可以重写默认的生成器、添加 Rake 任务,而且继续使用 Rails 选择的处理方式(如日志记录器和缓存后端)。 63 | 64 | 当然,Rails 启动过程还是要把各个注册的组件连接起来。例如,Rails 启动时会使用 `config/database.yml` 文件配置 Active Record。 65 | 66 | 简单来说,你可能没有想过去掉视图层之后要把 Rails 的哪些部分保留下来,不过答案是,多数都要保留。 67 | 68 | [[the-basic-configuration]] 69 | === 基本配置 70 | 71 | 如果你构建的 Rails 应用主要用作 API,可以从较小的 Rails 子集开始,然后再根据需要添加功能。 72 | 73 | [[creating-a-new-application]] 74 | ==== 新建应用 75 | 76 | 生成 Rails API 应用使用下述命令: 77 | 78 | [source,sh] 79 | ---- 80 | $ rails new my_api --api 81 | ---- 82 | 83 | 这个命令主要做三件事: 84 | 85 | - 配置应用,使用有限的中间件(比常规应用少)。具体而言,不含默认主要针对浏览器应用的中间件(如提供 cookie 支持的中间件)。 86 | - 让 `ApplicationController` 继承 `ActionController::API`,而不继承 `ActionController::Base`。与中间件一样,这样做是为了去除主要针对浏览器应用的 Action Controller 模块。 87 | - 配置生成器,生成资源时不生成视图、辅助方法和静态资源。 88 | 89 | [[changing-an-existing-application]] 90 | ==== 修改现有应用 91 | 92 | 如果你想把现有的应用改成 API 应用,请阅读下述步骤。 93 | 94 | 在 `config/application.rb` 文件中,把下面这行代码添加到 `Application` 类定义的顶部: 95 | 96 | [source,ruby] 97 | ---- 98 | config.api_only = true 99 | ---- 100 | 101 | 在 `config/environments/development.rb` 文件中,设定 `config.debug_exception_response_format` 选项,配置在开发环境中出现错误时响应使用的格式。 102 | 103 | 如果想使用 HTML 页面渲染调试信息,把值设为 `:default`: 104 | 105 | [source,ruby] 106 | ---- 107 | config.debug_exception_response_format = :default 108 | ---- 109 | 110 | 如果想使用响应所用的格式渲染调试信息,把值设为 `:api`: 111 | 112 | [source,ruby] 113 | ---- 114 | config.debug_exception_response_format = :api 115 | ---- 116 | 117 | 默认情况下,`config.api_only` 的值为 `true` 时,`config.debug_exception_response_format` 的值是 `:api`。 118 | 119 | 最后,在 `app/controllers/application_controller.rb` 文件中,把下述代码 120 | 121 | [source,ruby] 122 | ---- 123 | class ApplicationController < ActionController::Base 124 | end 125 | ---- 126 | 127 | 改为 128 | 129 | [source,ruby] 130 | ---- 131 | 132 | class ApplicationController < ActionController::API 133 | end 134 | ---- 135 | 136 | [[choosing-middleware]] 137 | === 选择中间件 138 | 139 | API 应用默认包含下述中间件: 140 | 141 | - `Rack::Sendfile` 142 | - `ActionDispatch::Static` 143 | - `ActionDispatch::Executor` 144 | - `ActiveSupport::Cache::Strategy::LocalCache::Middleware` 145 | - `Rack::Runtime` 146 | - `ActionDispatch::RequestId` 147 | - `ActionDispatch::RemoteIp` 148 | - `Rails::Rack::Logger` 149 | - `ActionDispatch::ShowExceptions` 150 | - `ActionDispatch::DebugExceptions` 151 | - `ActionDispatch::Reloader` 152 | - `ActionDispatch::Callbacks` 153 | - `ActiveRecord::Migration::CheckPending` 154 | - `Rack::Head` 155 | - `Rack::ConditionalGet` 156 | - `Rack::ETag` 157 | - `MyApi::Application::Routes` 158 | 159 | 各个中间件的作用参见 <>。 160 | 161 | 其他插件,包括 Active Record,可能会添加额外的中间件。一般来说,这些中间件对要构建的应用类型一无所知,可以在只提供 API 的 Rails 应用中使用。 162 | 163 | 可以通过下述命令列出应用中的所有中间件: 164 | 165 | [source,sh] 166 | ---- 167 | $ rails middleware 168 | ---- 169 | 170 | [[using-the-cache-middleware]] 171 | ==== 使用缓存中间件 172 | 173 | 默认情况下,Rails 会根据应用的配置提供一个缓存存储器(默认为 memcache)。因此,内置的 HTTP 缓存依靠这个中间件。 174 | 175 | 例如,使用 `stale?` 方法: 176 | 177 | [source,ruby] 178 | ---- 179 | def show 180 | @post = Post.find(params[:id]) 181 | 182 | if stale?(last_modified: @post.updated_at) 183 | render json: @post 184 | end 185 | end 186 | ---- 187 | 188 | 上述 `stale?` 调用比较请求中的 `If-Modified-Since` 首部和 `@post.updated_at`。如果首部的值比最后修改时间晚,这个动作返回“304 未修改”响应;否则,渲染响应,并且设定 `Last-Modified` 首部。 189 | 190 | 通常,这个机制会区分客户端。缓存中间件支持跨客户端共享这种缓存机制。跨客户端缓存可以在调用 `stale?` 时启用: 191 | 192 | [source,ruby] 193 | ---- 194 | def show 195 | @post = Post.find(params[:id]) 196 | 197 | if stale?(last_modified: @post.updated_at, public: true) 198 | render json: @post 199 | end 200 | end 201 | ---- 202 | 203 | 这表明,缓存中间件会在 Rails 缓存中存储 URL 的 `Last-Modified` 值,而且为后续对同一个 URL 的入站请求添加 `If-Modified-Since` 首部。 204 | 205 | 可以把这种机制理解为使用 HTTP 语义的页面缓存。 206 | 207 | [[using-rack-sendfile]] 208 | ==== 使用 Rack::Sendfile 209 | 210 | 在 Rails 控制器中使用 `send_file` 方法时,它会设定 `X-Sendfile` 首部。`Rack::Sendfile` 负责发送文件。 211 | 212 | 如果前端服务器支持加速发送文件,`Rack::Sendfile` 会把文件交给前端服务器发送。 213 | 214 | 此时,可以在环境的配置文件中设定 `config.action_dispatch.x_sendfile_header` 选项,为前端服务器指定首部的名称。 215 | 216 | 关于如何在流行的前端服务器中使用 `Rack::Sendfile`,参见 http://rubydoc.info/github/rack/rack/master/Rack/Sendfile[`Rack::Sendfile` 的文档]。 217 | 218 | 下面是两个流行的服务器的配置。这样配置之后,就能支持加速文件发送功能了。 219 | 220 | [source,ruby] 221 | ---- 222 | # Apache 和 lighttpd 223 | config.action_dispatch.x_sendfile_header = "X-Sendfile" 224 | 225 | # Nginx 226 | config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" 227 | ---- 228 | 229 | 请按照 `Rack::Sendfile` 文档中的说明配置你的服务器。 230 | 231 | [[using-actiondispatch-request]] 232 | ==== 使用 ActionDispatch::Request 233 | 234 | `ActionDispatch::Request#params` 获取客户端发来的 JSON 格式参数,将其存入 `params`,可在控制器中访问。 235 | 236 | 为此,客户端要发送 JSON 编码的参数,并把 `Content-Type` 设为 `application/json`。 237 | 238 | 下面以 jQuery 为例: 239 | 240 | [source,js] 241 | ---- 242 | jQuery.ajax({ 243 | type: 'POST', 244 | url: '/people', 245 | dataType: 'json', 246 | contentType: 'application/json', 247 | data: JSON.stringify({ person: { firstName: "Yehuda", lastName: "Katz" } }), 248 | success: function(json) { } 249 | }); 250 | ---- 251 | 252 | `ActionDispatch::Request` 检查 `Content-Type` 后,把参数转换成: 253 | 254 | [source,ruby] 255 | ---- 256 | { :person => { :firstName => "Yehuda", :lastName => "Katz" } } 257 | ---- 258 | 259 | [[other-middleware]] 260 | ==== 其他中间件 261 | 262 | Rails 自带的其他中间件在 API 应用中可能也会用到,尤其是 API 客户端包含浏览器时: 263 | 264 | * `Rack::MethodOverride` 265 | * `ActionDispatch::Cookies` 266 | * `ActionDispatch::Flash` 267 | * 管理会话 268 | ** `ActionDispatch::Session::CacheStore` 269 | ** `ActionDispatch::Session::CookieStore` 270 | ** `ActionDispatch::Session::MemCacheStore` 271 | 272 | 这些中间件可通过下述方式添加: 273 | 274 | [source,ruby] 275 | ---- 276 | config.middleware.use Rack::MethodOverride 277 | ---- 278 | 279 | [[removing-middleware]] 280 | ==== 删除中间件 281 | 282 | 如果默认的 API 中间件中有不需要使用的,可以通过下述方式将其删除: 283 | 284 | [source,ruby] 285 | ---- 286 | config.middleware.delete ::Rack::Sendfile 287 | ---- 288 | 289 | 注意,删除中间件后 Action Controller 的特定功能就不可用了。 290 | 291 | [[choosing-controller-modules]] 292 | === 选择控制器模块 293 | 294 | API 应用(使用 `ActionController::API`)默认有下述控制器模块: 295 | 296 | - `ActionController::UrlFor`:提供 `url_for` 等辅助方法。 297 | - `ActionController::Redirecting`:提供 `redirect_to`。 298 | - `AbstractController::Rendering` 和 `ActionController::ApiRendering`:提供基本的渲染支持。 299 | - `ActionController::Renderers::All`:提供 `render :json` 等。 300 | - `ActionController::ConditionalGet`:提供 `stale?`。 301 | - `ActionController::BasicImplicitRender`:如果没有显式响应,确保返回一个空响应。 302 | - `ActionController::StrongParameters`:结合 Active Model 批量赋值,提供参数白名单过滤功能。 303 | - `ActionController::ForceSSL`:提供 `force_ssl`。 304 | - `ActionController::DataStreaming`:提供 `send_file` 和 `send_data`。 305 | - `AbstractController::Callbacks`:提供 `before_action` 等方法。 306 | - `ActionController::Rescue`:提供 `rescue_from`。 307 | - `ActionController::Instrumentation`:提供 Action Controller 定义的监测钩子(详情参见 <>)。 308 | - `ActionController::ParamsWrapper`:把参数散列放到一个嵌套散列中,这样在发送 POST 请求时无需指定根元素。 309 | - `ActionController::Head`:返回只有首部没有内容的响应。 310 | 311 | 其他插件可能会添加额外的模块。`ActionController::API` 引入的模块可以在 Rails 控制台中列出: 312 | 313 | [source,sh] 314 | ---- 315 | $ bin/rails c 316 | >> ActionController::API.ancestors - ActionController::Metal.ancestors 317 | => [ActionController::API, 318 | ActiveRecord::Railties::ControllerRuntime, 319 | ActionDispatch::Routing::RouteSet::MountedHelpers, 320 | ActionController::ParamsWrapper, 321 | ... , 322 | AbstractController::Rendering, 323 | ActionView::ViewPaths] 324 | ---- 325 | 326 | [[adding-other-modules]] 327 | ==== 添加其他模块 328 | 329 | 所有 Action Controller 模块都知道它们所依赖的模块,因此在控制器中可以放心引入任何模块,所有依赖都会自动引入。 330 | 331 | 可能想添加的常见模块有: 332 | 333 | - `AbstractController::Translation`:提供本地化和翻译方法 `l` 和 `t`。 334 | - `ActionController::HttpAuthentication::Basic`(或 `Digest` 或 `Token`):提供基本、摘要或令牌 HTTP 身份验证。 335 | - `ActionView::Layouts`:渲染时支持使用布局。 336 | - `ActionController::MimeResponds`:提供 `respond_to`。 337 | - `ActionController::Cookies`:提供 `cookies`,包括签名和加密 cookie。需要 cookies 中间件支持。 338 | 339 | 模块最好添加到 `ApplicationController` 中,不过也可以在各个控制器中添加。 340 | -------------------------------------------------------------------------------- /manuscript/api_documentation_guidelines.adoc: -------------------------------------------------------------------------------- 1 | [[api-documentation-guidelines]] 2 | == API 文档指导方针 3 | 4 | // 安道翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 本文说明 Ruby on Rails 的 API 文档指导方针。 9 | 10 | 读完本文后,您将学到: 11 | 12 | - 如何编写有效的文档; 13 | - 为不同 Ruby 代码编写文档的风格指导方针。 14 | -- 15 | 16 | [[rdoc]] 17 | === RDoc 18 | 19 | http://api.rubyonrails.org/[Rails API 文档]使用 http://docs.seattlerb.org/rdoc/[RDoc] 生成。如果想生成 API 文档,要在 Rails 根目录中执行 `bundle install`,然后再执行: 20 | 21 | [source,sh] 22 | ---- 23 | $ bundle exec rake rdoc 24 | ---- 25 | 26 | 得到的 HTML 文件在 `./doc/rdoc` 目录中。 27 | 28 | RDoc 的link:http://docs.seattlerb.org/rdoc/RDoc/Markup.html[标记]和link:http://docs.seattlerb.org/rdoc/RDoc/Parser/Ruby.html[额外的指令]参见文档。 29 | 30 | [[wording]] 31 | === 用词 32 | 33 | 使用简单的陈述句。简短更好,要说到点子上。 34 | 35 | 使用现在时:“Returns a hash that...”,而非“Returned a hash that...”或“Will return a hash that...”。 36 | 37 | 注释的第一个字母大写,后续内容遵守常规的标点符号规则: 38 | 39 | [source,sh] 40 | ---- 41 | # Declares an attribute reader backed by an internally-named 42 | # instance variable. 43 | def attr_internal_reader(*attrs) 44 | ... 45 | end 46 | ---- 47 | 48 | 使用通行的方式与读者交流,可以直言,也可以隐晦。使用当下推荐的习语。如有必要,调整内容的顺序,强调推荐的方式。文档应该说明最佳实践和现代的权威 Rails 用法。 49 | 50 | 文档应该简洁全面,要指明边缘情况。如果模块是匿名的呢?如果集合是空的呢?如果参数是 nil 呢? 51 | 52 | Rails 组件的名称在单词之间有个空格,如“Active Support”。`ActiveRecord` 是一个 Ruby 模块,而 Active Record 是一个 ORM。所有 Rails 文档都应该始终使用正确的名称引用 Rails 组件。如果你在下一篇博客文章或演示文稿中这么做,人们会觉得你很正规。 53 | 54 | 拼写要正确:Arel、Test::Unit、RSpec、HTML、MySQL、JavaScript、ERB。如果不确定,请查看一些权威资料,如各自的官方文档。 55 | 56 | “SQL”前面使用不定冠词“an”,如“an SQL statement”和“an SQLite database”。 57 | 58 | 避免使用“you”和“your”。例如,较之 59 | 60 | [source] 61 | ---- 62 | If you need to use `return` statements in your callbacks, it is recommended that you explicitly define them as methods. 63 | ---- 64 | 65 | 这样写更好: 66 | 67 | [source] 68 | ---- 69 | If `return` is needed it is recommended to explicitly define a method. 70 | ---- 71 | 72 | 不过,使用代词指代虚构的人时,例如“有会话 cookie 的用户”,应该使用中性代词(they/their/them)。 73 | 74 | - 不用 he 或 she,用 they 75 | - 不用 him 或 her,用 them 76 | - 不用 his 或 her,用 their 77 | - 不用 his 或 hers,用 theirs 78 | - 不用 himself 或 herself,用 themselves 79 | 80 | [[english]] 81 | === 英语 82 | 83 | 请使用美式英语(color、center、modularize,等等)。美式英语与英式英语之间的拼写差异参见link:http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences[这里]。 84 | 85 | [[oxford-comma]] 86 | === 牛津式逗号 87 | 88 | 请使用link:http://en.wikipedia.org/wiki/Serial_comma[牛津式逗号](“red, white, and blue”,而非“red, white and blue”)。 89 | 90 | [[example-code]] 91 | === 示例代码 92 | 93 | 选择有意义的示例,说明基本用法和有趣的点或坑。 94 | 95 | 代码使用两个空格缩进,即根据标记在左外边距的基础上增加两个空格。示例应该遵守 <>。 96 | 97 | 简短的文档无需明确使用“Examples”标注引入代码片段,直接跟在段后即可: 98 | 99 | [source,ruby] 100 | ---- 101 | # Converts a collection of elements into a formatted string by 102 | # calling +to_s+ on all elements and joining them. 103 | # 104 | # Blog.all.to_formatted_s # => "First PostSecond PostThird Post" 105 | ---- 106 | 107 | 但是大段文档可以单独有个“Examples”部分: 108 | 109 | [source,ruby] 110 | ---- 111 | # ==== Examples 112 | # 113 | # Person.exists?(5) 114 | # Person.exists?('5') 115 | # Person.exists?(name: "David") 116 | # Person.exists?(['name LIKE ?', "%#{query}%"]) 117 | ---- 118 | 119 | 表达式的结果在表达式之后,使用 “pass:[# => ]”给出,而且要纵向对齐: 120 | 121 | [source,ruby] 122 | ---- 123 | # For checking if an integer is even or odd. 124 | # 125 | # 1.even? # => false 126 | # 1.odd? # => true 127 | # 2.even? # => true 128 | # 2.odd? # => false 129 | ---- 130 | 131 | 如果一行太长,结果可以放在下一行: 132 | 133 | [source,ruby] 134 | ---- 135 | # label(:article, :title) 136 | # # => 137 | # 138 | # label(:article, :title, "A short title") 139 | # # => 140 | # 141 | # label(:article, :title, "A short title", class: "title_label") 142 | # # => 143 | ---- 144 | 145 | 不要使用打印方法,如 `puts` 或 `p` 给出结果。 146 | 147 | 常规的注释不使用箭头: 148 | 149 | [source,ruby] 150 | ---- 151 | # polymorphic_url(record) # same as comment_url(record) 152 | ---- 153 | 154 | [[booleans]] 155 | === 布尔值 156 | 157 | 在判断方法或旗标中,尽量使用布尔语义,不要用具体的值。 158 | 159 | 如果所用的“true”或“false”与 Ruby 定义的一样,使用常规字体。`true` 和 `false` 两个单例要使用等宽字体。请不要使用“truthy”,Ruby 语言定义了什么是真什么是假,“true”和“false”就能表达技术意义,无需使用其他词代替。 160 | 161 | 通常,如非绝对必要,不要为单例编写文档。这样能阻止智能的结构,如 `!!` 或三元运算符,便于重构,而且代码不依赖方法返回的具体值。 162 | 163 | 例如: 164 | 165 | [source] 166 | ---- 167 | `config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default 168 | ---- 169 | 170 | 用户无需知道旗标具体的默认值,因此我们只说明它的布尔语义。 171 | 172 | 下面是一个判断方法的文档示例: 173 | 174 | [source,ruby] 175 | ---- 176 | # Returns true if the collection is empty. 177 | # 178 | # If the collection has been loaded 179 | # it is equivalent to collection.size.zero?. If the 180 | # collection has not been loaded, it is equivalent to 181 | # collection.exists?. If the collection has not already been 182 | # loaded and you are going to fetch the records anyway it is better to 183 | # check collection.length.zero?. 184 | def empty? 185 | if loaded? 186 | size.zero? 187 | else 188 | @target.blank? && !scope.exists? 189 | end 190 | end 191 | ---- 192 | 193 | 这个 API 没有提到任何具体的值,知道它具有判断功能就够了。 194 | 195 | [[file-names]] 196 | === 文件名 197 | 198 | 通常,文件名相对于应用的根目录: 199 | 200 | [source,ruby] 201 | ---- 202 | config/routes.rb # YES 203 | routes.rb # NO 204 | RAILS_ROOT/config/routes.rb # NO 205 | ---- 206 | 207 | [[fonts]] 208 | === 字体 209 | 210 | [[fixed-width-font]] 211 | ==== 等宽字体 212 | 213 | 使用等宽字体编写: 214 | 215 | - 常量,尤其是类名和模块名 216 | - 方法名 217 | - 字面量,如 `nil`、`false`、`true`、`self` 218 | - 符号 219 | - 方法的参数 220 | - 文件名 221 | 222 | [source,ruby] 223 | ---- 224 | class Array 225 | # Calls +to_param+ on all its elements and joins the result with 226 | # slashes. This is used by +url_for+ in Action Pack. 227 | def to_param 228 | collect { |e| e.to_param }.join '/' 229 | end 230 | end 231 | ---- 232 | 233 | [WARNING] 234 | ==== 235 | 只有简单的内容才能使用 `pass:[+...+]` 标记使用等宽字体,如常规的方法名、符号、路径(含有正斜线),等等。其他内容应该使用 `...`,尤其是带有命名空间的类名或模块名,如 `ActiveRecord::Base`。 236 | ==== 237 | 238 | 可以使用下述命令测试 RDoc 的输出: 239 | 240 | [source,sh] 241 | ---- 242 | $ echo "+:to_param+" | rdoc --pipe 243 | # =>

:to_param

244 | ---- 245 | 246 | [[regular-font]] 247 | ==== 常规字体 248 | 249 | “true”和“false”是英语单词而不是 Ruby 关键字时,使用常规字体: 250 | 251 | [source,ruby] 252 | ---- 253 | # Runs all the validations within the specified context. 254 | # Returns true if no errors are found, false otherwise. 255 | # 256 | # If the argument is false (default is +nil+), the context is 257 | # set to :create if new_record? is true, 258 | # and to :update if it is not. 259 | # 260 | # Validations with no :on option will run no 261 | # matter the context. Validations with # some :on 262 | # option will only run in the specified context. 263 | def valid?(context = nil) 264 | ... 265 | end 266 | ---- 267 | 268 | [[description-lists]] 269 | === 描述列表 270 | 271 | 在选项、参数等列表中,在项目和描述之间使用一个连字符(而不是一个冒号,因为选项一般是符号): 272 | 273 | [source,ruby] 274 | ---- 275 | # * :allow_nil - Skip validation if attribute is +nil+. 276 | ---- 277 | 278 | 描述开头是大写字母,结尾有一个句号——这是标准的英语。 279 | 280 | [[dynamically-generated-methods]] 281 | === 动态生成的方法 282 | 283 | 使用 `(module|class)_eval(STRING)` 创建的方法在旁边有个注释,举例说明生成的代码。这种注释与模板之间相距两个空格。 284 | 285 | [source,ruby] 286 | ---- 287 | for severity in Severity.constants 288 | class_eval <<-EOT, __FILE__, __LINE__ 289 | def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block) 290 | add(#{severity}, message, progname, &block) # add(DEBUG, message, progname, &block) 291 | end # end 292 | # 293 | def #{severity.downcase}? # def debug? 294 | #{severity} >= @level # DEBUG >= @level 295 | end # end 296 | EOT 297 | end 298 | ---- 299 | 300 | 如果这样得到的行太长,比如说有 200 多列,把注释放在上方: 301 | 302 | [source,ruby] 303 | ---- 304 | # def self.find_by_login_and_activated(*args) 305 | # options = args.extract_options! 306 | # ... 307 | # end 308 | self.class_eval %{ 309 | def self.#{method_id}(*args) 310 | options = args.extract_options! 311 | ... 312 | end 313 | } 314 | ---- 315 | 316 | [[method-visibility]] 317 | === 方法可见性 318 | 319 | 为 Rails 编写文档时,要区分公开 API 和内部 API。 320 | 321 | 与多数库一样,Rails 使用 Ruby 提供的 `private` 关键字定义内部 API。然而,公开 API 遵照的约定稍有不同。不是所有公开方法都旨在供用户使用,Rails 使用 `:nodoc:` 指令注解内部 API 方法。 322 | 323 | 因此,在 Rails 中有些可见性为 `public` 的方法不是供用户使用的。 324 | 325 | `ActiveRecord::Core::ClassMethods#arel_table` 就是一例: 326 | 327 | [source,sh] 328 | ---- 329 | module ActiveRecord::Core::ClassMethods 330 | def arel_table #:nodoc: 331 | # do some magic.. 332 | end 333 | end 334 | ---- 335 | 336 | 你可能想,“这是 `ActiveRecord::Core` 的一个公开类方法”,没错,但是 Rails 团队不希望用户使用这个方法。因此,他们把它标记为 `:nodoc:`,不包含在公开文档中。这样做,开发团队可以根据内部需要在发布新版本时修改这个方法。方法的名称可能会变,或者返回值有变化,也可能是整个类都不复存在——有太多不确定性,因此不应该在你的插件或应用中使用这个 API。如若不然,升级新版 Rails 时,你的应用或 gem 可能遭到破坏。 337 | 338 | 为 Rails 做贡献时一定要考虑清楚 API 是否供最终用户使用。未经完整的弃用循环之前,Rails 团队不会轻易对公开 API 做大的改动。如果没有定义为私有的(默认是内部 API),建议你使用 `:nodoc:` 标记所有内部的方法和类。API 稳定之后,可见性可以修改,但是为了向后兼容,公开 API 往往不宜修改。 339 | 340 | 使用 `:nodoc:` 标记一个类或模块表示里面的所有方法都是内部 API,不应该直接使用。 341 | 342 | 综上,Rails 团队使用 `:nodoc:` 标记供内部使用的可见性为公开的方法和类,对 API 可见性的修改要谨慎,必须先通过一个拉取请求讨论。 343 | 344 | [[regarding-the-rails-stack]] 345 | === 考虑 Rails 栈 346 | 347 | 为 Rails API 编写文档时,一定要记住所有内容都身处 Rails 栈中。 348 | 349 | 这意味着,方法或类的行为在不同的作用域或上下文中可能有所不同。 350 | 351 | 把整个栈考虑进来之后,行为在不同的地方可能有变。`ActionView::Helpers::AssetTagHelper#image_tag` 就是一例: 352 | 353 | [source,ruby] 354 | ---- 355 | # image_tag("icon.png") 356 | # # => Icon 357 | ---- 358 | 359 | 虽然 `#image_tag` 的默认行为是返回 `/images/icon.png`,但是把整个 Rails 栈(包括 Asset Pipeline)考虑进来之后,可能会得到上述结果。 360 | 361 | 我们只关注考虑整个 Rails 默认栈的行为。 362 | 363 | 因此,我们要说明的是框架的行为,而不是单个方法。 364 | 365 | 如果你对 Rails 团队处理某个 API 的方式有疑问,别迟疑,在link:https://github.com/rails/rails/issues[问题追踪系统]中发一个工单,或者提交补丁。 366 | -------------------------------------------------------------------------------- /manuscript/caching_with_rails.adoc: -------------------------------------------------------------------------------- 1 | [[caching-with-rails-an-overview]] 2 | == Rails 缓存概览 3 | 4 | // 安道翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 本文简述如何使用缓存提升 Rails 应用的速度。 9 | 10 | 缓存是指存储请求-响应循环中生成的内容,在类似请求的响应中复用。 11 | 12 | 通常,缓存是提升应用性能最有效的方式。通过缓存,在单个服务器中使用单个数据库的网站可以承受数千个用户并发访问。 13 | 14 | Rails 自带了一些缓存功能。本文说明它们的适用范围和作用。掌握这些技术之后,你的 Rails 应用能承受大量访问,而不必花大量时间生成响应,或者支付高昂的服务器账单。 15 | 16 | 读完本文后,您将学到: 17 | 18 | - 片段缓存和俄罗斯套娃缓存; 19 | - 如何管理缓存依赖; 20 | - 不同的缓存存储器; 21 | - 对条件 GET 请求的支持。 22 | -- 23 | 24 | [[basic-caching]] 25 | === 基本缓存 26 | 27 | 本节简介三种缓存技术:页面缓存(page caching)、动作缓存(action caching)和片段缓存(fragment caching)。Rails 默认提供了片段缓存。如果想使用页面缓存或动作缓存,要把 `actionpack-page_caching` 或 `actionpack-action_caching` 添加到 `Gemfile` 中。 28 | 29 | 默认情况下,缓存只在生产环境启用。如果想在本地启用缓存,要在相应的 `config/environments/*.rb` 文件中把 `config.action_controller.perform_caching` 设为 `true`。 30 | 31 | [source,ruby] 32 | ---- 33 | config.action_controller.perform_caching = true 34 | ---- 35 | 36 | [NOTE] 37 | ==== 38 | 修改 `config.action_controller.perform_caching` 的值只对 Action Controller 组件提供的缓存有影响。例如,对低层缓存没影响,<>。 39 | ==== 40 | 41 | [[page-caching]] 42 | ==== 页面缓存 43 | 44 | 页面缓存时 Rails 提供的一种缓存机制,让 Web 服务器(如 Apache 和 NGINX)直接伺服生成的页面,而不经由 Rails 栈处理。虽然这种缓存的速度超快,但是不适用于所有情况(例如需要验证身份的页面)。此外,因为 Web 服务器直接从文件系统中伺服文件,所以你要自行实现缓存失效机制。 45 | 46 | [TIP] 47 | ==== 48 | Rails 4 删除了页面缓存。参见 https://github.com/rails/actionpack-page_caching[actionpack-page_caching gem]。 49 | ==== 50 | 51 | [[action-caching]] 52 | ==== 动作缓存 53 | 54 | 有前置过滤器的动作不能使用页面缓存,例如需要验证身份的页面。此时,应该使用动作缓存。动作缓存的工作原理与页面缓存类似,不过入站请求会经过 Rails 栈处理,以便运行前置过滤器,然后再伺服缓存。这样,可以做身份验证和其他限制,同时还能从缓存的副本中伺服结果。 55 | 56 | [TIP] 57 | ==== 58 | Rails 4 删除了动作缓存。参见 https://github.com/rails/actionpack-action_caching[actionpack-action_caching gem]。最新推荐的做法参见 DHH 写的“link:https://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works[How key-based cache expiration works]”一文。 59 | ==== 60 | 61 | [[fragment-caching]] 62 | ==== 片段缓存 63 | 64 | 动态 Web 应用一般使用不同的组件构建页面,不是所有组件都能使用同一种缓存机制。如果页面的不同部分需要使用不同的缓存机制,在不同的条件下失效,可以使用片段缓存。 65 | 66 | 片段缓存把视图逻辑的一部分放在 `cache` 块中,下次请求使用缓存存储器中的副本伺服。 67 | 68 | 例如,如果想缓存页面中的各个商品,可以使用下述代码: 69 | 70 | [source,erb] 71 | ---- 72 | <% @products.each do |product| %> 73 | <% cache product do %> 74 | <%= render product %> 75 | <% end %> 76 | <% end %> 77 | ---- 78 | 79 | 首次访问这个页面时,Rails 会创建一个具有唯一键的缓存条目。缓存键类似下面这种: 80 | 81 | [source] 82 | ---- 83 | views/products/1-201505056193031061005000/bea67108094918eeba42cd4a6e786901 84 | ---- 85 | 86 | 中间的数字是 `product_id` 加上商品记录的 `updated_at` 属性中存储的时间戳。Rails 使用时间戳确保不伺服过期的数据。如果 `updated_at` 的值变了,Rails 会生成一个新键,然后在那个键上写入一个新缓存,旧键上的旧缓存不再使用。这叫基于键的失效方式。 87 | 88 | 视图片段有变化时(例如视图的 HTML 有变),缓存的片段也失效。缓存键末尾那个字符串是模板树摘要,是基于缓存的视图片段的内容计算的 MD5 哈希值。如果视图片段有变化,MD5 哈希值就变了,因此现有文件失效。 89 | 90 | TIP: Memcached 等缓存存储器会自动删除旧的缓存文件。 91 | 92 | 如果想在特定条件下缓存一个片段,可以使用 `cache_if` 或 `cache_unless`: 93 | 94 | [source,erb] 95 | ---- 96 | <% cache_if admin?, product do %> 97 | <%= render product %> 98 | <% end %> 99 | ---- 100 | 101 | [[collection-caching]] 102 | ===== 集合缓存 103 | 104 | `render` 辅助方法还能缓存渲染集合的单个模板。这甚至比使用 `each` 的前述示例更好,因为是一次性读取所有缓存模板的,而不是一次读取一个。若想缓存集合,渲染集合时传入 `cached: true` 选项: 105 | 106 | [source,erb] 107 | ---- 108 | <%= render partial: 'products/product', collection: @products, cached: true %> 109 | ---- 110 | 111 | 上述代码中所有的缓存模板一次性获取,速度更快。此外,尚未缓存的模板也会写入缓存,在下次渲染时获取。 112 | 113 | [[russian-doll-caching]] 114 | ==== 俄罗斯套娃缓存 115 | 116 | 有时,可能想把缓存的片段嵌套在其他缓存的片段里。这叫俄罗斯套娃缓存(Russian doll caching)。 117 | 118 | 俄罗斯套娃缓存的优点是,更新单个商品后,重新生成外层片段时,其他内存片段可以复用。 119 | 120 | 前一节说过,如果缓存的文件对应的记录的 `updated_at` 属性值变了,缓存的文件失效。但是,内层嵌套的片段不失效。 121 | 122 | 对下面的视图来说: 123 | 124 | [source,erb] 125 | ---- 126 | <% cache product do %> 127 | <%= render product.games %> 128 | <% end %> 129 | ---- 130 | 131 | 而它渲染这个视图: 132 | 133 | [source,erb] 134 | ---- 135 | <% cache game do %> 136 | <%= render game %> 137 | <% end %> 138 | ---- 139 | 140 | 如果游戏的任何一个属性变了,`updated_at` 的值会设为当前时间,因此缓存失效。然而,商品对象的 `updated_at` 属性不变,因此它的缓存不失效,从而导致应用伺服过期的数据。为了解决这个问题,可以使用 `touch` 方法把模型绑在一起: 141 | 142 | [source,ruby] 143 | ---- 144 | class Product < ApplicationRecord 145 | has_many :games 146 | end 147 | 148 | class Game < ApplicationRecord 149 | belongs_to :product, touch: true 150 | end 151 | ---- 152 | 153 | 把 `touch` 设为 `true` 后,导致游戏的 `updated_at` 变化的操作,也会修改关联的商品的 `updated_at` 属性,从而让缓存失效。 154 | 155 | [[managing-dependencies]] 156 | ==== 管理依赖 157 | 158 | 为了正确地让缓存失效,要正确地定义缓存依赖。Rails 足够智能,能处理常见的情况,无需自己指定。但是有时需要处理自定义的辅助方法(以此为例),因此要自行定义。 159 | 160 | [[implicit-dependencies]] 161 | ===== 隐式依赖 162 | 163 | 多数模板依赖可以从模板中的 `render` 调用中推导出来。下面举例说明 `ActionView::Digestor` 知道如何解码的 `render` 调用: 164 | 165 | [source,ruby] 166 | ---- 167 | render partial: "comments/comment", collection: commentable.comments 168 | render "comments/comments" 169 | render 'comments/comments' 170 | render('comments/comments') 171 | 172 | render "header" => render("comments/header") 173 | 174 | render(@topic) => render("topics/topic") 175 | render(topics) => render("topics/topic") 176 | render(message.topics) => render("topics/topic") 177 | ---- 178 | 179 | 而另一方面,有些调用要做修改方能让缓存正确工作。例如,如果传入自定义的集合,要把下述代码: 180 | 181 | [source,ruby] 182 | ---- 183 | render @project.documents.where(published: true) 184 | ---- 185 | 186 | 改为: 187 | 188 | [source,ruby] 189 | ---- 190 | render partial: "documents/document", collection: @project.documents.where(published: true) 191 | ---- 192 | 193 | [[explicit-dependencies]] 194 | ===== 显式依赖 195 | 196 | 有时,模板依赖推导不出来。在辅助方法中渲染时经常是这样。下面举个例子: 197 | 198 | [source,erb] 199 | ---- 200 | <%= render_sortable_todolists @project.todolists %> 201 | ---- 202 | 203 | 此时,要使用一种特殊的注释格式: 204 | 205 | [source,erb] 206 | ---- 207 | <%# Template Dependency: todolists/todolist %> 208 | <%= render_sortable_todolists @project.todolists %> 209 | ---- 210 | 211 | 某些情况下,例如设置单表继承,可能要显式定义一堆依赖。此时无需写出每个模板,可以使用通配符匹配一个目录中的全部模板: 212 | 213 | [source,erb] 214 | ---- 215 | <%# Template Dependency: events/* %> 216 | <%= render_categorizable_events @person.events %> 217 | ---- 218 | 219 | 对集合缓存来说,如果局部模板不是以干净的缓存调用开头,依然可以使用集合缓存,不过要在模板中的任意位置添加一种格式特殊的注释,如下所示: 220 | 221 | [source,erb] 222 | ---- 223 | <%# Template Collection: notification %> 224 | <% my_helper_that_calls_cache(some_arg, notification) do %> 225 | <%= notification.name %> 226 | <% end %> 227 | ---- 228 | 229 | [[external-dependencies]] 230 | ===== 外部依赖 231 | 232 | 如果在缓存的块中使用辅助方法,而后更新了辅助方法,还要更新缓存。具体方法不限,只要能改变模板文件的 MD5 值就行。推荐的方法之一是添加一个注释,如下所示: 233 | 234 | [source,erb] 235 | ---- 236 | <%# Helper Dependency Updated: Jul 28, 2015 at 7pm %> 237 | <%= some_helper_method(person) %> 238 | ---- 239 | 240 | [[low-level-caching]] 241 | ==== 低层缓存 242 | 243 | 有时需要缓存特定的值或查询结果,而不是缓存视图片段。Rails 的缓存机制能存储任何类型的信息。 244 | 245 | 实现低层缓存最有效的方式是使用 `Rails.cache.fetch` 方法。这个方法既能读取也能写入缓存。传入单个参数时,获取指定的键,返回缓存中的值。如果传入块,块中的代码在缓存缺失时执行。块返回的值将写入缓存,存在指定键的名下,然后返回那个返回值。如果命中缓存,直接返回缓存的值,而不执行块中的代码。 246 | 247 | 下面举个例子。应用中有个 `Product` 模型,它有个实例方法,在竞争网站中查找商品的价格。这个方法返回的数据特别适合使用低层缓存: 248 | 249 | [source,ruby] 250 | ---- 251 | class Product < ApplicationRecord 252 | def competing_price 253 | Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do 254 | Competitor::API.find_price(id) 255 | end 256 | end 257 | end 258 | ---- 259 | 260 | [NOTE] 261 | ==== 262 | 注意,这个示例使用了 `cache_key` 方法,因此得到的缓存键类似这种:`products/233-20140225082222765838000/competing_price`。`cache_key` 方法根据模型的 `id` 和 `updated_at` 属性生成一个字符串。这是常见的约定,有个好处是,商品更新后缓存自动失效。一般来说,使用低层缓存缓存实例层信息时,需要生成缓存键。 263 | ==== 264 | 265 | [[sql-caching]] 266 | ==== SQL 缓存 267 | 268 | 查询缓存是 Rails 提供的一个功能,把各个查询的结果集缓存起来。如果在同一个请求中遇到了相同的查询,Rails 会使用缓存的结果集,而不再次到数据库中运行查询。 269 | 270 | 例如: 271 | 272 | [source,ruby] 273 | ---- 274 | class ProductsController < ApplicationController 275 | 276 | def index 277 | # 运行查找查询 278 | @products = Product.all 279 | 280 | ... 281 | 282 | # 再次运行相同的查询 283 | @products = Product.all 284 | end 285 | 286 | end 287 | ---- 288 | 289 | 再次运行相同的查询时,根本不会发给数据库。首次运行查询得到的结果存储在查询缓存中(内存里),第二次查询从内存中获取。 290 | 291 | 然而要知道,查询缓存在动作开头创建,到动作末尾销毁,只在动作的存续时间内存在。如果想持久化存储查询结果,使用低层缓存也能实现。 292 | 293 | [[cache-stores]] 294 | === 缓存存储器 295 | 296 | Rails 为存储缓存数据(SQL 缓存和页面缓存除外)提供了不同的存储器。 297 | 298 | [[configuration]] 299 | ==== 配置 300 | 301 | `config.cache_store` 配置选项用于设定应用的默认缓存存储器。可以设定其他参数,传给缓存存储器的构造方法: 302 | 303 | [source,ruby] 304 | ---- 305 | config.cache_store = :memory_store, { size: 64.megabytes } 306 | ---- 307 | 308 | NOTE: 此外,还可以在配置块外部调用 `ActionController::Base.cache_store`。 309 | 310 | 缓存存储器通过 `Rails.cache` 访问。 311 | 312 | [[activesupport-cache-store]] 313 | ==== `ActiveSupport::Cache::Store` 314 | 315 | 这个类是在 Rails 中与缓存交互的基础。这是个抽象类,不能直接使用。你必须根据存储器引擎具体实现这个类。Rails 提供了几个实现,说明如下。 316 | 317 | 主要调用的方法有 `read`、`write`、`delete`、`exist?` 和 `fetch`。`fetch` 方法接受一个块,返回缓存中现有的值,或者把新值写入缓存。 318 | 319 | 所有缓存实现有些共用的选项,可以传给构造方法,或者传给与缓存条目交互的各个方法。 320 | 321 | - `:namespace`:在缓存存储器中创建命名空间。如果与其他应用共用同一个缓存存储器,这个选项特别有用。 322 | - `:compress`:指定压缩缓存。通过缓慢的网络传输大量缓存时用得着。 323 | - `:compress_threshold`:与 `:compress` 选项搭配使用,指定一个阈值,未达到时不压缩缓存。默认为 16 千字节。 324 | - `:expires_in`:为缓存条目设定失效时间(秒数),失效后自动从缓存中删除。 325 | - `:race_condition_ttl`:与 `:expires_in` 选项搭配使用。避免多个进程同时重新生成相同的缓存条目(也叫 dog pile effect),防止让缓存条目过期时出现条件竞争。这个选项设定在重新生成新值时失效的条目还可以继续使用多久(秒数)。如果使用 `:expires_in` 选项, 最好也设定这个选项。 326 | 327 | [[custom-cache-stores]] 328 | ===== 自定义缓存存储器 329 | 330 | 缓存存储器可以自己定义,只需扩展 `ActiveSupport::Cache::Store` 类,实现相应的方法。这样,你可以把任何缓存技术带到你的 Rails 应用中。 331 | 332 | 若想使用自定义的缓存存储器,只需把 `cache_store` 设为自定义类的实例: 333 | 334 | [source,ruby] 335 | ---- 336 | config.cache_store = MyCacheStore.new 337 | ---- 338 | 339 | [[activesupport-cache-memorystore]] 340 | ==== `ActiveSupport::Cache::MemoryStore` 341 | 342 | 这个缓存存储器把缓存条目放在内存中,与 Ruby 进程放在一起。可以把 `:size` 选项传给构造方法,指定缓存的大小限制(默认为 32Mb)。超过分配的大小后,会清理缓存,把最不常用的条目删除。 343 | 344 | [source,ruby] 345 | ---- 346 | config.cache_store = :memory_store, { size: 64.megabytes } 347 | ---- 348 | 349 | 如果运行多个 Ruby on Rails 服务器进程(例如使用 Phusion Passenger 或 Puma 集群模式),各个实例之间无法共享缓存数据。这个缓存存储器不适合大型应用使用。不过,适合只有几个服务器进程的低流量小型应用使用,也适合在开发环境和测试环境中使用。 350 | 351 | [[activesupport-cache-filestore]] 352 | ==== `ActiveSupport::Cache::FileStore` 353 | 354 | 这个缓存存储器使用文件系统存储缓存条目。初始化这个存储器时,必须指定存储文件的目录: 355 | 356 | [source,ruby] 357 | ---- 358 | config.cache_store = :file_store, "/path/to/cache/directory" 359 | ---- 360 | 361 | 使用这个缓存存储器时,在同一台主机中运行的多个服务器进程可以共享缓存。这个缓存存储器适合一到两个主机的中低流量网站使用。运行在不同主机中的多个服务器进程若想共享缓存,可以使用共享的文件系统,但是不建议这么做。 362 | 363 | 缓存量一直增加,直到填满磁盘,所以建议你定期清理旧缓存条目。 364 | 365 | 这是默认的缓存存储器。 366 | 367 | [[activesupport-cache-memcachestore]] 368 | ==== `ActiveSupport::Cache::MemCacheStore` 369 | 370 | 这个缓存存储器使用 Danga 的 `memcached` 服务器为应用提供中心化缓存。Rails 默认使用自带的 `dalli` gem。这是生产环境的网站目前最常使用的缓存存储器。通过它可以实现单个共享的缓存集群,效率很高,有较好的冗余。 371 | 372 | 初始化这个缓存存储器时,要指定集群中所有 memcached 服务器的地址。如果不指定,假定 memcached 运行在本地的默认端口上,但是对大型网站来说,这样做并不好。 373 | 374 | 这个缓存存储器的 `write` 和 `fetch` 方法接受两个额外的选项,以便利用 memcached 的独有特性。指定 `:raw` 时,直接把值发给服务器,不做序列化。值必须是字符串或数字。memcached 的直接操作,如 `increment` 和 `decrement`,只能用于原始值。还可以指定 `:unless_exist` 选项,不让 memcached 覆盖现有条目。 375 | 376 | [source,ruby] 377 | ---- 378 | config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com" 379 | ---- 380 | 381 | [[activesupport-cache-nullstore]] 382 | ==== `ActiveSupport::Cache::NullStore` 383 | 384 | 这个缓存存储器只应该在开发或测试环境中使用,它并不存储任何信息。在开发环境中,如果代码直接与 `Rails.cache` 交互,但是缓存可能对代码的结果有影响,可以使用这个缓存存储器。在这个缓存存储器上调用 `fetch` 和 `read` 方法不返回任何值。 385 | 386 | [source,ruby] 387 | ---- 388 | config.cache_store = :null_store 389 | ---- 390 | 391 | [[cache-keys]] 392 | === 缓存键 393 | 394 | 缓存中使用的键可以是能响应 `cache_key` 或 `to_param` 方法的任何对象。如果想定制生成键的方式,可以覆盖 `cache_key` 方法。Active Record 根据类名和记录 ID 生成缓存键。 395 | 396 | 缓存键的值可以是散列或数组: 397 | 398 | [source,ruby] 399 | ---- 400 | # 这是一个有效的缓存键 401 | Rails.cache.read(site: "mysite", owners: [owner_1, owner_2]) 402 | ---- 403 | 404 | `Rails.cache` 使用的键与存储引擎使用的并不相同,存储引擎使用的键可能含有命名空间,或者根据后端的限制做调整。这意味着,使用 `Rails.cache` 存储值时使用的键可能无法用于供 `dalli` gem 获取缓存条目。然而,你也无需担心会超出 memcached 的大小限制,或者违背句法规则。 405 | 406 | [[conditional-get-support]] 407 | === 对条件 GET 请求的支持 408 | 409 | 条件 GET 请求是 HTTP 规范的一个特性,以此告诉 Web 浏览器,GET 请求的响应自上次请求之后没有变化,可以放心从浏览器的缓存中读取。 410 | 411 | 为此,要传递 `HTTP_IF_NONE_MATCH` 和 `HTTP_IF_MODIFIED_SINCE` 首部,其值分别为唯一的内容标识符和上一次改动时的时间戳。浏览器发送的请求,如果内容标识符(etag)或上一次修改的时间戳与服务器中的版本匹配,那么服务器只需返回一个空响应,把状态设为未修改。 412 | 413 | 服务器(也就是我们自己)要负责查看最后修改时间戳和 `HTTP_IF_NONE_MATCH` 首部,判断要不要返回完整的响应。既然 Rails 支持条件 GET 请求,那么这个任务就非常简单: 414 | 415 | [source,ruby] 416 | ---- 417 | class ProductsController < ApplicationController 418 | 419 | def show 420 | @product = Product.find(params[:id]) 421 | 422 | # 如果根据指定的时间戳和 etag 值判断请求的内容过期了 423 | # (即需要重新处理)执行这个块 424 | if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key) 425 | respond_to do |wants| 426 | # ... 正常处理响应 427 | end 428 | end 429 | 430 | # 如果请求的内容还新鲜(即未修改),无需做任何事 431 | # render 默认使用前面 stale? 中的参数做检查,会自动发送 :not_modified 响应 432 | # 就这样,工作结束 433 | end 434 | end 435 | ---- 436 | 437 | 除了散列,还可以传入模型。Rails 会使用 `updated_at` 和 `cache_key` 方法设定 `last_modified` 和 `etag`: 438 | 439 | [source,ruby] 440 | ---- 441 | class ProductsController < ApplicationController 442 | def show 443 | @product = Product.find(params[:id]) 444 | 445 | if stale?(@product) 446 | respond_to do |wants| 447 | # ... 正常处理响应 448 | end 449 | end 450 | end 451 | end 452 | ---- 453 | 454 | 如果无需特殊处理响应,而且使用默认的渲染机制(即不使用 `respond_to`,或者不自己调用 `render`),可以使用 `fresh_when` 简化这个过程: 455 | 456 | [source,ruby] 457 | ---- 458 | class ProductsController < ApplicationController 459 | 460 | # 如果请求的内容是新鲜的,自动返回 :not_modified 461 | # 否则渲染默认的模板(product.*) 462 | 463 | def show 464 | @product = Product.find(params[:id]) 465 | fresh_when last_modified: @product.published_at.utc, etag: @product 466 | end 467 | end 468 | ---- 469 | 470 | 有时,我们需要缓存响应,例如永不过期的静态页面。为此,可以使用 `http_cache_forever` 辅助方法,让浏览器和代理无限期缓存。 471 | 472 | 默认情况下,缓存的响应是私有的,只在用户的 Web 浏览器中缓存。如果想让代理缓存响应,设定 `public: true`,让代理把缓存的响应提供给所有用户。 473 | 474 | 使用这个辅助方法时,`last_modified` 首部的值被设为 `Time.new(2011, 1, 1).utc`,`expires` 首部的值被设为 100 年。 475 | 476 | WARNING: 使用这个方法时要小心,因为浏览器和代理不会作废缓存的响应,除非强制清除浏览器缓存。 477 | 478 | [source,ruby] 479 | ---- 480 | class HomeController < ApplicationController 481 | def index 482 | http_cache_forever(public: true) do 483 | render 484 | end 485 | end 486 | end 487 | ---- 488 | 489 | [[strong-v-s-weak-etags]] 490 | ==== 强 Etag 与弱 Etag 491 | 492 | Rails 默认生成弱 ETag。这种 Etag 允许语义等效但主体不完全匹配的响应具有相同的 Etag。如果响应主体有微小改动,而不想重新渲染页面,可以使用这种 Etag。 493 | 494 | 为了与强 Etag 区别,弱 Etag 前面有 `W/`。 495 | 496 | [source] 497 | ---- 498 | W/"618bbc92e2d35ea1945008b42799b0e7" => 弱 ETag 499 | "618bbc92e2d35ea1945008b42799b0e7" => 强 ETag 500 | ---- 501 | 502 | 与弱 Etag 不同,强 Etag 要求响应完全一样,不能有一个字节的差异。在大型视频或 PDF 文件内部做 Range 查询时用得到。有些 CDN,如 Akamai,只支持强 Etag。如果确实想生成强 Etag,可以这么做: 503 | 504 | [source,ruby] 505 | ---- 506 | class ProductsController < ApplicationController 507 | def show 508 | @product = Product.find(params[:id]) 509 | fresh_when last_modified: @product.published_at.utc, strong_etag: @product 510 | end 511 | end 512 | ---- 513 | 514 | 也可以直接在响应上设定强 Etag: 515 | 516 | [source,ruby] 517 | ---- 518 | response.strong_etag = response.body 519 | # => "618bbc92e2d35ea1945008b42799b0e7" 520 | ---- 521 | 522 | [[caching-in-development]] 523 | === 在开发环境中测试缓存 524 | 525 | 我们经常需要在开发模式中测试应用采用的缓存策略。Rails 提供的 Rake 任务 `dev:cache` 能轻易启停缓存。 526 | 527 | [source,sh] 528 | ---- 529 | $ bin/rails dev:cache 530 | Development mode is now being cached. 531 | $ bin/rails dev:cache 532 | Development mode is no longer being cached. 533 | ---- 534 | 535 | [[references]] 536 | === 参考资源 537 | 538 | - https://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works[DHH 写的文章:How key-based cache expiration works] 539 | - http://railscasts.com/episodes/387-cache-digests[Railscast 中介绍缓存摘要的视频] 540 | -------------------------------------------------------------------------------- /manuscript/contributing.adoc: -------------------------------------------------------------------------------- 1 | [part] 2 | = 第七部分 为 Ruby on Rails 做贡献 3 | -------------------------------------------------------------------------------- /manuscript/controllers.adoc: -------------------------------------------------------------------------------- 1 | [part] 2 | = 第四部分 控制器 3 | -------------------------------------------------------------------------------- /manuscript/development_dependencies_install.adoc: -------------------------------------------------------------------------------- 1 | [[development-dependencies-install]] 2 | == 安装开发依赖 3 | 4 | // 安道翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 本文说明如何搭建 Ruby on Rails 核心开发环境。 9 | 10 | 读完本文后,您将学到: 11 | 12 | - 如何设置你的设备供 Rails 开发; 13 | - 如何运行 Rails 测试组件中特定的单元测试组; 14 | - Rails 测试组件中的 Active Record 部分是如何运作的。 15 | -- 16 | 17 | [[development-dependencies-install-the-easy-way]] 18 | === 简单方式 19 | 20 | 搭建开发环境最简单、也是推荐的方式是使用 https://github.com/rails/rails-dev-box[Rails 开发虚拟机]。 21 | 22 | [[development-dependencies-install-the-hard-way]] 23 | === 笨拙方式 24 | 25 | 如果你不便使用 Rails 开发虚拟机,参见下述说明。这些步骤说明如何自己动手搭建开发环境,供 Ruby on Rails 核心开发使用。 26 | 27 | [[install-git]] 28 | ==== 安装 Git 29 | 30 | Ruby on Rails 使用 Git 做源码控制。Git 的安装说明参见link:http://git-scm.com/[官网]。网上有很多学习 Git 的资源: 31 | 32 | - http://try.github.io/[Try Git] 是个交互式课程,教你基本用法。 33 | - http://git-scm.com/documentation[官方文档]十分全面,也有一些 Git 基本用法的视频。 34 | - http://schacon.github.io/git/everyday.html[Everyday Git] 教你一些技能,足够日常使用。 35 | - http://help.github.com/[GitHub 帮助页面]中有很多 Git 资源的链接。 36 | - http://git-scm.com/book[Pro Git] 是一本讲解 Git 的书,基于知识共享许可证发布。 37 | 38 | [[clone-the-ruby-on-rails-repository]] 39 | ==== 克隆 Ruby on Rails 仓库 40 | 41 | 进入你想保存 Ruby on Rails 源码的文件夹,然后执行(会创建 `rails` 子目录): 42 | 43 | [source,sh] 44 | ---- 45 | $ git clone git://github.com/rails/rails.git 46 | $ cd rails 47 | ---- 48 | 49 | [[set-up-and-run-the-tests]] 50 | ==== 准备工作和运行测试 51 | 52 | 提交的代码必须通过测试组件。不管你是编写新的补丁,还是评估别人的代码,都要运行测试。 53 | 54 | 首先,安装 `sqlite3` gem 所需的 SQLite3 及其开发文件 。macOS 用户这么做: 55 | 56 | [source,sh] 57 | ---- 58 | $ brew install sqlite3 59 | ---- 60 | 61 | Ubuntu 用户这么做: 62 | 63 | [source,sh] 64 | ---- 65 | $ sudo apt-get install sqlite3 libsqlite3-dev 66 | ---- 67 | 68 | Fedora 或 CentOS 用户这么做: 69 | 70 | [source,sh] 71 | ---- 72 | $ sudo yum install sqlite3 sqlite3-devel 73 | ---- 74 | 75 | Arch Linux 用户要这么做: 76 | 77 | [source,sh] 78 | ---- 79 | $ sudo pacman -S sqlite 80 | ---- 81 | 82 | FreeBSD 用户这么做: 83 | 84 | [source,sh] 85 | ---- 86 | # pkg install sqlite3 87 | ---- 88 | 89 | 或者编译 `databases/sqlite3` port。 90 | 91 | 然后安装最新版 http://bundler.io/[Bundler]: 92 | 93 | [source,sh] 94 | ---- 95 | $ gem install bundler 96 | $ gem update bundler 97 | ---- 98 | 99 | 再执行: 100 | 101 | [source,sh] 102 | ---- 103 | $ bundle install --without db 104 | ---- 105 | 106 | 这个命令会安装除了 MySQL 和 PostgreSQL 的 Ruby 驱动之外的所有依赖。稍后再安装那两个驱动。 107 | 108 | [NOTE] 109 | ==== 110 | 如果想运行使用 memcached 的测试,要安装并运行 memcached。 111 | 112 | 在 macOS 中可以使用 http://brew.sh/[Homebrew] 安装 memcached: 113 | 114 | [source,sh] 115 | ---- 116 | $ brew install memcached 117 | ---- 118 | 119 | 在 Ubuntu 中可以使用 apt-get 安装 memcached: 120 | 121 | [source,sh] 122 | ---- 123 | $ sudo apt-get install memcached 124 | ---- 125 | 126 | 在 Fedora 或 CentOS 中这么做: 127 | 128 | [source,sh] 129 | ---- 130 | $ sudo yum install memcached 131 | ---- 132 | 133 | 在 Arch Linux 中这么做: 134 | 135 | [source,sh] 136 | ---- 137 | $ sudo pacman -S memcached 138 | ---- 139 | 140 | 在 FreeBSD 中这么做: 141 | 142 | [source,sh] 143 | ---- 144 | # pkg install memcached 145 | ---- 146 | 147 | 或者编译 `databases/memcached` port。 148 | ==== 149 | 150 | 安装好依赖之后,可以执行下述命令运行测试组件: 151 | 152 | [source,sh] 153 | ---- 154 | $ bundle exec rake test 155 | ---- 156 | 157 | 还可以运行某个组件(如 Action Pack)的测试,方法是进入组件所在的目录,然后执行相同的命令: 158 | 159 | [source,sh] 160 | ---- 161 | $ cd actionpack 162 | $ bundle exec rake test 163 | ---- 164 | 165 | 如果想运行某个目录中的测试,使用 `TEST_DIR` 环境变量指定。例如,下述命令只运行 `railties/test/generators` 目录中的测试: 166 | 167 | [source,sh] 168 | ---- 169 | $ cd railties 170 | $ TEST_DIR=generators bundle exec rake test 171 | ---- 172 | 173 | 可以像下面这样运行某个文件中的测试: 174 | 175 | [source,sh] 176 | ---- 177 | $ cd actionpack 178 | $ bundle exec ruby -Itest test/template/form_helper_test.rb 179 | ---- 180 | 181 | 还可以运行某个文件中的某个测试: 182 | 183 | [source,sh] 184 | ---- 185 | $ cd actionpack 186 | $ bundle exec ruby -Itest path/to/test.rb -n test_name 187 | ---- 188 | 189 | [[railties-setup]] 190 | ==== 为 Railties 做准备 191 | 192 | 有些 Railties 测试依赖 JavaScript 运行时环境,因此要安装 https://nodejs.org/[Node.js]。 193 | 194 | [[active-record-setup]] 195 | ==== 为 Active Record 做准备 196 | 197 | Active Record 的测试组件运行三次:一次针对 SQLite3,一次针对 MySQL,还有一次针对 PostgreSQL。下面说明如何为这三种数据库搭建环境。 198 | 199 | [WARNING] 200 | ==== 201 | 编写 Active Record 代码时,必须确保测试至少能在 MySQL、PostgreSQL 和 SQLite3 中通过。如果只使用 MySQL 测试,虽然测试能通过,但是不同适配器之间的差异没有考虑到。 202 | ==== 203 | 204 | [[database-configuration]] 205 | ===== 数据库配置 206 | 207 | Active Record 测试组件需要一个配置文件:`activerecord/test/config.yml`。`activerecord/test/config.example.yml` 文件中有些示例。你可以复制里面的内容,然后根据你的环境修改。 208 | 209 | [[mysql-and-postgresql]] 210 | ===== MySQL 和 PostgreSQL 211 | 212 | 为了运行针对 MySQL 和 PostgreSQL 的测试组件,要安装相应的 gem。首先安装服务器、客户端库和开发文件。 213 | 214 | 在 macOS 中可以这么做: 215 | 216 | [source,sh] 217 | ---- 218 | $ brew install mysql 219 | $ brew install postgresql 220 | ---- 221 | 222 | 然后按照 Homebrew 给出的说明做。 223 | 224 | 在 Ubuntu 中只需这么做: 225 | 226 | [source,sh] 227 | ---- 228 | $ sudo apt-get install mysql-server libmysqlclient-dev 229 | $ sudo apt-get install postgresql postgresql-client postgresql-contrib libpq-dev 230 | ---- 231 | 232 | 在 Fedora 或 CentOS 中只需这么做: 233 | 234 | [source,sh] 235 | ---- 236 | $ sudo yum install mysql-server mysql-devel 237 | $ sudo yum install postgresql-server postgresql-devel 238 | ---- 239 | 240 | MySQL 不再支持 Arch Linux,因此你要使用 MariaDB(参见link:https://www.archlinux.org/news/mariadb-replaces-mysql-in-repositories/[这个声明]): 241 | 242 | [source,sh] 243 | ---- 244 | $ sudo pacman -S mariadb libmariadbclient mariadb-clients 245 | $ sudo pacman -S postgresql postgresql-libs 246 | ---- 247 | 248 | FreeBSD 用户要这么做: 249 | 250 | [source,sh] 251 | ---- 252 | # pkg install mysql56-client mysql56-server 253 | # pkg install postgresql94-client postgresql94-server 254 | ---- 255 | 256 | 或者通过 port 安装(在 `databases` 文件夹中)。在安装 MySQL 的过程中如何遇到问题,请查阅 http://dev.mysql.com/doc/refman/5.1/en/freebsd-installation.html[MySQL 文档]。 257 | 258 | 安装好之后,执行下述命令: 259 | 260 | [source,sh] 261 | ---- 262 | $ rm .bundle/config 263 | $ bundle install 264 | ---- 265 | 266 | 首先,我们要删除 `.bundle/config` 文件,因为 Bundler 记得那个文件中的配置。我们前面配置了,不安装“db”分组(此外也可以修改那个文件)。 267 | 268 | 为了使用 MySQL 运行测试组件,我们要创建一个名为 `rails` 的用户,并且赋予它操作测试数据库的权限: 269 | 270 | [source,sh] 271 | ---- 272 | $ mysql -uroot -p 273 | 274 | mysql> CREATE USER 'rails'@'localhost'; 275 | mysql> GRANT ALL PRIVILEGES ON activerecord_unittest.* 276 | to 'rails'@'localhost'; 277 | mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.* 278 | to 'rails'@'localhost'; 279 | mysql> GRANT ALL PRIVILEGES ON inexistent_activerecord_unittest.* 280 | to 'rails'@'localhost'; 281 | ---- 282 | 283 | 然后创建测试数据库: 284 | 285 | [source,sh] 286 | ---- 287 | $ cd activerecord 288 | $ bundle exec rake db:mysql:build 289 | ---- 290 | 291 | PostgreSQL 的身份验证方式有所不同。为了使用开发账户搭建开发环境,在 Linux 或 BSD 中要这么做: 292 | 293 | [source,sh] 294 | ---- 295 | $ sudo -u postgres createuser --superuser $USER 296 | ---- 297 | 298 | 在 macOS 中这么做: 299 | 300 | [source,sh] 301 | ---- 302 | $ createuser --superuser $USER 303 | ---- 304 | 305 | 然后,执行下述命令创建测试数据库: 306 | 307 | [source,sh] 308 | ---- 309 | $ cd activerecord 310 | $ bundle exec rake db:postgresql:build 311 | ---- 312 | 313 | 可以执行下述命令创建 PostgreSQL 和 MySQL 的测试数据库: 314 | 315 | [source,sh] 316 | ---- 317 | $ cd activerecord 318 | $ bundle exec rake db:create 319 | ---- 320 | 321 | 可以使用下述命令清理数据库: 322 | 323 | [source,sh] 324 | ---- 325 | $ cd activerecord 326 | $ bundle exec rake db:drop 327 | ---- 328 | 329 | [NOTE] 330 | ==== 331 | 使用 rake 任务创建测试数据库能保障数据库使用正确的字符集和排序规则。 332 | ==== 333 | 334 | [NOTE] 335 | ==== 336 | 在 PostgreSQL 9.1.x 及早期版本中激活 HStore 扩展会看到这个提醒(或本地化的提醒):“WARNING: pass:[=>] is deprecated as an operator”。 337 | ==== 338 | 339 | 如果使用其他数据库,默认的连接信息参见 `activerecord/test/config.yml` 或 `activerecord/test/config.example.yml` 文件。如果有必要,可以在你的设备中编辑 `activerecord/test/config.yml` 文件,提供不同的凭据。不过显然,不应该把这种改动推送回 Rails 仓库。 340 | 341 | [[action-cable-setup]] 342 | ==== 为 Action Cable 做准备 343 | 344 | Action Cable 默认使用 Redis 作为订阅适配器(<>),因此为了运行 Action Cable 的测试,要安装并运行 Redis。 345 | 346 | [[install-redis-from-source]] 347 | ===== 从源码安装 Redis 348 | 349 | Redis 的文档不建议通过包管理器安装,因为那里的包往往是过时的。link:http://redis.io/download#installation[Redis 的文档]详细说明了如何从源码安装,以及如何运行 Redis 服务器。 350 | 351 | [[install-redis-from-package-manager]] 352 | ===== 使用包管理器安装 353 | 354 | 在 macOS 中可以执行下述命令: 355 | 356 | [source,sh] 357 | ---- 358 | $ brew install redis 359 | ---- 360 | 361 | 然后按照 Homebrew 给出的说明做。 362 | 363 | 在 Ubuntu 中只需运行: 364 | 365 | [source,sh] 366 | ---- 367 | $ sudo apt-get install redis-server 368 | ---- 369 | 370 | 在 Fedora 或 CentOS(要启用 EPEL)中运行: 371 | 372 | [source,sh] 373 | ---- 374 | $ sudo yum install redis 375 | ---- 376 | 377 | 如果使用 Arch Linux,运行: 378 | 379 | [source,sh] 380 | ---- 381 | $ sudo pacman -S redis 382 | $ sudo systemctl start redis 383 | ---- 384 | 385 | FreeBSD 用户要运行下述命令: 386 | 387 | [source,sh] 388 | ---- 389 | # portmaster databases/redis 390 | ---- 391 | -------------------------------------------------------------------------------- /manuscript/digging_depper.adoc: -------------------------------------------------------------------------------- 1 | [part] 2 | = 第五部分 深入探索 3 | -------------------------------------------------------------------------------- /manuscript/extending_rails.adoc: -------------------------------------------------------------------------------- 1 | [part] 2 | = 第六部分 扩展 Rails 3 | -------------------------------------------------------------------------------- /manuscript/foreword.adoc: -------------------------------------------------------------------------------- 1 | [foreword] 2 | [[translation-notes]] 3 | == 翻译记 4 | 5 | 经过 chinakr 和我几个月的翻译,终于把 Rails Guides 翻译完了,这个坑算是填上了。footnote:[简体中文版没有 100% 翻译英语原版,尤其是一些 Rails 旧版的发布记。因为版本太旧,而且发布记中大多是罗列各种变化,对最终用户没有多大参考价值,因此决定不翻译。请读者朋友谅解。] 6 | 7 | 说是“坑”,因为这是我几年前就启动的项目,但是一直没有完成。在过了那股新鲜劲头之后,我便转向能为我带来经济回报的翻译项目上了。就这么一拖再拖,翻译的书一本接着一本出版。2016 年 10 月,我再次把目光转向这个项目。因为我找到了一份工作,经济压力不是那么大了,所以想回过头去把以前的坑填上。 8 | 9 | 于是,我启动了本书的翻译众筹活动。我很欣慰,此次众筹得到了众多读者的支持,以及云梯和 Ruby China 的鼎力赞助。在此,我谨代表我自己,对你们的支持表示由衷的感谢! 10 | 11 | 此外,我还把 chinakr 拉入坑了。我深知翻译这样一份资料对一个人来说实在吃力,因此我找了一位合译人。chinakr 在翻译过程中兢兢业业,保有持续的动力,为本书的完成开足马力。而且,他对我的“挑剔”和“固执”容忍颇多。我对 chinakr 的贡献和包容致以真诚的感谢! 12 | 13 | 这本书已经呈现在你手中,希望能在你学习和使用 Rails 的过程中助你一臂之力! 14 | 15 | === 关于版本号 16 | 17 | 本书采用精益出版策略,不断更新。为了区别不同的版本,本书采用一种特殊的版本号编制策略。通常,本书的版本号分成四部分,前三部分是 Rails 的版本号,最后一部分是修订版本号,从 1 开始。在针对某一个 Rails 版本的修订过程中,最后一位不断增加,上不设限。如果 Rails 发布了新版本,前三部分则相应调整,而最后一位归一。 18 | 19 | 我们原则上只提供最新版本的下载,如果你需要使用以前的旧版本,请自行存储。 20 | 21 | === 问题反馈 22 | 23 | 如果你在阅读的过程中发现错误,或者有什么建议,请到link:https://github.com/AndorChen/rails-guides/issues[本书译稿的仓库]中反馈。 24 | 25 | 如果你想修改译稿,请一定要阅读link:https://github.com/AndorChen/rails-guides/blob/master/CONTRIBUTING.md[贡献说明]。其中最值得重点强调的一点是,你的贡献可能被纳入译稿中,而我们会将其用在电子书中销售,由此得到的收入不会分给你。 26 | 27 | === 赞助商 28 | 29 | 本书的翻译活动得到以下赞助商的鼎力支持,特此鸣谢! 30 | 31 | [.sponsors] 32 | -- 33 | image::ruby-china-logo.jpg[] 34 | 35 | link:https://ruby-china.org/[Ruby China] 36 | -- 37 | 38 | === 封面 39 | 40 | 本书封面使用的图片出自 http://www.freepik.com[Freepik],特此感谢! 41 | -------------------------------------------------------------------------------- /manuscript/initialization.adoc: -------------------------------------------------------------------------------- 1 | [[the-rails-initialization-process]] 2 | == Rails 初始化过程 3 | 4 | // chinakr 翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 本文介绍 Rails 初始化过程的内部细节,内容较深,建议 Rails 高级开发者阅读。 9 | 10 | 读完本文后,您将学到: 11 | 12 | * 如何使用 `rails server`; 13 | * Rails 初始化过程的时间线; 14 | * 引导过程中所需的不同文件的所在位置; 15 | * `Rails::Server` 接口的定义和使用方式。 16 | -- 17 | 18 | NOTE: 本文原文尚未完工! 19 | 20 | 本文介绍默认情况下,Rails 应用初始化过程中的每一个方法调用,详细解释各个步骤的具体细节。本文将聚焦于使用 `rails server` 启动 Rails 应用时发生的事情。 21 | 22 | NOTE: 除非另有说明,本文中出现的路径都是相对于 Rails 或 Rails 应用所在目录的相对路径。 23 | 24 | TIP: 如果想一边阅读本文一边查看 link:https://github.com/rails/rails[Rails 源代码],推荐在 GitHub 中使用 `t` 快捷键打开文件查找器,以便快速查找相关文件。 25 | 26 | [[launch]] 27 | === 启动 28 | 29 | 首先介绍 Rails 应用引导和初始化的过程。我们可以通过 `rails console` 或 `rails server` 命令启动 Rails 应用。 30 | 31 | [[railties-exe-rails]] 32 | ==== `railties/exe/rails` 文件 33 | 34 | `rails server` 命令中的 `rails` 是位于加载路径中的一个 Ruby 可执行文件。这个文件包含如下内容: 35 | 36 | [source,ruby] 37 | ---- 38 | version = ">= 0" 39 | load Gem.bin_path('railties', 'rails', version) 40 | ---- 41 | 42 | 在 Rails 控制台中运行上述代码,可以看到加载的是 `railties/exe/rails` 文件。footnote:[在 Rails 5.0.1 中看到的是 `rails` 命令的使用帮助。——译者注]`railties/exe/rails` 文件的部分内容如下: 43 | 44 | [source,ruby] 45 | ---- 46 | require "rails/cli" 47 | ---- 48 | 49 | `railties/lib/rails/cli` 文件又会调用 `Rails::AppLoader.exec_app` 方法。 50 | 51 | [[railties-lib-rails-app-loader-rb]] 52 | ==== `railties/lib/rails/app_loader.rb` 文件 53 | 54 | `exec_app` 方法的主要作用是执行应用中的 `bin/rails` 文件。如果在当前文件夹中未找到 `bin/rails` 文件,就会继续在上层文件夹中查找,直到找到为止。因此,我们可以在 Rails 应用中的任何位置执行 `rails` 命令。 55 | 56 | 执行 `rails server` 命令时,实际执行的是等价的下述命令: 57 | 58 | [source,sh] 59 | ---- 60 | $ exec ruby bin/rails server 61 | ---- 62 | 63 | [[bin-rails]] 64 | ==== `bin/rails` 文件 65 | 66 | 此文件包含如下内容: 67 | 68 | [source,ruby] 69 | ---- 70 | #!/usr/bin/env ruby 71 | APP_PATH = File.expand_path('../../config/application', __FILE__) 72 | require_relative '../config/boot' 73 | require 'rails/commands' 74 | ---- 75 | 76 | 其中 `APP_PATH` 常量稍后将在 `rails/commands` 中使用。所加载的 `config/boot` 是应用中的 `config/boot.rb` 文件,用于加载并设置 Bundler。 77 | 78 | [[config-boot-rb]] 79 | ==== `config/boot.rb` 文件 80 | 81 | 此文件包含如下内容: 82 | 83 | [source,ruby] 84 | ---- 85 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 86 | 87 | require 'bundler/setup' # 设置 Gemfile 中列出的所有 gem 88 | ---- 89 | 90 | 标准的 Rails 应用中包含 `Gemfile` 文件,用于声明应用的所有依赖关系。`config/boot.rb` 文件会把 `ENV['BUNDLE_GEMFILE']` 设置为 `Gemfile` 文件的路径。如果 `Gemfile` 文件存在,就会加载 `bundler/setup`,Bundler 通过它设置 Gemfile 中依赖关系的加载路径。 91 | 92 | 标准的 Rails 应用依赖多个 gem,包括: 93 | 94 | * actionmailer 95 | * actionpack 96 | * actionview 97 | * activemodel 98 | * activerecord 99 | * activesupport 100 | * activejob 101 | * arel 102 | * builder 103 | * bundler 104 | * erubi 105 | * i18n 106 | * mail 107 | * mime-types 108 | * rack 109 | * rack-cache 110 | * rack-mount 111 | * rack-test 112 | * rails 113 | * railties 114 | * rake 115 | * sqlite3 116 | * thor 117 | * tzinfo 118 | 119 | [[rails-commands-rb]] 120 | ==== `rails/commands.rb` 文件 121 | 122 | 执行完 `config/boot.rb` 文件,下一步就要加载 `rails/commands`,其作用是扩展命令别名。在本例中(输入的命令为 `rails server`),`ARGV` 数组只包含将要传递的 `server` 命令: 123 | 124 | [source,ruby] 125 | ---- 126 | require "rails/command" 127 | 128 | aliases = { 129 | "g" => "generate", 130 | "d" => "destroy", 131 | "c" => "console", 132 | "s" => "server", 133 | "db" => "dbconsole", 134 | "r" => "runner", 135 | "t" => "test" 136 | } 137 | 138 | command = ARGV.shift 139 | command = aliases[command] || command 140 | 141 | Rails::Command.invoke command, ARGV 142 | ---- 143 | 144 | 如果输入的命令使用的是 `s` 而不是 `server`,Rails 就会在上面定义的 `aliases` 散列中查找对应的命令。 145 | 146 | [[rails-command-rb]] 147 | ==== `rails/command.rb` 文件 148 | 149 | 输入 Rails 命令时,`invoke` 尝试查找指定命名空间中的命令,如果找到就执行那个命令。 150 | 151 | 如果找不到命令,Rails 委托 Rake 执行同名任务。 152 | 153 | 如下述代码所示,`args` 为空时,`Rails::Command` 自动显示帮助信息。 154 | 155 | [source,ruby] 156 | ---- 157 | module Rails::Command 158 | class << self 159 | def invoke(namespace, args = [], **config) 160 | namespace = namespace.to_s 161 | namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace) 162 | namespace = "version" if %w( -v --version ).include? namespace 163 | 164 | if command = find_by_namespace(namespace) 165 | command.perform(namespace, args, config) 166 | else 167 | find_by_namespace("rake").perform(namespace, args, config) 168 | end 169 | end 170 | end 171 | end 172 | ---- 173 | 174 | 本例中输入的是 `server` 命令,因此 Rails 会进一步运行下述代码: 175 | 176 | [source,ruby] 177 | ---- 178 | module Rails 179 | module Command 180 | class ServerCommand < Base # :nodoc: 181 | def perform 182 | set_application_directory! 183 | 184 | Rails::Server.new.tap do |server| 185 | # Require application after server sets environment to propagate 186 | # the --environment option. 187 | require APP_PATH 188 | Dir.chdir(Rails.application.root) 189 | server.start 190 | end 191 | end 192 | end 193 | end 194 | end 195 | ---- 196 | 197 | 仅当 `config.ru` 文件无法找到时,才会切换到 Rails 应用根目录(`APP_PATH` 所在文件夹的上一层文件夹,其中 `APP_PATH` 指向 `config/application.rb` 文件)。然后运行 `Rails::Server` 类。 198 | 199 | [[actionpack-lib-action-dispatch-rb]] 200 | ==== `actionpack/lib/action_dispatch.rb` 文件 201 | 202 | Action Dispatch 是 Rails 框架的路由组件,提供路由、会话、常用中间件等功能。 203 | 204 | [[rails-commands-server-server-command-rb]] 205 | ==== `rails/commands/server/server_command.rb` 文件 206 | 207 | 此文件中定义的 `Rails::Server` 类,继承自 `Rack::Server` 类。当调用 `Rails::Server.new` 方法时,会调用此文件中定义的 `initialize` 方法: 208 | 209 | [source,ruby] 210 | ---- 211 | def initialize(*) 212 | super 213 | set_environment 214 | end 215 | ---- 216 | 217 | 首先调用的 `super` 方法,会调用 `Rack::Server` 类的 `initialize` 方法。 218 | 219 | [[rack-lib-rack-server-rb]] 220 | ==== Rack:`lib/rack/server.rb` 文件 221 | 222 | `Rack::Server` 类负责为所有基于 Rack 的应用(包括 Rails)提供通用服务器接口。 223 | 224 | `Rack::Server` 类的 `initialize` 方法的作用是设置几个变量: 225 | 226 | [source,ruby] 227 | ---- 228 | def initialize(options = nil) 229 | @options = options 230 | @app = options[:app] if options && options[:app] 231 | end 232 | ---- 233 | 234 | 在本例中,`options` 的值是 `nil`,因此这个方法什么也没做。 235 | 236 | 当 `super` 方法完成 `Rack::Server` 类的 `initialize` 方法的调用后,程序执行流程重新回到 `rails/commands/server/server_command.rb` 文件中。此时,会在 `Rails::Server` 对象的上下文中调用 `set_environment` 方法。乍一看这个方法什么也没做: 237 | 238 | [source,ruby] 239 | ---- 240 | def set_environment 241 | ENV["RAILS_ENV"] ||= options[:environment] 242 | end 243 | ---- 244 | 245 | 实际上,其中的 `options` 方法做了很多工作。`options` 方法在 `Rack::Server` 类中定义: 246 | 247 | [source,ruby] 248 | ---- 249 | def options 250 | @options ||= parse_options(ARGV) 251 | end 252 | ---- 253 | 254 | 而 `parse_options` 方法的定义如下: 255 | 256 | [source,ruby] 257 | ---- 258 | def parse_options(args) 259 | options = default_options 260 | 261 | # 请不要计算 CGI `ISINDEX` 参数的值。 262 | # http://www.meb.uni-bonn.de/docs/cgi/cl.html 263 | args.clear if ENV.include?("REQUEST_METHOD") 264 | 265 | options.merge! opt_parser.parse!(args) 266 | options[:config] = ::File.expand_path(options[:config]) 267 | ENV["RACK_ENV"] = options[:environment] 268 | options 269 | end 270 | ---- 271 | 272 | 其中 `default_options` 方法的定义如下: 273 | 274 | [source,ruby] 275 | ---- 276 | def default_options 277 | super.merge( 278 | Port: ENV.fetch("PORT", 3000).to_i, 279 | Host: ENV.fetch("HOST", "localhost").dup, 280 | DoNotReverseLookup: true, 281 | environment: (ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development").dup, 282 | daemonize: false, 283 | caching: nil, 284 | pid: Options::DEFAULT_PID_PATH, 285 | restart_cmd: restart_command) 286 | end 287 | ---- 288 | 289 | 在 `ENV` 散列中不存在 `REQUEST_METHOD` 键,因此可以跳过该行。下一行会合并 `opt_parser` 方法返回的选项,其中 `opt_parser` 方法在 `Rack::Server` 类中定义: 290 | 291 | [source,ruby] 292 | ---- 293 | def opt_parser 294 | Options.new 295 | end 296 | ---- 297 | 298 | `Options` 类在 `Rack::Server` 类中定义,但在 `Rails::Server` 类中被覆盖了,目的是为了接受不同参数。`Options` 类的 `parse!` 方法的定义如下: 299 | 300 | [source,ruby] 301 | ---- 302 | def parse!(args) 303 | args, options = args.dup, {} 304 | 305 | option_parser(options).parse! args 306 | 307 | options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" 308 | options[:server] = args.shift 309 | options 310 | end 311 | ---- 312 | 313 | 此方法为 `options` 散列的键赋值,稍后 Rails 将使用此散列确定服务器的运行方式。`initialize` 方法运行完成后,程序执行流程会跳回 `server` 命令,然后加载之前设置的 `APP_PATH`。 314 | 315 | [[config-application]] 316 | ==== `config/application.rb` 文件 317 | 318 | 执行 `require APP_PATH` 时,会加载 `config/application.rb` 文件(前文说过 `APP_PATH` 已经在 `bin/rails` 中定义)。这个文件也是应用的一部分,我们可以根据需要修改这个文件的内容。 319 | 320 | [[rails-server-start]] 321 | ==== `Rails::Server#start` 方法 322 | 323 | `config/application.rb` 文件加载完成后,会调用 `server.start` 方法。这个方法的定义如下: 324 | 325 | [source,ruby] 326 | ---- 327 | def start 328 | print_boot_information 329 | trap(:INT) { exit } 330 | create_tmp_directories 331 | setup_dev_caching 332 | log_to_stdout if options[:log_stdout] 333 | 334 | super 335 | ... 336 | end 337 | 338 | private 339 | def print_boot_information 340 | ... 341 | puts "=> Run `rails server -h` for more startup options" 342 | end 343 | 344 | def create_tmp_directories 345 | %w(cache pids sockets).each do |dir_to_make| 346 | FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) 347 | end 348 | end 349 | 350 | def setup_dev_caching 351 | if options[:environment] == "development" 352 | Rails::DevCaching.enable_by_argument(options[:caching]) 353 | end 354 | end 355 | 356 | def log_to_stdout 357 | wrapped_app # 对应用执行 touch 操作,以便设置记录器 358 | 359 | console = ActiveSupport::Logger.new(STDOUT) 360 | console.formatter = Rails.logger.formatter 361 | console.level = Rails.logger.level 362 | 363 | unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT) 364 | Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) 365 | end 366 | ---- 367 | 368 | 这是 Rails 初始化过程中第一次输出信息。`start` 方法为 `INT` 信号创建了一个陷阱,只要在服务器运行时按下 `CTRL-C`,服务器进程就会退出。我们看到,上述代码会创建 `tmp/cache`、`tmp/pids` 和 `tmp/sockets` 文件夹。然后,如果运行 `rails server` 命令时指定了 `--dev-caching` 参数,在开发环境中启用缓存。最后,调用 `wrapped_app` 方法,其作用是先创建 Rack 应用,再创建 `ActiveSupport::Logger` 类的实例。 369 | 370 | `super` 方法会调用 `Rack::Server.start` 方法,后者的定义如下: 371 | 372 | [source,ruby] 373 | ---- 374 | 375 | def start &blk 376 | if options[:warn] 377 | $-w = true 378 | end 379 | 380 | if includes = options[:include] 381 | $LOAD_PATH.unshift(*includes) 382 | end 383 | 384 | if library = options[:require] 385 | require library 386 | end 387 | 388 | if options[:debug] 389 | $DEBUG = true 390 | require 'pp' 391 | p options[:server] 392 | pp wrapped_app 393 | pp app 394 | end 395 | 396 | check_pid! if options[:pid] 397 | 398 | # 对包装后的应用执行 touch 操作,以便在创建守护进程之前 399 | # 加载 `config.ru` 文件(例如在 `chdir` 等操作之前) 400 | wrapped_app 401 | 402 | daemonize_app if options[:daemonize] 403 | 404 | write_pid if options[:pid] 405 | 406 | trap(:INT) do 407 | if server.respond_to?(:shutdown) 408 | server.shutdown 409 | else 410 | exit 411 | end 412 | end 413 | 414 | server.run wrapped_app, options, &blk 415 | end 416 | ---- 417 | 418 | 代码块最后一行中的 `server.run` 非常有意思。这里我们再次遇到了 `wrapped_app` 方法,这次我们要更深入地研究它(前文已经调用过 `wrapped_app` 方法,现在需要回顾一下)。 419 | 420 | [source,ruby] 421 | ---- 422 | @wrapped_app ||= build_app app 423 | ---- 424 | 425 | 其中 `app` 方法定义如下: 426 | 427 | [source,ruby] 428 | ---- 429 | def app 430 | @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config 431 | end 432 | ... 433 | private 434 | def build_app_and_options_from_config 435 | if !::File.exist? options[:config] 436 | abort "configuration #{options[:config]} not found" 437 | end 438 | 439 | app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) 440 | self.options.merge! options 441 | app 442 | end 443 | 444 | def build_app_from_string 445 | Rack::Builder.new_from_string(self.options[:builder]) 446 | end 447 | ---- 448 | 449 | `options[:config]` 的默认值为 `config.ru`,此文件包含如下内容: 450 | 451 | [source,ruby] 452 | ---- 453 | # 基于 Rack 的服务器使用此文件来启动应用。 454 | 455 | require_relative 'config/environment' 456 | run <%= app_const %> 457 | ---- 458 | 459 | `Rack::Builder.parse_file` 方法读取 `config.ru` 文件的内容,并使用下述代码解析文件内容: 460 | 461 | [source,ruby] 462 | ---- 463 | app = new_from_string cfgfile, config 464 | 465 | ... 466 | 467 | def self.new_from_string(builder_script, file="(rackup)") 468 | eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app", 469 | TOPLEVEL_BINDING, file, 0 470 | end 471 | ---- 472 | 473 | `Rack::Builder` 类的 `initialize` 方法会把接收到的代码块在 `Rack::Builder` 类的实例中执行,Rails 初始化过程中的大部分工作都在这一步完成。在 `config.ru` 文件中,加载 `config/environment.rb` 文件的这一行代码首先被执行: 474 | 475 | [source,ruby] 476 | ---- 477 | require_relative 'config/environment' 478 | ---- 479 | 480 | [[config-environment-rb]] 481 | ==== `config/environment.rb` 文件 482 | 483 | `config.ru` 文件(`rails server`)和 Passenger 都需要加载此文件。这两种运行服务器的方式直到这里才出现了交集,此前的一切工作都只是围绕 Rack 和 Rails 的设置进行的。 484 | 485 | 此文件以加载 `config/application.rb` 文件开始: 486 | 487 | [source,ruby] 488 | ---- 489 | require_relative 'application' 490 | ---- 491 | 492 | [[config-application-rb]] 493 | ==== `config/application.rb` 文件 494 | 495 | 此文件会加载 `config/boot.rb` 文件: 496 | 497 | [source,ruby] 498 | ---- 499 | require_relative 'boot' 500 | ---- 501 | 502 | 对于 `rails server` 这种启动服务器的方式,之前并未加载过 `config/boot.rb` 文件,因此这里会加载该文件;对于 Passenger,之前已经加载过该文件,这里就不会重复加载了。 503 | 504 | 接下来,有趣的事情就要开始了! 505 | 506 | [[loading-rails]] 507 | === 加载 Rails 508 | 509 | `config/application.rb` 文件的下一行是: 510 | 511 | [source,ruby] 512 | ---- 513 | require 'rails/all' 514 | ---- 515 | 516 | [[railties-lib-rails-all-rb]] 517 | ==== `railties/lib/rails/all.rb` 文件 518 | 519 | 此文件负责加载 Rails 中所有独立的框架: 520 | 521 | [source,ruby] 522 | ---- 523 | 524 | require "rails" 525 | 526 | %w( 527 | active_record/railtie 528 | action_controller/railtie 529 | action_view/railtie 530 | action_mailer/railtie 531 | active_job/railtie 532 | action_cable/engine 533 | rails/test_unit/railtie 534 | sprockets/railtie 535 | ).each do |railtie| 536 | begin 537 | require railtie 538 | rescue LoadError 539 | end 540 | end 541 | ---- 542 | 543 | 这些框架加载完成后,就可以在 Rails 应用中使用了。这里不会深入介绍每个框架,而是鼓励读者自己动手试验和探索。 544 | 545 | 现在,我们只需记住,Rails 的常见功能,例如 Rails 引擎、I18n 和 Rails 配置,都在这里定义好了。 546 | 547 | [[config-environment-rb-1]] 548 | ==== 回到 `config/environment.rb` 文件 549 | 550 | `config/application.rb` 文件的其余部分定义了 `Rails::Application` 的配置,当应用的初始化全部完成后就会使用这些配置。当 `config/application.rb` 文件完成了 Rails 的加载和应用命名空间的定义后,程序执行流程再次回到 `config/environment.rb` 文件。在这里会通过 `rails/application.rb` 文件中定义的 `Rails.application.initialize!` 方法完成应用的初始化。 551 | 552 | [[railties-lib-rails-application-rb]] 553 | ==== `railties/lib/rails/application.rb` 文件 554 | 555 | `initialize!` 方法的定义如下: 556 | 557 | [source,ruby] 558 | ---- 559 | def initialize!(group=:default) #:nodoc: 560 | raise "Application has been already initialized." if @initialized 561 | run_initializers(group, self) 562 | @initialized = true 563 | self 564 | end 565 | ---- 566 | 567 | 我们看到,一个应用只能初始化一次。`railties/lib/rails/initializable.rb` 文件中定义的 `run_initializers` 方法负责运行初始化程序: 568 | 569 | [source,ruby] 570 | ---- 571 | def run_initializers(group=:default, *args) 572 | return if instance_variable_defined?(:@ran) 573 | initializers.tsort_each do |initializer| 574 | initializer.run(*args) if initializer.belongs_to?(group) 575 | end 576 | @ran = true 577 | end 578 | ---- 579 | 580 | `run_initializers` 方法的代码比较复杂,Rails 会遍历所有类的祖先,以查找能够响应 `initializers` 方法的类。对于找到的类,首先按名称排序,然后依次调用 `initializers` 方法。例如,`Engine` 类通过为所有的引擎提供 `initializers` 方法而使它们可用。 581 | 582 | `railties/lib/rails/application.rb` 文件中定义的 `Rails::Application` 类,定义了 `bootstrap`、`railtie` 和 `finisher` 初始化程序。`bootstrap` 初始化程序负责完成应用初始化的准备工作(例如初始化记录器),而 `finisher` 初始化程序(例如创建中间件栈)总是最后运行。`railtie` 初始化程序在 `Rails::Application` 类自身中定义,在 `bootstrap` 之后、`finishers` 之前运行。 583 | 584 | 应用初始化完成后,程序执行流程再次回到 `Rack::Server` 类。 585 | 586 | [[rack-lib-rack-server-rb-1]] 587 | ==== Rack:`lib/rack/server.rb` 文件 588 | 589 | 程序执行流程上一次离开此文件是在定义 `app` 方法时: 590 | 591 | [source,ruby] 592 | ---- 593 | def app 594 | @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config 595 | end 596 | ... 597 | private 598 | def build_app_and_options_from_config 599 | if !::File.exist? options[:config] 600 | abort "configuration #{options[:config]} not found" 601 | end 602 | 603 | app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) 604 | self.options.merge! options 605 | app 606 | end 607 | 608 | def build_app_from_string 609 | Rack::Builder.new_from_string(self.options[:builder]) 610 | end 611 | ---- 612 | 613 | 此时,`app` 就是 Rails 应用本身(一个中间件),接下来 Rack 会调用所有已提供的中间件: 614 | 615 | [source,ruby] 616 | ---- 617 | def build_app(app) 618 | middleware[options[:environment]].reverse_each do |middleware| 619 | middleware = middleware.call(self) if middleware.respond_to?(:call) 620 | next unless middleware 621 | klass = middleware.shift 622 | app = klass.new(app, *middleware) 623 | end 624 | app 625 | end 626 | ---- 627 | 628 | 记住,在 `Server#start` 方法定义的最后一行代码中,通过 `wrapped_app` 方法调用了 `build_app` 方法。让我们回顾一下这行代码: 629 | 630 | [source,ruby] 631 | ---- 632 | server.run wrapped_app, options, &blk 633 | ---- 634 | 635 | 此时,`server.run` 方法的实现方式取决于我们所使用的服务器。例如,如果使用的是 Puma,`run` 方法的实现方式如下: 636 | 637 | [source,ruby] 638 | ---- 639 | ... 640 | DEFAULT_OPTIONS = { 641 | :Host => '0.0.0.0', 642 | :Port => 8080, 643 | :Threads => '0:16', 644 | :Verbose => false 645 | } 646 | 647 | def self.run(app, options = {}) 648 | options = DEFAULT_OPTIONS.merge(options) 649 | 650 | if options[:Verbose] 651 | app = Rack::CommonLogger.new(app, STDOUT) 652 | end 653 | 654 | if options[:environment] 655 | ENV['RACK_ENV'] = options[:environment].to_s 656 | end 657 | 658 | server = ::Puma::Server.new(app) 659 | min, max = options[:Threads].split(':', 2) 660 | 661 | puts "Puma #{::Puma::Const::PUMA_VERSION} starting..." 662 | puts "* Min threads: #{min}, max threads: #{max}" 663 | puts "* Environment: #{ENV['RACK_ENV']}" 664 | puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}" 665 | 666 | server.add_tcp_listener options[:Host], options[:Port] 667 | server.min_threads = min 668 | server.max_threads = max 669 | yield server if block_given? 670 | 671 | begin 672 | server.run.join 673 | rescue Interrupt 674 | puts "* Gracefully stopping, waiting for requests to finish" 675 | server.stop(true) 676 | puts "* Goodbye!" 677 | end 678 | 679 | end 680 | ---- 681 | 682 | 我们不会深入介绍服务器配置本身,不过这已经是 Rails 初始化过程的最后一步了。 683 | 684 | 本文高度概括的介绍,旨在帮助读者理解 Rails 应用的代码何时执行、如何执行,从而使读者成为更优秀的 Rails 开发者。要想掌握更多这方面的知识,Rails 源代码本身也许是最好的研究对象。 685 | -------------------------------------------------------------------------------- /manuscript/maintenance.adoc: -------------------------------------------------------------------------------- 1 | [part] 2 | = 第八部分 维护方针 3 | -------------------------------------------------------------------------------- /manuscript/maintenance_policy.adoc: -------------------------------------------------------------------------------- 1 | [[maintenance-policy-for-ruby-on-rails]] 2 | == Ruby on Rails 的维护方针 3 | 4 | // 安道翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 对 Rails 框架的支持分为四种:新功能、缺陷修正、安全问题和严重安全问题。各自的处理方式如下,所有版本号都使用 `X.Y.Z` 格式。 9 | -- 10 | 11 | Rails 遵照link:http://semver.org/[语义版本]更替版本号: 12 | 13 | 补丁版 Z:: 只修正缺陷,不改变 API,也不新增功能。安全修正可能例外。 14 | 15 | 小版本 Y:: 新增功能,可能改变 API(相当于语义版本中的大版本)。重大改变在之前的小版本或大版本中带有弃用提示。 16 | 17 | 大版本 X:: 新增功能,可能改变 API。Rails 的大版本和小版本之间的区别是对重大改变的处理方式不同,有时也有例外。 18 | 19 | [[new-features]] 20 | === 新功能 21 | 22 | 新功能只添加到 master 分支,不会包含在补丁版中。 23 | 24 | [[bug-fixes]] 25 | === 缺陷修正 26 | 27 | 只有最新的发布系列接收缺陷修正。如果修正的缺陷足够多,值得发布新的 gem,从这个分支中获取代码。 28 | 29 | 如果核心团队中有人同意支持更多的发布系列,也会包含在支持的系列中——这是特殊情况。 30 | 31 | 目前支持的系列:`5.1.Z`。 32 | 33 | [[security-issues]] 34 | === 安全问题 35 | 36 | 发现安全问题时,当前发布系列和下一个最新版接收补丁和新版本。 37 | 38 | 新版代码从最近的发布版中获取,应用安全补丁之后发布。然后把安全补丁应用到 x-y-stable 分支。例如,1.2.3 安全发布在 1.2.2 版的基础上得来,然后再把安全补丁应用到 1-2-stable 分支。因此,如果你使用 Rails 的最新版,很容易升级安全修正版。 39 | 40 | 目前支持的系列:`5.1.Z`、`5.0.Z`。 41 | 42 | [[severe-security-issues]] 43 | === 严重安全问题 44 | 45 | 发现严重安全问题时,会发布新版,最近的主发布系列也会接收补丁和新版。安全问题由核心团队甄别分类。 46 | 47 | 目前支持的系列:`5.1.Z`、`5.0.Z`、`4.2.Z`。 48 | 49 | [[unsupported-release-series]] 50 | === 不支持的发布系列 51 | 52 | 如果一个发布系列不再得到支持,你要自己负责处理缺陷和安全问题。我们可能会逆向移植,把修正代码发布到 Git 仓库中,但是不会发布新版本。如果你不想自己维护,应该升级到我们支持的版本。 53 | -------------------------------------------------------------------------------- /manuscript/models.adoc: -------------------------------------------------------------------------------- 1 | :sample: 2 | 3 | [part] 4 | = 第二部分 模型 5 | -------------------------------------------------------------------------------- /manuscript/plugins.adoc: -------------------------------------------------------------------------------- 1 | [[the-basics-of-creating-rails-plugins]] 2 | == Rails 插件开发简介 3 | 4 | // 安道翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | Rails 插件是对核心框架的扩展或修改。插件有下述作用: 9 | 10 | - 供开发者分享突发奇想,但不破坏稳定的代码基 11 | - 碎片式架构,代码自成一体,能按照自己的日程表修正或更新 12 | - 核心开发者使用的外延工具,不必把每个新特性都集成到核心框架中 13 | 14 | 读完本文后,您将学到: 15 | 16 | - 如何从零开始创建一个插件 17 | - 如何编写插件的代码和测试 18 | 19 | 本文使用测试驱动开发方式编写一个插件,它具有下述功能: 20 | 21 | - 扩展 Ruby 核心类,如 Hash 和 String 22 | - 通过传统的 `acts_as` 插件形式为 `ApplicationRecord` 添加方法 23 | - 说明生成器放在插件的什么位置 24 | 25 | 本文暂且假设你是热衷观察鸟类的人。你钟爱的鸟是绿啄木鸟(Yaffle),因此你想创建一个插件,供其他开发者分享心得。 26 | -- 27 | 28 | NOTE: 本文原文尚未完工! 29 | 30 | [[setup]] 31 | === 准备 32 | 33 | 目前,Rails 插件构建成 gem 的形式,叫做 gem 式插件(gemified plugin)。如果愿意,可以通过 RubyGems 和 Bundler 在多个 Rails 应用中共享。 34 | 35 | [[generate-a-gemified-plugin]] 36 | ==== 生成 gem 式插件 37 | 38 | Rails 自带一个 `rails plugin new` 命令,用于创建任何 Rails 扩展的骨架。这个命令还会生成一个虚设的 Rails 应用,用于运行集成测试。请使用下述命令创建这个插件: 39 | 40 | [source,sh] 41 | ---- 42 | $ rails plugin new yaffle 43 | ---- 44 | 45 | 如果想查看用法和选项,执行下述命令: 46 | 47 | [source,sh] 48 | ---- 49 | $ rails plugin new --help 50 | ---- 51 | 52 | [[testing-your-newly-generated-plugin]] 53 | === 测试新生成的插件 54 | 55 | 进入插件所在的目录,运行 `bundle install` 命令,然后使用 `bin/test` 命令运行生成的一个测试。 56 | 57 | 你会看到下述输出: 58 | 59 | [source] 60 | ---- 61 | 1 runs, 1 assertions, 0 failures, 0 errors, 0 skips 62 | ---- 63 | 64 | 这表明一切都正确生成了,接下来可以添加功能了。 65 | 66 | [[extending-core-classes]] 67 | === 扩展核心类 68 | 69 | 本节说明如何为 String 类添加一个方法,让它在整个 Rails 应用中都可以使用。 70 | 71 | 这里,我们为 String 添加的方法名为 `to_squawk`。首先,创建一个测试文件,写入几个断言: 72 | 73 | [source,ruby] 74 | ---- 75 | # yaffle/test/core_ext_test.rb 76 | 77 | require 'test_helper' 78 | 79 | class CoreExtTest < ActiveSupport::TestCase 80 | def test_to_squawk_prepends_the_word_squawk 81 | assert_equal "squawk! Hello World", "Hello World".to_squawk 82 | end 83 | end 84 | ---- 85 | 86 | 然后使用 `bin/test` 运行测试。这个测试应该失败,因为我们还没实现 `to_squawk` 方法。 87 | 88 | [source] 89 | ---- 90 | E 91 | 92 | Error: 93 | CoreExtTest#test_to_squawk_prepends_the_word_squawk: 94 | NoMethodError: undefined method `to_squawk' for "Hello World":String 95 | 96 | 97 | bin/test /path/to/yaffle/test/core_ext_test.rb:4 98 | 99 | . 100 | 101 | Finished in 0.003358s, 595.6483 runs/s, 297.8242 assertions/s. 102 | 103 | 2 runs, 1 assertions, 0 failures, 1 errors, 0 skips 104 | ---- 105 | 106 | 很好,下面可以开始开发了。 107 | 108 | 在 `lib/yaffle.rb` 文件中添加 `require 'yaffle/core_ext'`: 109 | 110 | [source,ruby] 111 | ---- 112 | # yaffle/lib/yaffle.rb 113 | 114 | require 'yaffle/core_ext' 115 | 116 | module Yaffle 117 | end 118 | ---- 119 | 120 | 最后,创建 `core_ext.rb` 文件,添加 `to_squawk` 方法: 121 | 122 | [source,ruby] 123 | ---- 124 | # yaffle/lib/yaffle/core_ext.rb 125 | 126 | String.class_eval do 127 | def to_squawk 128 | "squawk! #{self}".strip 129 | end 130 | end 131 | ---- 132 | 133 | 为了测试方法的行为是否得当,在插件目录中使用 `bin/test` 运行单元测试: 134 | 135 | [source] 136 | ---- 137 | 2 runs, 2 assertions, 0 failures, 0 errors, 0 skips 138 | ---- 139 | 140 | 为了实测一下,进入 `test/dummy` 目录,打开控制台: 141 | 142 | [source,sh] 143 | ---- 144 | $ bin/rails console 145 | >> "Hello World".to_squawk 146 | => "squawk! Hello World" 147 | ---- 148 | 149 | [[add-an-acts-as-method-to-active-record]] 150 | === 为 Active Record 添加“acts_as”方法 151 | 152 | 插件经常为模型添加名为 `acts_as_something` 的方法。这里,我们要编写一个名为 `acts_as_yaffle` 的方法,为 Active Record 添加 `squawk` 方法。 153 | 154 | 首先,创建几个文件: 155 | 156 | [source,ruby] 157 | ---- 158 | # yaffle/test/acts_as_yaffle_test.rb 159 | 160 | require 'test_helper' 161 | 162 | class ActsAsYaffleTest < ActiveSupport::TestCase 163 | end 164 | ---- 165 | 166 | [source,ruby] 167 | ---- 168 | # yaffle/lib/yaffle.rb 169 | 170 | require 'yaffle/core_ext' 171 | require 'yaffle/acts_as_yaffle' 172 | 173 | module Yaffle 174 | end 175 | ---- 176 | 177 | [source,ruby] 178 | ---- 179 | # yaffle/lib/yaffle/acts_as_yaffle.rb 180 | 181 | module Yaffle 182 | module ActsAsYaffle 183 | # 在这里编写你的代码 184 | end 185 | end 186 | ---- 187 | 188 | [[add-a-class-method]] 189 | ==== 添加一个类方法 190 | 191 | 这个插件将为模型添加一个名为 `last_squawk` 的方法。然而,插件的用户可能已经在模型中定义了同名方法,做其他用途使用。这个插件将允许修改插件的名称,为此我们要添加一个名为 `yaffle_text_field` 的类方法。 192 | 193 | 首先,为预期行为编写一个失败测试: 194 | 195 | [source,ruby] 196 | ---- 197 | # yaffle/test/acts_as_yaffle_test.rb 198 | 199 | require 'test_helper' 200 | 201 | class ActsAsYaffleTest < ActiveSupport::TestCase 202 | def test_a_hickwalls_yaffle_text_field_should_be_last_squawk 203 | assert_equal "last_squawk", Hickwall.yaffle_text_field 204 | end 205 | 206 | def test_a_wickwalls_yaffle_text_field_should_be_last_tweet 207 | assert_equal "last_tweet", Wickwall.yaffle_text_field 208 | end 209 | end 210 | ---- 211 | 212 | 执行 `bin/test` 命令,应该看到下述输出: 213 | 214 | [source] 215 | ---- 216 | # Running: 217 | 218 | ..E 219 | 220 | Error: 221 | ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet: 222 | NameError: uninitialized constant ActsAsYaffleTest::Wickwall 223 | 224 | 225 | bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8 226 | 227 | E 228 | 229 | Error: 230 | ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk: 231 | NameError: uninitialized constant ActsAsYaffleTest::Hickwall 232 | 233 | 234 | bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4 235 | 236 | 237 | 238 | Finished in 0.004812s, 831.2949 runs/s, 415.6475 assertions/s. 239 | 240 | 4 runs, 2 assertions, 0 failures, 2 errors, 0 skips 241 | ---- 242 | 243 | 输出表明,我们想测试的模型(Hickwall 和 Wickwall)不存在。为此,可以在 `test/dummy` 目录中运行下述命令生成: 244 | 245 | [source,sh] 246 | ---- 247 | $ cd test/dummy 248 | $ bin/rails generate model Hickwall last_squawk:string 249 | $ bin/rails generate model Wickwall last_squawk:string last_tweet:string 250 | ---- 251 | 252 | 然后,进入虚设的应用,迁移数据库,创建所需的数据库表。首先,执行: 253 | 254 | [source,sh] 255 | ---- 256 | $ cd test/dummy 257 | $ bin/rails db:migrate 258 | ---- 259 | 260 | 同时,修改 Hickwall 和 Wickwall 模型,让它们知道自己的行为像绿啄木鸟。 261 | 262 | [source,ruby] 263 | ---- 264 | # test/dummy/app/models/hickwall.rb 265 | 266 | class Hickwall < ApplicationRecord 267 | acts_as_yaffle 268 | end 269 | 270 | # test/dummy/app/models/wickwall.rb 271 | 272 | class Wickwall < ApplicationRecord 273 | acts_as_yaffle yaffle_text_field: :last_tweet 274 | end 275 | ---- 276 | 277 | 再添加定义 `acts_as_yaffle` 方法的代码: 278 | 279 | [source,ruby] 280 | ---- 281 | # yaffle/lib/yaffle/acts_as_yaffle.rb 282 | 283 | module Yaffle 284 | module ActsAsYaffle 285 | extend ActiveSupport::Concern 286 | 287 | included do 288 | end 289 | 290 | module ClassMethods 291 | def acts_as_yaffle(options = {}) 292 | # your code will go here 293 | end 294 | end 295 | end 296 | end 297 | 298 | # test/dummy/app/models/application_record.rb 299 | 300 | class ApplicationRecord < ActiveRecord::Base 301 | include Yaffle::ActsAsYaffle 302 | 303 | self.abstract_class = true 304 | end 305 | ---- 306 | 307 | 然后,回到插件的根目录(`cd ../..`),使用 `bin/test` 再次运行测试: 308 | 309 | [source] 310 | ---- 311 | # Running: 312 | 313 | .E 314 | 315 | Error: 316 | ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk: 317 | NoMethodError: undefined method `yaffle_text_field' for # 318 | 319 | 320 | bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4 321 | 322 | E 323 | 324 | Error: 325 | ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet: 326 | NoMethodError: undefined method `yaffle_text_field' for # 327 | 328 | 329 | bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8 330 | 331 | . 332 | 333 | Finished in 0.008263s, 484.0999 runs/s, 242.0500 assertions/s. 334 | 335 | 4 runs, 2 assertions, 0 failures, 2 errors, 0 skips 336 | ---- 337 | 338 | 快完工了……接下来实现 `acts_as_yaffle` 方法,让测试通过: 339 | 340 | [source,ruby] 341 | ---- 342 | # yaffle/lib/yaffle/acts_as_yaffle.rb 343 | 344 | module Yaffle 345 | module ActsAsYaffle 346 | extend ActiveSupport::Concern 347 | 348 | included do 349 | end 350 | 351 | module ClassMethods 352 | def acts_as_yaffle(options = {}) 353 | cattr_accessor :yaffle_text_field 354 | self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s 355 | end 356 | end 357 | end 358 | end 359 | 360 | # test/dummy/app/models/application_record.rb 361 | 362 | class ApplicationRecord < ActiveRecord::Base 363 | include Yaffle::ActsAsYaffle 364 | 365 | self.abstract_class = true 366 | end 367 | ---- 368 | 369 | 再次运行 `bin/test`,测试应该都能通过: 370 | 371 | [source] 372 | ---- 373 | 4 runs, 4 assertions, 0 failures, 0 errors, 0 skips 374 | ---- 375 | 376 | [[add-an-instance-method]] 377 | ==== 添加一个实例方法 378 | 379 | 这个插件能为任何模型添加调用 `acts_as_yaffle` 方法的 `squawk` 方法。`squawk` 方法的作用很简单,设定数据库中某个字段的值。 380 | 381 | 首先,为预期行为编写一个失败测试: 382 | 383 | [source,ruby] 384 | ---- 385 | # yaffle/test/acts_as_yaffle_test.rb 386 | require 'test_helper' 387 | 388 | class ActsAsYaffleTest < ActiveSupport::TestCase 389 | def test_a_hickwalls_yaffle_text_field_should_be_last_squawk 390 | assert_equal "last_squawk", Hickwall.yaffle_text_field 391 | end 392 | 393 | def test_a_wickwalls_yaffle_text_field_should_be_last_tweet 394 | assert_equal "last_tweet", Wickwall.yaffle_text_field 395 | end 396 | 397 | def test_hickwalls_squawk_should_populate_last_squawk 398 | hickwall = Hickwall.new 399 | hickwall.squawk("Hello World") 400 | assert_equal "squawk! Hello World", hickwall.last_squawk 401 | end 402 | 403 | def test_wickwalls_squawk_should_populate_last_tweet 404 | wickwall = Wickwall.new 405 | wickwall.squawk("Hello World") 406 | assert_equal "squawk! Hello World", wickwall.last_tweet 407 | end 408 | end 409 | ---- 410 | 411 | 运行测试,确保最后两个测试的失败消息中有“NoMethodError: undefined method pass:[`]squawk'”。然后,按照下述方式修改 `acts_as_yaffle.rb` 文件: 412 | 413 | [source,ruby] 414 | ---- 415 | # yaffle/lib/yaffle/acts_as_yaffle.rb 416 | 417 | module Yaffle 418 | module ActsAsYaffle 419 | extend ActiveSupport::Concern 420 | 421 | included do 422 | end 423 | 424 | module ClassMethods 425 | def acts_as_yaffle(options = {}) 426 | cattr_accessor :yaffle_text_field 427 | self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s 428 | 429 | include Yaffle::ActsAsYaffle::LocalInstanceMethods 430 | end 431 | end 432 | 433 | module LocalInstanceMethods 434 | def squawk(string) 435 | write_attribute(self.class.yaffle_text_field, string.to_squawk) 436 | end 437 | end 438 | end 439 | end 440 | 441 | # test/dummy/app/models/application_record.rb 442 | 443 | class ApplicationRecord < ActiveRecord::Base 444 | include Yaffle::ActsAsYaffle 445 | 446 | self.abstract_class = true 447 | end 448 | ---- 449 | 450 | 最后再运行一次 `bin/test`,应该看到: 451 | 452 | [source] 453 | ---- 454 | 6 runs, 6 assertions, 0 failures, 0 errors, 0 skips 455 | ---- 456 | 457 | [NOTE] 458 | ==== 459 | 这里使用 `write_attribute` 写入模型中的字段,这只是插件与模型交互的方式之一,并不总是应该使用它。例如,也可以使用: 460 | 461 | [source,ruby] 462 | ---- 463 | send("#{self.class.yaffle_text_field}=", string.to_squawk) 464 | ---- 465 | ==== 466 | 467 | [[generators]] 468 | === 生成器 469 | 470 | gem 中可以包含生成器,只需将其放在插件的 `lib/generators` 目录中。创建生成器的更多信息参见<>。 471 | 472 | [[publishing-your-gem]] 473 | === 发布 gem 474 | 475 | 正在开发的 gem 式插件可以通过 Git 仓库轻易分享。如果想与他人分享这个 Yaffle gem,只需把代码纳入一个 Git 仓库(如 GitHub),然后在想使用它的应用中,在 Gemfile 中添加一行代码: 476 | 477 | [source,ruby] 478 | ---- 479 | gem 'yaffle', git: 'git://github.com/yaffle_watcher/yaffle.git' 480 | ---- 481 | 482 | 运行 `bundle install` 之后,应用就可以使用插件提供的功能了。 483 | 484 | gem 式插件准备好正式发布之后,可以发布到 http://www.rubygems.org/[RubyGems] 网站中。关于这个话题的详细信息,参阅“link:http://blog.thepete.net/2010/11/creating-and-publishing-your-first-ruby.html[Creating and Publishing Your First Ruby Gem]”一文。 485 | 486 | [[rdoc-documentation]] 487 | === RDoc 文档 488 | 489 | 插件稳定后可以部署了,为了他人使用方便,一定要编写文档!幸好,为插件编写文档并不难。 490 | 491 | 首先,更新 README 文件,说明插件的用法。要包含以下几个要点: 492 | 493 | - 你的名字 494 | - 插件用法 495 | - 如何把插件的功能添加到应用中(举几个示例,说明常见用例) 496 | - 提醒、缺陷或小贴士,这样能节省用户的时间 497 | 498 | README 文件写好之后,为开发者将使用的方法添加 rdoc 注释。通常,还要为不在公开 API 中的代码添加 `#:nodoc:` 注释。 499 | 500 | 添加好注释之后,进入插件所在的目录,执行: 501 | 502 | [source,sh] 503 | ---- 504 | $ bundle exec rake rdoc 505 | ---- 506 | 507 | [[references]] 508 | === 参考资料 509 | 510 | - https://github.com/radar/guides/blob/master/gem-development.md[Developing a RubyGem using Bundler] 511 | - http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended/[Using .gemspecs as Intended] 512 | - http://guides.rubygems.org/specification-reference/[Gemspec Reference] 513 | -------------------------------------------------------------------------------- /manuscript/rails_application_templates.adoc: -------------------------------------------------------------------------------- 1 | [[rails-application-templates]] 2 | == Rails 应用模板 3 | 4 | // 安道翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 应用模板是包含 DSL 的 Ruby 文件,作用是为新建的或现有的 Rails 项目添加 gem 和初始化脚本等。 9 | 10 | 读完本文后,您将学到: 11 | 12 | - 如何使用模板生成和定制 Rails 应用; 13 | - 如何使用 Rails Templates API 编写可复用的应用模板。 14 | -- 15 | 16 | [[usage]] 17 | === 用法 18 | 19 | 若想使用模板,调用 Rails 生成器时把模板的位置传给 `-m` 选项。模板的位置可以是文件路径,也可以是 URL。 20 | 21 | [source,sh] 22 | ---- 23 | $ rails new blog -m ~/template.rb 24 | $ rails new blog -m http://example.com/template.rb 25 | ---- 26 | 27 | 可以使用 `app:template` 任务在现有的 Rails 应用中使用模板。模板的位置要通过 `LOCATION` 环境变量指定。同样,模板的位置可以是文件路径,也可以是 URL。 28 | 29 | [source,sh] 30 | ---- 31 | $ bin/rails app:template LOCATION=~/template.rb 32 | $ bin/rails app:template LOCATION=http://example.com/template.rb 33 | ---- 34 | 35 | [[template-api]] 36 | === Templates API 37 | 38 | Rails Templates API 易于理解。下面是一个典型的 Rails 模板: 39 | 40 | [source,ruby] 41 | ---- 42 | # template.rb 43 | generate(:scaffold, "person name:string") 44 | route "root to: 'people#index'" 45 | rails_command("db:migrate") 46 | 47 | after_bundle do 48 | git :init 49 | git add: "." 50 | git commit: %Q{ -m 'Initial commit' } 51 | end 52 | ---- 53 | 54 | 下面各小节简介这个 API 提供的主要方法。 55 | 56 | [[gem-args]] 57 | ==== `gem(*args)` 58 | 59 | 在生成的应用的 `Gemfile` 中添加指定的 `gem` 条目。 60 | 61 | 例如,如果应用依赖 `bj` 和 `nokogiri`: 62 | 63 | [source,ruby] 64 | ---- 65 | gem "bj" 66 | gem "nokogiri" 67 | ---- 68 | 69 | 请注意,这么做不会为你安装 gem,你要执行 `bundle install` 命令安装。 70 | 71 | [source,sh] 72 | ---- 73 | $ bundle install 74 | ---- 75 | 76 | [[gem-group-names-block]] 77 | ==== `gem_group(*names, &block)` 78 | 79 | 把指定的 gem 条目放在一个分组中。 80 | 81 | 例如,如果只想在 `development` 和 `test` 组中加载 `rspec-rails`: 82 | 83 | [source,ruby] 84 | ---- 85 | gem_group :development, :test do 86 | gem "rspec-rails" 87 | end 88 | ---- 89 | 90 | [[add-source-source-options-block]] 91 | ==== `add_source(source, options={}, &block)` 92 | 93 | 在生成的应用的 `Gemfile` 中添加指定的源。 94 | 95 | 例如,如果想安装 `"http://code.whytheluckystiff.net"` 源中的 gem: 96 | 97 | [source,ruby] 98 | ---- 99 | add_source "http://code.whytheluckystiff.net" 100 | ---- 101 | 102 | 如果提供块,块中的 gem 条目放在指定的源分组里: 103 | 104 | [source,ruby] 105 | ---- 106 | add_source "http://gems.github.com/" do 107 | gem "rspec-rails" 108 | end 109 | ---- 110 | 111 | [[environment-application-data-nil-options-block]] 112 | ==== `environment`/`application(data=nil, options={}, &block)` 113 | 114 | 在 `config/application.rb` 文件中的 `Application` 类里添加一行代码。 115 | 116 | 如果指定了 `options[:env]`,代码添加到 `config/environments` 目录中对应的文件中。 117 | 118 | [source,ruby] 119 | ---- 120 | environment 'config.action_mailer.default_url_options = {host: "http://yourwebsite.example.com"}', env: 'production' 121 | ---- 122 | 123 | `data` 参数的位置可以使用块。 124 | 125 | [[vendor-lib-file-initializer-filename-data-nil-block]] 126 | ==== `vendor`/`lib`/`file`/`initializer(filename, data = nil, &block)` 127 | 128 | 在生成的应用的 `config/initializers` 目录中添加一个初始化脚本。 129 | 130 | 假设你想使用 `Object#not_nil?` 和 `Object#not_blank?` 方法: 131 | 132 | [source,ruby] 133 | ---- 134 | initializer 'bloatlol.rb', <<-CODE 135 | class Object 136 | def not_nil? 137 | !nil? 138 | end 139 | 140 | def not_blank? 141 | !blank? 142 | end 143 | end 144 | CODE 145 | ---- 146 | 147 | 类似地,`lib()` 方法在 `lib/ directory` 目录中创建一个文件,`vendor()` 方法在 `vendor/` 目录中创建一个文件。 148 | 149 | 此外还有个 `file()` 方法,它的参数是一个相对于 `Rails.root` 的路径,用于创建所需的目录和文件: 150 | 151 | [source,ruby] 152 | ---- 153 | file 'app/components/foo.rb', <<-CODE 154 | class Foo 155 | end 156 | CODE 157 | ---- 158 | 159 | 上述代码会创建 `app/components` 目录,然后在里面创建 `foo.rb` 文件。 160 | 161 | [[rakefile-filename-data-nil-block]] 162 | ==== `rakefile(filename, data = nil, &block)` 163 | 164 | 在 `lib/tasks` 目录中创建一个 Rake 文件,写入指定的任务: 165 | 166 | [source,ruby] 167 | ---- 168 | rakefile("bootstrap.rake") do 169 | <<-TASK 170 | namespace :boot do 171 | task :strap do 172 | puts "i like boots!" 173 | end 174 | end 175 | TASK 176 | end 177 | ---- 178 | 179 | 上述代码会创建 `lib/tasks/bootstrap.rake` 文件,写入 `boot:strap` rake 任务。 180 | 181 | [[generate-what-args]] 182 | ==== `generate(what, *args)` 183 | 184 | 运行指定的 Rails 生成器,并传入指定的参数。 185 | 186 | [source,ruby] 187 | ---- 188 | generate(:scaffold, "person", "name:string", "address:text", "age:number") 189 | ---- 190 | 191 | [[run-command]] 192 | ==== `run(command)` 193 | 194 | 运行任意命令。作用类似于反引号。假如你想删除 `README.rdoc` 文件: 195 | 196 | [source,ruby] 197 | ---- 198 | run "rm README.rdoc" 199 | ---- 200 | 201 | [[rails-command-command-options]] 202 | ==== `rails_command(command, options = {})` 203 | 204 | 在 Rails 应用中运行指定的任务。假如你想迁移数据库: 205 | 206 | [source,ruby] 207 | ---- 208 | rails_command "db:migrate" 209 | ---- 210 | 211 | 还可以在不同的 Rails 环境中运行任务: 212 | 213 | [source,ruby] 214 | ---- 215 | rails_command "db:migrate", env: 'production' 216 | ---- 217 | 218 | 还能以超级用户的身份运行任务: 219 | 220 | [source,ruby] 221 | ---- 222 | rails_command "log:clear", sudo: true 223 | ---- 224 | 225 | [[route-routing-code]] 226 | ==== `route(routing_code)` 227 | 228 | 在 `config/routes.rb` 文件中添加一条路由规则。在前面几节中,我们使用脚手架生成了 Person 资源,还删除了 `README.rdoc` 文件。现在,把 `PeopleController#index` 设为应用的首页: 229 | 230 | [source,ruby] 231 | ---- 232 | route "root to: 'person#index'" 233 | ---- 234 | 235 | [[inside-dir]] 236 | ==== `inside(dir)` 237 | 238 | 在指定的目录中执行命令。假如你有一份最新版 Rails,想通过符号链接指向 `rails` 命令,可以这么做: 239 | 240 | [source,ruby] 241 | ---- 242 | inside('vendor') do 243 | run "ln -s ~/commit-rails/rails rails" 244 | end 245 | ---- 246 | 247 | [[ask-question]] 248 | ==== `ask(question)` 249 | 250 | `ask()` 方法获取用户的反馈,供模板使用。假如你想让用户为新添加的库起个响亮的名称: 251 | 252 | [source,ruby] 253 | ---- 254 | lib_name = ask("What do you want to call the shiny library ?") 255 | lib_name << ".rb" unless lib_name.index(".rb") 256 | 257 | lib lib_name, <<-CODE 258 | class Shiny 259 | end 260 | CODE 261 | ---- 262 | 263 | [[yes-questionmark-question-or-no-questionmark-question]] 264 | ==== `yes?(question)` 或 `no?(question)` 265 | 266 | 这两个方法用于询问用户问题,然后根据用户的回答决定流程。假如你想在用户同意时才冰封 Rails: 267 | 268 | [source,ruby] 269 | ---- 270 | rails_command("rails:freeze:gems") if yes?("Freeze rails gems?") 271 | # no?(question) 的作用正好相反 272 | ---- 273 | 274 | [[git-command]] 275 | ==== `git(:command)` 276 | 277 | 在 Rails 模板中可以运行任意 Git 命令: 278 | 279 | [source,ruby] 280 | ---- 281 | git :init 282 | git add: "." 283 | git commit: "-a -m 'Initial commit'" 284 | ---- 285 | 286 | [[after-bundle-block]] 287 | ==== `after_bundle(&block)` 288 | 289 | 注册一个回调,在安装好 gem 并生成 binstubs 之后执行。可以用来把生成的文件纳入版本控制: 290 | 291 | [source,ruby] 292 | ---- 293 | after_bundle do 294 | git :init 295 | git add: '.' 296 | git commit: "-a -m 'Initial commit'" 297 | end 298 | ---- 299 | 300 | 即便传入 `--skip-bundle` 和(或) `--skip-spring` 选项,也会执行这个回调。 301 | 302 | [[advanced-usage]] 303 | === 高级用法 304 | 305 | 应用模板在 `Rails::Generators::AppGenerator` 实例的上下文中运行,用到了 https://github.com/erikhuda/thor/blob/master/lib/thor/actions.rb#L207[Thor 提供的 `apply` 方法]。因此,你可以扩展或修改这个实例,满足自己的需求。 306 | 307 | 例如,覆盖指定模板位置的 `source_paths` 方法。现在,`copy_file` 等方法能接受相对于模板位置的相对路径。 308 | 309 | [source,ruby] 310 | ---- 311 | def source_paths 312 | [File.expand_path(File.dirname(__FILE__))] 313 | end 314 | ---- 315 | -------------------------------------------------------------------------------- /manuscript/rails_on_rack.adoc: -------------------------------------------------------------------------------- 1 | [[rails-on-rack]] 2 | == Rails on Rack 3 | 4 | // 安道翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 本文简介 Rails 与 Rack 的集成,以及与其他 Rack 组件的配合。 9 | 10 | 读完本文后,您将学到: 11 | 12 | - 如何在 Rails 应用中使用 Rack 中间件; 13 | - Action Pack 内部的中间件栈; 14 | - 如何自定义中间件栈。 15 | -- 16 | 17 | WARNING: 本文假定你对 Rack 协议和相关概念有一定了解,例如中间件、URL 映射和 `Rack::Builder`。 18 | 19 | [[introduction-to-rack]] 20 | === Rack 简介 21 | 22 | Rack 为使用 Ruby 开发的 Web 应用提供最简单的模块化接口,而且适应性强。Rack 使用最简单的方式包装 HTTP 请求和响应,从而抽象了 Web 服务器、Web 框架,以及二者之间的软件(称为中间件)的 API,统一成一个方法调用。 23 | 24 | - http://rack.github.io/[Rack API 文档] 25 | 26 | 本文不详尽说明 Rack。如果你不了解 Rack 的基本概念,请参阅 <>。 27 | 28 | [[rails-on-rack-section]] 29 | === Rails on Rack 30 | 31 | // 这个标题的 ID 与本章的 ID 相同,因此在后面加上“section”以示区别。——Andor 32 | 33 | [[rails-application-s-rack-object]] 34 | ==== Rails 应用的 Rack 对象 35 | 36 | `Rails.application` 是 Rails 应用的主 Rack 应用对象。任何兼容 Rack 的 Web 服务器都应该使用 `Rails.application` 对象伺服 Rails 应用。 37 | 38 | [[rails-server]] 39 | ==== `rails server` 40 | 41 | `rails server` 负责创建 `Rack::Server` 对象和启动 Web 服务器。 42 | 43 | `rails server` 创建 `Rack::Server` 实例的方式如下: 44 | 45 | [source,ruby] 46 | ---- 47 | Rails::Server.new.tap do |server| 48 | require APP_PATH 49 | Dir.chdir(Rails.application.root) 50 | server.start 51 | end 52 | ---- 53 | 54 | `Rails::Server` 继承自 `Rack::Server`,像下面这样调用 `Rack::Server#start` 方法: 55 | 56 | [source,ruby] 57 | ---- 58 | class Server < ::Rack::Server 59 | def start 60 | ... 61 | super 62 | end 63 | end 64 | ---- 65 | 66 | [[rackup]] 67 | ==== `rackup` 68 | 69 | 如果不想使用 Rails 提供的 `rails server` 命令,而是使用 `rackup`,可以把下述代码写入 Rails 应用根目录中的 `config.ru` 文件里: 70 | 71 | [source,ruby] 72 | ---- 73 | # Rails.root/config.ru 74 | require_relative 'config/environment' 75 | run Rails.application 76 | ---- 77 | 78 | 然后使用下述命令启动服务器: 79 | 80 | [source,sh] 81 | ---- 82 | $ rackup config.ru 83 | ---- 84 | 85 | `rackup` 命令的各个选项可以通过下述命令查看: 86 | 87 | [source,sh] 88 | ---- 89 | $ rackup --help 90 | ---- 91 | 92 | [[development-and-auto-reloading]] 93 | ==== 开发和自动重新加载 94 | 95 | 中间件只加载一次,不会监视变化。若想让改动生效,必须重启服务器。 96 | 97 | [[action-dispatcher-middleware-stack]] 98 | === Action Dispatcher 中间件栈 99 | 100 | Action Dispatcher 的内部组件很多都实现为 Rack 中间件。`Rails::Application` 使用 `ActionDispatch::MiddlewareStack` 把不同的内部和外部中间件组合在一起,构成完整的 Rails Rack 中间件。 101 | 102 | NOTE: Rails 中的 `ActionDispatch::MiddlewareStack` 相当于 `Rack::Builder`,但是为了满足 Rails 的需求,前者更灵活,而且功能更多。 103 | 104 | [[inspecting-middleware-stack]] 105 | ==== 审查中间件栈 106 | 107 | Rails 提供了一个方便的任务,用于查看在用的中间件栈: 108 | 109 | [source,sh] 110 | ---- 111 | $ bin/rails middleware 112 | ---- 113 | 114 | 在新生成的 Rails 应用中,上述命令可能会输出下述内容: 115 | 116 | [source] 117 | ---- 118 | use Rack::Sendfile 119 | use ActionDispatch::Static 120 | use ActionDispatch::Executor 121 | use ActiveSupport::Cache::Strategy::LocalCache::Middleware 122 | use Rack::Runtime 123 | use Rack::MethodOverride 124 | use ActionDispatch::RequestId 125 | use ActionDispatch::RemoteIp 126 | use Sprockets::Rails::QuietAssets 127 | use Rails::Rack::Logger 128 | use ActionDispatch::ShowExceptions 129 | use WebConsole::Middleware 130 | use ActionDispatch::DebugExceptions 131 | use ActionDispatch::RemoteIp 132 | use ActionDispatch::Reloader 133 | use ActionDispatch::Callbacks 134 | use ActiveRecord::Migration::CheckPending 135 | use ActionDispatch::Cookies 136 | use ActionDispatch::Session::CookieStore 137 | use ActionDispatch::Flash 138 | use Rack::Head 139 | use Rack::ConditionalGet 140 | use Rack::ETag 141 | run MyApp.application.routes 142 | ---- 143 | 144 | 这里列出的默认中间件(以及其他一些)在 <>概述。 145 | 146 | [[configuring-middleware-stack]] 147 | ==== 配置中间件栈 148 | 149 | Rails 提供了一个简单的配置接口,`config.middleware`,用于在 `application.rb` 或针对环境的配置文件 `environments/.rb` 中添加、删除和修改中间件栈。 150 | 151 | [[adding-a-middleware]] 152 | ===== 添加中间件 153 | 154 | 可以通过下述任意一种方法向中间件栈里添加中间件: 155 | 156 | - `config.middleware.use(new_middleware, args)`:在中间件栈的末尾添加一个中间件。 157 | - `config.middleware.insert_before(existing_middleware, new_middleware, args)`:在中间件栈里指定现有中间件的前面添加一个中间件。 158 | - `config.middleware.insert_after(existing_middleware, new_middleware, args)`:在中间件栈里指定现有中间件的后面添加一个中间件。 159 | 160 | [source,ruby] 161 | ---- 162 | # config/application.rb 163 | 164 | # 把 Rack::BounceFavicon 放在默认 165 | config.middleware.use Rack::BounceFavicon 166 | 167 | # 在 ActionDispatch::Executor 后面添加 Lifo::Cache 168 | # 把 { page_cache: false } 参数传给 Lifo::Cache. 169 | config.middleware.insert_after ActionDispatch::Executor, Lifo::Cache, page_cache: false 170 | ---- 171 | 172 | [[swapping-a-middleware]] 173 | ===== 替换中间件 174 | 175 | 可以使用 `config.middleware.swap` 替换中间件栈里的现有中间件: 176 | 177 | [source,ruby] 178 | ---- 179 | # config/application.rb 180 | 181 | # 把 ActionDispatch::ShowExceptions 换成 Lifo::ShowExceptions 182 | config.middleware.swap ActionDispatch::ShowExceptions, Lifo::ShowExceptions 183 | ---- 184 | 185 | [[deleting-a-middleware]] 186 | ===== 删除中间件 187 | 188 | 在应用的配置文件中添加下面这行代码: 189 | 190 | [source,ruby] 191 | ---- 192 | # config/application.rb 193 | config.middleware.delete Rack::Runtime 194 | ---- 195 | 196 | 然后审查中间件栈,你会发现没有 `Rack::Runtime` 了: 197 | 198 | [source,sh] 199 | ---- 200 | $ bin/rails middleware 201 | (in /Users/lifo/Rails/blog) 202 | use ActionDispatch::Static 203 | use # 204 | ... 205 | run Rails.application.routes 206 | ---- 207 | 208 | 若想删除会话相关的中间件,这么做: 209 | 210 | [source,ruby] 211 | ---- 212 | # config/application.rb 213 | config.middleware.delete ActionDispatch::Cookies 214 | config.middleware.delete ActionDispatch::Session::CookieStore 215 | config.middleware.delete ActionDispatch::Flash 216 | ---- 217 | 218 | 若想删除浏览器相关的中间件,这么做: 219 | 220 | [source,ruby] 221 | ---- 222 | # config/application.rb 223 | config.middleware.delete Rack::MethodOverride 224 | ---- 225 | 226 | [[internal-middleware-stack]] 227 | ==== 内部中间件栈 228 | 229 | Action Controller 的大部分功能都实现成中间件。下面概述它们的作用。 230 | 231 | `Rack::Sendfile`:: 在服务器端设定 X-Sendfile 首部。通过 `config.action_dispatch.x_sendfile_header` 选项配置。 232 | 233 | `ActionDispatch::Static`:: 用于伺服 public 目录中的静态文件。如果把 `config.public_file_server.enabled` 设为 `false`,禁用这个中间件。 234 | 235 | `Rack::Lock`:: 把 `env["rack.multithread"]` 设为 `false`,把应用包装到 Mutex 中。 236 | 237 | `ActionDispatch::Executor`:: 用于在开发环境中以线程安全方式重新加载代码。 238 | 239 | `ActiveSupport::Cache::Strategy::LocalCache::Middleware`:: 用于缓存内存。这个缓存对线程不安全。 240 | 241 | `Rack::Runtime`:: 设定 X-Runtime 首部,包含执行请求的用时(单位为秒)。 242 | 243 | `Rack::MethodOverride`:: 如果设定了 `params[:_method]`,允许覆盖请求方法。`PUT` 和 `DELETE` 两个 HTTP 方法就是通过这个中间件提供支持的。 244 | 245 | `ActionDispatch::RequestId`:: 在响应中设定唯一的 `X-Request-Id` 首部,并启用 `ActionDispatch::Request#request_id` 方法。 246 | 247 | `ActionDispatch::RemoteIp`:: 检查 IP 欺骗攻击。 248 | 249 | `Sprockets::Rails::QuietAssets`:在日志中输出对静态资源的请求。 250 | 251 | `Rails::Rack::Logger`:: 通知日志,请求开始了。请求完毕后,清空所有相关日志。 252 | 253 | `ActionDispatch::ShowExceptions`:: 拯救应用返回的所有异常,调用处理异常的应用,把异常包装成对终端用户友好的格式。 254 | 255 | `ActionDispatch::DebugExceptions`:: 如果是本地请求,负责在日志中记录异常,并显示调试页面。 256 | 257 | `ActionDispatch::Reloader`:: 提供准备和清理回调,目的是在开发环境中协助重新加载代码。 258 | 259 | `ActionDispatch::Callbacks`:: 提供回调,在分派请求前后执行。 260 | 261 | `ActiveRecord::Migration::CheckPending`:: 检查有没有待运行的迁移,如果有,抛出 `ActiveRecord::PendingMigrationError`。 262 | 263 | `ActionDispatch::Cookies`:: 为请求设定 cookie。 264 | 265 | `ActionDispatch::Session::CookieStore`:: 负责把会话存储在 cookie 中。 266 | 267 | `ActionDispatch::Flash`:: 设置闪现消息的键。仅当为 `config.action_controller.session_store` 设定值时才启用。 268 | 269 | `Rack::Head`:: 把 HEAD 请求转换成 GET 请求,然后伺服 GET 请求。 270 | 271 | `Rack::ConditionalGet`:: 支持“条件 GET 请求”,如果页面没变,服务器不做响应。 272 | 273 | `Rack::ETag`:: 为所有字符串主体添加 ETag 首部。ETag 用于验证缓存。 274 | 275 | TIP: 在自定义的 Rack 栈中可以使用上述任何一个中间件。 276 | 277 | [[resources]] 278 | === 资源 279 | 280 | [[learning-rack]] 281 | ==== 学习 Rack 282 | 283 | - http://rack.github.io/[Rack 官方网站] 284 | - http://chneukirchen.org/blog/archive/2007/02/introducing-rack.html[Introducing Rack] 285 | 286 | [[understanding-middlewares]] 287 | ==== 理解中间件 288 | 289 | - http://railscasts.com/episodes/151-rack-middleware[Railscast 中讲解 Rack 中间件的视频] 290 | -------------------------------------------------------------------------------- /manuscript/release_notes.adoc: -------------------------------------------------------------------------------- 1 | [part] 2 | = 第九部分 发布记 3 | -------------------------------------------------------------------------------- /manuscript/ruby_on_rails_guides_guidelines.adoc: -------------------------------------------------------------------------------- 1 | [[ruby-on-rails-guides-guidelines]] 2 | == Ruby on Rails 指南指导方针 3 | 4 | // 安道翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 本文说明编写 Ruby on Rails 指南的指导方针。本文也遵守这一方针,本身就是个示例。 9 | 10 | 读完本文后,您将学到: 11 | 12 | - Rails 文档使用的约定; 13 | - 如何在本地生成指南。 14 | -- 15 | 16 | [[markdown]] 17 | === Markdown 18 | 19 | 指南使用 https://help.github.com/articles/github-flavored-markdown[GitHub Flavored Markdown] 编写。Markdown 有link:http://daringfireball.net/projects/markdown/syntax[完整的文档],还有link:http://daringfireball.net/projects/markdown/basics[速查表]。 20 | 21 | [[prologue]] 22 | === 序言 23 | 24 | 每篇文章的开头要有介绍性文字(蓝色区域中的简短介绍)。序言应该告诉读者文章的主旨,以及能让读者学到什么。可以以<>为例。 25 | 26 | [[headings]] 27 | === 标题 28 | 29 | 每篇文章的标题使用 `h1` 标签,文章中的小节使用 `h2` 标签,子节使用 `h3` 标签,以此类推。注意,生成的 HTML 从 `

` 标签开始。 30 | 31 | [source,md] 32 | ---- 33 | Guide Title 34 | =========== 35 | 36 | Section 37 | ------- 38 | 39 | ### Sub Section 40 | ---- 41 | 42 | 标题中除了介词、连词、冠词和“to be”这种形式的动词之外,每个词的首字母大写: 43 | 44 | [source,md] 45 | ---- 46 | #### Middleware Stack is an Array 47 | #### When are Objects Saved? 48 | ---- 49 | 50 | 行内格式与正文一样: 51 | 52 | [source,md] 53 | ---- 54 | ##### The `:content_type` Option 55 | ---- 56 | 57 | [[linking-to-the-api]] 58 | === 指向 API 的链接 59 | 60 | 指南生成程序使用下述方式处理指向 API(`api.rubyonrails.org`)的链接。 61 | 62 | 包含版本号的链接原封不动。例如,下述链接不做修改: 63 | 64 | [source] 65 | ---- 66 | http://api.rubyonrails.org/v5.0.1/classes/ActiveRecord/Attributes/ClassMethods.html 67 | ---- 68 | 69 | 请在发布记中使用这种链接,因为不管生成哪个版本的指南,发布记中的链接不应该变。 70 | 71 | 如果链接中没有版本号,而且生成的是最新开发版的指南,域名会替换成 `edgeapi.rubyonrails.org`。例如: 72 | 73 | [source] 74 | ---- 75 | http://api.rubyonrails.org/classes/ActionDispatch/Response.html 76 | ---- 77 | 78 | 会变成: 79 | 80 | [source] 81 | ---- 82 | http://edgeapi.rubyonrails.org/classes/ActionDispatch/Response.html 83 | ---- 84 | 85 | 如果链接中没有版本号,而生成的是某个版本的指南,会在链接中插入版本号。例如,生成 v5.1.0 的指南时,下述链接: 86 | 87 | [source] 88 | ---- 89 | http://api.rubyonrails.org/classes/ActionDispatch/Response.html 90 | ---- 91 | 92 | 会变成: 93 | 94 | [source] 95 | ---- 96 | http://api.rubyonrails.org/v5.1.0/classes/ActionDispatch/Response.html 97 | ---- 98 | 99 | 请勿直接链接到 `edgeapi.rubyonrails.org`。 100 | 101 | [[ruby-on-rails-guides-guidelines-api-documentation-guidelines]] 102 | === API 文档指导方针 103 | 104 | 指南和 API 应该连贯一致。尤其是<>中的下述几节,同样适用于指南: 105 | 106 | - <> 107 | - <> 108 | - <> 109 | - <> 110 | - <> 111 | 112 | [[html-guides]] 113 | === HTML 版指南 114 | 115 | 在生成指南之前,先确保你的系统中安装了 Bundler 的最新版。写作本文时,要在你的设备中安装 Bundler 1.3.5 或以上版本。 116 | 117 | 安装最新版 Bundler 的方法是,执行 `gem install bundler` 命令。 118 | 119 | [[html-guides-generation]] 120 | ==== 生成 121 | 122 | 若想生成全部指南,进入 `guides` 目录,执行 `bundle install` 命令之后再执行: 123 | 124 | [source,sh] 125 | ---- 126 | $ bundle exec rake guides:generate 127 | ---- 128 | 129 | 或者 130 | 131 | [source,sh] 132 | ---- 133 | $ bundle exec rake guides:generate:html 134 | ---- 135 | 136 | 得到的 HTML 文件在 `./output` 目录中。 137 | 138 | 如果只想处理 `my_guide.md`,使用 `ONLY` 环境变量: 139 | 140 | [source,sh] 141 | ---- 142 | $ touch my_guide.md 143 | $ bundle exec rake guides:generate ONLY=my_guide 144 | ---- 145 | 146 | 默认情况下,没有改动的文章不会处理,因此实际使用中很少用到 `ONLY`。 147 | 148 | 如果想强制处理所有文章,传入 `ALL=1`。 149 | 150 | 如果想生成英语之外的指南,可以把译文放在 `source` 中的子目录里(如 `source/es`),然后使用 `GUIDES_LANGUAGE` 环境变量: 151 | 152 | [source,sh] 153 | ---- 154 | $ bundle exec rake guides:generate GUIDES_LANGUAGE=es 155 | ---- 156 | 157 | 如果想查看可用于配置生成脚本的全部环境变量,只需执行: 158 | 159 | [source,sh] 160 | ---- 161 | $ rake 162 | ---- 163 | 164 | [[validation]] 165 | ==== 验证 166 | 167 | 请使用下述命令验证生成的 HTML: 168 | 169 | [source,sh] 170 | ---- 171 | $ bundle exec rake guides:validate 172 | ---- 173 | 174 | 尤其要注意,ID 是从标题的内容中生成的,往往会重复。生成指南时请设定 `WARNINGS=1`,监测重复的 ID。提醒消息中有建议的解决方案。 175 | 176 | [[kindle-guides]] 177 | === Kindle 版指南 178 | 179 | [[kindle-guides-generation]] 180 | ==== 生成 181 | 182 | 如果想生成 Kindle 版指南,使用下述 Rake 任务: 183 | 184 | [source,sh] 185 | ---- 186 | $ bundle exec rake guides:generate:kindle 187 | ---- 188 | -------------------------------------------------------------------------------- /manuscript/start_here.adoc: -------------------------------------------------------------------------------- 1 | :sample: 2 | 3 | [part] 4 | = 第一部分 新手入门 5 | -------------------------------------------------------------------------------- /manuscript/supplement.adoc: -------------------------------------------------------------------------------- 1 | [part] 2 | = 第十部分 补遗 3 | -------------------------------------------------------------------------------- /manuscript/views.adoc: -------------------------------------------------------------------------------- 1 | [part] 2 | = 第三部分 视图 3 | -------------------------------------------------------------------------------- /manuscript/working_with_javascript_in_rails.adoc: -------------------------------------------------------------------------------- 1 | [[working-with-javascript-in-rails]] 2 | == 在 Rails 中使用 JavaScript 3 | 4 | // 安道翻译 5 | 6 | [.chapter-abstract] 7 | -- 8 | 本文介绍 Rails 内建对 Ajax 和 JavaScript 等的支持,使用这些功能可以轻易地开发强大的 Ajax 动态应用。 9 | 10 | 本完本文后,您将学到: 11 | 12 | * Ajax 基础知识; 13 | * 非侵入式 JavaScript; 14 | * 如何使用 Rails 内建的辅助方法; 15 | * 如何在服务器端处理 Ajax; 16 | * Turbolinks gem。 17 | -- 18 | 19 | [[an-introduction-to-ajax]] 20 | === Ajax 简介 21 | 22 | 在理解 Ajax 之前,要先知道 Web 浏览器常规的工作原理。 23 | 24 | 在浏览器的地址栏中输入 `http://localhost:3000` 后,浏览器(客户端)会向服务器发起一个请求。然后浏览器处理响应,获取相关的静态资源文件,比如 JavaScript、样式表和图像,然后显示页面内容。点击链接后发生的事情也是如此:获取页面,获取静态资源,把全部内容放在一起,显示最终的网页。这个过程叫做“请求响应循环”。 25 | 26 | JavaScript 也可以向服务器发起请求,并解析响应。而且还能更新网页中的内容。因此,JavaScript 程序员可以编写只更新部分内容的网页,而不用从服务器获取完整的页面数据。这是一种强大的技术,我们称之为 Ajax。 27 | 28 | Rails 默认支持 CoffeeScript,后文所有的示例都用 CoffeeScript 编写。本文介绍的技术,在普通的 JavaScript 中也可以使用。 29 | 30 | 例如,下面这段 CoffeeScript 代码使用 jQuery 库发起一个 Ajax 请求: 31 | 32 | [source,coffeescript] 33 | ---- 34 | $.ajax(url: "/test").done (html) -> 35 | $("#results").append html 36 | ---- 37 | 38 | 这段代码从 `/test` 地址上获取数据,然后把结果追加到 `div#results` 元素中。 39 | 40 | Rails 内建了很多使用这种技术开发应用的功能,基本上无需自己动手编写上述代码。后文介绍 Rails 如何为开发这种应用提供协助,不过都构建在这种简单的技术之上。 41 | 42 | [[unobtrusive-javascript]] 43 | === 非侵入式 JavaScript 44 | 45 | Rails 使用一种叫做“非侵入式 JavaScript”(Unobtrusive JavaScript)的技术把 JavaScript 依附到 DOM 上。非侵入式 JavaScript 是前端开发社区推荐的做法,但有些教程可能会使用其他方式。 46 | 47 | 下面是编写 JavaScript 最简单的方式,你可能见过,这叫做“行间 JavaScript”: 48 | 49 | [source,html] 50 | ---- 51 | Paint it red 52 | ---- 53 | 54 | 点击链接后,链接的背景会变成红色。这种用法的问题是,如果点击链接后想执行大量 JavaScript 代码怎么办? 55 | 56 | [source,html] 57 | ---- 58 | Paint it green 59 | ---- 60 | 61 | 太别扭了,不是吗?我们可以把处理点击的代码定义成一个函数,用 CoffeeScript 编写如下: 62 | 63 | [source,coffeescript] 64 | ---- 65 | @paintIt = (element, backgroundColor, textColor) -> 66 | element.style.backgroundColor = backgroundColor 67 | if textColor? 68 | element.style.color = textColor 69 | ---- 70 | 71 | 然后在页面中这么写: 72 | 73 | [source,html] 74 | ---- 75 | Paint it red 76 | ---- 77 | 78 | 这种方法好点儿,但是如果很多链接需要同样的效果该怎么办呢? 79 | 80 | [source,html] 81 | ---- 82 | Paint it red 83 | Paint it green 84 | Paint it blue 85 | ---- 86 | 87 | 这样非常不符合 DRY 原则。为了解决这个问题,我们可以使用“事件”。在链接上添加一个 `data-*` 属性,然后把处理程序绑定到拥有这个属性的点击事件上: 88 | 89 | [source,coffee] 90 | ---- 91 | @paintIt = (element, backgroundColor, textColor) -> 92 | element.style.backgroundColor = backgroundColor 93 | if textColor? 94 | element.style.color = textColor 95 | 96 | $ -> 97 | $("a[data-background-color]").click (e) -> 98 | e.preventDefault() 99 | 100 | backgroundColor = $(this).data("background-color") 101 | textColor = $(this).data("text-color") 102 | paintIt(this, backgroundColor, textColor) 103 | ---- 104 | 105 | [source,html] 106 | ---- 107 | Paint it red 108 | Paint it green 109 | Paint it blue 110 | ---- 111 | 112 | 我们把这种方法称为“非侵入式 JavaScript”,因为 JavaScript 代码不再和 HTML 混合在一起。这样做正确分离了关注点,易于修改功能。我们可以轻易地把这种效果应用到其他链接上,只要添加相应的 `data` 属性即可。我们可以简化并拼接全部 JavaScript,然后在各个页面加载一个 JavaScript 文件,这样只在第一次请求时需要加载,后续请求都会直接从缓存中读取。“非侵入式 JavaScript”带来的好处太多了。 113 | 114 | Rails 团队极力推荐使用这种方式编写 CoffeeScript(以及 JavaScript),而且你会发现很多代码库都采用了这种方式。 115 | 116 | [[built-in-helpers]] 117 | === 内置的辅助方法 118 | 119 | [[remote-elements]] 120 | ==== 远程元素 121 | 122 | Rails 提供了很多视图辅助方法协助你生成 HTML,如果想在元素上实现 Ajax 效果也没问题。 123 | 124 | 因为使用的是非侵入式 JavaScript,所以 Ajax 相关的辅助方法其实分成两部分,一部分是 JavaScript 代码,一部分是 Ruby 代码。 125 | 126 | 如果没有禁用 Asset Pipeline,link:https://github.com/rails/rails/tree/master/actionview/app/assets/javascripts[rails-ujs] 负责提供 JavaScript 代码,常规的 Ruby 视图辅助方法负责生成 DOM 标签。 127 | 128 | 应用在处理远程元素的过程中触发的不同事件参见下文。 129 | 130 | [[form-with]] 131 | ===== `form_with` 132 | 133 | http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with[`form_with` 方法]协助编写表单,默认假定表单使用 Ajax。如果不想使用 Ajax,把 `:local` 选项传给 `form_with`。 134 | 135 | [source,erb] 136 | ---- 137 | <%= form_with(model: @article) do |f| %> 138 | ... 139 | <% end %> 140 | ---- 141 | 142 | 生成的 HTML 如下: 143 | 144 | [source,html] 145 | ---- 146 |
147 | ... 148 |
149 | ---- 150 | 151 | 注意 `data-remote="true"` 属性,现在这个表单不会通过常规的方式提交,而是通过 Ajax 提交。 152 | 153 | 或许你并不需要一个只能填写内容的表单,而是想在表单提交成功后做些事情。为此,我们要绑定 `ajax:success` 事件。处理表单提交失败的程序要绑定到 `ajax:error` 事件上。例如: 154 | 155 | [source,coffee] 156 | ---- 157 | $(document).ready -> 158 | $("#new_article").on("ajax:success", (e, data, status, xhr) -> 159 | $("#new_article").append xhr.responseText 160 | ).on "ajax:error", (e, xhr, status, error) -> 161 | $("#new_article").append "

ERROR

" 162 | ---- 163 | 164 | 显然你需要的功能比这要复杂,上面的例子只是个入门。 165 | 166 | [[link-to]] 167 | ===== `link_to` 168 | 169 | http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to[`link_to` 方法]用于生成链接,可以指定 `:remote` 选项,用法如下: 170 | 171 | [source,erb] 172 | ---- 173 | <%= link_to "an article", @article, remote: true %> 174 | ---- 175 | 176 | 生成的 HTML 如下: 177 | 178 | [source,html] 179 | ---- 180 | an article 181 | ---- 182 | 183 | 绑定的 Ajax 事件和 `form_with` 方法一样。下面举个例子。假如有一个文章列表,我们想只点击一个链接就删除所有文章。视图代码如下: 184 | 185 | [source,erb] 186 | ---- 187 | <%= link_to "Delete article", @article, remote: true, method: :delete %> 188 | ---- 189 | 190 | CoffeeScript 代码如下: 191 | 192 | [source,coffee] 193 | ---- 194 | $ -> 195 | $("a[data-remote]").on "ajax:success", (e, data, status, xhr) -> 196 | alert "The article was deleted." 197 | ---- 198 | 199 | [[button-to]] 200 | ===== `button_to` 201 | 202 | http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-button_to[`button_to` 方法]用于生成按钮,可以指定 `:remote` 选项,用法如下: 203 | 204 | [source,erb] 205 | ---- 206 | <%= button_to "An article", @article, remote: true %> 207 | ---- 208 | 209 | 生成的 HTML 如下: 210 | 211 | [source,html] 212 | ---- 213 |
214 | 215 |
216 | ---- 217 | 218 | 因为生成的就是一个表单,所以 `form_with` 的全部信息都可使用。 219 | 220 | [[customize-remote-elements]] 221 | ==== 定制远程元素 222 | 223 | 不编写任何 JavaScript 代码,仅通过 `data-remote` 属性就能定制元素的行为。此外,还可以指定额外的 `data-` 属性。 224 | 225 | [[data-method]] 226 | ===== `data-method` 227 | 228 | 链接始终发送 HTTP GET 请求。然而,如果你的应用使用 http://en.wikipedia.org/wiki/Representational_State_Transfer[REST 架构],有些链接其实要对服务器中的数据做些操作,因此必须发送 GET 之外的请求。这个属性用于标记这类链接,明确指定使用“post”、“put”或“delete”方法。 229 | 230 | Rails 的处理方式是,点击链接后,在文档中构建一个隐藏的表单,把表单的 `action` 属性的值设为链接的 `href` 属性值,把表单的 `method` 属性的值设为链接的 `data-method` 属性值,然后提交表单。 231 | 232 | NOTE: 由于通过表单提交 GET 和 POST 之外的请求未得到浏览器的广泛支持,所以其他 HTTP 方法其实是通过 POST 发送的,意欲发送的请求在 `_method` 参数中指明。Rails 能自动检测并处理这种情况。 233 | 234 | [[data-url-and-data-params]] 235 | ===== `data-url` 和 `data-params` 236 | 237 | 页面中有些元素并不指向任何 URL,但是却想让它们触发 Ajax 调用。为元素设定 `data-url` 和 `data-remote` 属性将向指定的 URL 发送 Ajax 请求。还可以通过 `data-params` 属性指定额外的参数。 238 | 239 | 例如,可以利用这一点在复选框上触发操作: 240 | 241 | [source,html] 242 | ---- 243 | 245 | ---- 246 | 247 | [[data-type]] 248 | ===== `data-type` 249 | 250 | 此外,在含有 `data-remote` 属性的元素上还可以通过 `data-type` 属性明确定义 Ajax 的 `dataType`。 251 | 252 | [[confirmations]] 253 | ==== 确认 254 | 255 | 可以在链接和表单上添加 `data-confirm` 属性,让用户确认操作。呈献给用户的是 JavaScript `confirm()` 对话框,内容为 `data-confirm` 属性的值。如果用户选择“取消”,操作不会执行。 256 | 257 | 在链接上添加这个属性后,对话框在点击链接后弹出;在表单上添加这个属性后,对话框在提交时弹出。例如: 258 | 259 | [source,erb] 260 | ---- 261 | <%= link_to "Dangerous zone", dangerous_zone_path, 262 | data: { confirm: 'Are you sure?' } %> 263 | ---- 264 | 265 | 生成的 HTML 为: 266 | 267 | [source,html] 268 | ---- 269 | Dangerous zone 270 | ---- 271 | 272 | 在表单的提交按钮上也可以设定这个属性。这样可以根据所按的按钮定制提醒消息。此时,不能在表单元素上设定 `data-confirm` 属性。 273 | 274 | 默认使用的是 JavaScript 确认对话框,不过你可以定制这一行为,监听 `confirm` 时间,在对话框弹出之前触发。若想禁止弹出默认的对话框,让事件句柄返回 `false`。 275 | 276 | [[automatic-disabling]] 277 | ==== 自动禁用 278 | 279 | 还可以使用 `disable-with` 属性在提交表单的过程中禁用输入元素。这样能避免用户不小心点击两次,发送两个重复的 HTTP 请求,导致后端无法正确处理。这个属性的值是按钮处于禁用状态时显示的新值。 280 | 281 | 带有 `data-method` 属性的链接也可设定这个属性。 282 | 283 | 例如: 284 | 285 | [source,erb] 286 | ---- 287 | <%= form_with(model: @article.new) do |f| %> 288 | <%= f.submit data: { "disable-with": "Saving..." } %> 289 | <%= end %> 290 | ---- 291 | 292 | 生成的表单包含: 293 | 294 | [source,html] 295 | ---- 296 | 297 | ---- 298 | 299 | [[dealing-with-ajax-events]] 300 | === 处理 Ajax 事件 301 | 302 | 带 `data-remote` 属性的元素具有下述事件。 303 | 304 | NOTE: 这些事件绑定的句柄的第一个参数始终是事件对象。下面列出的是事件对象之后的其他参数。例如,如果列出的参数是 `xhr, settings`,那么定义句柄时要写为 `function(event, xhr, settings)`。 305 | 306 | |=== 307 | | 事件名 | 额外参数 | 触发时机 308 | 309 | | `ajax:before` 310 | | 311 | | 在整个 Ajax 调用开始之前,如果被停止了,就不再调用。 312 | 313 | | `ajax:beforeSend` 314 | | `xhr, options` 315 | | 在发送请求之前,如果被停止了,就不再发送。 316 | 317 | | `ajax:send` 318 | | `xhr` 319 | | 发送请求时。 320 | 321 | | `ajax:success` 322 | | `xhr, status, err` 323 | | Ajax 调用结束,返回表示成功的响应时。 324 | 325 | | `ajax:error` 326 | | `xhr, status, err` 327 | | Ajax 调用结束,返回表示失败的响应时。 328 | 329 | | `ajax:complete` 330 | | `xhr, status` 331 | | Ajax 调用结束时,不管成功还是失败。 332 | 333 | | `ajax:aborted:file` 334 | | `elements` 335 | | 有非空文件输入时,如果被停止了,就不再调用。 336 | |=== 337 | 338 | [[stoppable-events]] 339 | ==== 可停止的事件 340 | 341 | 如果在 `ajax:before` 或 `ajax:beforeSend` 的句柄中返回 `false`,不会发送 Ajax 请求。`ajax:before` 事件可用于在序列化之前处理表单数据。`ajax:beforeSend` 事件也可用于添加额外的请求首部。 342 | 343 | 如果停止 `ajax:aborted:file` 事件,允许浏览器通过常规方式(即不是 Ajax)提交表单这个默认行为将失效,表单根本无法提交。利用这一点可以自行实现通过 Ajax 上传文件的变通方式。 344 | 345 | [[server-side-concerns]] 346 | === 服务器端处理 347 | 348 | Ajax 不仅涉及客户端,服务器端也要做处理。Ajax 请求一般不返回 HTML,而是 JSON。下面详细说明处理过程。 349 | 350 | [[a-simple-example]] 351 | ==== 一个简单的例子 352 | 353 | 假设在网页中要显示一系列用户,还有一个新建用户的表单。控制器的 `index` 动作如下所示: 354 | 355 | [source,ruby] 356 | ---- 357 | class UsersController < ApplicationController 358 | def index 359 | @users = User.all 360 | @user = User.new 361 | end 362 | # ... 363 | ---- 364 | 365 | `index` 视图(`app/views/users/index.html.erb`)如下: 366 | 367 | [source,erb] 368 | ---- 369 | Users 370 | 371 |
    372 | <%= render @users %> 373 |
374 | 375 |
376 | 377 | <%= form_with(model: @user) do |f| %> 378 | <%= f.label :name %>
379 | <%= f.text_field :name %> 380 | <%= f.submit %> 381 | <% end %> 382 | ---- 383 | 384 | `app/views/users/_user.html.erb` 局部视图的内容如下: 385 | 386 | [source,erb] 387 | ---- 388 |
  • <%= user.name %>
  • 389 | ---- 390 | 391 | `index` 页面的上部显示用户列表,下部显示新建用户的表单。 392 | 393 | 下部的表单会调用 `UsersController` 的 `create` 动作。因为表单的 `remote` 选项为 `true`,所以发给 `UsersController` 的是 Ajax 请求,使用 JavaScript 处理。要想处理这个请求,控制器的 `create` 动作应该这么写: 394 | 395 | [source,ruby] 396 | ---- 397 | # app/controllers/users_controller.rb 398 | # ...... 399 | def create 400 | @user = User.new(params[:user]) 401 | 402 | respond_to do |format| 403 | if @user.save 404 | format.html { redirect_to @user, notice: 'User was successfully created.' } 405 | format.js 406 | format.json { render json: @user, status: :created, location: @user } 407 | else 408 | format.html { render action: "new" } 409 | format.json { render json: @user.errors, status: :unprocessable_entity } 410 | end 411 | end 412 | end 413 | ---- 414 | 415 | 注意,在 `respond_to` 块中使用了 `format.js`,这样控制器才能响应 Ajax 请求。然后还要新建 `app/views/users/create.js.erb` 视图文件,编写发送响应以及在客户端执行的 JavaScript 代码。 416 | 417 | [source,erb] 418 | ---- 419 | $("<%= escape_javascript(render @user) %>").appendTo("#users"); 420 | ---- 421 | 422 | [[turbolinks]] 423 | === Turbolinks 424 | 425 | Rails 提供了 https://github.com/turbolinks/turbolinks[Turbolinks 库],它使用 Ajax 渲染页面,在多数应用中可以提升页面加载速度。 426 | 427 | [[how-turbolinks-works]] 428 | ==== Turbolinks 的工作原理 429 | 430 | Turbolinks 为页面中所有的 `` 元素添加一个点击事件处理程序。如果浏览器支持 https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history#The_pushState%28%29_method[PushState],Turbolinks 会发起 Ajax 请求,解析响应,然后使用响应主体替换原始页面的整个 `` 元素。最后,使用 PushState 技术更改页面的 URL,让新页面可刷新,并且有个精美的 URL。 431 | 432 | 要想使用 Turbolinks,只需将其加入 `Gemfile`,然后在 `app/assets/javascripts/application.js` 中加入 `//= require turbolinks`。 433 | 434 | 如果某个链接不想使用 Turbolinks,可以在链接中添加 `data-turbolinks="false"` 属性: 435 | 436 | [source,html] 437 | ---- 438 | No turbolinks here. 439 | ---- 440 | 441 | [[page-change-events]] 442 | ==== 页面内容变更事件 443 | 444 | 编写 CoffeeScript 代码时,经常需要在页面加载时做一些事情。在 jQuery 中,我们可以这么写: 445 | 446 | [source,coffee] 447 | ---- 448 | $(document).ready -> 449 | alert "page has loaded!" 450 | ---- 451 | 452 | 不过,Turbolinks 改变了常规的页面加载流程,不会触发这个事件。如果编写了类似上面的代码,要将其修改为: 453 | 454 | [source,coffee] 455 | ---- 456 | $(document).on "turbolinks:load", -> 457 | alert "page has loaded!" 458 | ---- 459 | 460 | 其他可用事件的详细信息,参阅 https://github.com/turbolinks/turbolinks/blob/master/README.md[Turbolinks 的自述文件]。 461 | 462 | [[other-resources]] 463 | === 其他资源 464 | 465 | 下面列出一些链接,可以帮助你进一步学习: 466 | 467 | * https://github.com/rails/jquery-ujs/wiki[jquery-ujs 的维基] 468 | * https://github.com/rails/jquery-ujs/wiki/External-articles[其他介绍 jquery-ujs 的文章] 469 | * http://www.alfajango.com/blog/rails-3-remote-links-and-forms/[Rails 3 Remote Links and Forms: A Definitive Guide] 470 | * http://railscasts.com/episodes/205-unobtrusive-javascript[Railscasts: Unobtrusive JavaScript] 471 | * http://railscasts.com/episodes/390-turbolinks[Railscasts: Turbolinks] 472 | -------------------------------------------------------------------------------- /md_tpl/block_admonition.html.erb: -------------------------------------------------------------------------------- 1 | <%= attr('name').upcase %>: <%= content %> 2 | 3 | <%# -%> 4 | -------------------------------------------------------------------------------- /md_tpl/block_dlist.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | items.each do |terms, dd| 3 | [*terms].each do |dt| 4 | %>**<%= dt.text %>** 5 | 6 | <% end 7 | unless dd.nil? 8 | if dd.text? 9 | %><%= dd.text %> 10 | 11 | <% end 12 | if dd.blocks? 13 | %><%= dd.content %> 14 | <% end 15 | end 16 | end 17 | -%> 18 | -------------------------------------------------------------------------------- /md_tpl/block_image.html.erb: -------------------------------------------------------------------------------- 1 | ![<%= attr?('alt') ? attr('alt') : nil %>](<%= image_uri(attr 'target') %>) 2 | 3 | <%# -%> 4 | -------------------------------------------------------------------------------- /md_tpl/block_listing.html.erb: -------------------------------------------------------------------------------- 1 | ```<%= attr?('language') ? attr('language') : nil %> 2 | <%= CGI::unescape_html(content) %> 3 | ``` 4 | 5 | <%# -%> 6 | -------------------------------------------------------------------------------- /md_tpl/block_olist.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | content.each do |item| 3 | %>1. <%= item.text %> 4 | <% if item.blocks? %> 5 | 6 | <%= Rebus::Helpers.indent(item.content) %> 7 | <% 8 | end 9 | end 10 | %> 11 | -------------------------------------------------------------------------------- /md_tpl/block_open.html.erb: -------------------------------------------------------------------------------- 1 | <% if role == 'chapter-abstract' %><%= content %> 2 | <%= '-' * 77 %> 3 | <% else %><%= content %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /md_tpl/block_paragraph.html.erb: -------------------------------------------------------------------------------- 1 | <%= content %> 2 | 3 | <%# -%> 4 | -------------------------------------------------------------------------------- /md_tpl/block_quote.html.erb: -------------------------------------------------------------------------------- 1 | > <%= content.chomp %> 2 | 3 | <%# -%> 4 | -------------------------------------------------------------------------------- /md_tpl/block_table.html.erb: -------------------------------------------------------------------------------- 1 | <% if @id %> 2 | 3 | <% end %><% 4 | [:head, :body, :foot].select {|tsec| !@rows[tsec].empty? }.each do |tsec| 5 | @rows[tsec].each do |row| 6 | row.each do |cell| 7 | %><%= %(| #{cell.text} ) %><% 8 | end %> | 9 | <% if tsec == :head 10 | %><%= '|---' * row.count << '|' %> 11 | <% end 12 | end 13 | end %> 14 | -------------------------------------------------------------------------------- /md_tpl/block_ulist.html.erb: -------------------------------------------------------------------------------- 1 | <% content.each do |item| -%> 2 | * <%= item.text %><% 3 | if item.blocks? %> 4 | 5 | <%= Rebus::Helpers.indent(item.content) %> 6 | <% end %> 7 | <% end %> 8 | -------------------------------------------------------------------------------- /md_tpl/document.html.erb: -------------------------------------------------------------------------------- 1 | <%= content %> 2 | -------------------------------------------------------------------------------- /md_tpl/embedded.html.erb: -------------------------------------------------------------------------------- 1 | <%= content %> 2 | -------------------------------------------------------------------------------- /md_tpl/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'cgi/util' 2 | require 'asciidoctor' 3 | require 'asciidoctor/extensions' 4 | 5 | module Rebus 6 | # Register a `term' inline macro to define terminology. 7 | # 8 | # Example 9 | # 10 | # There is a terminology called term:[application]. 11 | # 12 | # Output 13 | # 14 | # There is a terminology called application. 15 | class TermInlineMacro < ::Asciidoctor::Extensions::InlineMacroProcessor 16 | use_dsl 17 | 18 | named :term 19 | using_format :short 20 | 21 | def process parent, target, attributes 22 | target 23 | end 24 | end 25 | 26 | module Helpers 27 | 28 | CHAPTER_IDS = [ 29 | 'ruby-on-rails-5-0-release-notes', 30 | 'ruby-on-rails-5-1-release-notes', 31 | 'action-cable-overview', 32 | 'action-controller-overview', 33 | 'action-mailer-basics', 34 | 'action-view-overview', 35 | 'active-jobs-basics', 36 | 'active-model-basics', 37 | 'active-record-basics', 38 | 'active-record-callbacks', 39 | 'active-record-migrations', 40 | 'active-record-query-interface', 41 | 'active-record-validations', 42 | 'active-support-core-extensions', 43 | 'active-support-instrumentation', 44 | 'using-rails-for-api-only-applications', 45 | 'api-documentation-guidelines', 46 | 'the-asset-pipeline', 47 | 'active-record-associations', 48 | 'autoloading-and-reloading-constants', 49 | 'caching-with-rails-an-overview', 50 | 'the-rails-command-line', 51 | 'configuring-rails-applications', 52 | 'contributing-to-ruby-on-rails', 53 | 'debugging-rails-applications', 54 | 'development-dependencies-install', 55 | 'getting-started-with-engines', 56 | 'action-view-form-helpers', 57 | 'creating-and-customizing-rails-generators-and-templates', 58 | 'getting-started-with-rails', 59 | 'rails-internationalization-api', 60 | 'the-rails-initialization-process', 61 | 'layouts-and-rendering-in-rails', 62 | 'maintenance-policy-for-ruby-on-rails', 63 | 'the-basics-of-creating-rails-plugins', 64 | 'a-guide-to-profiling-rails-applications', 65 | 'rails-application-templates', 66 | 'rails-on-rack', 67 | 'rails-routing-from-the-outside-in', 68 | 'ruby-on-rails-guides-guidelines', 69 | 'ruby-on-rails-security-guide', 70 | 'a-guide-to-testing-rails-applications', 71 | 'a-guide-for-upgrading-ruby-on-rails', 72 | 'working-with-javascript-in-rails' 73 | ] 74 | 75 | def self.get_xref_text(id) 76 | id = id.include?('#') ? id.split('#', 2).last : id 77 | 78 | refs_file = File.expand_path('../../tmp/markdown/refs.yml', __FILE__) 79 | refs = YAML.load_file(refs_file) 80 | 81 | refs[id] 82 | end 83 | 84 | def self.get_xref_target(target) 85 | return target unless target.include? '.html#' 86 | 87 | parts = target.split('#', 2) 88 | return parts.first if CHAPTER_IDS.include? parts.last 89 | 90 | target 91 | end 92 | 93 | def self.indent(text, space=4) 94 | lines = [] 95 | text.each_line { |l| lines << l.prepend(' ' * space) } 96 | lines.join 97 | end 98 | 99 | end 100 | end 101 | 102 | ::Asciidoctor::Extensions.register do 103 | inline_macro ::Rebus::TermInlineMacro 104 | end 105 | -------------------------------------------------------------------------------- /md_tpl/inline_anchor.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | case @type 3 | when :xref 4 | refid = attr :refid || @target 5 | %><% if text.nil? 6 | %>[<%= Rebus::Helpers.get_xref_text(refid) || refid %>](<%= Rebus::Helpers.get_xref_target(@target) %>)<% 7 | else 8 | %>[<%= @text %>](<%= Rebus::Helpers.get_xref_target(@target) %>)<% 9 | end %><% 10 | when :ref %><%= @target %><% 11 | when :bibref %>[<%= @target %>]<% 12 | else %><% 13 | if @text != @target 14 | %>[<%= @text %>](<%= @target %>)<% 15 | else %><<%= @target %>><% 16 | end %><% 17 | end %> 18 | -------------------------------------------------------------------------------- /md_tpl/inline_footnote.html.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/md_tpl/inline_footnote.html.erb -------------------------------------------------------------------------------- /md_tpl/inline_quoted.html.erb: -------------------------------------------------------------------------------- 1 | <% case @type 2 | when :emphasis %><%= @text %><% 3 | when :strong %><%= @text %><% 4 | when :monospaced %><%= CGI.unescape_html @text %><% 5 | when :superscript %>><%= @text %><% 6 | when :subscript %>><%= @text %><% 7 | end -%> 8 | -------------------------------------------------------------------------------- /md_tpl/section.html.erb: -------------------------------------------------------------------------------- 1 | <% if @level != 1 2 | %><%= attr?('id') ? %() : nil %> 3 | 4 | <% end 5 | %><%= '#' * @level %> <%= title %> 6 | 7 | <%= content %> 8 | -------------------------------------------------------------------------------- /plugins/after_build_site.rb: -------------------------------------------------------------------------------- 1 | module Persie 2 | class MultipleHTMLs 3 | # Run after build complete 4 | def after_build 5 | rename_toc 6 | end 7 | 8 | private 9 | 10 | # Renames toc.html to index.html 11 | def rename_toc 12 | base = File.join @book.builds_dir, 'html', 'multiple' 13 | puts 14 | info 'Renaming toc.html...' 15 | FileUtils.mv File.join(base, 'toc.html'), File.join(base, 'index.html') 16 | fix_link 17 | confirm ' Done' 18 | puts 19 | end 20 | 21 | def fix_link 22 | base = File.join @book.builds_dir, 'html', 'multiple' 23 | index_file = File.join base, 'index.html' 24 | index_root = ::Nokogiri::HTML.fragment File.read(index_file) 25 | 26 | # delete secondary toc 27 | index_root.css('li[data-type="chapter"] > ol').unlink 28 | 29 | # remove parts' link 30 | index_root.css('li[data-type="part"]').each do | part| 31 | part.first_element_child.replace("#{part.first_element_child.text}") 32 | end 33 | 34 | text = index_root.to_s.sub('permalink: /book/toc.html', 'permalink: /book/index.html') 35 | 36 | File.write index_file, text 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /plugins/term_macro.rb: -------------------------------------------------------------------------------- 1 | require 'asciidoctor' 2 | require 'asciidoctor/extensions' 3 | 4 | module RailsGuides 5 | # Register a `term' inline macro to define terminology. 6 | # 7 | # Example 8 | # 9 | # There is a terminology called term:[application]. 10 | # 11 | # Output 12 | # 13 | # There is a terminology called application. 14 | class TermInlineMacro < ::Asciidoctor::Extensions::InlineMacroProcessor 15 | use_dsl 16 | 17 | named :term 18 | using_format :short 19 | 20 | def process parent, target, attributes 21 | %(#{target}) 22 | end 23 | end 24 | end 25 | 26 | ::Asciidoctor::Extensions.register do 27 | inline_macro ::RailsGuides::TermInlineMacro 28 | end 29 | -------------------------------------------------------------------------------- /release_notes.md: -------------------------------------------------------------------------------- 1 | # 发布点检表 2 | 3 | 1. 更新 Changelog 4 | 2. 更新 `book.adoc` 中的版本号和发布日期 5 | 3. 构建并检查电子书 6 | 4. 新增一个 git tag 7 | 5. 把电子书上传到 CDN 中 8 | 6. 更新 cs.about.ac 中的电子书版本号 9 | 7. 更新网站中的 Changelog 10 | 8. 更新在线阅读 11 | 9. 发送更新通知邮件 12 | 10. 🍻 13 | -------------------------------------------------------------------------------- /themes/epub/epub.css: -------------------------------------------------------------------------------- 1 | /* Write ePub stylesheets here. */ 2 | -------------------------------------------------------------------------------- /themes/epub/rails-guides-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/themes/epub/rails-guides-cover.jpg -------------------------------------------------------------------------------- /themes/html/multiple.html.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: book 3 | title: {{ page_title }} 4 | book_page: true 5 | body_class: book-page 6 | permalink: /book/{{ page_url }} 7 | --- 8 | 9 |
    10 | 在线版的内容可能落后于电子书,如果想及时获得更新,请购买电子书。 11 |
    12 | 13 |
    14 | {{ content }} 15 |
    16 | 17 | {{ footnotes }} 18 | -------------------------------------------------------------------------------- /themes/pdf/part-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/themes/pdf/part-bg.jpg -------------------------------------------------------------------------------- /themes/pdf/pdf.css: -------------------------------------------------------------------------------- 1 | /* Write PDF stylesheets here. */ 2 | -------------------------------------------------------------------------------- /themes/pdf/rails-guides-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/themes/pdf/rails-guides-cover.jpg -------------------------------------------------------------------------------- /themes/pdf/sample-cover.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndorChen/rails-guides/26d87e6c34dec94e7d1fdabc75dba2ff3143109d/themes/pdf/sample-cover.pdf --------------------------------------------------------------------------------