Rails 6 Prawn PDF引入实例变量的方法

环境

  • Ruby On Rails 6.0
  • Ruby 2.6.5
  • Prawn PDF 2.2.2

全都是 nil

   在使用Prawn PDF的导出PDF时,发现了一个问题。使用Prawn::Document.new并传入一个块时,块的作用域是产生的Prawn::Document实例对象的内部,而不是当前代码块的作用域。因此,当你试图在块里边使用当前类的实例变量时,会发现实例变量的值为nil

class Document
    def title
        "How to import instance variable to prawn document instance"
    end

    def content
        "Page Not Found."
    end
end

class  DocumentExporter
    def initialize
        @document = Document.new
    end

    def export
        Prawn::Document.new do 
            text @document.title # 这里的 @document 实际上是 nil

            text @document.content
        end.render
    end
end

DocumentExporter.new.export    # => undefined method 'title' for nil:NilClass


原因

   一般来说,块的作用域对应当前环境的作用域(详情参考元编程),这里应该是有地方将块执行作用域改成了pdf对象的作用域。看了Prawn::Document.new的定义,发现initialize方法最后用了instance_eval,这就是问题所在了。

   def initialize(options = {}, &block)
      # 这里省略了很多无关的内容

      if block
        # 当块没有参数时,调用 instance_eval ,有参数时,将当前对象传给块的参数
        # 这里的 block[self] 相当于 block.call(self),  在 proc 里边,[] 是 call 的别名
        block.arity < 1 ? instance_eval(&block) : block[self] 
      end
    end

   Object#instance_eval会把块里边的内容放到调用者的作用域下执行。上面这一句的意思是,在传入的块没有参数时, 块会被传入instance_eval 中执行,因为这里的instance_eval没有指明调用对象,那么就是self了。好处在于我们可以像这样定义 PDF 的内容:

class  DocumentExporter
    def export
        Prawn::Document.new do 
            text "This is title"

            table([[1, 2], [3, 4]])
        end.render
    end
end

   而不需要像下面这样

class  DocumentExporter
    def export
        Prawn::Document.new do |pdf|
            pdf.text  "This is title"

            pdf.table([[1, 2], [3, 4]])
        end.render
    end
end

   坏处就是像一开始所说的,作用域变成了 Prawn::Document 实例对象的作用域,那么就拿不到当前环境的实例变量了。


解决方案

要想拿到当前作用域,有三种办法,推荐使用继承,其余两种供参考:

一、继承(推荐)

class Document
    def title
        "How to import instance variable to prawn document instance"
    end

    def content
        "Page Not Found."
    end
end

# 封装基类
class BasePDF < Prawn::Document
    def initialize(options = {}, &block)
        # 这里也可以直接用 super(options, &block) 的方式调用父类初始化方法,
        # 但是为了可以在基类中执行一些通用的操作,所以使用手动传入块的方式
        super(options) do

        # 可以执行一些公共设置,如设置字体之类的

        block.call
      end
    end
end

class DocumentExporter < BasePDF
    def initialize(document, options = {}, &block)
        @document = document

        super(options) do
            set_title
            set_content

            block.call
      end
    end

    def set_title
        text @document.title
    end

    def set_content
        text @document.content
    end
end

# 使用方式
DocumentExporter.new(Document.new, page_size: "A4").render

二、传入的块带参数,那样就不会调用instance_eval

class Document
    def title
        "How to import instance variable to prawn document instance"
    end

    def content
        "Page Not Found."
    end
end

class  DocumentExporter
    def initialize
        @document = Document.new
    end

    def render
        Prawn::Document.new do |pdf|
            pdf.text @document.title

            pdf.text @document.content
        end.render
    end

end

DocumentExporter.new.render    # => 这样就可以达到想要的效果

   如果 PDF 的内容不是很多的话,用上面这种方法是比较方便的,如果内容多的话(一般来说都很多),而且你不想看到满屏幕都是pdf.text的话,那么你可以使用下面的办法。


三、用魔法打败魔法:使用instance_exec

class Document
    def title
        "How to import instance variable to prawn document instance"
    end

    def content
        "Page Not Found."
    end
end


class  DocumentExporter
    def initialize
        @document = Document.new
    end

    def render
        # 这里不在创建 pdf 的时候定义内容,只是创建一个 pdf 对象
        pdf = Prawn::Document.new

        # 定义 pdf 内容
        # instance_exec 和 instance_eval 的作用一样,区别在于 instance_exec 可以传入参数
        pdf.instance_exec(@document) do |document|
            @document = document

            text  @document.title

            text @document.content
        end.render
    end
end

DocumentExporter.new.render    # => 这样也可以达到想要的效果


写在最后

最後までご覧いただいてありがとうございます

点赞

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注