环境
- 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 # => 这样也可以达到想要的效果
写在最后
最後までご覧いただいてありがとうございます