如何编写自动创建模板文件的命令行程序

Posted by WWJ Blog on February 13, 2020

如何编写自动创建模板文件的命令行程序

初衷(不想偷懒的程序员不是一个好程序员)


试想一下,我们在日常开发的过程中是不是有被下面三个问题困扰

  1. 开启一个新项目,想复用以前项目的一些基础代码,这个时候的做法就是把就项目的基础文件一个一个的拷到新项目当中,可能一不留神还出错;或者就直接把就项目整个拷过来,然后进行复杂的增删改查;
  2. 新增一个页面Carousel组件,那么我需要去IDE中,手动创建index.js(组件出口文件),index.vue(组件逻辑页面),index.less(组件样式文件)。因为每个组件的输出文件都是一样的,这样还需要把之前写好了的组件的出口文件拷贝过来,然后删除一些逻辑代码,并修改相关名称,忙的不亦乐乎,最后发现其实比手写没省多少时间。
  3. 文件的自动插入引用

预期效果


  1. 在需要创建模板文件的目录下输入xm-create-tpl,首先会让你选择需要创建哪个项目的模板文件(项目不同,模板文件自然就不同)
  2. 输入文件名称并询问你是否只创建但文件风格的模板文件
  3. 检测当前目录下是否已经有了需要被创建的文件,并询问是否需要覆盖,不覆盖,直接退出,覆盖则成功创建模板文件

Alt text Alt text Alt text

fs模块


fs模块用于对系统文件及目录进行读写操作,本次主要用到的fs模块的功能有:

  • fs.existsSync:检查文件是否存在
  • fs.mkdir:新建文件夹
  • fs.readFileSync: 读取文件内容
  • fs.writeFile:将内容写入文件

开发


流程图

Alt text

代码实施

一、主流程

// 判断文件是否存在
const checkFileExists = (projeceName, filePath, fileName) => {
  // some code 
};

// 开始创建文件夹
const startMkdir = (projeceName, fileName) => {
  // soma code
};

// 开始执行
choiceProject().then((projeceName) => {
  // 输入文件名称
  inputTplName(projeceName).then((fileName) => {
    // 确认是否只创建单独文件,默认创建的模板文件为文件夹风格
    confirmTplType(fileName).then((isFolderStyle) => {
      if (isFolderStyle) {
        // 生成文件夹风格的模板文件
        generateTpl(projeceName, fileName);
      } else {
        // 只生成单独的模板文件
        generateOnlyTpl(projeceName, fileName);
      }
    });
  });
});

二、选择需要创建哪个项目的模板文件 这里需要用到一个第三方插件inquirer,使用之前需要执行npm install inquirer --saveinquirer的具体用法可以移步官网或者上一篇博客。

const inquirer = require('inquirer');
const prompt = inquirer.createPromptModule();

const choiceProject = () => {
  return new Promise((resolve) =>{
    prompt({
      type: 'list',
      message: '请选择项目',
      name: 'projectName',
      choices: [
        "xiaomai-miniapp-show",
        "xiaomai-web-b-new",
        "xiaomai-web-parents",
        "xiaomai-template-template",
      ]
    }).then((answer) => {
      resolve(answer.projectName);
    });
  });
};

三、输入需要创建的文件名称

// 输入需要创建的文件名称
const inputTplName = (projectName) => {
  return new Promise((resolve) => {
    prompt({
      type: 'input',
      message: '请输入文件名称',
      name: 'fileName'
    }).then((answer) => {
      resolve(answer.fileName);
    });
  });
};

四、确认是否只创建单独文件

// 确认是否只创建单独文件,默认创建的模板文件为文件夹风格
const confirmTplType = (fileName) => {
  return new Promise((resolve) => {
    prompt({
      type: 'confirm',
      name: 'isFolderStyle',
      message: '默认创建文件夹风格的模板文件,输入n则会创建单独的文件'
    }).then((answer) => {
      resolve(answer.isFolderStyle);
    });
  });
};

五、判断文件是否存在 这里需要用到Node fs系统的 一个API,如果路径存在,则返回 true,否则返回 false。 和fs.existsSync(filePath)功能相似的是fs.exists(filePath),但不同的是fs.existsSync(filePath)是阻塞的,而fs.exists(filePath)是非阻塞的。不过fs.exists(filePath)现在已经废弃了

if(fs.existsSync(filePath)) {
	// 是否选择覆盖,不覆盖的话退出程序,覆盖的话则创建模板文件
} else {
	// 直接创建模板文件
}

六、创建文件夹 这里需要用到Node fs系统的 一个另一个API,fs.mkdir(fileName, options, callback),options可选 异步地创建目录。 除了可能的异常,完成回调没有其他参数。

const startMkdir = (projeceName, fileName) => {
  return new Promise((resolve) => {
    fs.mkdir(fileName, () => {
      // some code
    });
  });
};

七、读取模板内容 存放模板内容的文件夹和执行命令的逻辑文件是放在同个目录下的,所以要读取模板内容首先需要获取执行命令的绝对路径,所以需要通过__dirname来获取当前执行文件所在目录的完整目录名,然后通过模板字符串拼接的方式获取模板文件的所在目录 Alt text

获取到模板文件的完整目录名之后,执行fs.readFileSync(file).toString(),并将模板文件中的所有temp字符替换成执行命令时输入的文件名

// 获取执行命令的绝对路径
const basePath = __dirname;
const fileList = [];
fs.mkdir(fileName, () => {
  // 读取模板文件
  const reads = [
    `${basePath}/${projeceName}-template/index.js`,
    `${basePath}/${projeceName}-template/index.json`,
    `${basePath}/${projeceName}-template/index.wxss`,
    `${basePath}/${projeceName}-template/index.wxml`,
  ];
  reads.forEach(file => {
    let text = fs.readFileSync(file).toString();
    text = text.replace(/temp/g, fileName);
    fileList.push(text);
  });
});
 

八、生成文件并写入内容 获取到模板文件之后,我们需要在当前执行node命令的目录下的login目录下写入模板文件,那么这个时候就需要使用process.cwd(),来获得当前执行node命令时候的文件夹目录名

const basePath = process.cwd();
const writes = ['index.js', 'index.json', 'index.wxss', 'index.wxml'];
writes.forEach((file, index) => {
  fs.writeFile(`${basePath}/${fileName}/${file}`, fileList[index], (err) => {
    if (err) { throw err; }
  });
});

Alt text Alt text

Node.js的__dirname__filename,和process.cwd()

__dirname: 获得当前执行文件所在目录的完整目录名 __filename: 获得当前执行文件的带有完整绝对路径的文件名 process.cwd():获得当前执行node命令时候的文件夹目录名

总结


以上就是创建一个模板文件的构思和小脚本实现过程。在实现的过程中最大的问题在于Node.js的 __dirname__filename,和process.cwd()这三者之间的区别一开始没有搞明白,导致不是获取不到模板文件所在的目录就是无法往正确的地方写入模板文件。理清楚这三者的关系之后一切问题都解决了。 虽然实现这个小脚本用到知识点不多,但Node 的fs系统有了更深的了解,成长就是一步一步慢慢积累的~