go 导出 html 报告(使用 hero 预编译 html 模板引擎)

前言

项目需求,需要将服务器的数据进行导出,方便携带展示,做了一次 html 报告的导出,考虑如何实现。

  1. 使用 vue 等前端框架,将数据导出成一个个的 json 文件,然后通过读取 js 来动态渲染 html 视图
  2. 直接将数据写入 html 页面,多导出一些 html,通过 iframe 来进行页面的加载

这里使用的是第二种方式。

本篇主要介绍一下 go 的一个开源库 hero 预编译模板引擎,用于快速的渲染 html

常见的模板引擎一般有两种实现方式,一种是直接解析 HTML 语法树,然后根据一定的规则动态的拼接,另外一种是把模板预先生成代码,渲染模板时调用相关的函数即可。

go 官方内置的 template 包是第一种实现方式,本篇的主角用的是第二种。

hero 简介

可以直接去 github 上看,这里做一个简短的介绍。

安装

go get github.com/shiyanhui/hero
go get github.com/shiyanhui/hero/hero

// Hero需要goimports处理生成的go代码,所以需要安装goimports.
go get golang.org/x/tools/cmd/goimports

使用

hero [options]

options:
	- source:     模板目录,默认为当前目录
	- dest:       生成的go代码的目录,如果没有设置的话,和source一样
	- pkgname:    生成的go代码包的名称,默认为template
  - extensions: source文件的后缀, 如果有多个则用英文逗号隔开, 默认为.html
	- watch:      是否监控模板文件改动并自动编译

example:
	hero -source="./"
	hero -source="$GOPATH/src/app/template" -watch

基本语法

Hero 总共有九种语句,他们分别是:

  • 函数定义语句 <%: func define %>

    • 该语句定义了该模板所对应的函数,如果一个模板中没有函数定义语句,那么最终结果不会生成对应的函数。
    • 该函数最后一个参数必须为*bytes.Buffer或者io.Writer, hero会自动识别该参数的名字,并把把结果写到该参数里。
    • 例:
      • <%: func UserList(userList []string, buffer *bytes.Buffer) %>
      • <%: func UserList(userList []string, w io.Writer) %>
      • <%: func UserList(userList []string, w io.Writer) (int, error) %>
  • 模板继承语句 <%~ "parent template" %>

    • 该语句声明要继承的模板。
    • 例: <%~ "index.html" >
  • 模板include语句 <%+ "sub template" %>

    • 该语句把要include的模板加载进该模板,工作原理和C++中的#include有点类似。
    • 例: <%+ "user.html" >
  • 包导入语句 <%! go code %>

    • 该语句用来声明所有在函数外的代码,包括依赖包导入、全局变量、const等。

    • 该语句不会被子模板所继承

    • 例:

      <%!
      	import (
            	"fmt"
          	"strings"
          )
      
      	var a int
      
      	const b = "hello, world"
      
      	func Add(a, b int) int {
          	return a + b
      	}
      
      	type S struct {
          	Name string
      	}
      
      	func (s S) String() string {
          	return s.Name
      	}
      %>
      
  • 块语句 <%@ blockName { %> <% } %>

    • 块语句是用来在子模板中重写父模中的同名块,进而实现模板的继承。

    • 例:

      <!DOCTYPE html>
      <html>
          <head>
              <meta charset="utf-8">
          </head>
      
          <body>
              <%@ body { %>
              <% } %>
          </body>
      </html>
      
  • Go代码语句 <% go code %>

    • 该语句定义了函数内部的代码部分。

    • 例:

      <% for _, user := range userList { %>
          <% if user != "Alice" { %>
          	<%= user %>
          <% } %>
      <% } %>
      
      <%
      	a, b := 1, 2
      	c := Add(a, b)
      %>
      
  • 原生值语句 <%==[t] variable %>

    • 该语句把变量转换为string。

    • t是变量的类型,hero会自动根据t来选择转换函数。t的待选值有:

      • b: bool
      • i: int, int8, int16, int32, int64
      • u: byte, uint, uint8, uint16, uint32, uint64
      • f: float32, float64
      • s: string
      • bs: []byte
      • v: interface

      注意:

      • 如果t没有设置,那么t默认为s.
      • 最好不要使用v,因为其对应的转换函数为fmt.Sprintf("%v", variable),该函数很慢。
    • 例:

      <%== "hello" %>
      <%==i 34  %>
      <%==u Add(a, b) %>
      <%==s user.Name %>
      
  • 转义值语句 <%= statement %>

    • 该语句把变量转换为string后,又通过html.EscapesString记性转义。

    • t跟上面原生值语句中的t一样。

    • 例:

      <%= a %>
      <%= a + b %>
      <%= Add(a, b) %>
      <%= user.Name %>
      
  • 注释语句 <%# note %>

    • 该语句注释相关模板,注释不会被生成到go代码里边去。
    • 例: <# 这是一个注释 >.

原理

最终生成的代码,就是通过字符串拼接,写入 io.Writer。下面是一个例子,生成后的代码如下:

func WriteTreeNodeHtml(param *RenderTemplateParam, w io.Writer) {
	_buffer := hero.GetBuffer()
	defer hero.PutBuffer(_buffer)
	_buffer.WriteString(`

<html>
    <head>
        <meta charset="utf-8" />
        <link rel="stylesheet" href="css/build.css" />
        <link rel="stylesheet" href="css/jquery.treeview.css" />
        <link rel="stylesheet" href="css/screen.css" />

        <script src="js/jquery.min.js"></script>
        <script src="js/jquery.cookie.js"></script>
        <script src="js/jquery.treeview.js" type="text/javascript"></script>
        <script type="text/javascript">
        $(function() {
            $("#tree").treeview({
                collapsed: true,
                animated: "fast",
                control: "#sidetreecontrol",
                prerendered: true,
                persist: "location"
            });
        })
        </script>
    </head>

    <body style="margin: 10px;">
        <div>
            <h3>`)
	hero.EscapeHTML(GetAppName(), _buffer)
	_buffer.WriteString(`报告</h3>
            <div id=jstree style="font-size:14px">
            <ul class="treeview" id="tree" style="margin-top:6px;">
                <li><a class="jstree-anchor" href="page1.html#case" target="pageframe">
                <i style="margin-left: 4px;margin-right: 4px;" class="icon-file iconfont"></i>案件</a></li>
                <li><a class="jstree-anchor" href="page1.html#evidences" target="pageframe">
                <i style="margin-left: 4px;margin-right: 4px;" class="icon-evidence iconfont"></i>检材信息</a></li>
                <li><a class="jstree-anchor" href="page1.html#brief" target="pageframe">
                <i style="margin-left: 4px;margin-right: 4px;" class="icon-evidence iconfont"></i>数据统计文字概括</a></li>
                <li><a class="jstree-anchor" href="page1.html#summary" target="pageframe">
                <i style="margin-left: 4px;margin-right: 4px;" class="icon-summary iconfont"></i>数据统计清单</a></li>
                `)
	treeNodes, ok := param.Data.([]*ReportTreeNode)
	if !ok {
		return
	}
	for _, node := range treeNodes {
		GenerateTreeNode(node, _buffer)
	}
	_buffer.WriteString(`
            </ul>
            </div>
        </div>
    </body>
</html>

`)
	w.Write(_buffer.Bytes())

}

总结

使用 go 生成 html,原理很简单,通过字符串拼接,将数据都写入对应的地方即可。麻烦的点就在于 html 页面的布局和数据的插入对应。

参考

https://github.com/shiyanhui/hero

上一篇:为什么在Scala中出现OutOfMemoryError编译错误?


下一篇:定时任务,AlarmManager使用