最近,我写公司项目word导出功能,应该只有2小时的工作量,却被硬生生的拉长2天,项目上线到业务正常运行也被拉长到2个星期。
为什么如此浪费时间呢?
1)公司的项目比较老,采用硬编码模式,意味着word改一个字就要发布一次代码。发布检验就浪时间了。
2)由于硬编码,采用的是<html>这种格式,手写代码比较废时,而且编写表格时会遇到单元格字数变多被撑大,表格变形的情况。表格长度需要人工计算。这类意想不到的问题。
3)公司测试库数据不全,测试库数据无法全面覆盖线上环境。这又拉长了检验时间。
4)项目分支被正在开发的分支合并了,一下子被拉长了4天。
这简单功能浪费太多时间了,我在网上搜了一下word导出的方案:
第一种:硬编码,就是公司的方案,问题太多了不用考虑。
第二种:通过Sql查询数据,存入字典,再通过第三方组件替换word的文字。这种方案,简单容操作,sql查询可以换成存储过程,也存在缺点,1)存储过程要写提很细,逻辑算法都写在存储过程,存储过程可能变得很复杂。2)不支持表格内插入多条数据。
第三种:通过Sql查询数据,使用Razor模板引擎生成word。这种方案解决了存储过程复杂问题,但Razor模板内使用<html>这种格式,所以写模板时很麻烦。
第四种:通过Sql查询数据,存入字典,再通过第三方组件替换word的域。这种方案与第二种方案类似,对我个人来说,我不喜欢修改域。
但是,我想要一个简单、容易控制、表格内能插入多条数据、可商用的方案。
简单:类似第二种方案,数据存入字典,循环替换word的文字,存储过程可以写得简单。
容易控制:模板不能使用<html>这种格式,最好能用office直接控制表格文字大小、颜色。
表格内能插入多条数据:我写的组件内必须有索引。
可商用:拒绝商用组件。
经过几天琢磨,我找到可行的方案:存储过程+模板+算法可控
依赖组件:
DocumentFormat.OpenXml,微软官方开源组件,支持docx文件,MIT协议。
ToolGood.Algorithm,本人的Excel计算引擎组件,MIT协议,可简化存储过程。
核心代码:
ReplaceTemplate 替换Word文字
ReplaceTable 替换Word表格并支持插入
ReplaceTemplate 替换Word文字
public class WordTemplate : AlgorithmEngine { private readonly static Regex _tempEngine = new Regex("^###([^::]*)[::](.*)$");// 定义临时变量 private readonly static Regex _tempMatch = new Regex("(#[^#]+#)");// private readonly static Regex _simplifyMatch = new Regex(@"(\{[^\{\}]*\})");//简化文本 只读取字段 private void ReplaceTemplate(Body body) { var tempMatches = new List<string>(); List<Paragraph> deleteParagraph = new List<Paragraph>(); foreach (var paragraph in body.Descendants<Paragraph>()) { var text = paragraph.InnerText.Trim(); var m = _tempEngine.Match(text); if (m.Success) { var name = m.Groups[1].Value.Trim(); var engine = m.Groups[2].Value.Trim(); var value = this.TryEvaluate(engine, ""); this.AddParameter(name, value); deleteParagraph.Add(paragraph); continue; } var m2 = _tempMatch.Match(text); if (m2.Success) { tempMatches.Add(m2.Groups[1].Value); continue; } var m3 = _simplifyMatch.Match(text); if (m3.Success) { tempMatches.Add(m3.Groups[1].Value); continue; } } foreach (var paragraph in deleteParagraph) { paragraph.Remove(); } Regex nameReg = new Regex(string.Join("|", listNames)); foreach (var m in tempMatches) { string value; if (m.StartsWith("#")) { var eval = m.Trim('#'); …… value = this.TryEvaluate(eval, ""); } else { value = this.TryEvaluate(m.Replace("{", "[").Replace("}", "]"), ""); } foreach (var paragraph in body.Descendants<Paragraph>()) { ReplaceText(paragraph, m, value); } } } // 代码来源 https://stackoverflow.com/questions/19094388/openxml-replace-text-in-all-document private void ReplaceText(Paragraph paragraph, string find, string replaceWith){ …. } }
ReplaceTable 替换Word表格并支持插入
private readonly static Regex _rowMatch = new Regex(@"({{(.*?)}})");// private int _idx; private List<string> listNames = new List<string>(); private void ReplaceTable(Body body) { foreach (Table table in body.Descendants<Table>()) { foreach (TableRow row in table.Descendants<TableRow>()) { bool isRowData = false; foreach (var paragraph in row.Descendants<Paragraph>()) { var text = paragraph.InnerText.Trim(); if (_rowMatch.IsMatch(text)) { isRowData = true; break; } } if (isRowData) { // 防止 list[i].Id 写成 [list][[i]].Id 这种繁杂的方式 Regex nameReg = new Regex(string.Join("|", listNames)); Dictionary<string, string> tempMatches = new Dictionary<string, string>(); foreach (Paragraph ph in row.Descendants<Paragraph>()) { var m2 = _rowMatch.Match(ph.InnerText.Trim()); if (m2.Success) { var txt = m2.Groups[1].Value; var eval = txt.Substring(2, txt.Length - 4).Trim(); eval = nameReg.Replace(eval, new MatchEvaluator((k) => { return "[" + k.Value + "]"; })); tempMatches[txt] = eval; } } TableRow tpl = row.CloneNode(true) as TableRow; TableRow lastRow = row; TableRow opRow = row; var startIndex = UseExcelIndex ? 1 : 0; _idx = startIndex; while (true) { if (_idx > startIndex) { opRow = tpl.CloneNode(true) as TableRow; } bool isMatch = true; foreach (var m in tempMatches) { string value = this.TryEvaluate(m.Value, null); if (value == null) { isMatch = false; break; } foreach (var ph in opRow.Descendants<Paragraph>()) { ReplaceText(ph, m.Key, value); } } if (isMatch==false) { //当数据为空时,清空数据 if (_idx == startIndex) { foreach (var ph in opRow.Descendants<Paragraph>()) { ph.RemoveAllChildren(); } } break; } if (_idx > startIndex) { table.InsertAfter(opRow, lastRow); } lastRow = opRow; _idx++; } } } } }
案例上手:
后台代码:
// 获取数据 var helper = SqlHelperFactory.OpenSqliteFile("test.db"); ....... var dt = helper.ExecuteDataTable("select * from Introduction"); var tableTests = helper.Select<TableTest>("select * from TableTest"); ToolGood.OutputWord.WordTemplate openXmlTemplate = new ToolGood.OutputWord.WordTemplate(); // 加载数据 openXmlTemplate.SetData(dt); openXmlTemplate.SetListData("list", JsonConvert.SerializeObject(tableTests)); // 生成模板 一 openXmlTemplate.BuildTemplate("test.docx", "openxml_2.docx"); // 生成模板 二 var bs = openXmlTemplate.BuildTemplate("test.docx"); File.WriteAllBytes("openxml_1.docx", bs);
Word模板:
Word生成后:
完整代码:https://github.com/toolgood/ToolGood.OutputWord
该组件已上传到Nuget:Install-Package ToolGood.OutputWord
Excel公式参考:https://github.com/toolgood/ToolGood.Algorithm
JAVA版本:暂时没有,ToolGood.Algorithm已支持JAVA版本。