❝ 年关将至,你今年成长了吗?
大家好,我是柒八九 。一个专注于前端开发技术/Rust
及AI
应用知识分享 的Coder
❝ 此篇文章所涉及到的技术有
使用 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
文件sheetData
用于定义excel
中每个cell
的值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 = `` ; const worksheetXml = ` ; // 创建表格行 let rowsXml = '' ; data.forEach(row => { rowsXml += ''
; row.forEach(cell => { rowsXml += `${cell} | ` ; }); rowsXml += '' ; }); const footerXml = ` ` ; // 合并所有部分 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 文件的版本和编码方式。
:工作簿的根元素,Excel
使用 ss
命名空间来定义 XML 文件的结构。
:工作表定义,每个工作簿可以有多个工作表,这里定义了一个工作表 Sheet1
。保存文件 :将生成的 XML
内容写入 workbook.xml
文件。然后,我们运行上面的代码后,就会生成一个 workbook.xml
文件。随后,我们将该文件拖入到WPS
中。
看到的效果如下:
可以看到,我们刚才用代码生成的xml
,是正常显示为excel
格式。并且数据也是正确的。
❝ 还有一点需要说明,当我们把刚才生成的xml
拖入到WPS
时,它会跳出一个提示框,问你需要将该xml
以何种模式展示。这步也反向证明了Office_Open_XML 是微软开发的一种基于 XML 的压缩文件格式,用于表示 spreadsheets(也就是 excel)、ppt 和 word 这个概念。
2. 代码结构 项目初始化 该内容,在上一篇讲过,我们就直接复制过来了。
我们通过cargo new --lib table2excel
来构建一个项目。
同时呢,我们在项目根目录中创建用于打包优化的文件。
这个我们在之前的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
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#" "# ;
wasm_bindgen [6] 这是Rust
编译为WebAssembly
绕不开的大山,这里就不再展示细说了。
zip [7] :前面说了,excel
就是一堆xml
的zip
压缩包。所以,我们使用zip
来处理压缩
std::io::Cursor
:Cursor
是一种用于内存缓冲区的类型,它提供了对内存中的数据进行读取和写入的功能。
通过实现 Seek
,Cursor
使得这些缓冲区可以像文件一样进行随机访问 。 Cursor
可用于多种类型的缓冲区,比如 Vec
和切片 (&[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 > = vec! (); // 将行数据处理成 InnerCell 格式 if let Some (cell) = &sheet.cells { for (row_index, row) in cell.iter().enumerate() { let mut inner_row: Vec = 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
),并将其包装在 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 = vec! (); for (col_index, cell) in row.iter().enumerate() { // 省略部分代码 } rows.push(inner_row); } }
这一部分将从 cells
(一个包含 Excel
工作表所有行的 Vec>>
)中获取每一行数据,逐个单元格处理,将每个单元格的数据转换为 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
,它包含了压缩后的 .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 格式的字符串(即
元素) 。它生成的 XML
数据可以嵌入到一个 Excel
文件(.xlsx
文件)中,作为excel
的数据部分 。这个过程是通过构造 XML 元素并为其添加属性和子元素来实现的 。
pub fn get_sheet_data ( cells: Vec <Vec >, columns: &Option <Vec <Option >>, rows: &Option <Vec <Option >>, merged: &Option <Vec > ) -> 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
)。如果列数据存在,遍历每一列,并根据列的宽度生成
元素,并将其添加到 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
(单元格数据),并为每一行生成一个 |
元素。每个单元格会根据其类型(值或共享字符串)生成不同的
元素(单元格元素)。每个单元格会包含以下子元素:
t="s"
:如果单元格是共享字符串,
元素会有一个属性 t="s"
,并在
中存储字符串的索引。❝ 为了让结构看起来顺畅,我们将解压后的数据,做了部分删减。
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
)生成一个
元素。最终,将这些合并单元格包装在
元素中,并将其添加到工作表的子元素中。
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 'a>>, content: Content<'a > }enum Content <'a > { Empty, Value(Cow<'a , str >), Children(Vec 'a>>) }impl <'a > Element<'a > { pub fn new (tag: S) -> Element<'a > where S: Into 'a, str >> { Element { tag: tag.into(), attributes: vec! (), content: Content::Empty } } pub fn add_attr (&mut self , name: S, value: T) -> &mut Self where S: Into 'a, str >>, T: Into 'a, str >> { self .attributes.push(Attr(name.into(), to_safe_attr_value(value.into()))); self } pub fn add_value (&mut self , value: S) where S: Into 'a, str >> { self .content = Content::Value(to_safe_string(value.into())); } pub fn add_children (&mut self , children: Vec '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#""# ); result.push_str(&self .to_string()); result } }
❝ 这段代码实现了一个简单的 XML 生成器 ,它允许通过构建 Element
结构体及其子元素来生成符合 XML
格式的字符串
我们可以从Element
的结构体定义就知道。
pub struct Element <'a > { tag: Cow<'a , str >, attributes: Vec '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
这块呢,其实没啥含金量,只是一些配置信息的堆叠全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。
[1] 维基百科_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