今天我没写需求,但是我写了工具。

4,415 阅读5分钟

(Node 工具提效)根据模版文件生成特定组件

简介

背景

每一次的需求都需要在某个文件夹下面新建一个 pages 然后在创建组件,在创建对应的 scss 文件,而且比如需求的页面和之前类似,又得去 Ant Design Pro Component 复制对应的代码,然后今天在做需求时就想在项目内引用一个通过模版自动生成组件的小工具

个人感觉做这种小工具肯定是要比做需求爽的多呀~先说说这篇文章能带来什么?

  • Node 简单的应用
  • fs 模块读写文件
  • 命令行询问?类似于 Vue-CLI 这种
  • 运用 JS 原型继承对模版文件的信息封装
  • 简单的 Buffer 数据获取以及使用

看一下成果

询问使用哪个模版 image

创建完成 image

完成的文件以及目录 image

这样其实就节省了我先去创建文件夹,然后再去创建组件文件以及样式文件,并且不需要我去 UI 库的官方文档上去复制粘贴代码了~从高级 CV 工程师变成了高级摸鱼工程师了~

整体思路

整体思路非常简单,如下:

  1. 创建几个模版文件(项目中常用的)
  2. 询问用户需要的组件类型 => 组件名称 => 组件位置
  3. 根据第(1)步创建的模版文件供用户选择
  4. 创建文件信息构造函数,保存用户输入的内容以及对状态进行派生
  5. 根据存入的状态信息生成文件
  6. 对模版文件简单处理,输出用户最终文件

是不是非常简单,只需 100 行代码就可以完成上述内容

代码

1. 创建模版文件

比如我们在写一个 React 组件的时候,经常会在 pages 目录下新建一个文件夹,在新建一个 index.jsx 来作为这个模块的入口(entry),我们只是把这个 index.jsx 文件提取出来成为一个通用的模版文件即可,每次只需要运行一段命令就自动根据我们的配置信息去自动创建对应的 JSX 文件和 scss 文件

首先创建一个 component-generate 目录,里面的 index.js 作为我们的核心代码文件,其他均为模版文件

<!-- component-generate 文件夹下 -->
. ├── ProForm.jsx // 模版文件 ├── ProTable.jsx // 模版文件 ├── index.js //
核心代码文件 └── index.jsx // 模版文件

2. 用 CLI 与用户交互

首先需要考虑的问题,如何使 Node.js CLI 程序具有交互性?比如 yarn generate 后来询问用户,与之交互

Node 原生模块从版本 7 开始,Node.js 提供了 readline 模块

感兴趣可以过去看看,但是我看官网的内容其实也是推荐使用第三方库 inquirer

示例代码如下:

const inquirer = require("inquirer");

var questions = [
  {
    type: "input",
    name: "name",
    message: "你叫什么名字?",
  },
];

inquirer.prompt(questions).then((answers) => {
  console.log(`你好 ${answers["name"]}!`);
});

根据 Inquirer 库,我们创建自己的 CLI 询问程序

const inquirer = require("inquirer");
const GENERATE = {
  TYPE: "type",
  PATH: "path",
  FILE_NAME: "fileName",
};

const questions = [
  {
    type: "list",
    name: GENERATE.TYPE,
    message: "您希望生成什么组件?",
    choices: getTypeList(), // 获取自定义的模版,在第三步
  },
  {
    type: "input",
    name: GENERATE.FILE_NAME,
    message: "组件名称?(默认 index.jsx,可以不写后缀名)",
  },
  {
    type: "input",
    name: GENERATE.PATH,
    message: `您希望您的组件生成在什么位置(可选,默认生成在组件根目录 ${DEFAULT_PATH} 下,默认拼接路径)?`,
  },
];

inquirer.prompt(questions).then(async (answers) => {
  // ...
  console.log(answers); // { type: '', path: '', fileName: '' }
});

3. 根据模版文件供用户选择

我们用 Node 的 fs 模块来对当前模版文件夹进行遍历,但是排除掉我们的核心文件,其他都认为是模版文件

const COMPONENT_PATH = "./component-generate"; // 我们的模版文件夹路径
function getTypeList() {
  const response = fs.readdirSync(COMPONENT_PATH);
  // 排除 index.js 核心文件
  const list = response.filter((item) => item !== "index.js");
  // 去除所有的后缀名,显示模版类型
  return list.map((item) => {
    return item.substring(0, item.lastIndexOf("."));
  });
}

根据模版自动生成的选项 image

4. 创建文件信息构造函数,保存用户输入的内容以及对状态进行派生

我们已经获取到用户的交互数据了,这时就该存起来,方便之后使用了,我也不知道为什么(可能是为了复习一下原型的知识?),自觉的就用这种方式来做了~

// 文件信息构造函数
function FileInfo(info) {
  this.path = info.path || "/";
  this.type = info.type;
  this.fileName = info.fileName || "index.jsx";
}

// 获取拼接后的 PATH
FileInfo.prototype.getPath = function() {
  this.fileName =
    Array.prototype.shift.call(
      String.prototype.split.call(this.fileName, ".")
    ) + ".jsx";
  return `.${DEFAULT_PATH}${this.path}/${this.fileName}`;
};

// 根据用户选择的模版类型获取对应的代码
FileInfo.prototype.getCodeByType = async function() {
  const response = await fs.readFileSync(`${COMPONENT_PATH}/${this.type}.jsx`, {
    encoding: "utf-8",
  });
  const fileContent = response.replace(
    "DEFAULT_NAME",
    // 说明用的默认名称,需要获取上级目录名称
    this.fileName === "index.jsx"
      ? this.path.substring(1, this.path.length)
      : this.fileName.substring(0, this.fileName.lastIndexOf("."))
  );
  // eslint-disable-next-line no-undef
  const bufferBytes = Buffer.from(fileContent);
  return bufferBytes;
};

// 获取文件名
FileInfo.prototype.getFileName = function() {
  return this.fileName;
};

5. 根据存入的状态信息生成文件

我们数据都已经存入到 FileInfo 这个构造函数里面了,可以根据用户的数据进行文件的生成

核心思路就是拿用户需要在哪里创建文件,以及创建的文件名称是什么。

但是有一个问题就是 Node 不支持在没有的路径下新建文件,这时我们就得去创建对应的路径,然后在去创建对应的文件

核心代码如下:

function recursiveWriteFile(path, buffer, callback) {
  const lastPath = path.substring(0, path.lastIndexOf('/'))
  // 首先创建目录
  fs.mkdir(lastPath, { recursive: true }, err => {
    if (err) {
      return callback(err)
    }
    // 文件是否存在,如果存在不进行写入,命令行提示用户更换
    fs.access(path, fs.constants.F_OK, err => {
      if (err) {
        // 文件不存在,可以根据模版内容写入
        fs.writeFile(path, buffer, err => {
          if (err) {
            return callback(err)
          }

          callback(undefined)
        })
        return
      }

      console.log('文件已经存在,请重新命名或删除文件后重试')
    })
  })
}

以上代码就是如何递归的创建,接下来根据用户输入的内容将参数传递进去

inquirer.prompt(questions).then(async answers => {
  const fileInfo = new FileInfo(answers)
  const generatePath = fileInfo.getPath()
  const generateCode = await fileInfo.getCodeByType()

  recursiveWriteFile(generatePath, generateCode, err => {
    if (err) {
      throw err
    }

    console.log('组件创建成功')

    recursiveWriteFile(generatePath.replace('.jsx', '.scss'), '', err => {
      if (err) {
        throw err
      }

      console.log('组件样式文件创建成功')
    })
  })
})

此时大功告成!最后稍微说一下模版的处理

6. 对模版文件简单处理,输出用户最终文件

这里有个小细节,如果用户选择默认的 index.jsx 文件做为命名的话,那么我会去取他的父级目录名称,实现组件的命名规范

const fileContent = response.replace(
  'DEFAULT_NAME',
  // 说明用的默认名称,需要获取上级目录名称
  this.fileName === 'index.jsx'
    ? this.path.substring(1, this.path.length)
    : this.fileName.substring(0, this.fileName.lastIndexOf('.'))
)

最后还有 Buffer 这个缓冲区,其实他是将内容转为 2 进制进行存储,以 16 进制进行展示的。我也不知道说没说错哈。最开始我直接去获取组件的内容,然后写入就会报错说必须使用字符串或者 Buffer 来进行 writeFile

const bufferBytes = Buffer.from(fileContent)

以上就是一个根据模版自动生成对应组件的小 Demo,以后看看再继续完善,先这么用着开发需求

需求还没写,这个写了一天了~谁让程序员懒呢。。。散会!