给Rails程序添加主题切换功能

都明白是什么意思的,比如像Wordpress一样的主题切换,或者说皮肤?不管怎么个叫法,在Rails里实现这种功能是很简单的。

我最近在做Android开发,我们的团队需要一个门面吧,继续使用我自己的博客程序,它可是支持主题切换功能的哟。看我为植物人新做的一个主题

eskit theme for lifeblock

点图片可以看大图的。我们是騎乘草泥马远征马勒戈壁的植物人

展示完成,讲一下是怎么简单实现的


下面的代码基于Rails 3,但基本适用于其他版本,除了Rails代码可能不一样

原理上其实很简单,controller在接受到访问請求的最后会render一个模版。那这个模版在哪?

Rails默认模版位置显然是app/views目录下以相应的controller命名的文件夹。我们要做的就是改变它!!!这一切都存在于一个view_paths变量中。

Rails总是在Convention的同时给我们Configuration的自由,借助于controllerprepend_view_path方法,我们可以轻松的在模版查找路径之前加一个新的路径。

假如我们的主题放在app/themes/abitno里,那我们只需要在controller加一个类似下面的before_filter,你的主题文件就优先被render了!

prepend_view_path(File.join(Rails.root, "app/themes/abitno"))

很简单吧?哈哈,如果你不关心性能问题,那一切都结束了。


究竟会有什么性能问题?虽然我的小博客不太在乎,但是发现问题就得解决掉,这可是做一个优秀软件、做一个像我这样的优秀程序员的根本呀==!

性能问题的根源在于view_paths并不是简单的字符串数组

irb(main):020:0> vp = ApplicationController.view_paths
irb(main):021:0> vp.class
=> ActionView::PathSet
irb(main):022:0> vp.first.class
=> ActionView::FileSystemResolver

若只是简单的一个ActionView::PathSet倒也没什么大不了,我们来看Rails在运行过程中是怎样通过view_paths来查找模版的。得从ActionView::LookupContext说起

LookupContext is the object responsible to hold all information required to lookup templates, i.e. view paths and details. The LookupContext is also responsible to generate a key, given to view paths, used in the resolver cache lookup. Since this key is generated just once during the request, it speeds up all cache accesses.

从它的描述里可以明显看到它把对模版的查询作了cache,让我们顺着它的find方法看它都会cache哪些东西

def view_paths=(paths)
  @view_paths = ActionView::Base.process_view_paths(paths)
end

def find(name, prefix = nil, partial = false)
  @view_paths.find(*args_for_lookup(name, prefix, partial))
end

于是查看ActionView::Base.process_view_paths作了什么

def self.process_view_paths(value)
  ActionView::PathSet.new(Array.wrap(value))
end

原来只是生成了一个ActionView::PathSet对象,我们离目标很近了

def find(path, prefix = nil, partial = false, details = {}, key = nil)
  template = find_all(path, prefix, partial, details, key).first
  raise MissingTemplate.new(self, "#{prefix}/#{path}", details, partial) unless template
  template
end

def find_all(*args)
  each do |resolver|
    templates = resolver.find_all(*args)
    return templates unless templates.empty?
  end
  []
end

PathSet是Array,上面的find_all显然是对自身的每一个元素执行了find_all,我们来看它的元素究竟是什么属性

def typecast!
  each_with_index do |path, i|
    next unless path.is_a?(String)
    self[i] = FileSystemResolver.new(path)
  end
end

相信我,这次确实是离真相很近了!我们终于在ActionView::FileSystemResolver中找到了cache所在,泪流满面啊TAT

def find_all(name, prefix=nil, partial=false, details={}, key=nil)
  cached(key, prefix, name, partial) do
    find_templates(name, prefix, partial, details)
  end
end

原来是要进行这么一项操作

def query(path, exts, formats)
  query = File.join(@path, path)

  exts.each do |ext|
    query << '{' << ext.map {|e| e && ".#{e}" }.join(',') << ',}'
  end

  Dir[query].reject { |p| File.directory?(p) }.map do |p|
    handler, format = extract_handler_and_format(p, formats)
    Template.new(File.read(p), File.expand_path(p), handler,
      :virtual_path => path, :format => format)
  end
end

是的,你没有看错,我们进行文件读取生成了一个Template,这难道不是性能问题吗?

秉承伟大的无产阶级革命精神,发现问题然后就要解决问题


我们要做的也很简单,学习Rails这种策略,本着lazy和cache精神

@cached[theme_name] ||= ActionView::Base.process_view_paths(theme_path)
self.view_paths = ActionController::Base.view_paths.dup.unshift @cached[theme_name]

如是而已,被我说了这么一通,我果然还是啰嗦型的么~~

假如忍心忽略这一点性能上的损失,就可以在你修改主题文件后即时生效了。不过就算不忽略这一性能问题,我们依然有方法让主题修改后即时生效,不是吗?

TAGS:Rails,Ruby

1 COMMENTS >>LEAVE<<

  1. vzomik

    好勤奋~可惜我不是学这个~加油!

LEAVE A RESPONSE >>CANCEL<<

loader