Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
天給大家分享SpringBoot集成FreeMarker模板引擎生成word文件的用法,感興趣的可以學一下,完整源碼地址在文章末尾處,歡迎互相溝通交流!
FreeMarker 是一款開源的模板引擎: 是一種基于模板和要動態(tài)填充的數(shù)據(jù),可以用來動態(tài)渲染生成輸出文本(HTML網(wǎng)頁,Word文檔,電子郵件,配置文件,源代碼等)的通用技術。
模板編寫為FreeMarker Template Language (FTL):它是簡單的,專用的語言, 不是像PHP那樣擁有完整成熟的編程語言。所以它主要專注于如何展現(xiàn)數(shù)據(jù),具體要展示什么數(shù)據(jù)那就需要成熟的編程語言來實現(xiàn)(Java、C#、Python)等。
FreeMarker原理圖如下:
<!--freemarker制作Html郵件模板依賴包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
freemarker:
cache: false #是否啟用緩存,開發(fā)環(huán)境不建議啟動因為涉及經(jīng)常修改模板調(diào)試
settings:
classic_compatible: true
suffix: .html #一般格式tpl居多
charset: UTF-8
template-loader-path: classpath:/templates/ #模板路徑,一般都是這個
package com.springboot.email.email.service;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
public interface IExportService {
/**
* 導出word文件到指定目錄
*/
void exportDocFile(String fileName, String tplName, Map<String, Object> data) throws Exception;
/**
* 導出word文件到客戶端
*/
void exportDocToClient(HttpServletResponse response, String fileName, String tplName, Map<String, Object> data) throws Exception;
}
package com.springboot.email.email.service.impl;
import com.springboot.email.email.service.IExportService;
import freemarker.template.Template;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.Map;
@Service
public class ExportServiceImpl implements IExportService {
private String encoding;
private String exportPath = "D:\\export\\";
@Autowired
private FreeMarkerConfigurer freeMarkerConfigurer;
public Template getTemplate(String name) throws Exception {
return freeMarkerConfigurer.getConfiguration().getTemplate("test.html");
}
/**
* 導出本地文件到指定的目錄
*/
@Override
public void exportDocFile(String fileName, String tplName, Map<String, Object> data) throws Exception {
//如果目錄不存在,則創(chuàng)建目錄
File exportDirs = new File(exportPath);
if (!exportDirs.exists()) {
exportDirs.mkdirs();
}
Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(exportPath + fileName), encoding));
getTemplate(tplName).process(data, writer);
}
/**
* 導出word文件到瀏覽器客戶端
*/
@Override
public void exportDocToClient(HttpServletResponse response, String fileName, String tplName, Map<String, Object> data) throws Exception {
response.reset();
response.setCharacterEncoding("UTF-8");
response.setContentType("application/msword");
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName , "UTF-8"));
// 把本地文件發(fā)送給客戶端
Writer out = response.getWriter();
Template template = getTemplate(tplName);
template.process(data, out);
out.close();
}
}
具體文件參考源碼,模板文件的制作方式,新建word文件調(diào)整格式后→另存為xml格式的文件→局部調(diào)整文件循環(huán)標記→然后格式保存為html格式的文件 放在項目當中去。
如果有不清楚的可以留言交流。
@RestController
@RequestMapping("/export")
public class ExportController {
@Autowired
private IExportService exportService;
@RequestMapping(value = "/testWord", method= RequestMethod.GET)
public void exportWord(HttpServletRequest request, HttpServletResponse response) throws Exception {
String fileName = "測試word導出.doc"; //文件名稱
// 設置頭部數(shù)據(jù)
Map<String,Object> dataMap = new HashMap<>();
dataMap.put("name","小明");
dataMap.put("regAddress","蘇州");
// 設置表格數(shù)據(jù)
List<ScoreVo> list=new ArrayList<>();
ScoreVo vo1=new ScoreVo();
vo1.setCourseName("英語");
vo1.setScore(95);
vo1.setRank(3);
ScoreVo vo2=new ScoreVo();
vo2.setCourseName("數(shù)學");
vo2.setScore(100);
vo2.setRank(1);
list.add(vo1);
list.add(vo2);
dataMap.put("courseList",list);
exportService.exportDocToClient(response, fileName, "test.html", dataMap);
}
}
訪問地址:http://localhost:8080/export/testWord
導出文件的效果:
Gitee地址:https://gitee.com/hgm1989/springboot-email.git
ender,顧名思義,要進行頁面渲染。Go 語言不但自帶有強大的 http 庫,還自帶了 HTML 模板引擎。Echo 框架對模板引擎進行了一些額外處理,并提供了給用戶自定義頁面渲染的接口。本文就相關問題進行探討。
Echo 框架的 Context 接口提供了下面的方法進行頁面渲染:
// echo 包中 Context 接口的方法
Render(code int, name string, data interface{}) error
其中,code 是 HTTP Status,name 是定義的模板名,data 是模板可能需要的數(shù)據(jù)。執(zhí)行這個方法后,通過數(shù)據(jù)渲染模板,并發(fā)送帶有 HTTP 狀態(tài)的 text/html 響應。可以通過 Echo.Renderer 來注冊模板,從而允許我們使用任何模板引擎。
Renderer 接口定義如下:
// Renderer is the interface that wraps the Render function.
type Renderer interface {
Render(io.Writer, string, interface{}, Context) error
}
這里可能會有點迷糊,怎么有兩個 Render 方法,而且它們的簽名還不一樣。這里的邏輯是這樣的:
這里是具體的渲染源碼:
func (c *context) Render(code int, name string, data interface{}) (err error) {
if c.echo.Renderer == nil {
return ErrRendererNotRegistered
}
buf := new(bytes.Buffer)
if err = c.echo.Renderer.Render(buf, name, data, c); err != nil {
return
}
return c.HTMLBlob(code, buf.Bytes())
}
可見,如果調(diào)用了 Context#Render 進行模板渲染,但并沒有注冊模板引擎則會報錯(ErrRendererNotRegistered)。
1、我們先定義一個類型:Template,然后實現(xiàn) Echo.Renderer 接口,即提供 Render 方法。
type Template struct {
templates *template.Template
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
2、接著預編譯一個模板。定義一個模板文件:template/index.html,內(nèi)容如下:
{{define "index"}}Hello, {{.}}!{{end}}
然后預編譯得到 Template 的實例:
tpl := &Template{
templates: template.Must(template.ParseGlob("template/*.html")),
}
3、注冊模板引擎:
e := echo.New()
e.Renderer = tpl
4、在 Handler 中渲染模板:
e.GET("/", func(ctx echo.Context) error {
return ctx.Render(http.StatusOK, "index", "studygolang")
})
注意這里的 index 是模板文件中 define "index" ,而不是文件名。
編譯后運行,瀏覽器正常顯示:Hello,studygolang!
一般的,頁面會有一些通用的部分,比如頭部、尾部等。所以業(yè)界通常的做法是有一個 layout,而且還可能不止一個 layout,因為普通用戶看到的和后臺看到的頭部、尾部一般會不一樣。那這樣的通用化定制需求該如何集成到 Echo 的 Render 中呢?
先考慮只有一種 layout 的情況。定義一個類型 layoutTemplate,實現(xiàn) Echo.Renderer 接口:
type layoutTemplate struct{}
var LayoutTemplate = &layoutTemplate{}
func (l *layoutTemplate) Render(w io.Writer, contentTpl string, data interface{}, ctx echo.Context) error {
layout := "layout.html"
tpl, err := template.New(layout).ParseFiles("template/common/"+layout, "template/"+contentTpl)
if err != nil {
return err
}
return tpl.Execute(w, data)
}
然后注冊該 Renderer,并在 Handler 中渲染,注意 ctx.Render 的第二個參數(shù),跟上面說的不一樣,我們傳遞的是子模板的文件名:index.html。
e := echo.New()
e.Renderer = render.LayoutTemplate
e.GET("/", func(ctx echo.Context) error {
return ctx.Render(http.StatusOK, "index.html", nil)
})
這里用到了兩個模板文件:layout.html 和 index.html,來源 Hugo 的 soho 這個模板。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Echo博客系統(tǒng)</title>
<meta name="author" content="Go語言中文網(wǎng)站長polaris">
<meta name="keywords" content="" />
<meta name="description" content="" />
<link type="text/css" rel="stylesheet" href="/static/css/print.css" media="print">
<link type="text/css" rel="stylesheet" href="/static/css/poole.css">
<link type="text/css" rel="stylesheet" href="/static/css/hyde.css">
<link rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700&display=swap">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/all.min.css"
integrity="sha256-mmgLkCYLUQbXn0B1SRqzHar6dCnv9oZFPEC1g1cwlkk="
crossorigin="anonymous" />
<link rel="apple-touch-icon-precomposed"
sizes="144x144"
href="https://themes.gohugo.io//theme/soho/apple-touch-icon-144-precomposed.png">
<link rel="shortcut icon" href="https://themes.gohugo.io//theme/soho/favicon.png">
</head>
<body>
<aside class="sidebar">
<div class="container">
<div class="sidebar-about">
<div class="author-image">
<img src="https://themes.gohugo.io/theme/soho/images/profile.png" class="img-circle img-headshot center" alt="Profile Picture">
</div>
<h1>Echo-Gopher</h1>
</div>
<nav>
<ul class="sidebar-nav">
<li> <a href="/">Home</a> </li>
<li> <a href="/about/"> About </a> </li>
</ul>
</nav>
<section class="social-icons">
<a href="https://github.com/polaris1119" rel="me" title="GitHub">
<i class="fab fa-github" aria-hidden="true"></i>
</a>
<a href="https://weibo.com/studygolang" rel="me" title="Weibo">
<i class="fab fa-weibo" aria-hidden="true"></i>
</a>
</section>
</div>
</aside>
<main class="content container">
{{template "content" .}}
</main>
<footer>
<div class="copyright">
? polaris 2020 · <a href="https://creativecommons.org/licenses/by-sa/4.0">CC BY-SA 4.0</a>
</div>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/js/all.min.js"
integrity="sha256-MAgcygDRahs+F/Nk5Vz387whB4kSK9NXlDN3w58LLq0="
crossorigin="anonymous"></script>
</body>
</html>
這是 layout.html 的內(nèi)容,核心在于 {{template "content" .}},表示具體內(nèi)容模板需要定義 content,所以看看 index.html 文件:
{{define "content"}}
<div class="posts">
<article class="post">
<h2 class="post-title">
<a href="/">Echo 系列教程 — 定制篇3:自定義 Logger,用你喜歡的日志庫</a>
</h2>
<div class="post-date">
<time datetime="2020-03-06T00:00:00Z">Mar 06, 2020</time> · 3 min read
</div>
在知識星球簡書項目中,我們分析對比了目前的一些日志庫。雖然 Go 標準庫有一個 log,但功能有限,所以才出現(xiàn)了很多第三方的日志庫。
<div class="read-more-link">
<a href="http://blog.studygolang.com/2020/03/echo-custom-logger/">閱讀全文</a>
</div>
</article>
<article class="post">
<h2 class="post-title">
<a href="/">Echo 系列教程 — 定制篇2:自定義 Validator,進行輸入校驗</a>
</h2>
<div class="post-date">
<time datetime="2020-02-28T00:00:00Z">Feb 28, 2020</time> · 4 min read
</div>
上一篇講 Binder 時提到,參數(shù)自動綁定和校驗是 Web 框架很重要的兩個功能,可以極大的提升開發(fā)速度,并更好的保證數(shù)據(jù)的可靠性(服務端數(shù)據(jù)校驗很重要)。
<div class="read-more-link">
<a href="http://blog.studygolang.com/2020/02/echo-custom-validator/">閱讀全文</a>
</div>
</article>
</div>
{{end}}
運行后打開瀏覽器訪問 http://localhost:2020 :
接下來看看如何處理多個 layout 的情況。
因為 Render 的簽名是固定的,不同的 layout 通過什么方式告知 Render 呢?觀察 Render 方法的參數(shù):
Render(w io.Writer, name string, data interface{}, ctx echo.Context)
可以在 data 和 ctx 上下功夫:
先看第 1 種方式:
// NoNavRender 沒有導航的 layout html 輸出
func NoNavRender(ctx echo.Context, contentTpl string, data map[string]interface{}) error {
if data == nil {
data = make(map[string]interface{})
}
data["layout"] = "nonav_layout.html"
return ctx.Render(http.StatusOK, contentTpl, data)
}
在 render 包中增加了一個 NoVaRender 函數(shù),該函數(shù)要求 data 必須是 map[string]interface{},這樣就可以做到將 layout 傳遞給 Render 方法,不過因為 Render 方法的 data 參數(shù)是 interface{} 類型,因此得做類型斷言。
layout := "layout.html"
if data != nil {
if dataMap, ok := data.(map[string]interface{}); ok {
if layoutInter, ok := dataMap["layout"]; ok {
layout = layoutInter.(string)
}
}
}
看看第 2 種方式如何實現(xiàn):
// NoNavRender 沒有導航的 layout html 輸出
func NoNavRender(ctx echo.Context, contentTpl string, data interface{}) error {
ctx.Set("layout", "nonav_layout.html")
return ctx.Render(http.StatusOK, contentTpl, data)
}
在 Render 中獲取 layout 的值:
layout := "layout.html"
layoutInter := ctx.Get("layout")
if layoutInter != nil {
layout = layoutInter.(string)
}
兩種方式個人覺得第 2 種更優(yōu)雅。不過需要注意的是,兩種方式要注意 layout 不能沖突,也就是不能他用。
另外,我個人建議,data 參數(shù)永遠要么傳遞 nil,要么傳遞 map[string]interface{} 。個人感覺 Echo 的 Render 方法 data 參數(shù)的類型不應該用 interface{} 而是用 map[string]interface{},這樣可以更方便地往 data 中加入更多全局的數(shù)據(jù)。在簡書項目中,我們會通過其他方式彌補這個問題。
通過本節(jié),你應該掌握了 Render 的使用、集成和大項目 layout 的處理。
額外提一句,因為 Context.Render 方法最終是調(diào)用的 Context.HTML 方法進行渲染,因此我們也完全可以拋棄 Render 方法,而是使用自己的 Render。目前簡書的代碼(后續(xù)會改掉)和 studygolang 的源碼采用的就是完全拋棄 Context.Render 的方式,主要考慮還是有一些 Render 不能很好滿足的地方,比如上面說的多 layout、data 類型等,不過也是可以解決的。因此還是建議采用 Echo 框架的 Render。
本節(jié)完整代碼點這里: https://github.com/polaris1119/go-echo-example/tree/0cd46e8b1f38317439e95d55e3fe29a173a2e3c1。
寫公共方法,這里以賦值到 laytpl 對象為例
之所以將方法暴露給寫成方法,是便于能讀取到。
toDateString(d, format) 方法接受兩個參數(shù)。其中 d 可以是日期對象,也可以是毫秒數(shù)。format 是日期字符的格式,你可以隨意定義,如:yyyy年MM月dd日
在列模板中調(diào)用時間戳的處理方法
講解:
d.time 中的 time 即是你接口返回的字段,如果是 unix 時間戳,這里記得要 d.time*1000,如果是毫秒數(shù),這里直接傳 d.time 即可。
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。