[关闭]
@elibinary 2017-01-01T07:08:44.000000Z 字数 5351 阅读 993

关于 carrierwave 的实践报告

Ruby


Classier solution for file uploads for Rails, Sinatra and other Ruby web frameworks.
This gem provides a simple and extremely flexible way to upload files from Ruby applications. It works well with Rack based web applications, such as Ruby on Rails.

What

Carrierwave - GitHub

正如其 README 中所描述的,这个 gem 提供了一套文件上传的解决方案来提高开发效率。使用起来简单方便,不需要再自己手写 File.open file.write 之类的文件操作,一切交给 Carrierwave 的 upload 去做。具体用法详见上面链接的README文件。

看完使用手册后,这里多说一句,关于你的 uploader 类:

  1. class YourUploader < CarrierWave::Uploader::Base
  2. # Do something
  3. end

如果你只是单纯的想要使用文件上传的功能方法,而并不想要其辅助于你的 model 那么你就直接使用你的 uploader 类就行了。 CarrierWave 的 Uploader 提供了很多方便的方法,比如:

  1. uploader = YourUploader.new
  2. # 存储
  3. uploader.store!(file)
  4. # 取出
  5. uploader.retrieve_from_store!(filename)

同时如果你想用于辅助你的 ORM ,除了关系数据库外,它对 DataMapper, Mongoid, Sequel 也进行了支持,具体怎么实践也可以在文档中找到,这里就不赘述了。

How

当你想要为你的 ORM mount 一个 upload 时,do this:

  1. class User < ActiveRecord::Base
  2. mount_uploader :avatar, AvatarUploader
  3. end

之后就可以使用 carrierwave 提供的一系列方便的方法了。比如直接使用赋值来 assign 一个 file

  1. u = User.new
  2. u.avatar = params[:file]

那么 mount_uploader 到底做了什么?让我们来深入看看

先看入口,由于 carrierwave 的代码量还真是挺大,这里就只捡要点看了

  1. module CarrierWave
  2. class Railtie < Rails::Railtie
  3. initializer "carrierwave.setup_paths" do |app|
  4. CarrierWave.root = Rails.root.join(Rails.public_path).to_s
  5. CarrierWave.base_path = ENV['RAILS_RELATIVE_URL_ROOT']
  6. end
  7. initializer "carrierwave.active_record" do
  8. ActiveSupport.on_load :active_record do
  9. require 'carrierwave/orm/activerecord'
  10. end
  11. end
  12. end
  13. end

看这一段,它使用 lazy-load hooks (稍后如果我还记得的话,再来单独写一篇关于 lazy-load hooks 的介绍),这里先往下看它在 load :active_record 的时候 require 了自己的 carrierwave/orm/activerecord ,来把目光转入这里

  1. # carrierwave/orm/activerecord.rb
  2. # See +CarrierWave::Mount#mount_uploader+ for documentation
  3. #
  4. def mount_uploader(column, uploader=nil, options={}, &block)
  5. super
  6. class_eval <<-RUBY, __FILE__, __LINE__+1
  7. def remote_#{column}_url=(url)
  8. column = _mounter(:#{column}).serialization_column
  9. send(:"\#{column}_will_change!")
  10. super
  11. end
  12. RUBY
  13. end

第一眼就看到了我们想要找的方法,看到方法第一行的 super 就知道这个方法来自别处

  1. include CarrierWave::Mount

看它 include 了 CarrierWave::Mount,进去看看

  1. # carrierwave/mount.rb
  2. def mount_uploader(column, uploader=nil, options={}, &block)
  3. mount_base(column, uploader, options, &block)
  4. mod = Module.new
  5. include mod
  6. mod.class_eval <<-RUBY, __FILE__, __LINE__+1
  7. def #{column}
  8. _mounter(:#{column}).uploaders[0] ||= _mounter(:#{column}).blank_uploader
  9. end
  10. def #{column}=(new_file)
  11. _mounter(:#{column}).cache([new_file])
  12. end
  13. def #{column}_url(*args)
  14. #{column}.url(*args)
  15. end
  16. def #{column}_cache
  17. _mounter(:#{column}).cache_names[0]
  18. end
  19. def #{column}_cache=(cache_name)
  20. _mounter(:#{column}).cache_names = [cache_name]
  21. end
  22. def remote_#{column}_url
  23. [_mounter(:#{column}).remote_urls].flatten[0]
  24. end
  25. def remote_#{column}_url=(url)
  26. _mounter(:#{column}).remote_urls = [url]
  27. end
  28. def remote_#{column}_request_header=(header)
  29. _mounter(:#{column}).remote_request_headers = [header]
  30. end
  31. def write_#{column}_identifier
  32. return if frozen?
  33. mounter = _mounter(:#{column})
  34. if mounter.remove?
  35. write_uploader(mounter.serialization_column, nil)
  36. elsif mounter.identifiers.first
  37. write_uploader(mounter.serialization_column, mounter.identifiers.first)
  38. end
  39. end
  40. def #{column}_identifier
  41. _mounter(:#{column}).read_identifiers[0]
  42. end
  43. def store_previous_changes_for_#{column}
  44. @_previous_changes_for_#{column} = changes[_mounter(:#{column}).serialization_column]
  45. end
  46. def remove_previously_stored_#{column}
  47. before, after = @_previous_changes_for_#{column}
  48. _mounter(:#{column}).remove_previous([before], [after])
  49. end
  50. RUBY
  51. end

果然,这里定义了一系列的辅助方法,按照惯例从此 module 的注释看起

  1. # If a Class is extended with this module, it gains the mount_uploader
  2. # method, which is used for mapping attributes to uploaders and allowing
  3. # easy assignment.

从上面代码可以看出,当你使用 mount_uploader 把一个 attribute mount 到目标 uploader 上时,它首先做的一件事就是复写读取和赋值此 attribute 的方法,来看看 _mounter(:#{column}) 取出的到底是什么东西,首先看到 mount_uploader 方法第一行
mount_base(column, uploader, options, &block)
我们来看下 mount_base 方法

  1. # carrierwave/mount.rb
  2. def mount_base(column, uploader=nil, options={}, &block)
  3. include CarrierWave::Mount::Extension
  4. uploader = build_uploader(uploader, &block)
  5. uploaders[column.to_sym] = uploader
  6. uploader_options[column.to_sym] = options
  7. ...
  8. ...
  9. end
  10. def build_uploader(uploader, &block)
  11. return uploader if uploader && !block_given?
  12. uploader = Class.new(uploader || CarrierWave::Uploader::Base)
  13. const_set("Uploader#{uploader.object_id}".tr('-', '_'), uploader)
  14. if block_given?
  15. uploader.class_eval(&block)
  16. uploader.recursively_apply_block_to_versions(&block)
  17. end
  18. uploader
  19. end

首先我们传进来的是我们自己的 uploader 类,并且此类继承自 CarrierWave::Uploader::Base
mount_base 方法除了定义一些辅助方法外主要做了一件事,那就是为 @uploaders @uploader_options 这两个变量赋值。看方法 _mounter

  1. # carrierwave/mount.rb
  2. def _mounter(column)
  3. # We cannot memoize in frozen objects :(
  4. return Mounter.new(self, column) if frozen?
  5. @_mounters ||= {}
  6. @_mounters[column] ||= Mounter.new(self, column)
  7. end

我们来看 Mounter 类

  1. # carrierwave/mounter.rb
  2. def initialize(record, column, options={})
  3. @record = record
  4. @column = column
  5. @options = record.class.uploader_options[column]
  6. end
  7. def uploader_class
  8. record.class.uploaders[column]
  9. end
  10. def blank_uploader
  11. uploader_class.new(record, column)
  12. end
  13. def identifiers
  14. uploaders.map(&:identifier)
  15. end
  16. def read_identifiers
  17. [record.read_uploader(serialization_column)].flatten.reject(&:blank?)
  18. end
  19. def uploaders
  20. @uploaders ||= read_identifiers.map do |identifier|
  21. uploader = blank_uploader
  22. uploader.retrieve_from_store!(identifier) if identifier.present?
  23. uploader
  24. end
  25. end

主要看这几个方法,可以看出最终 _mounter(:#{column}).uploaders 返回的是一组 uploader 的实例。
这里再回头看文档中的描述:

Note: u.avatar will never return nil, even if there is no photo associated to it. To check if a > photo was saved to the model, use u.avatar.file.nil? instead.

直接取 u.avatar 时返回值是绝对不会为空的,因为就算什么也没赋值,它返回的也是一个 uploader 的实例。如果你想要取数据库中真是存储的值时,可以使用 attributes 来取,像这样:

  1. u.attributes['avatar']

Trap

在使用的过程中也遇到了不少的问题,大多问题都可以在 wiki 中找到解决方法,下篇我会列几个把我害惨的坑来与大家分享。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注