移动mime类型可以回退到Rails中的“html”吗?

我在ApplicationController中使用此代码(取自此处 )来检测iPhone,iPod Touch和iPad请求:

before_filter :detect_mobile_request, :detect_tablet_request protected def detect_mobile_request request.format = :mobile if mobile_request? end def mobile_request? #request.subdomains.first == 'm' request.user_agent =~ /iPhone/ || request.user_agent =~ /iPod/ end def detect_tablet_request request.format = :tablet if tablet_request? end def tablet_request? #request.subdomains.first == 't' request.user_agent =~ /iPad/ end 

这允许我有像show.html.erb,show.mobile.erb和show.tablet.erb这样的模板,这很好,但是有一个问题:似乎我必须为每个mime类型定义每个模板。 例如,即使定义了show.html.erb,在不定义show.mobile.erb的情况下从iPhone请求“show”操作也会引发错误。 如果缺少移动设备或平板电脑模板,我只想简单地使用html设备。 由于“mobile”被定义为mime_types.rb中“text / html”的别名,因此它似乎并不太牵强。

那么,有几个问题:

  1. 我做错了吗? 或者,有更好的方法吗?
  2. 如果没有,如果移动设备或平板电脑文件不存在,我可以将移动设备和平板电脑的mime类型重新设置为html吗?

如果重要,我正在使用Rails 3.0.1。 提前感谢任何指针。

编辑:我忘了提到的东西:我最终会转移到单独的子域(正如你在我的例子中看到的那样),所以模板加载确实需要自动发生,无论before_filter运行了什么。

rails 3.1 中 更改视图格式 可能重复 (提供移动html格式,在普通html上回退)

然而,我在这个完全相同的问题上苦苦挣扎,并提出了一个相当优雅的解决方案,完美地满足了我的需求。 这是我在另一篇文章中的回答。

我想我找到了最好的办法。 我正在尝试与你相同的事情,但后来我记得在rails 3.1中引入了模板inheritance ,这正是我们需要这样的东西才能工作的东西。 我真的不能为这个实现付出太多的荣誉,因为它全部都是由Ryan Bates在railscasts链接中提出的。

所以这基本上是这样的。

app/views创建一个子目录。 我把我的mobile标记为

嵌套要覆盖的所有视图模板,其格式与它们在views目录中的结构格式相同。 views/posts/index.html.erb -> views/mobile/posts/index.html.erb

Application_Controller创建一个before_filter并执行此操作。

  before_filter :prep_mobile def is_mobile? request.user_agent =~ /Mobile|webOS|iPhone/ end def prep_mobile prepend_view_path "app/views/mobile" if is_mobile? end 

完成后,如果移动设备位于移动设备上,则您的文件将默认为移动视图,如果移动设备不存在,则会回退到常规模板。

你需要做几件事情来解决这个问题,但好消息是Rails 3实际上比以前简单得多,你可以让路由器为你完成大部分的艰苦工作。

首先,您需要创建一个特殊的路由,为您设置正确的mime类型:

 # In routes.rb: resources :things, :user_agent => /iPhone/, :format => :iphone resources :things 

现在,您可以通过标记为iphone mime类型的iphone用户代理访问这些内容。 虽然Rails会因为你丢失的mime类型而爆炸,所以请转到config / initializers / mime_types.rb并取消注释iphone:

 Mime::Type.register_alias "text/html", :iphone 

现在你已经准备好使用mime类型,但你的控制器可能还不知道你的新mime类型,因此你会看到406个响应。 要解决此问题,只需使用repsond_to在控制器顶部添加mime类型的限额:

 class ThingsController < ApplicationController respond_to :html, :xml, :iphone 

现在您可以正常使用respond_to块或respond_with。

除了已经讨论过的monkeypatch或非mime模板方法之外,目前没有API可以轻松执行自动回退。 您可以使用专门的响应程序类更干净地连接覆盖。

其他推荐阅读包括:

https://github.com/plataformatec/responders

http://www.railsdispatch.com/posts/rails-3-makes-life-better

尝试从.html.erb和iPhone和浏览器中删除.html将回退到公共文件。

我为版本3.2.X添加了新的答案。 这个答案适用于<~3.0.1。

我在寻找能够在视图上有多个回退时遇到了这个问题。 例如,如果我的产品可以是白色标签,反过来如果我的白标合作伙伴能够出售赞助商,那么我需要在每个页面上都有一系列视图,如下所示:

  • 赞助商查看:.sponsor_html
  • 合作伙伴视图:.partner_html
  • 默认视图:.html

如果只有一个级别高于默认值,Joe的答案只是删除.html(非常好),但在实际应用中,我需要5个级别。

似乎没有任何方式可以实现与Jeremy相同的一些猴子补丁。

Rails核心做了一些相当广泛的假设,你只需要一种格式,并且它映射到一个扩展(默认为NO扩展)。

我需要一个适用于所有视图元素的解决方案 – 布局,模板和局部视图。

为了使这更符合惯例,我想出了以下内容。

 # app/config/initializers/resolver.rb module ActionView class Base cattr_accessor :extension_fallbacks @@extension_fallbacks = nil end class PathResolver < Resolver private def find_templates_with_fallbacks(name, prefix, partial, details) fallbacks = Rails.application.config.action_view.extension_fallbacks format = details[:formats].first unless fallbacks && fallbacks[format] return find_templates_without_fallbacks(name, prefix, partial, details) end deets = details.dup deets[:formats] = fallbacks[format] path = build_path(name, prefix, partial, deets) query(path, EXTENSION_ORDER.map {|ext| deets[ext] }, details[:formats]) end alias_method_chain :find_templates, :fallbacks end end # config/application.rb config.after_initialize do config.action_view.extension_fallbacks = { html: [:sponsor_html, :partner_html, :html], mobile: [:sponsor_mobile, :partner_mobile, :sponsor_html, :partner_html, :html] } # config/initializers/mime_types.rb register_alias 'text/html', :mobile # app/controllers/examples_controller.rb class ExamplesController respond_to :html, :mobile def index @examples = Examples.all respond_with(@examples) end end 

注意:我确实看到了围绕alias_method_chain的评论,并且最初确实在适当的位置拨打了super。 在某些情况下,这实际上称为ActionView :: Resolver#find_templates(它引发了一个NotImplementedexception),而不是ActionView :: PathResolver#find_templates。 我没有耐心去追查原因。 我怀疑它是因为是一种私人方法。

此外,Rails目前不会将alias_method_chain报告为已弃用。 就是这个post。

我不喜欢这个答案,因为它涉及到find_templates调用的一些非常脆弱的实现。 特别假设您只有一种格式,但这是在模板请求中的所有地方做出的假设。

经过4天尝试解决这个问题并梳理整个模板请求堆栈后,我能想出最好的解决方案。

我处理这个问题的方法就是对那些我知道要渲染HTML视图的请求简单地skip_before_filter 。 显然,这将适用于部分。

如果您的站点有很多移动和/或平板电脑视图,您可能希望在ApplicationController设置filter并在子类中跳过它们,但如果只有少数操作具有移动特定视图, 则只应对这些操作调用beforefilter/控制器你想要的。

如果你的操作系统有符号链接,你可以使用它们。

 $ ln -s show.html.erb show.mobile.erb 

我现在已经更新到3.2.X,我正在添加另一个答案。 留下旧的答案,以防有人需要那个。 但是,我会编辑它以引导人们使用当前版本。

这里的重要区别是利用自定义路径解析器中添加的“新”(自3.1起)的可用性。 正如Jeroen建议的那样,这使代码更短。 但采取了一点进一步。 特别是#find_templates不再是私有的,并且预计您将编写一个自定义的。

 # lib/fallback_resolver.rb class FallbackResolver < ::ActionView::FileSystemResolver def initialize(path, fallbacks = nil) @fallback_list = fallbacks super(path) end def find_templates(name, prefix, partial, details) format = details[:formats].first return super unless @fallback_list && @fallback_list[format] formats = Array.wrap(@fallback_list[format]) details_copy = details.dup details_copy[:formats] = formats path = Path.build(name, prefix, partial) query(path, details_copy, formats) end end # app/controllers/application_controller.rb class ApplicationController < ActionController::Base append_view_path 'app/views', { mobile: [:sponsor_mobile, :mobile, :sponsor_html, :html], html: [:sponsor_html, :html] } respond_to :html, :mobile # config/initializers/mime_types.rb register_alias 'text/html', :mobile 

这是一个更简单的解决方案:

 class ApplicationController ... def formats=(values) values << :html if values == [:mobile] super(values) end ... end 

事实certificate,Rails(3.2.11)为:js格式的请求添加了一个:html回退。 以下是它的工作原理:

  • ActionController :: Rendering#process_action从请求中分配格式数组(请参阅action_controller / metal / rendering.rb)
  • 使用结果调用ActionView :: LookupContext#formats =

这是ActionView :: LookupContext#formats =,

 # Override formats= to expand ["*/*"] values and automatically # add :html as fallback to :js. def formats=(values) if values values.concat(default_formats) if values.delete "*/*" values << :html if values == [:js] end super(values) end 

这个解决方案很糟糕,但我不知道让Rails将请求MIME类型“mobile”解释为格式化程序[:mobile,:html]的更好方法 - 而且 Rails已经这样做了。

是的,我很确定这是在rails中执行此操作的正确方法。 我以前用这种方式定义了iphone格式。 这是一个关于将格式恢复为默认值的好问题:如果iphone的模板不存在,则为html。 这听起来很简单,但我认为你必须添加一个monkeypath来拯救丢失的模板错误,或者在渲染之前检查模板是否存在。 看看这个问题中显示的补丁类型。 像这样的东西可能会做的伎俩(在我的浏览器中编写这个代码,所以更多的伪代码),但把它扔在初始化器中

 # config/initializers/default_html_view.rb module ActionView class PathSet def find_template_with_exception_handling(original_template_path, format = nil, html_fallback = true) begin find_template_without_exception_handling(original_template_path, format, html_fallback) rescue ActionView::MissingTemplate => e # Template wasn't found template_path = original_template_path.sub(/^\//, '') # Check to see if the html version exists if template = load_path["#{template_path}.#{I18n.locale}.html"] # Return html version return template else # The html format doesn't exist either raise e end end end alias_method_chain :find_template, :exception_handling end end 

这是另一个如何做到这一点的例子,受Simon的代码启发,但是更短,更少hacky:

 # application_controller.rb class ApplicationController < ActionController::Base # ... # When the format is iphone have it also fallback on :html append_view_path ExtensionFallbackResolver.new("app/views", :iphone => :html) # ... end 

以及autoload_path中的某个地方或明确要求:

 # extension_fallback_resolver.rb class ExtensionFallbackResolver < ActionView::FileSystemResolver attr_reader :format_fallbacks # In controller do append_view_path ExtensionFallbackResolver.new("app/views", :iphone => :html) def initialize(path, format_fallbacks = {}) super(path) @format_fallbacks = format_fallbacks end private def find_templates(name, prefix, partial, details) fallback_details = details.dup fallback_details[:formats] = Array(format_fallbacks[details[:formats].first]) path = build_path(name, prefix, partial, details) query(path, EXTENSION_ORDER.map { |ext| fallback_details[ext] }, details[:formats]) end end 

以上仍然是一个黑客,因为它使用私有API,但可能不像Simon的原始提案那么脆弱。

请注意,您需要单独处理布局。 您需要实现一个基于用户代理或类似方法选择布局的方法。 这只会处理普通模板的后备。

Rails 4.1包含Variants ,这是一个很棒的function,允许您为同一个mime设置不同的视图。 你现在可以简单地添加一个before_action并让变体来做魔术:

 before_action :detect_device_variant def detect_device_variant case request.user_agent when /iPad/i request.variant = :tablet when /iPhone/i request.variant = :phone end end 

然后,在你的行动中:

 respond_to do |format| format.json format.html # /app/views/the_controller/the_action.html.erb format.html.phone # /app/views/the_controller/the_action.html+phone.erb format.html.tablet do @some_tablet_specific_variable = "foo" end end 

更多信息在这里 。

在这种情况下,您可以将格式设置为html。 例如,您希望始终在用户show方法中使用html

 class UserController def show ..your_code.. render :show, :format => :html end end 

在这种情况下,如果您在用户控制器上请求显示,则会一直渲染html版本。

如果你想通过示例渲染JSON,你可以对你的类型做一些测试,如:

 class UserController def show ..your_code.. if [:mobile, :tablet, :html].include?(request.format) render :show, :format => :html else respond_with(@user) end end end 

我做了一个猴子补丁 ,但现在,我使用了更好的解决方案:

在application_controller.rb中:

 layout :which_layout def which_layout mobile? ? 'mobile' : 'application' end 

随着mobile? 你可以写的方法。

所以我有不同的布局,但所有相同的视图,在mobile.html.erb布局中,我使用不同的CSS文件。

我需要同样的东西。 我研究了这个包括这个堆栈溢出问题(和其他类似的问题)以及跟随rails线程(如本问题中提到的)在https://github.com/rails/rails/issues/3855并遵循其线程/学家/gem。

继续我最终做的事情与Rails 3.1和引擎一起工作。 此解决方案允许您将* .mobile.haml(或* .mobile.erb等)放在与其他视图文件相同的位置,而不需要2个层次结构(一个用于常规,一个用于移动)。

发动机和准备代码

在我的’base’引擎中,我在config/initializers/resolvers.rb添加了这个:

  module Resolvers # this resolver graciously shared by jdelStrother at # https://github.com/rails/rails/issues/3855#issuecomment-5028260 class MobileFallbackResolver < ::ActionView::FileSystemResolver def find_templates(name, prefix, partial, details) if details[:formats] == [:mobile] # Add a fallback for html, for the case where, eg, 'index.html.haml' exists, but not 'index.mobile.haml' details = details.dup details[:formats] = [:mobile, :html] end super end end end ActiveSupport.on_load(:action_controller) do tmp_view_paths = view_paths.dup # avoid endless loop as append_view_path modifies view_paths tmp_view_paths.each do |path| append_view_path(Resolvers::MobileFallbackResolver.new(path.to_s)) end end 

然后,在我的“基础”引擎的应用程序控制器中,我添加了一个移动设备? 方法:

  def mobile? request.user_agent && request.user_agent.downcase =~ /mobile|iphone|webos|android|blackberry|midp|cldc/ && request.user_agent.downcase !~ /ipad/ end 

还有这个before_filter

  before_filter :set_layout def set_layout request.format = :mobile if mobile? end 

最后,我将其添加到config/initializers/mime_types.rb

  Mime::Type.register_alias "text/html", :mobile 

用法

现在我可以(在我的应用程序级别或引擎中):

  • app/views/layouts/application.mobile.haml
  • 并在任何视图中.mobile.haml而不是.html.haml文件。

如果我在任何控制器中设置它,我甚至可以使用特定的移动布局:layout'mobile'

它将使用app/views/layouts/mobile.html.haml (甚至是mobile.mobile.haml )。

我在ApplicationController中使用这个before_filter解决了这个问题:

 def set_mobile_format request.formats.unshift(Mime::MOBILE) if mobile_client? end 

这将移动格式置于可接受格式列表的前面。 因此,解析器更喜欢.mobile.erb模板,但如果找不到移动版本,则会回.html.erb

当然,要实现这一点,你需要实现某种#mobile_client? 函数并将Mime::Type.register_alias "text/html", :mobile放入config/initializers/mime_types.rb