Rust赋能前端:写一个 Excel 生成引擎
❝年关将至,你今年成长了吗?
大家好,我是柒八九。一个专注于前端开发技术/Rust
及AI
应用知识分享的Coder
❝此篇文章所涉及到的技术有
Rust
WebAssembly
Excel
引擎xml
Rust
解析JSON
Rust
操作内存缓冲区使用 zip::ZipWriter
创建ZIP
文件
因为,行文字数所限,有些概念可能会一带而过亦或者提供对应的学习资料。请大家酌情观看。
前言
在上一篇Rust 赋能前端: 纯血前端将 Table 导出 Excel我们用很大的篇幅描述了,如何在前端页面中使用我们的table2excel
(WebAssembly
)。
❝有同学想获取上一篇的前端项目,等有空我会上传到
github
中。同时,也想着把table2excel
发布到npm
中。到时候,会通知大家的。
具体展示了,如何在前端对静态表格/静态长表格(1 万条数据)/静态表格合并/动态表格合并等表格进行导出为excel
。
运行效果
静态表格
静态长表格(1 万条数据)
静态表格合并
动态表格合并
但是呢,对于源码的解读,我们只是浅尝辄止。只是介绍了,如何将在前端构建的Table
的信息转换为我们Excel
引擎需要的信息。
那么我们今天就来讲讲如何用 Rust 写一个 Excel 引擎。
好了,天不早了,干点正事哇。
我们能所学到的知识点
❝
设计思路 代码结构 核心代码解释
1. 设计思路
Excel
是一个压缩文件
先说可能打破大家认知的结论
❝
Excel
的.xlsx
文件实际上是一个包含多个XML
文件的压缩文件。
为了论证这个结论,我们来实际操作一下。(我用的是Mac
,所以下面的操作都是基于Mac
,至于其他环境大家可自行验证)
这是我们上一篇文件生成的excle
文件。当然,你也可以用你本机的资源。
我们使用终端命令来执行excel
的解压操作。(并且该文件的名字为test.xlsx
)
-
假设
.xlsx
文件在桌面上:cd ~/Desktop
-
更改扩展名: 将
.xlsx
文件扩展名更改为.zip
:mv test.xlsx test.zip
-
解压 ZIP 文件: 使用
unzip
命令解压 ZIP 文件:unzip test.zip -d test_folder
这将会把
.zip
文件解压到test_folder
目录中。
然后,我们切换到test_folder
目录中,执行Vscode
的快捷命令 -code .
。
就会看到下面的目录结构
我们来简单解释一下比较重要文件的含义
-
worksheets
文件夹用于存放excel
的sheet
信息,由于我们之前的excel
只有一个sheet
。所以这里只有一个sheet1.xml
。如果生成的excel
有多个sheet
。那么这里就有多个sheetN.xml
文件-
clos
定义每个列的宽度 -
sheetData
用于定义excel
中每个cell
的值 -
merge
维护每个sheet
的合并信息
-
-
sharedStrings.xml
是一种优化方案,excel
中存在多个相同的值,那么我们可以存放到这里,然后在sheetN.xml
引用这些值,可以节省excel
的存储空间。 -
styles.xml
用于存放excel
的样式信息。虽然,我们的引擎暂未支持样式的处理,但是后期也是可以把这块给加上的。
啥是 XML
关于xml
有很多文章来介绍它。我们在摘录关于维基百科\_xml[1]的定义。
❝
XML
是一种用于存储、传输和重建任意数据的标记语言和文件格式。其定义了一套用于编码文档的规则,这些规则使得文档既易于人类阅读,也易于机器处理。
然后,如果大家不想看英文内容,大家也可以看xml 中文解释[2],这里就不过多解释了。但是呢,有一点还是想多啰嗦下。
❝
Open XML Formats
到此为止,我已经默认大家已经对xml
有了些许的了解。然后,我们再解释一个概念。
上面说了,excel
是一堆xml
组成的压缩文件。其实呢,还有一个定语,是符合Open XML Formats
格式的xml
。
我们还是直接从Office*Open_XML*维基百科[3]中寻找答案。
❝
Office Open XML
(也非正式地称为OOXML
)是微软开发的一种基于 XML 的压缩文件格式,用于表示spreadsheets
(也就是excel
)、ppt
和word
。
在 Excel 中使用 XML
为了更加深大家对Excel
的理解,或者更准确的说是Excel
和xml
之前的关系。我们写一个简单的Node
应用。
❝注意:我们需要构造符合
Excel
标准的XML
结构
具体代码如下:
const fs = require('fs');
// 用来生成 Excel XML 格式的函数
function generateExcelXml(data) {
const xmlHeader = `<?xml version="1.0" encoding="UTF-8"?>`;
const worksheetXml = `
<ss:Workbook xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
<ss:Worksheet ss:Name="Sheet1">
<ss:Table>`;
// 创建表格行
let rowsXml = '';
data.forEach(row => {
rowsXml += '<ss:Row>';
row.forEach(cell => {
rowsXml += `<ss:Cell><ss:Data ss:Type="String">${cell}</ss:Data></ss:Cell>`;
});
rowsXml += '</ss:Row>';
});
const footerXml = `
</ss:Table>
</ss:Worksheet>
</ss:Workbook>`;
// 合并所有部分
const fullXml = `${xmlHeader}${worksheetXml}${rowsXml}${footerXml}`;
return fullXml;
}
// 示例数据
const data = [
['Name', 'Age', 'City'],
['北宸', 25, '北京'],
['南蓁', 30, '山西'],
['Front789', 35, '晋中']
];
// 生成 XML 内容
const xmlContent = generateExcelXml(data);
// 保存为 Excel 可读取的 XML 文件
fs.writeFileSync('workbook.xml', xmlContent, 'utf8');
代码说明:
-
XML 头部:指定了 XML 文件的版本和编码方式。 -
<ss:Workbook>
:工作簿的根元素,Excel
使用ss
命名空间来定义 XML 文件的结构。 -
<ss:Worksheet>
:工作表定义,每个工作簿可以有多个工作表,这里定义了一个工作表Sheet1
。 -
<ss:Table>
:表格,包含多行数据。 -
<ss:Row>
:行元素,每行包含多个单元格。 -
<ss:Cell>
:单元格,里面包含数据。 -
保存文件:将生成的 XML
内容写入workbook.xml
文件。
然后,我们运行上面的代码后,就会生成一个 workbook.xml
文件。随后,我们将该文件拖入到WPS
中。
看到的效果如下:
可以看到,我们刚才用代码生成的xml
,是正常显示为excel
格式。并且数据也是正确的。
❝还有一点需要说明,当我们把刚才生成的
xml
拖入到WPS
时,它会跳出一个提示框,问你需要将该xml
以何种模式展示。这步也反向证明了Office_Open_XML 是微软开发的一种基于 XML 的压缩文件格式,用于表示 spreadsheets(也就是 excel)、ppt 和 word这个概念。
2. 代码结构
项目初始化
该内容,在上一篇讲过,我们就直接复制过来了。
我们通过cargo new --lib table2excel
来构建一个项目。
同时呢,我们在项目根目录中创建用于打包优化的文件。
-
build.sh
-
tools/optimize-rust.sh
-
tools/optimize-wasm.sh
这个我们在之前的Rust 赋能前端:为 WebAssembly 瘦身中介绍过相关概念,这里就不再赘述了。
项目结构
在src
目录下,我们有如下的目录结构
├── json2sheet.rs
├── lib.rs
├── sheet_data.rs
├── struct_define.rs
├── utils.rs
├── xml.rs
└── xml_meta.rs
-
json2sheet.rs
在上一篇文章中讲过,它的作用就是将前端页面中传入的json
转换为构建xml
的所需结构 -
lib.rs
这里只有一个函数,就是我们在前端调用的主函数generate_excel
-
sheet_data.rs
:该文件用于基于json2sheet.rs
返回的数据和json
中特定的数据,构建xml
的数据部分 -
struct_define.rs
:用于存放该项目中用到的Struct
-
utils.rs
:用于定义一下工具方法。-
log_to_console
封装 web_sys [4]::console,用于在前端中打印信息 -
set_panic_hook
封装 console_error_panic_hook [5],让错误更好的控制台捕获
-
-
xml.rs
:基于sheet_data
拼装xml
信息 -
xml_meta
:用于生成符合open xml
的元数据信息
下面,我们就会拿我认为主要的代码,来讲讲核心逻辑。
3. 核心代码解释
lib.rs
引入第三方包和自定义模块
use struct_define::{ CellValue, InnerCell };
use wasm_bindgen::prelude::*;
use std::io::prelude::*;
use zip;
use zip::write::FileOptions;
use std::io::Cursor;
pub mod struct_define;
pub mod xml;
pub mod utils;
pub mod json2sheet;
pub mod xml_meta;
pub mod sheet_data;
const ROOT_RELS: &'static [u8] = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>"#;
-
wasm_bindgen[6]这是
Rust
编译为WebAssembly
绕不开的大山,这里就不再展示细说了。 -
zip[7]:前面说了,
excel
就是一堆xml
的zip
压缩包。所以,我们使用zip
来处理压缩 -
std::io::Cursor
:Cursor
是一种用于内存缓冲区的类型,它提供了对内存中的数据进行读取和写入的功能。-
通过实现 Seek
,Cursor
使得这些 缓冲区可以像文件一样进行随机访问。 -
Cursor
可用于多种类型的缓冲区,比如Vec<u8>
和切片 (&[u8]
),并能够利用标准库中的 I/O 特性实现 数据的读取和写入。
-
核心代码
❝该代码的主要功能是生成一个
Excel
文件(.xlsx
格式),它通过将JSON
数据处理为Excel
格式并使用zip
压缩库将其封装成一个.xlsx
文件。
#[wasm_bindgen]
pub async fn generate_excel(raw_data: &JsValue) -> Result<Vec<u8>, JsValue> {
utils::set_panic_hook();
// 解析前端传入的数据
let data = json2sheet::process_json(raw_data);
let mut shared_strings = vec!();
let mut sheets_info: Vec<(String, String)> = vec!();
// 创建压缩文件的内存缓冲区
let buf: Vec<u8> = vec!();
let w = Cursor::new(buf);
let mut zip = zip::ZipWriter::new(w);
let options = FileOptions::default()
.compression_method(zip::CompressionMethod::Stored)
.unix_permissions(0o755);
let sheet = &data.data;
let mut rows: Vec<Vec<InnerCell>> = vec!();
// 将行数据处理成 InnerCell 格式
if let Some(cell) = &sheet.cells {
for (row_index, row) in cell.iter().enumerate() {
let mut inner_row: Vec<InnerCell> = vec!();
for (col_index, cell) in row.iter().enumerate() {
if let Some(value) = cell {
let cell_name = sheet_data::cell_offsets_to_index(row_index, col_index);
let mut inner_cell = InnerCell::new(cell_name);
if let Ok(_) = value.parse::<f64>() {
inner_cell.value = CellValue::Value(value.to_owned());
} else {
inner_cell.value = CellValue::SharedString(shared_strings.len() as u32);
shared_strings.push(value.to_owned());
}
inner_row.push(inner_cell);
}
}
rows.push(inner_row);
}
}
// 获取 sheet 信息并开始写入压缩文件
let sheet_info = sheet_data::get_sheet_info(sheet.name.clone(), 0);
zip.start_file(sheet_info.0.clone(), options).unwrap();
zip.write_all(
sheet_data::get_sheet_data(rows, &sheet.cols, &sheet.rows, &sheet.merged).as_bytes()
).unwrap();
sheets_info.push(sheet_info);
// 写入 _rels/.rels 文件
zip.start_file("_rels/.rels", options).unwrap();
zip.write_all(ROOT_RELS).unwrap();
// 创建 XML 元数据
let (content_types, rels, workbook) = xml_meta::create_open_xml_meta(sheets_info);
// 写入各种 XML 文件
zip.start_file("[Content_Types].xml", options).unwrap();
zip.write_all(content_types.as_bytes()).unwrap();
zip.start_file("xl/_rels/workbook.xml.rels", options).unwrap();
zip.write_all(rels.as_bytes()).unwrap();
zip.start_file("xl/workbook.xml", options).unwrap();
zip.write_all(workbook.as_bytes()).unwrap();
// 写入 sharedStrings.xml 文件
zip.start_file("xl/sharedStrings.xml", options).unwrap();
zip.write_all(sheet_data::get_shared_strings_data(shared_strings, 0).as_bytes()).unwrap();
// 完成压缩并返回结果
let res = zip.finish().unwrap();
Ok(res.get_ref().to_vec())
}
❝该函数的主要核心步骤如下:
接收 JSON 数据并处理:接收 JsValue
类型的输入数据,这个数据是通过json2sheet::process_json
函数处理后的JSON
数据。构建 Excel 数据结构:解析并转换 JSON
数据为InnerCell
格式的行数据,以便在Excel
中进行存储。生成 Excel 压缩文件(.xlsx 格式):通过 zip
库创建一个内存中的 ZIP 文件,并将Excel
文件的不同部分(如workbook.xml
,sharedStrings.xml
)写入该ZIP
文件。异步处理:通过 async/await
使得函数能够在JavaScript
中异步执行,避免阻塞主线程。
下面我们就简单来对代码中重要的核心部分做一个简单的解释。
1. 设置 Panic Hook
utils::set_panic_hook();
这行代码设置了一个 Panic Hook
,用于在 Rust
中发生 panic
时,能够捕获并进行适当的处理。通常在 WebAssembly
中使用它来处理错误。
2. 处理 JSON 数据
let data = json2sheet::process_json(raw_data);
process_json
函数处理传入的 JSON
数据,将其转换成适合构建 Excel
的数据结构。raw_data
是通过 JsValue
类型传入的,在调用该函数后,它被转换成一个包含 Excel
工作表数据的结构(例如:行、列、单元格等)。
3. 初始化压缩文件 (ZIP)
let buf: Vec<u8> = vec!();
let w = Cursor::new(buf);
let mut zip = zip::ZipWriter::new(w);
这段代码创建了一个内存缓冲区(Vec<u8>
),并将其包装在 Cursor
中。zip::ZipWriter
用于创建一个 ZIP
文件,在其中写入 Excel
文件的各个部分。
4. 写入工作表数据(行数据)
if let Some(cell) = &sheet.cells {
for (row_index, row) in cell.iter().enumerate() {
let mut inner_row: Vec<InnerCell> = vec!();
for (col_index, cell) in row.iter().enumerate() {
// 省略部分代码
}
rows.push(inner_row);
}
}
这一部分将从 cells
(一个包含 Excel
工作表所有行的 Vec<Vec<Option<String>>>
)中获取每一行数据,逐个单元格处理,将每个单元格的数据转换为 InnerCell
对象,并将它们组织成行。每个 InnerCell
可能是直接存储值(如数字),或者是共享字符串(如果该单元格是文本)。所有的共享字符串都会被存储在 shared_strings
中。
5. 写入 Excel 文件的各个部分
let sheet_info = sheet_data::get_sheet_info(sheet.name.clone(), 0);
zip.start_file(sheet_info.0.clone(), options).unwrap();
zip.write_all(
sheet_data::get_sheet_data(rows, &sheet.cols, &sheet.rows, &sheet.merged).as_bytes()
).unwrap();
这段代码处理工作表(sheet_info
),并将其写入 ZIP
文件中。它还将当前工作表的数据(如行、列、合并单元格等)写入到 ZIP
文件中。
6. 写入其他 Excel 文件元数据
zip.start_file("_rels/.rels", options).unwrap();
zip.write_all(ROOT_RELS).unwrap();
这部分写入 Excel
文件的关系文件(_rels/.rels
),它用于描述文件之间的关系,例如工作表与数据文件之间的关系。
接下来的代码还会写入 Excel 文件所需的其他 XML 文件:
-
[Content_Types].xml
:描述 Excel 文件中各种文件类型。 -
xl/_rels/workbook.xml.rels
:描述工作簿的关系文件。 -
xl/workbook.xml
:工作簿的主 XML 文件。 -
xl/sharedStrings.xml
:存储共享字符串(如文本)数据。
❝这些文件,我们在文章刚开始就用见到过了,也就是说这些文件是构成
excel
压缩文件的基础
7. 完成 ZIP 压缩并返回结果
let res = zip.finish().unwrap();
Ok(res.get_ref().to_vec())
在完成所有数据写入后,调用 zip.finish()
来结束 ZIP 文件的创建。最后,返回一个 Vec<u8>
,它包含了压缩后的 .xlsx
文件内容。
json2sheet.rs - 处理 JSON 数据
这步,我们在上一篇文章中(Rust 赋能前端: 纯血前端将 Table 导出 Excel讲过了,为了不让文章看起来又臭又长,所以这里就不再过多解释了。
❝总结一句话,其实就是将从前端环境传入的
Table
的配置信息,转换为我们生成xml
需要的数据格式。
sheet_data.rs - 基于信息构建 xml
我们在lib.rs
中,当基于sheet.cells
信息构建完rows
信息后,我们此时其实已经收集了可以构建xml
的所有数据信息。那么,我们就可以调用sheet_data::get_sheet_data
来处理相关的逻辑。
sheet_data::get_sheet_data(xx).as_bytes()
主要代码
❝该函数的主要功能是将传入的 Excel 数据(如单元格内容、列、行、高度、合并单元格等)转换成符合 Excel 2006 XML 格式的字符串(即
<worksheet>
元素)。它生成的XML
数据可以嵌入到一个Excel
文件(.xlsx
文件)中,作为excel
的数据部分。这个过程是通过构造 XML 元素并为其添加属性和子元素来实现的。
pub fn get_sheet_data(
cells: Vec<Vec<InnerCell>>,
columns: &Option<Vec<Option<ColumnData>>>,
rows: &Option<Vec<Option<RowData>>>,
merged: &Option<Vec<MergedCell>>
) -> String {
let mut worksheet = Element::new("worksheet");
let mut sheet_view = Element::new("sheetView");
sheet_view.add_attr("workbookViewId", "0");
let mut sheet_views = Element::new("sheetViews");
sheet_views.add_children(vec![sheet_view]);
let mut sheet_format_pr = Element::new("sheetFormatPr");
sheet_format_pr
.add_attr("customHeight", "1")
.add_attr("defaultRowHeight", "15.75")
.add_attr("defaultColWidth", "14.43");
let mut cols = Element::new("cols");
let mut cols_children = vec!();
match columns {
Some(columns) => {
for (index, column) in columns.iter().enumerate() {
match column {
Some(col) => {
let mut column_element = Element::new("col");
column_element
.add_attr("min", (index + 1).to_string())
.add_attr("max", (index + 1).to_string())
.add_attr("customWidth", "1")
.add_attr("width", (col.width / WIDTH_COEF).to_string());
cols_children.push(column_element);
}
None => (),
}
}
}
None => (),
}
let mut rows_info: HashMap<usize, &RowData> = HashMap::new();
match rows {
Some(rows) => {
for (index, column) in rows.iter().enumerate() {
match column {
Some(row) => {
rows_info.insert(index, row);
}
None => (),
}
}
}
None => (),
}
let mut sheet_data = Element::new("sheetData");
let mut sheet_data_rows = vec!();
for (index, row) in cells.iter().enumerate() {
let mut row_el = Element::new("row");
row_el.add_attr("r", (index + 1).to_string());
match rows_info.get(&index) {
Some(row_data) => {
row_el
.add_attr("ht", (row_data.height * HEIGHT_COEF).to_string())
.add_attr("customHeight", "1");
}
None => (),
}
let mut row_cells = vec!();
for cell in row {
let mut cell_el = Element::new("c");
cell_el.add_attr("r", &cell.cell);
match &cell.value {
CellValue::Value(ref v) => {
let mut value_cell = Element::new("v");
value_cell.add_value(v);
cell_el.add_children(vec![value_cell]);
utils::log!("value {}", v);
}
CellValue::SharedString(ref s) => {
cell_el.add_attr("t", "s");
let mut value_cell = Element::new("v");
value_cell.add_value(s.to_string());
cell_el.add_children(vec![value_cell]);
}
CellValue::None => (),
}
row_cells.push(cell_el);
}
row_el.add_children(row_cells);
sheet_data_rows.push(row_el);
}
sheet_data.add_children(sheet_data_rows);
let mut worksheet_children = vec![sheet_views, sheet_format_pr];
if cols_children.len() > 0 {
cols.add_children(cols_children);
worksheet_children.push(cols);
}
worksheet_children.push(sheet_data);
match merged {
Some(merged) => {
if merged.len() > 0 {
let mut merged_cells_element = Element::new("mergeCells");
merged_cells_element.add_attr("count", merged.len().to_string()).add_children(
merged
.iter()
.map(|MergedCell { from, to }| {
let p1 = cell_offsets_to_index(from.row as usize, from.column as usize);
let p2 = cell_offsets_to_index(to.row as usize, to.column as usize);
let cell_ref = format!("{}:{}", p1, p2);
let mut merged_cell = Element::new("mergeCell");
merged_cell.add_attr("ref", cell_ref);
merged_cell
})
.collect()
);
worksheet_children.push(merged_cells_element);
}
}
None => (),
}
worksheet
.add_attr("xmlns:xm", "http://schemas.microsoft.com/office/excel/2006/main")
.add_attr("xmlns:x14ac", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac")
.add_attr("xmlns:x14", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main")
.add_attr("xmlns:mv", "urn:schemas-microsoft-com:mac:vml")
.add_attr("xmlns:mc", "http://schemas.openxmlformats.org/markup-compatibility/2006")
.add_attr("xmlns:mx", "http://schemas.microsoft.com/office/mac/excel/2008/main")
.add_attr("xmlns:r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships")
.add_attr("xmlns", "http://schemas.openxmlformats.org/spreadsheetml/2006/main")
.add_children(worksheet_children);
worksheet.to_xml()
}
核心功能分析
❝还记得我们文章刚开始的解压缩后的
test_folder
我们就来看看,我们是如何用代码生成这些信息的。
1. 初始化工作表元素
let mut worksheet = Element::new("worksheet");
首先,创建一个 worksheet
元素,这个元素将表示整个 Excel
工作表,并作为最终的 XML
输出。
该元素是sheet
的根元素
2. 创建 sheetView
和 sheetViews
let mut sheet_view = Element::new("sheetView");
sheet_view.add_attr("workbookViewId", "0");
let mut sheet_views = Element::new("sheetViews");
sheet_views.add_children(vec![sheet_view]);
sheetView
元素描述了工作表的视图设置(如显示模式等)。这里添加了一个 sheetView
元素,并设置了其 workbookViewId
属性。sheetViews
是一个容器元素,包含了多个 sheetView
元素。
3. 设置工作表格式
let mut sheet_format_pr = Element::new("sheetFormatPr");
sheet_format_pr
.add_attr("customHeight", "1")
.add_attr("defaultRowHeight", "15.75")
.add_attr("defaultColWidth", "14.43");
sheetFormatPr
元素定义了工作表的格式,包括行高(defaultRowHeight
)和列宽(defaultColWidth
)等属性。此处设置了默认行高为 15.75
和默认列宽为 14.43
。
4. 处理列数据并生成 cols
元素
let mut cols = Element::new("cols");
let mut cols_children = vec!();
这段代码处理传入的列数据(columns
)。如果列数据存在,遍历每一列,并根据列的宽度生成 <col>
元素,并将其添加到 cols
中。每个列元素会包含以下属性:
-
min
和max
:指定列的范围(这里是单列,min
和max
都是当前列的索引)。 -
customWidth
和width
:定义列宽度。
5. 处理行数据并生成 sheetData
let mut sheet_data = Element::new("sheetData");
let mut sheet_data_rows = vec!();
for (index, row) in cells.iter().enumerate() {
let mut row_el = Element::new("row");
row_el.add_attr("r", (index + 1).to_string());
...
for cell in row {
let mut cell_el = Element::new("c");
cell_el.add_attr("r", &cell.cell);
...
}
...
sheet_data.add_children(sheet_data_rows);
}
这部分代码处理传入的 cells
(单元格数据),并为每一行生成一个 <row>
元素。每个单元格会根据其类型(值或共享字符串)生成不同的 <c>
元素(单元格元素)。每个单元格会包含以下子元素:
-
<v>
:表示单元格的值。 -
t="s"
:如果单元格是共享字符串,<c>
元素会有一个属性t="s"
,并在<v>
中存储字符串的索引。
❝为了让结构看起来顺畅,我们将解压后的数据,做了部分删减。
6. 处理合并单元格
match merged {
Some(merged) => {
if merged.len() > 0 {
let mut merged_cells_element = Element::new("mergeCells");
merged_cells_element.add_attr("count", merged.len().to_string()).add_children(
merged
.iter()
.map(|MergedCell { from, to }| {
// 省略部分代码
})
.collect()
);
worksheet_children.push(merged_cells_element);
}
}
None => (),
}
这部分处理了合并单元格的情况。如果传入的 merged
列表不为空,会为每个合并的单元格范围(from
和 to
)生成一个 <mergeCell>
元素。最终,将这些合并单元格包装在 <mergeCells>
元素中,并将其添加到工作表的子元素中。
7. 构建最终的 XML 元素
worksheet
.add_attr("xmlns:xm", "http://schemas.microsoft.com/office/excel/2006/main")
.add_attr("xmlns:x14ac", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac")
.add_attr("xmlns:x14", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main")
.add_attr("xmlns:mv", "urn:schemas-microsoft-com:mac:vml")
.add_attr("xmlns:mc", "http://schemas.openxmlformats.org/markup-compatibility/2006")
.add_attr("xmlns:mx", "http://schemas.microsoft.com/office/mac/excel/2008/main")
.add_attr("xmlns:r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships")
.add_attr("xmlns", "http://schemas.openxmlformats.org/spreadsheetml/2006/main")
.add_children(worksheet_children);
这部分代码为工作表元素添加了多个 XML
命名空间(xmlns
),以确保生成的 XML
文件符合 Excel
文件的标准。接着,将所有的子元素(如 sheetView
、sheetData
、mergeCells
等)添加到 worksheet
元素中。
8. 返回 XML 字符串
worksheet.to_xml()
最后,将 worksheet
元素转化为 XML
字符串并返回。这是生成的工作表的 XML
格式,可以嵌入到 .xlsx
文件中。
xml.rs
可以从上面代码中,我们看到很多Element::new
的操作。
其实,这个Element
是在xml.rs
中维护的。
use std::borrow::Cow;
struct Attr<'a>(Cow<'a, str>, Cow<'a, str>);
pub struct Element<'a> {
tag: Cow<'a, str>,
attributes: Vec<Attr<'a>>,
content: Content<'a>
}
enum Content<'a> {
Empty,
Value(Cow<'a, str>),
Children(Vec<Element<'a>>)
}
impl<'a> Element<'a> {
pub fn new<S>(tag: S) -> Element<'a> where S: Into<Cow<'a, str>> {
Element {
tag: tag.into(),
attributes: vec!(),
content: Content::Empty
}
}
pub fn add_attr<S, T>(&mut self, name: S, value: T) -> &mut Self where S: Into<Cow<'a, str>>, T: Into<Cow<'a, str>> {
self.attributes.push(Attr(name.into(), to_safe_attr_value(value.into())));
self
}
pub fn add_value<S>(&mut self, value: S) where S: Into<Cow<'a, str>> {
self.content = Content::Value(to_safe_string(value.into()));
}
pub fn add_children(&mut self, children: Vec<Element<'a>>) {
if children.len() != 0 {
self.content = Content::Children(children);
}
}
pub fn to_xml(&mut self) -> String {
let mut result = String::new();
result.push_str(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
result.push_str(&self.to_string());
result
}
}
❝这段代码实现了一个简单的 XML 生成器,它允许通过构建
Element
结构体及其子元素来生成符合XML
格式的字符串
我们可以从Element
的结构体定义就知道。
pub struct Element<'a> {
tag: Cow<'a, str>,
attributes: Vec<Attr<'a>>,
content: Content<'a>
}
这个就是为了生成XML
元素量身打造的。(回想一下,我们在文章开头讲的XML
概念)
然后还为该结构体,实现了add_attr
/add_value
/add_children
/to_xml
等方法。用于执行对应的任务。
xml_meta.rs
接下来,我们就是要构建xml
的元数据信息。
我们在lib.rs
中通过调用xml_meta::create_open_xml_meta
来生成对应的信息。
// 创建 XML 元数据
let (content_types, rels, workbook) = xml_meta::create_open_xml_meta(sheets_info);
// 写入各种 XML 文件
zip.start_file("[Content_Types].xml", options).unwrap();
zip.write_all(content_types.as_bytes()).unwrap();
zip.start_file("xl/_rels/workbook.xml.rels", options).unwrap();
zip.write_all(rels.as_bytes()).unwrap();
zip.start_file("xl/workbook.xml", options).unwrap();
zip.write_all(workbook.as_bytes()).unwrap();
由于这块代码属于模板类型,也没啥逻辑可讲,我们就一带而过了哈。
该函数涉及到三个文件的信息构建。
[Content_Types].xml
对应我们excel
的文件就是[Content_Types].xml
。
xl/_rels/workbook.xml.rels
对应我们excel
的文件就是xl/_rels/workbook.xml.rels
。
xl/workbook.xml
对应我们excel
的文件就是xl/workbook.xml
。
最后,我们将这些拼装好的字符信息,返回给函数调用处。
(content_types.to_xml(), relationships.to_xml(), workbook.to_xml())
最后,传入到zip
中,进行文件的生成。
后记
分享是一种态度。
好了,到这里,我们已经把我认为的核心代码已经讲解完了,其实比较的核心的部分就是
-
json2sheet::process_json
处理前端传入的json
-
sheet_data::get_sheet_data
基于一些信息,用于构建符合excel
的xml
结构 -
xml_meta::create_open_xml_meta
这块呢,其实没啥含金量,只是一些配置信息的堆叠
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。
维基百科_xml: https://en.wikipedia.org/wiki/XML
[2]xml中文解释: https://aws.amazon.com/what-is/xml/
[3]Office_Open_XML_维基百科: https://en.wikipedia.org/wiki/Office_Open_XML
[4]web_sys: https://crates.io/crates/web-sys
[5]console_error_panic_hook: https://crates.io/crates/console_error_panic_hook
[6]wasm_bindgen: https://crates.io/crates/wasm-bindgen
[7]zip: https://crates.io/crates/zip
本文由 mdnice 多平台发布