Bootstrap

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

运行效果

静态表格

alt

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

alt

静态表格合并

alt

动态表格合并

alt

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

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


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

我们能所学到的知识点

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

1. 设计思路

Excel是一个压缩文件

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

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

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

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

我们使用终端命令来执行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 .

就会看到下面的目录结构

alt

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

  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]的定义。

alt

XML是一种用于存储、传输和重建任意数据的标记语言文件格式。其定义了一套用于编码文档的规则,这些规则使得文档既易于人类阅读,也易于机器处理。

然后,如果大家不想看英文内容,大家也可以看xml 中文解释[2],这里就不过多解释了。但是呢,有一点还是想多啰嗦下。

alt

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中。

看到的效果如下:

alt

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

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


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<(StringString)> = 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 alt 我们就来看看,我们是如何用代码生成这些信息的。

1. 初始化工作表元素
let mut worksheet = Element::new("worksheet");

首先,创建一个 worksheet 元素,这个元素将表示整个 Excel 工作表,并作为最终的 XML 输出。

该元素是sheet的根元素 alt

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 元素。

alt
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

alt
4. 处理列数据并生成 cols 元素
let mut cols = Element::new("cols");
let mut cols_children = vec!();

这段代码处理传入的列数据(columns)。如果列数据存在,遍历每一列,并根据列的宽度生成 <col> 元素,并将其添加到 cols 中。每个列元素会包含以下属性:

  • minmax:指定列的范围(这里是单列, minmax 都是当前列的索引)。
  • customWidthwidth:定义列宽度。
alt
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> 中存储字符串的索引。
alt

为了让结构看起来顺畅,我们将解压后的数据,做了部分删减。

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> 元素中,并将其添加到工作表的子元素中。

alt
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 元素中。

alt
8. 返回 XML 字符串
worksheet.to_xml()

最后,将 worksheet 元素转化为 XML 字符串并返回。这是生成的工作表的 XML 格式,可以嵌入到 .xlsx 文件中。


xml.rs

可以从上面代码中,我们看到很多Element::new的操作。 alt

其实,这个Element是在xml.rs中维护的。

use std::borrow::Cow;

struct Attr<'a>(Cow<'astr>, Cow<'astr>);

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

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

impl<'a> Element<'a> {
    pub fn new<S>(tag: S) -> Element<'awhere S: Into<Cow<'astr>> {
        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<'astr>>, T: Into<Cow<'astr>> {
        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<'astr>> {
        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<'astr>,
    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

alt

对应我们excel的文件就是[Content_Types].xml

alt

xl/_rels/workbook.xml.rels

alt

对应我们excel的文件就是xl/_rels/workbook.xml.rels

alt

xl/workbook.xml

alt

对应我们excel的文件就是xl/workbook.xml

alt

最后,我们将这些拼装好的字符信息,返回给函数调用处。

(content_types.to_xml(), relationships.to_xml(), workbook.to_xml())

最后,传入到zip中,进行文件的生成。 alt


后记

分享是一种态度

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

  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 多平台发布

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;