【Rust实战小项目】实现`npm init` 功能 (一)

980 阅读5分钟

本期写一个小项目,rust实现npm init,创建一个package.json文件。作为《rust学习之旅》,第一个练手项目。

基本功能

首先我们分析一下npm init功能做了什么?

  • 1、执行npm init 出现了以下提示 截屏2023-04-09 12.50.33.png

  • 2、要求我们输入基本信息 截屏2023-04-09 12.50.53.png

  • 3、完成创建json文件

    {
      "name": "npm-init",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC"
    }
    

功能实现

我们知道了npm init做了些什么,我们也要按照,上面的步骤一步一步来完成我们第一个rust小项目

创建一个npm-init项目

我们使用cargo new 创建一个项目。

cargo new npm-init

接下来我们就可以开整。

输出提示信息

第一步我们先实现npm init的第一步输出提示

作为一个前端er,第一步输出肯定是创建一个console模块。当然这个啥也没干,就是将println包装了一层。😄

pub mod console {
   pub fn log(msg: &str) {
       println!("{}", msg)
   }
   pub fn warn() {}
   pub fn error() {}
   pub fn success() {}
}

接下来我们就可以输出提示信息了

use npm_init::{console, tips};
fn main() {
   const DEFAULT_TIP: &str = "This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.";
   console::log(DEFAULT_TIP);
}

好的我们完美的完成了第一步。是不是超级厉害(其实这里什么也没有写🤔)。

执行cargo run:

截屏2023-04-09 13.10.47.png

是不是和我们npm int 第一步一样了。

获取输入信息

第二步我们需要获取到用户的输入,我们重构一下刚才的代码。将刚才那个提示信息,放到模块tips中来。并实现了一个函数get_input.

pub mod tips {
   use std::io;
   pub const DEFAULT_TIP: &str = "This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.";

pub fn get_input(msg: &str, default: &str) -> String {
   println!("{}{}:", msg, format!("({})", default));
   let mut input = String::new();
   io::stdin().read_line(&mut input).unwrap();
   input = input.replace("\n", "");
   if input == "".to_owned() {
       default.to_string()
   } else {
       input
   }
}

我们利用io::stdin().read_line来获取用户的输入。并返回回去。我们贴心的支持传递了默认数据。

use npm_init::{console, tips};

fn main() {
    console::log(tips::DEFAULT_TIP);
    let name = tips::get_input("package name:", "npm-init");
    let version = tips::get_input("version:", "1.0.0)");
    let description = tips::get_input("description", "");
    let entry = tips::get_input("entry point: ", "index.js");
    let test_command = tips::get_input(
        "test command",
        "echo \"Error: no test specified\" && exit 1",
    );
    let git_repo = tips::get_input("git repository:", "https://github.com/kinfuy/npm-init.git");
    let keyword = tips::get_input("keywords", "");
    let author = tips::get_input("author", "");
    let license = tips::get_input("license", "ISC");
    let is_create = tips::get_input("Is this OK?", "yes");
}

现在我们获取到了用户的输入,取得了package.json初始化需要的字段信息。

但是这里我们只是获取到了数据,我们得找个地方储存一下,这样直接放到文件里,显得太呆了

我们创建一个模块npm并暴露了结构体Package,注意我们在【Rust学习之旅】模块管理:包、crate、模块、path(七)中讲到的,模块中结构体中独立的字段需要单独设置pub.

pub mod npm {
    pub struct Script {
        pub test: String,
    }
    pub struct Package {
        pub name: String,
        pub version: String,
        pub description: String,
        pub entry: String,
        pub script: Script,
        pub git_repo: String,
        pub keyword: String,
        pub author: String,
        pub license: String,
        pub is_create: String,
    }
}

结合上面的代码,我们可以直接将获取到的信息打印在控制台上

fn main() {
    console::log(tips::DEFAULT_TIP);
    let name = tips::get_input("package name:", "npm-init");
    let version = tips::get_input("version:", "1.0.0)");
    let description = tips::get_input("description", "");
    let entry = tips::get_input("entry point: ", "index.js");
    let test_command = tips::get_input(
        "test command",
        "echo \"Error: no test specified\" && exit 1",
    );
    let git_repo = tips::get_input("git repository:", "https://github.com/kinfuy/npm-init.git");
    let keyword = tips::get_input("keywords", "");
    let author = tips::get_input("author", "");
    let license = tips::get_input("license", "ISC");
    let is_create = tips::get_input("Is this OK?", "yes");

    let pkg = Package {
        name,
        version,
        description,
        entry,
        script: Script { test: test_command },
        git_repo,
        keyword,
        author,
        license,
        is_create,
    };
    println!("{:#?}", pkg);
}

注意打印结构体需要为结构体派生 Debug trait

通过 #[derive] 属性,编译器能够提供某些 trait 的基本实现。如果需要更复杂的行为,这些 trait 也可以手动实现。

#[derive(Debug)]
pub struct Script {
    pub test: String,
}
#[derive(Debug)]
pub struct Package {
    pub name: String,
    pub version: String,
    pub description: String,
    pub entry: String,
    pub script: Script,
    pub git_repo: String,
    pub keyword: String,
    pub author: String,
    pub license: String,
    pub is_create: String,
}

至此我们的第二步也成功完成了

输出到文件

最后一步我们将获取到的数据输出到packages.json,这里我们需要将结构体数据转化为json.

在 Rust 中,如果想要将一个结构体转换为 JSON 字符串,你可以使用 serde 库。

首先,你需要在 Cargo.toml 文件中添加 serde 和 serde_json 作为依赖:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

接下来我们继续改造一下我们的npm模块

pub mod npm {
    use serde::{Deserialize, Serialize};
    use std::fs;

    #[derive(Debug, Serialize, Deserialize)]
    pub struct Script {
        pub test: String,
    }
    #[derive(Debug, Serialize, Deserialize)]
    pub struct Package {
        pub name: String,
        pub version: String,
        pub description: String,
        pub entry: String,
        pub script: Script,
        pub git_repo: String,
        pub keyword: String,
        pub author: String,
        pub license: String,
        pub is_create: String,
    }

    pub trait Output {
        /// 输出到package.json
        fn output(&self) -> Result<(), io::Error>
        where
            Self: Serialize;
    }
    impl Output for Package {
        fn output(&self) -> Result<(), std::io::Error>
        where
            Self: Serialize,
        {
            let serialized = serde_json::to_string(&self).unwrap();
            fs::write("package.json", serialized)?;
            Ok(())
        }
    }
}

我们定义 Output trait并为Package实现 output方法。

我们为结构体派生 Serialize, Deserialize 这两个第三方提供的trait后,我们就可以使用 serde_json::to_string() 讲结构体序列化。

接下来我们fs::write 就可以将我们的结构体输出到文件了。

在main方法中调用即可。

 pkg.output().unwrap();

结语

这个小项目虽然小,我们也使用到了很多《rust学习之旅》中讲到了很多知识点

  • 结构体 的创建使用
  • trait的创建并为结构体实现impl
  • 处理错误,Result<T,U> 枚举类型的使用
  • 怎么使用第三方库
  • fs::write将数据写入文件
  • mod模块化的使用
  • 了解#[derive] 派生

这个小项目,还有很多瑕疵,例如:没有处理输入参数的校验等,有兴趣的小伙伴可以自己尝试看看

相关链接