likes
comments
collection
share

从前端视角从零带你用 Rust 来操作 Json ~

作者站长头像
站长
· 阅读数 13

知识点

本篇文章同时收录在《泡芙玩编程》公众号中,主打前端、Node.js、Rust,同步更新文章中

在前端和服务端开发中,我们会经常去操作 json,使用 Rust 操作 json 的性能相当的高(系统级编程语言不是吃素的),所以本章我们就来聊聊如何使用 Rust 操作 json,后续文章我们再来继续说说如何集成到 Node.js 中,让 Node.js 的性能起飞。

这篇文章会以解析一个 JsonSchema 和对 package.json 文件进行增删改查为例,带你去了解如何使用 Rust 去操作 json 字符串,这两个东西在前端开发中都很常见。

JsonSchema 是一种描述 Json 数据结构的规范,常用于前端低代码领域,比如由 JsonSchema 快速生成表单/自定义组件等。详情可以去了解下 formily 这个库。

要解析的 JsonSchema

我在阿里的日常开发中,经常需要去用到 JsonSchema 结构,所以这里我就以一个比较简单并且典型的 JsonSchema 为例子,去用 serde_json 这个 Rust Json 序列化和反序列化的库来解析一下:

{
  "title": "Schema",
  "type": "object",
  "properties": {
    "firstName": {
      "type": "string"
    },
    "lastName": {
      "type": "string"
    },
    "age": {
      "type": "number"
    },
    "hairColor": {
      "enum": [
        {
          "title": "hair color1",
          "value": "color1"
        },
        {
          "title": "hair color2",
          "value": "color2"
        },
        {
          "title": "hair color3",
          "value": "color3"
        }
      ],
      "type": "string"
    },
    "hairColor2": {
      "enum": ["color1", "color2"],
      "type": "string"
    }
  }
}

创建 Rust 项目

cargo new <project-name>

我们这里就用 cargo new parse-json

然后 Cargo.toml 中引入依赖:

[dependencies]
# 这里记得开启 serde 特性,因为我们要在 serde_json 中使用
indexmap = {version = "2.0.0", features = ["serde"]}
serde = {version = "1.0", features = ["derive"]}
serde_json = {version = "1.0.104"}

接下来我们就创建一个 example.json 文件,然后把上面那一大串 json 复制进去,一会我们会用

编写结构体

我们现在需要根据上面的 JsonSchema 为它实现一个标准的结构体,我们在 src 目录下新建一个 types.rs 文件

// 引入依赖
use indexmap::IndexMap;
use serde::Deserialize;

/// 这里我们使用这两个派生宏用来反序列化 Json 和支持 Debug 打印
#[derive(Deserialize, Debug)]
pub struct JsonSchema {
  pub title: Option<String>,
  /// type 是关键字,所以我们以 json_type 来命名,反序列化的时候重命名为 type 字段
  #[serde(rename(deserialize = "type"))]
  pub json_type: Option<String>,
  /// 这里使用 indexmap 的原因是因为我们要严格保证字段的插入顺序
  pub properties: Option<IndexMap<String, JsonSchema>>,
  pub items: Option<Box<JsonSchema>>,
  /// enum 是关键字,所以我们以 enum_vals 来命名,反序列化的时候需要重命名为 enum 字段
  #[serde(rename(deserialize = "enum"))]
  pub enum_vals: Option<EnumTypes>,
  pub description: Option<String>,
}

#[derive(Deserialize, Debug)]
/// 这里的枚举我们使用了 untagged,表示在反序列化时,`serde_json` 会尝试将 JSON 数据解析为与字段类型匹配的任何一个枚举变体
#[serde(untagged)]
pub enum EnumTypes {
  EnumType(Vec<EnumType>),
  StringEnum(Vec<String>),
}

#[derive(Deserialize, Debug)]
pub struct EnumType {
  pub title: Option<String>,
  pub value: Option<String>,
}

上面使用枚举的原因是因为我们 JsonSchema 中的枚举会有两种不同的数据结构:

"enum": ["color1", "color2"],

"enum": [
  {
  "title": "hair color1",
  "value": "color1"
  },
  {
  "title": "hair color2",
  "value": "color2"
  }
]

为了更加方便大家理解上面的数据结构,我这里同时贴一个 TypeScript 版本的出来,它们是完全对应的

export type IEnum = IEnumType[] | string[];

export interface IJsonSchema {
  title?: string;
  type?: string;
  properties?: { [key: string]: IJsonSchema };
  items?: IJsonSchema;
  enum?: IEnum;
  description?: string;
}

export interface IEnumType {
  title?: string;
  value?: string;
}

解析 Json 字符串

接下来我们就解析一个 JsonSchema 字符串,现在我们去到 main.rs 入口中,编写一个解析 Json 的函数,入参是 Json 字符串,出参是一个符合 serde_json 反序列化特征的结构体,当解析出错的时候返回 None

fn parse_json<T: DeserializeOwned>(schema: &str) -> Option<T> {
    match serde_json::from_str(schema) {
        Ok(parsed) => Some(parsed),
        Err(e) => {
            eprintln!("{}", e);
            None
        }
    }
}

然后我们到 main.rs 主逻辑中去编写读取函数

fn main() {
    // 我们可以保证 example.json 文件一定存在,所以直接 unwrap 即可
    let example_json = fs::read_to_string("example.json").unwrap();
    // 使用 turbofish 写法传入泛型 JsonSchema
    let parsed_json = parse_json::<JsonSchema>(&example_json).unwrap();

    // 打印出结构体
    println!("{:#?}", &parsed_json);
}

最后我们解析得到的结构体是这样的:

JsonSchema {
    title: Some(
        "Schema",
    ),
    json_type: Some(
        "object",
    ),
    properties: Some(
        {
            "firstName": JsonSchema {
                title: None,
                json_type: Some(
                    "string",
                ),
                properties: None,
                items: None,
                enum_vals: None,
                description: None,
            },
            "lastName": JsonSchema {
                title: None,
                json_type: Some(
                    "string",
                ),
                properties: None,
                items: None,
                enum_vals: None,
                description: None,
            },
            "age": JsonSchema {
                title: None,
                json_type: Some(
                    "number",
                ),
                properties: None,
                items: None,
                enum_vals: None,
                description: None,
            },
            "hairColor": JsonSchema {
                title: None,
                json_type: Some(
                    "string",
                ),
                properties: None,
                items: None,
                enum_vals: Some(
                    EnumType(
                        [
                            EnumType {
                                title: Some(
                                    "hair color1",
                                ),
                                value: Some(
                                    "color1",
                                ),
                            },
                            EnumType {
                                title: Some(
                                    "hair color2",
                                ),
                                value: Some(
                                    "color2",
                                ),
                            },
                            EnumType {
                                title: Some(
                                    "hair color3",
                                ),
                                value: Some(
                                    "color3",
                                ),
                            },
                        ],
                    ),
                ),
                description: None,
            },
            "hairColor2": JsonSchema {
                title: None,
                json_type: Some(
                    "string",
                ),
                properties: None,
                items: None,
                enum_vals: Some(
                    StringEnum(
                        [
                            "color1",
                            "color2",
                        ],
                    ),
                ),
                description: None,
            },
        },
    ),
    items: None,
    enum_vals: None,
    description: None,
}

拿到这个数据结构之后,我们就可以做很多事情了,比如对它进行增删改查啥的。

后面我们以一个前端最常见的 json 文件 package.json 为例,讲解一下如何操作 json 数据

解析 package.json

在前端的一些工具中,经常会需要去重写 package.json 中的内容,比如添加依赖,删除依赖,修改版本号等。我们在项目根目录下我们新建一个 package.json 文件,写入下面的内容

{
  "name": "puffmeow",
  "version": "0.1.0",
  "description": "Testing",
  "devDependencies": {
    "typescript": "^5.0.2",
    "vite": "^4.4.7",
    "vitest": "^0.33.0"
  },
  "dependencies": {}
}

然后在 main.rs 中对它进行解析~ 接下来我们就来说下如何对 json 数据进行增删改查。这个步骤在 Node.js 中实现十分简单,但是在 Rust 中怎么去实现捏?

use serde_json::{Map, Value};
use std::fs;

fn main() {
    let example_json = fs::read_to_string("package.json").unwrap();
    let mut json_value: Map<String, Value> = serde_json::from_str(package_json.as_str()).unwrap();

    println!("{:#?}", &json_value);
}

上面的代码打印出来的结构是这样的,符合定义的 serde_json::Map<String, serde_json::Value>类型:

{
    "dependencies": Object {},
    "description": String("Testing"),
    "devDependencies": Object {
        "typescript": String("^5.0.2"),
        "vite": String("^4.4.7"),
        "vitest": String("^0.33.0"),
    },
    "name": String("puffmeow"),
    "version": String("0.1.0"),
}

下面我们就来看下如何在 Rust 中实现对 json 的增删改查

对 package.json 进行增删改查

fn main() {
    let package_json = fs::read_to_string("package.json").unwrap();
    let mut json_value: Map<String, Value> = serde_json::from_str(package_json.as_str()).unwrap();

    println!("{:#?}", &json_value);

    // 新增字段
    json_value.insert("author".to_string(), Value::from("Puffmeow"));

    // 往 devDependencies 中新增字段
    if let Some(dev_dependencies) = json_value
        .get_mut("devDependencies")
        .unwrap()
        .as_object_mut()
    {
        // dev_dependencies 中新增 axios 0.2.0 版本
        dev_dependencies.insert("axios".to_string(), json!("0.2.0"));

        // 修改 devDependencies 中的 vitest 版本号到 0.34.1
        dev_dependencies
            .insert("vitest".to_string(), Value::from("0.34.1"))
            .unwrap();

        // 删除 devDependencies 中的 typescript 字段
        dev_dependencies.remove("typescript");
    }

    // 修改字段,除了使用 Map::get_mut 的方式去修改之外也可以用这种方式进行修改
    json_value["version"] = json!("0.2.0");

    // 删除 description 字段
    json_value.remove("description");

    // 查询某个字段
    let version = json_value.get("version").unwrap();
    // 打印 0.2.0
    println!("{}", version);

    // 将更新后的 json 结构体重新转换回 json 字符串并写入到文件中
    let updated_json = serde_json::to_string_pretty(&json_value).unwrap();
    fs::write("package.json", &updated_json).unwrap();
}

最后更新后的字符串就是这样了

{
  "author": "Puffmeow",
  "dependencies": {},
  "devDependencies": {
    "axios": "0.2.0",
    "vite": "^4.4.7",
    "vitest": "0.34.1"
  },
  "name": "puffmeow",
  "version": "0.2.0"
}

但这时候有些人就会说,顺序怎么乱了?

这是因为 Map 默认使用的是 BtreeMap,它对插入顺序不保证,如果想要让插入顺序得到排序,那可以开启 preserve_order 特性

修改 Cargo.toml

[dependencies]
# 这里新增 preserve_order 特性
serde_json = {version = "1.0.104", features = ["preserve_order"]}

这时候我们重新跑一下,可以看到得到了正确的顺序~

{
  "name": "puffmeow",
  "version": "0.2.0",
  "author": "Puffmeow",
  "devDependencies": {
    "axios": "0.2.0",
    "vite": "^4.4.7",
    "vitest": "0.34.1"
  },
  "dependencies": {}
}

总结

以上就是使用 Rust 对 Json 操作的一些基础内容了~ 之前自己在学习的过程中对于插入顺序的保证也疑惑了挺久的,最后通过到 Rust 社区上提问了一下解决了问题(社区上的人还是挺友好的),特别是 indexmap 这个知识点,如果 serde_json 开启了 preser_order 特性,其内部也会将 Map 给转换成 indexmap 来保证插入顺序,对应的代码是这样的:

#[cfg(not(feature = "preserve_order"))]
type MapImpl<K, V> = BTreeMap<K, V>;
#[cfg(feature = "preserve_order")]
type MapImpl<K, V> = IndexMap<K, V>;

在 Js 里面我们用的 Map 数据结构默认就是帮我们排序好的,也就是用的 IndexMap。

以后遇到一些数据量大的涉及到操作 json 的场景,使用 Rust 来速度会比 Node.js 快至少 1 倍 ,同时也可以用 Rust 来给 Node.js 来做 Native addon 来提高 Node.js 的性能, Rust 可以为 Node.js 打开一扇大门,让 Node.js 本身就羸弱的计算性能得到大幅度提升。

转载自:https://juejin.cn/post/7267919941524963385
评论
请登录