我们的项目中大量使用了 xls.erb 模版来生成 excel 报表, 在我前面写的文章 Rails Respond Format 应用 中,我介绍过如何使用 xls.erb 模版来生成 excel 报表以及 xls.erb 模版的方便之处. 现在问题来了, xls.erb 模版生成的 excel 报表是 xls 类型的, 最新的 MS office excel 以及一些其它平台的 office excel 软件打不开 xls 类型的文件, 所以我们需要将 xls 类型的报表升级到 xlsx 类型的报表. 为了让升级的过程不那么痛苦,我们 需要大量复用以前的 xls.erb 模版, 升级的过程大致如下:

  1. 解析 xls.erb 模版

  2. 将第1步解析得到的数据输入给 Axlsx

  3. 渲染出 xlsx 报表

xls.erb 模版例子

xls.erb 模版其实就是一种用于生成 xml 数据的模版,和 html.erb 模版是一样的逻辑, 只不过 html.erb 模版是用于生成 html 数据的.

我给出一个xls.erb 模版例子:

<?xml version="1.0"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
  xmlns:o="urn:schemas-microsoft-com:office:office"
  xmlns:x="urn:schemas-microsoft-com:office:excel"
  xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
  xmlns:html="http://www.w3.org/TR/REC-html40">
  <Worksheet ss:Name="Sheet1">
    <Table>
      <Row>
        <Cell><Data ss:Type="String"><%= t('id', default: 'ID') %></Data></Cell>
        <Cell><Data ss:Type="String"><%= t('name', default: 'Name') %></Data></Cell>
        <Cell><Data ss:Type="String"><%= t('date', default: 'Date') %></Data></Cell>
        <Cell><Data ss:Type="String"><%= t('number', default: 'Number') %></Data></Cell>
      </Row>
      <% if @report %>
	<% @report.items.each do |item| %>
	  <Row>
            <Cell><Data ss:Type="String"><%= item['id'] %></Data></Cell>
            <Cell><Data ss:Type="String"><%= item['name'] %></Data></Cell>
	    <Cell><Data ss:Type="String"><%= item['date'].to_s.to_local_date %></Data></Cell>
            <Cell><Data ss:Type="String"><%= item['number'] %></Data></Cell>
	  </Row>
	<% end %>
      <% end %>
    </Table>
  </Worksheet>
</Workbook>

解析 xls.erb 模版

xls.erb 模版渲染后就是 xml 数据, 我写了一个 service 类专门用于解析 xls.erb 模版, 然后返回一个 Axlsx::Package 对象

class ParseXmlToXlsxService

  def initialize(xml)
    @xml = xml
    @ap = Axlsx::Package.new
  end

  def call
    h = Hash.from_xml(@xml)

    ws_list = h['Workbook']['Worksheet']
    if ws_list.is_a?(Hash)
      ws_list = [ws_list]
    end

    ws_list.each {|ws|
      add_sheet(@ap, ws)
    }

    @ap.use_shared_strings = true
    
    @ap
  end

  private

  def add_sheet(ap, ws)
    name = ws['ss:Name']
    ap.workbook.add_worksheet(:name => name) do |sheet|
      add_rows(sheet, ws)
    end
  end

  def add_rows(sheet, ws)
    ws['Table']['Row'].each {|item|
      row = []
      item['Cell'].each {|cell|
        if cell['Data'].is_a?(Hash)
          row << ''
        else
          row << cell['Data']
        end
      }
      sheet.add_row(row)
    }
    
  end


end

渲染 xlsx

在 ApplicationController 增加一个方法: render_xlsx,


def render_xlsx(filename, template = nil)
  template ||= File.join(controller_name, "#{action_name}.xls.erb")
  xml = view_context.render(template: template)
  ap = ParseXmlToXlsxService.new(xml).call
  send_data ap.to_stream.read, :type => :xlsx, :filename => filename
end

rendex_xlsx 比较方便的地方是它可以自动找到你需要渲染的 xls.erb 模版

实际使用

首先要在 config/initializers/mime_types.rb 这个文件中,注册 xlsx 类型,

Mime::Type.register "application/xlsx", :xlsx

然后, 假设我们要生成 order report,

class OrdersController < ApplicationController

  def index
    @orders = Order.all
	format.html
	format.xlsx do
	  render_xlsx('orders_report.xlsx')
	end
  end
  
end

在控制器里对应的 action 中增加一个 format.xlsx 即可, 这样我们只需作很少的改动,仍然可以利用 orders/index.xls.erb 模版来生成一个名为 orders_report.xlsx 的 excel 报表.