使用docx.js生成word
JS Docx生成库使用案例
百家饭OpenAPI v0.7.0准备引入通过Swagger文件自动生成word的功能,因此研究和实现了基于Docx.js(https://docx.js.org/)生成文档的功能,也和大家分享一下使用这个库的一些经验。
为什么要用前端库
docx文件结构其实就是个xml,比上一代doc文件结构简单多了,上一代是基于ole2的文件格式,比较复杂,我们当时写开源的xls库的时候就很麻烦,整个文件是个二进制的结构体系。因此首先觉得docx前端处理应该问题不大。
其次一个问题是,golang好像没有特别好的doc库……,只查到了一个模板生成库,这个库还没有控制功能,不能首先循环控制等功能,我们要实现根据api定义生成文档,难度就比较大。还有一个虽然代码开源,但是使用需要在一个网站上申请key的玩意,一看就不怀好心。
所以想来想去还是用js库好了,至少还搜到了这个docx.js。
前端生成word的其他方法
百家饭OpenAPI在v0.6.0版本的时候,其实引入了一个叫清洁模式的功能,就是把文档中所有的功能按钮都删除了,这样,其实希望直接复制粘贴生成word。
主体其实是没有问题的,但是有两个问题:
1)使用css控制的样式失效
2)表格宽度出错
这是对应的网上的样式
另外可以直接使用html代码生成word,只需要前面加上以下的对应的类型声明即可,我们的导出函数如下:
function Export2Word() {
var preHtml =
"<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'><head><meta charset='utf-8'><title>" +
docTitle.value +
"</title></head><body>";
var postHtml = "</body></html>";
var html = documentDiv.value.innerHTML + postHtml;
var blob = new Blob(["\ufeff", html], {
type: "application/msword",
});
// Specify link url
var url =
"data:application/vnd.ms-word;charset=utf-8," + encodeURIComponent(html);
// Specify file name
var filename = docTitle.value ? docTitle.value + ".doc" : "document.doc";
// Create download link element
var downloadLink = document.createElement("a");
document.body.appendChild(downloadLink);
if (navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(blob, filename);
} else {
// Create a link to the file
downloadLink.href = url;
// Setting the file name
downloadLink.download = filename;
//triggering the function
downloadLink.click();
}
document.body.removeChild(downloadLink);
}
这个的同样问题是样式丢失,和复制粘贴一个效果,如果你要生成的内容样式比较简单,你在页面上也处理的很干净,没有表格和编号等样式,可以直接考虑。
Docx.js库的使用
然后我们就要开始介绍Docx.js库的使用了,使用效果还是很满意的。
Docx.js只需要引入以下库
yarn add docx
在需要使用该功能的页面引入docx库
import * as docx from "docx";
注意,不是直接import docx
生成文档
我们使用的是7.4.1版本,导出就是生成一个docx.Document对象,传入参数就是文档的整体定义,我们使用的情况如下
const doc = new docx.Document({
//文档作者,显示在文档属性中
creator: "百家饭OpenAPI为用户生成",
//文档标题,显示在文档属性中
title: docTitle.value,
//文档介绍,显示在文档属性中
description:,
//文档中使用到的编号样式,稍后详细介绍
numbering: {...},
//文档中使用到的文字样式等,稍后详细介绍
styles: {},
//对应word中的节
sections: [
{
properties: {},
//页眉
headers: {
//默认页面,除了default,还可以通过first定义首页页面,通过even定义偶数页页眉
default:...
},
//主体内容
children: chidrenInDoc,
},
],
});
可以看出,对于页面内容的控制,基本都是通过这个选项表进行控制的,如果不是想开发一个完整功能的word编辑器,都可以沿用这个思路,
我们在搜索用法的时候,网上很多资料,包括官网资料都指向先定义一个元素,再使用setter函数进行各种编辑的方向,我们首先尝试有很多失败的地方,二是逻辑较为复杂,因此,我们还是建议,针对生成场景,都围绕这个选项表进行配置,一次性完成生成。
定义内容
首先要做的工作就是根据页面内容逐一生成对应的docx里的组件,这里的组件包括各类标题、段落、列表、表格等。经测试,标题、段落、列表基本都对应docx.Paragraph对象,例如我们基本就是把innerHTML再变成docx.Paragraph,如果是标题,则指定heading参数,如果是列表就指定bullet参数
return [
new docx.Paragraph({
text: item.childNodes[0].nodeValue,
//如果是各级标题,就附加下面这行参数,是几级标题,就把6改成几
heading: docx.HeadingLevel.HEADING_6,
//如果是列表,就附加下面这个定义
bullet: {
level: 0, // How deep you want the bullet to be. Maximum level is 9
},
}),
];
解释一下,为什么我会返回数组,这是因为docx里面并没有html这样的层级管理,基本都是平铺Paragraph,例如下面的例子ul并不需要ul-li两级,只需要对应的两个li对应的带Bullet属性的Paragraph,所以我都是返回数组,再在上层进行拼接。
<ul>
<li></li>
<li></li>
</ul>
定义样式
样式是传入参数的styles部分,内容如下
styles: {
paragraphStyles: [
{
id: "Normal",
name: "Normal",
run: {
font: props.displayOption.font,
},
},
{
id: "Heading1",
name: "Heading 1",
basedOn: "Normal",
next: "Normal",
quickFormat: true,
run: {
size: 32,
bold: true,
},
paragraph: {
alignment: docx.AlignmentType.CENTER,
spacing: {
before: 240,
after: 120,
},
},
},
...//更多样式
],
},
可以看出styles主要是定义了文档中使用的各种样式,style的类型定义如下:
export interface IStylesOptions {
readonly default?: IDefaultStylesOptions;
readonly initialStyles?: BaseXmlComponent;
readonly paragraphStyles?: IParagraphStyleOptions[];
readonly characterStyles?: ICharacterStyleOptions[];
readonly importedStyles?: (XmlComponent | StyleForParagraph | StyleForCharacter | ImportedXmlComponent)[];
}
我们主要定义了其中的paragraphStyles,至于其他几个类型的效果是什么样的,我们这次的工作没有涉及,大家可以尝试,这里只能说用paragraphStyles可以定义出样式来,这是没问题的。
而paragraphStyles的主要内容就是样式的数组,上面的例子可以看出,我们主要改了Normal(正文)的字体,然后定义了一个Heading 1(标题1)的样式,样式里定义了段落样式paragraph和文字样式run,其他的属性大家可以不用调整,有两个地方注意:
1)如果要定义标题样式,只能是Heading N,使用中文名称标题N,无效
2)next一般都是Normal,表示该段落之后的默认段落
这样,我们就可以定义出所有的标题样式出来,从Heading 1到Heading N就可以了。
定义多级序号
多级序号就是这种东西:
多级序号是单独定义在生成时传入的numbering里面,结构如下:
numbering: {
config: [
{
reference: "ref1",
levels: [
{
level: 0,//级别
//该级别显示的文字样式
format: docx.NumberFormat.DECIMAL,
//具体的文字样式,使用%N来指代具体的某个级别的文字
text: "%1",
//编号后是Tab、空格还是无
suffix: docx.LevelSuffix.SPACE,
start: 1,
},
{
level: 1,
format: docx.NumberFormat.DECIMAL,
text: "%1.%2",
suffix: docx.LevelSuffix.SPACE,
start: 1,
},
{
level: 2,
format: docx.NumberFormat.DECIMAL,
text: "%1.%2.%3",
suffix: docx.LevelSuffix.SPACE,
start: 1,
},
{
level: 3,
format: docx.NumberFormat.DECIMAL,
text: "%1.%2.%3.%4",
suffix: docx.LevelSuffix.SPACE,
start: 1,
},
{
level: 4,
format: docx.NumberFormat.DECIMAL,
text: "%1.%2.%3.%4.%5",
suffix: docx.LevelSuffix.SPACE,
start: 1,
},
],
},
],
},
numbering只有1个config参数,config里面主要要定义两个东西,一个是reference,就是给这个编号取个名字,其他地方引用的时候就能按名称找到,另外就是一个levels数组了,里面从0开始定义了多个级别的编号,每个编号的内容参考上面的注释说明,需要注意的是,虽然我们的level是从0开始的,但是在text中要引用这个级别的时候,却是从1开始的(可能和start的取值有关系,但是我们没有找到合适的文档,所以这个地方存疑)。
另外对format进行一下说明,这是format的枚举参数值:
export declare enum NumberFormat {
DECIMAL = "decimal",
UPPER_ROMAN = "upperRoman",
LOWER_ROMAN = "lowerRoman",
UPPER_LETTER = "upperLetter",
LOWER_LETTER = "lowerLetter",
ORDINAL = "ordinal",
CARDINAL_TEXT = "cardinalText",
ORDINAL_TEXT = "ordinalText",
HEX = "hex",
CHICAGO = "chicago",
IDEOGRAPH_DIGITAL = "ideographDigital",
JAPANESE_COUNTING = "japaneseCounting",
AIUEO = "aiueo",
IROHA = "iroha",
DECIMAL_FULL_WIDTH = "decimalFullWidth",
DECIMAL_HALF_WIDTH = "decimalHalfWidth",
JAPANESE_LEGAL = "japaneseLegal",
JAPANESE_DIGITAL_TEN_THOUSAND = "japaneseDigitalTenThousand",
DECIMAL_ENCLOSED_CIRCLE = "decimalEnclosedCircle",
DECIMAL_FULL_WIDTH_2 = "decimalFullWidth2",
AIUEO_FULL_WIDTH = "aiueoFullWidth",
IROHA_FULL_WIDTH = "irohaFullWidth",
DECIMAL_ZERO = "decimalZero",
BULLET = "bullet",
GANADA = "ganada",
CHOSUNG = "chosung",
DECIMAL_ENCLOSED_FULL_STOP = "decimalEnclosedFullstop",
DECIMAL_ENCLOSED_PAREN = "decimalEnclosedParen",
DECIMAL_ENCLOSED_CIRCLE_CHINESE = "decimalEnclosedCircleChinese",
IDEOGRAPH_ENCLOSED_CIRCLE = "ideographEnclosedCircle",
IDEOGRAPH_TRADITIONAL = "ideographTraditional",
IDEOGRAPH_ZODIAC = "ideographZodiac",
IDEOGRAPH_ZODIAC_TRADITIONAL = "ideographZodiacTraditional",
TAIWANESE_COUNTING = "taiwaneseCounting",
IDEOGRAPH_LEGAL_TRADITIONAL = "ideographLegalTraditional",
TAIWANESE_COUNTING_THOUSAND = "taiwaneseCountingThousand",
TAIWANESE_DIGITAL = "taiwaneseDigital",
CHINESE_COUNTING = "chineseCounting",
CHINESE_LEGAL_SIMPLIFIED = "chineseLegalSimplified",
CHINESE_COUNTING_TEN_THOUSAND = "chineseCountingThousand",
KOREAN_DIGITAL = "koreanDigital",
KOREAN_COUNTING = "koreanCounting",
KOREAN_LEGAL = "koreanLegal",
KOREAN_DIGITAL_2 = "koreanDigital2",
VIETNAMESE_COUNTING = "vietnameseCounting",
RUSSIAN_LOWER = "russianLower",
RUSSIAN_UPPER = "russianUpper",
NONE = "none",
NUMBER_IN_DASH = "numberInDash",
HEBREW_1 = "hebrew1",
HEBREW_2 = "hebrew2",
ARABIC_ALPHA = "arabicAlpha",
ARABIC_ABJAD = "arabicAbjad",
HINDI_VOWELS = "hindiVowels",
HINDI_CONSONANTS = "hindiConsonants",
HINDI_NUMBERS = "hindiNumbers",
HINDI_COUNTING = "hindiCounting",
THAI_LETTERS = "thaiLetters",
THAI_NUMBERS = "thaiNumbers",
THAI_COUNTING = "thaiCounting",
BAHT_TEXT = "bahtText",
DOLLAR_TEXT = "dollarText"
}
非常的多,包含了很多地区性的文字样式,和中国相关的有
CHINESE_COUNTING = "chineseCounting",
CHINESE_LEGAL_SIMPLIFIED = "chineseLegalSimplified",
CHINESE_COUNTING_TEN_THOUSAND = "chineseCountingThousand",
具体的我没有测试了,大家可以尝试更换这几个来达到输出中文数字的作用。
实现导出
文档生成好之后,导出的方式是先生成blob,再下载到本地,我们的代码如下
docx.Packer.toBlob(doc).then((blob) => {
// Specify link url
const blobUrl = URL.createObjectURL(blob);
// Specify file name
var filename = xxx
// Create download link element
var downloadLink = document.createElement("a");
document.body.appendChild(downloadLink);
if (navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(blob, filename);
} else {
// Create a link to the file
downloadLink.href = blobUrl;
// Setting the file name
downloadLink.download = filename;
//triggering the function
downloadLink.click();
}
document.body.removeChild(downloadLink);
});
使用评价
docx.js库很好的解决了我们生成docx文档的问题,生成性能也不错,还把工作转移到了客户端,我们很满意,但是这个库目前来看文档还是不全,没有很好的入口教材可以使用,希望作者能加强一下。
更多推荐
所有评论(0)