Rust赋能前端:写一个 Excel 生成引擎

年关将至,你今年成长了吗?

大家好,我是柒八九 。一个专注于前端开发技术/RustAI应用知识分享Coder

此篇文章所涉及到的技术有

  1. Rust
  2. WebAssembly
  3. Excel引擎
  4. xml
  5. Rust解析 JSON
  6. Rust操作内存缓冲区
  7. 使用 zip::ZipWriter 创建 ZIP 文件

因为,行文字数所限,有些概念可能会一带而过亦或者提供对应的学习资料。请大家酌情观看。


前言

在上一篇Rust 赋能前端: 纯血前端将 Table 导出 Excel我们用很大的篇幅描述了,如何在前端页面中使用我们的table2excel(WebAssembly)。

有同学想获取上一篇的前端项目,等有空我会上传到github中。同时,也想着把table2excel发布到npm中。到时候,会通知大家的。

具体展示了,如何在前端对静态表格 /静态长表格(1 万条数据) /静态表格合并 /动态表格合并 等表格进行导出为excel

运行效果

静态表格

静态长表格(1 万条数据)

静态表格合并

动态表格合并


但是呢,对于源码的解读,我们只是浅尝辄止。只是介绍了,如何将在前端构建的Table的信息转换为我们Excel引擎需要的信息。

那么我们今天就来讲讲如何用 Rust 写一个 Excel 引擎


好了,天不早了,干点正事哇。

我们能所学到的知识点

  1. 设计思路
  2. 代码结构
  3. 核心代码解释

1. 设计思路

Excel是一个压缩文件

先说可能打破大家认知的结论

Excel.xlsx 文件实际上是一个包含多个 XML 文件的压缩文件。

为了论证这个结论,我们来实际操作一下。(我用的是Mac,所以下面的操作都是基于Mac,至于其他环境大家可自行验证)

这是我们上一篇文件生成的excle文件。当然,你也可以用你本机的资源。

我们使用终端命令来执行excel的解压操作。(并且该文件的名字为test.xlsx)

  1. 假设 .xlsx 文件在桌面上:

    复制代码
    cd ~/Desktop
  2. 更改扩展名 : 将 .xlsx 文件扩展名更改为 .zip

    复制代码
    mv test.xlsx test.zip
  3. 解压 ZIP 文件 : 使用 unzip 命令解压 ZIP 文件:

    复制代码
    unzip test.zip -d test_folder

    这将会把 .zip 文件解压到 test_folder 目录中。

然后,我们切换到test_folder 目录中,执行Vscode的快捷命令 -code .

就会看到下面的目录结构

我们来简单解释一下比较重要文件的含义

  1. worksheets文件夹用于存放 excelsheet信息,由于我们之前的 excel只有一个 sheet。所以这里只有一个 sheet1.xml。如果生成的 excel有多个 sheet。那么这里就有多个 sheetN.xml文件
    • clos定义每个列的宽度
    • sheetData用于定义 excel中每个 cell的值
    • merge维护每个 sheet的合并信息
  2. sharedStrings.xml是一种优化方案, excel中存在多个相同的值,那么我们可以存放到这里,然后在 sheetN.xml引用这些值,可以节省 excel的存储空间。
  3. 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)、pptword


在 Excel 中使用 XML

为了更加深大家对Excel的理解,或者更准确的说是Excelxml之前的关系。我们写一个简单的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');

代码说明:

  1. XML 头部:指定了 XML 文件的版本和编码方式。
  2. <ss:Workbook>:工作簿的根元素, Excel 使用 ss 命名空间来定义 XML 文件的结构。
  3. <ss:Worksheet>:工作表定义,每个工作簿可以有多个工作表,这里定义了一个工作表 Sheet1
  4. <ss:Table>:表格,包含多行数据。
  5. <ss:Row>:行元素,每行包含多个单元格。
  6. <ss:Cell>:单元格,里面包含数据。
  7. 保存文件 :将生成的 XML 内容写入 workbook.xml 文件。

然后,我们运行上面的代码后,就会生成一个 workbook.xml 文件。随后,我们将该文件拖入到WPS中。

看到的效果如下:

可以看到,我们刚才用代码生成的xml,是正常显示为excel格式。并且数据也是正确的。

还有一点需要说明,当我们把刚才生成的xml拖入到WPS时,它会跳出一个提示框,问你需要将该xml以何种模式展示。这步也反向证明了Office_Open_XML 是微软开发的一种基于 XML 的压缩文件格式,用于表示 spreadsheets(也就是 excel)、ppt 和 word 这个概念。


2. 代码结构

项目初始化

该内容,在上一篇讲过,我们就直接复制过来了。

我们通过cargo new --lib table2excel来构建一个项目。

同时呢,我们在项目根目录中创建用于打包优化的文件。

  1. build.sh
  2. tools/optimize-rust.sh
  3. tools/optimize-wasm.sh

这个我们在之前的Rust 赋能前端:为 WebAssembly 瘦身中介绍过相关概念,这里就不再赘述了。

项目结构

src目录下,我们有如下的目录结构

复制代码
├── json2sheet.rs
├── lib.rs
├── sheet_data.rs
├── struct_define.rs
├── utils.rs
├── xml.rs
└── xml_meta.rs
  1. json2sheet.rs在上一篇文章中讲过,它的作用就是将前端页面中传入的 json转换为构建 xml的所需结构
  2. lib.rs这里只有一个函数,就是我们在前端调用的主函数 generate_excel
  3. sheet_data.rs:该文件用于基于 json2sheet.rs返回的数据和 json中特定的数据,构建 xml的数据部分
  4. struct_define.rs:用于存放该项目中用到的 Struct
  5. utils.rs:用于定义一下工具方法。
    • log_to_console封装 web_sys [4]::console,用于在前端中打印信息
    • set_panic_hook封装 console_error_panic_hook [5],让错误更好的控制台捕获
  6. xml.rs:基于 sheet_data拼装 xml信息
  7. 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>"#;
  1. wasm_bindgen[6]这是Rust编译为WebAssembly绕不开的大山,这里就不再展示细说了。
  2. zip[7]:前面说了,excel就是一堆xmlzip压缩包。所以,我们使用zip来处理压缩
  3. std::io::Cursor:Cursor 是一种用于内存缓冲区的类型,它提供了对内存中的数据进行读取和写入的功能。
    • 通过实现 SeekCursor 使得这些 缓冲区可以像文件一样进行随机访问
    • 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())
}

该函数的主要核心步骤如下:

  1. 接收 JSON 数据并处理 :接收 JsValue 类型的输入数据,这个数据是通过 json2sheet::process_json 函数处理后的 JSON 数据。
  2. 构建 Excel 数据结构 :解析并转换 JSON 数据为 InnerCell 格式的行数据,以便在 Excel 中进行存储。
  3. 生成 Excel 压缩文件(.xlsx 格式) :通过 zip 库创建一个内存中的 ZIP 文件,并将 Excel 文件的不同部分(如 workbook.xml, sharedStrings.xml)写入该 ZIP 文件。
  4. 异步处理 :通过 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. 创建 sheetViewsheetViews
复制代码
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 中。每个列元素会包含以下属性:

  • minmax:指定列的范围(这里是单列, minmax 都是当前列的索引)。
  • customWidthwidth:定义列宽度。
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 列表不为空,会为每个合并的单元格范围(fromto)生成一个 <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 文件的标准。接着,将所有的子元素(如 sheetViewsheetDatamergeCells 等)添加到 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中,进行文件的生成。


后记

分享是一种态度

好了,到这里,我们已经把我认为的核心代码已经讲解完了,其实比较的核心的部分就是

  1. json2sheet::process_json 处理前端传入的 json
  2. sheet_data::get_sheet_data 基于一些信息,用于构建符合 excelxml结构
  3. xml_meta::create_open_xml_meta这块呢,其实没啥含金量,只是一些配置信息的堆叠

全文完,既然看到这里了,如果觉得不错,随手点个赞和"在看"吧。
Reference [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

本文由mdnice多平台发布

相关推荐
酷爱码1 小时前
css中的 vertical-align与line-height作用详解
前端·css
沐土Arvin1 小时前
深入理解 requestIdleCallback:浏览器空闲时段的性能优化利器
开发语言·前端·javascript·设计模式·html
专注VB编程开发20年1 小时前
VB.NET关于接口实现与简化设计的分析,封装其他类
java·前端·数据库
小妖6661 小时前
css 中 content: “\e6d0“ 怎么变成图标的?
前端·css
L耀早睡2 小时前
mapreduce打包运行
大数据·前端·spark·mapreduce
HouGISer2 小时前
副业小程序YUERGS,从开发到变现
前端·小程序
outstanding木槿2 小时前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
霸王蟹3 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹3 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
专注VB编程开发20年3 小时前
asp.net IHttpHandler 对分块传输编码的支持,IIs web服务器后端技术
服务器·前端·asp.net