专栏名称: 奇舞精选
《奇舞精选》是由奇舞团维护的前端技术公众号。除周五外,每天向大家推荐一篇前端相关技术文章,每周五向大家推送汇总周刊内容。
目录
相关文章推荐
新浪科技  ·  【#岚图汽车11月交付10856辆#】 ... ·  3 天前  
新浪科技  ·  #CMF报告建议再造20家华为#【CMF年度 ... ·  4 天前  
新浪科技  ·  【#优衣库2024财年大中华区营收360亿# ... ·  5 天前  
51好读  ›  专栏  ›  奇舞精选

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

奇舞精选  · 公众号  · 科技媒体 前端  · 2024-12-02 18:30

正文

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

大家好,我是柒八九。一个专注于前端开发技术/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的合并信息
  • 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)、pptword


    在 Excel 中使用 XML

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

    代码说明:

    1. XML 头部:指定了 XML 文件的版本和编码方式。
    2. :工作簿的根元素,Excel 使用 ss 命名空间来定义 XML 文件的结构。
    3. :工作表定义,每个工作簿可以有多个工作表,这里定义了一个工作表 Sheet1
    4. :表格,包含多行数据。
    5. :行元素,每行包含多个单元格。
    6. :单元格,里面包含数据。
    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],让错误更好的控制台捕获
  • 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#"
    "#
    ;
    1. wasm_bindgen[6]这是Rust编译为WebAssembly绕不开的大山,这里就不再展示细说了。

    2. zip[7]:前面说了,excel就是一堆xmlzip压缩包。所以,我们使用zip来处理压缩

    3. std::io::Cursor:Cursor 是一种用于内存缓冲区的类型,它提供了对内存中的数据进行读取和写入的功能。

    • 通过实现 SeekCursor 使得这些缓冲区可以像文件一样进行随机访问
    • 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: VecString, 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())
    }

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

    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),并将其包装在 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. 创建 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)。如果列数据存在,遍历每一列,并根据列的宽度生成 元素,并将其添加到 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(单元格数据),并为每一行生成一个 元素。每个单元格会根据其类型(值或共享字符串)生成不同的 元素(单元格元素)。每个单元格会包含以下子元素:

    • :表示单元格的值。
    • 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 列表不为空,会为每个合并的单元格范围(fromto)生成一个 元素。最终,将这些合并单元格包装在 元素中,并将其添加到工作表的子元素中。

    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<'astr>, Cow<'astr>);

    pub struct Element<'a> {
        tag: Cow<'astr>,
        attributes: Vec'a>>,
        content: Content<'a>
    }

    enum Content<'a> {
        Empty,
        Value(Cow<'astr>),
        Children(Vec'a>>)
    }

    impl<'a> Element<'a> {
        pub fn new(tag: S) -> Element<'awhere 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<'astr>,
        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中,进行文件的生成。


    后记

    分享是一种态度

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

    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