从0开始搭建一个脚手架(入门基础)

440 阅读5分钟

前言

这篇文章会讲解如何搭建一个最简单的脚手架,即通过命令生成已设定好的模板

实现效果展示

9.gif

创建项目

我们先创建一个node-cli结构的文件

fishfan-cli           
├─ bin                
│  └─ cli.js  # 启动文件      
└─ package.json       

// package.json 
{
  "name": "fishfan-cli",
  "version": "0.0.1",
  "description": "a good cli",
  "main": "index.js",
  "bin": {
    "fishcli": "./bin/cli.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "fishfan",
  "license": "ISC"
}

1.png 如上图所示,我们在终端执行命令的时候,统一走bin下面的cli.js文件(这里并不是一定要在bin目录下面,而是根据package.json文件的bin字段所确定)

编辑cli.js文件

#!/usr/bin/env node

console.log('hello,fishfan')

为了方便在本地进行调试,使用npm link链接到全局,mac下需要使用管理员权限

sudo npm link

3.png

友情提示:必须在你所创建的脚手架的文件下进行该命令操作,不然会出现下面的错误

2.png

我们随后可以随意创建一个文件,在该文件下执行下fishcli

4.png

看到成功打印的hello,fishfan,算是第一步成功

终端命令交互

我们需要做的是让cli.js文件能够读懂我们的终端命令,比如fishcli create创建项目,为了能够在终端流利地操作命令行,我们引入commander模块

commander更多用法 👉 中文文档

安装依赖
npm install commander --save

编辑cli.js文件

#!/usr/bin/env node

const program = require('commander');
program
  .command('create')
  .description('create a project')
  .action(() => {
   console.log('欢迎使用fishcli脚手架')
  });
// 解析用户执行命令传入参数
program.parse(process.argv);  

在终端输入fishcli create查看命令是否创建成功

5.png

这时候我们的create命令已经创建成功,但是我们想实现和终端的交互功能,这就是要node的inquirer模块,同时我们发现很多的脚手架的文字都是彩色的,而我们的脚手架打印的都是默认颜色,我们引入chalk工具库

inquirer文档👉

命令交互

安装依赖
npm install inquirer chalk --save

inquirer的基础用法

const inquirer = require('inquirer');
inquirer
  .prompt([
    /* 把你的问题传过来 */
  ])
  .then(answers => {
    /* 反馈用户内容 */
  })
  .catch(error => {
    /* 出现错误 */
  });

假设我们这里做的是一个React脚手架,我们和用户设定的问题为

  • 是否创建新的项目(是/否)
  • 请输入项目名称(文本输入)
  • 请输入作者(文本输入)
  • 请选择公告管理状态(单选)

上述的prompt第一参数和我们所设置的问题进行结合,得到的question配置大致是这样

const question = [
   {
        name:'conf',              /* key */
        type:'confirm',           /* 确认 */
        message:'是否创建新的项目?' /* 提示 */
    },{
        name:'name',
        message:'请输入项目名称',
        when: res => Boolean(res.conf) /* 是否进行 */
    },{
        name:'author',
        message:'请输入作者',
        when: res => Boolean(res.conf)
    },{
        type: 'list',            /* 选择框 */
        message: '请选择公共管理状态?',
        name: 'state',
        choices: ['mobx','redux'], /* 选项*/
        filter: function(val) {    /* 过滤 */
          return val.toLowerCase()
        },
        when: res => Boolean(res.conf)
    }
]


我们接着完善cli.js的代码

#!/usr/bin/env node

const program = require('commander');
const chalk = require('chalk')
const inquirer = require('inquirer')
const question = [
  {
    name:'conf',              /* key */
    type:'confirm',           /* 确认 */
    message:'是否创建新的项目?' /* 提示 */
  },{
    name:'name',
    message:'请输入项目名称?',
    when: res => Boolean(res.conf) /* 是否进行 */
  },{
    name:'author',
    message:'请输入作者?',
    when: res => Boolean(res.conf)
  },{
    type: 'list',            /* 选择框 */
    message: '请选择公共管理状态?',
    name: 'state',
    choices: ['mobx','redux'], /* 选项*/
    filter: function(val) {    /* 过滤 */
      return val.toLowerCase()
    },
    when: res => Boolean(res.conf)
  }
]

program
  .command('create')
  .description('create a project')
  .action(() => {
    console.log(chalk.green('欢迎使用fishcli,轻松构建react ts项目~🎉🎉🎉'))
    inquirer.prompt(question).then(answer=>{
        console.log('answer=', answer )
    })

  });
// 解析用户执行命令传入参数
program.parse(process.argv);

效果如下

6.gif

这时候我们可以拿到用户所输入的信息,然后我们就可以根据用户输入的信息,来选择适合用户输入的模板进行创建,这里的作者名字和项目名称都是动态的,因此我们需要动态去修改模板里面的文件,将用户输入的内容进行替换,本文暂时不讲解。

拷贝文件

在最外层我们创建template文件夹,这里模板文件就是供用户下载的文件,如果你的脚手架有多种选择比如mobx或者redux,那么这里的模板文件就不止一个。这里的template文件我使用的是webpack5搭建的React项目,代码地址,关于如何使用webpack5搭建react项目可以查看该文章链接

由于template项目模板,有可能是深层的文件结构,我们需要深拷贝项目文件,需要node的fs模块

我们在src下创建create.js文件

src/craete.js

const fs = require('fs');
const chalk = require('chalk')
const { Buffer } = require('buffer');

/* 三变量判断异步操作 */
let fileCount = 0; /* 文件数量 */
let dirCount = 0; /* 文件夹数量 */
let flat = 0; /* readir数量 */

module.exports = function (res) {
  /* 创建文件 */
  console.log(chalk.green('------开始构建-------'));
  const sourcePath = __dirname.slice(0, -3) + 'template';
  console.log(chalk.blue('当前路径:' + process.cwd()));
  /* 修改package.json*/
  revisePackageJson(res, sourcePath).then(() => {
    copy(sourcePath, process.cwd());
  });
};

const copy = (sourcePath, currentPath)=> {
  flat++;
  fs.readdir(sourcePath, (err, paths) => {
    flat--;
    if (err) {
      throw err;
    }
    paths.forEach((path) => {
      if (path !== '.git' && path !== 'package.json') fileCount++;
      const newSoucePath = sourcePath + '/' + path;
      const newCurrentPath = currentPath + '/' + path;
      fs.stat(newSoucePath, (err, stat) => {
        if (err) {
          throw err;
        }
        if (stat.isFile() && path !== 'package.json') {
          const readSteam = fs.createReadStream(newSoucePath);
          const writeSteam = fs.createWriteStream(newCurrentPath);
          readSteam.pipe(writeSteam);
          console.log(chalk.green('创建文件:' + newCurrentPath));
          fileCount--;
        } else if (stat.isDirectory()) {
          if (path !== '.git' && path !== 'package.json') {
            dirCount++;
            dirExist(newSoucePath, newCurrentPath, copy);
          }
        }
      });
    });
  });
}

const dirExist =(sourcePath, currentPath, copyCallback)=> {
  fs.exists(currentPath,(exist) => {
    if (exist) {
      copyCallback(sourcePath, currentPath);
    } else {
      fs.mkdir(currentPath, () => {
        fileCount--;
        dirCount--;
        copyCallback(sourcePath, currentPath);
        console.log(chalk.yellow('创建文件夹:' + currentPath));
      });
    }
  });
}

const revisePackageJson = (res, sourcePath) => new Promise((resolve) => {
  fs.readFile(sourcePath + '/package.json', (err, data) => {
    if (err) throw err;
    const { author, name } = res;
    let json = data.toString();
    json = json.replace(/demoname/g, name.trim());
    json = json.replace(/demoAuthor/g, author.trim());
    const path = process.cwd() + '/package.json';
    const data1 = new Uint8Array(Buffer.from(json));
    fs.writeFile(path, data1, () => {
      console.log(chalk.green('创建文件:' + path));
      resolve();
    });
  });
});

在cli.js引入我们创建的create文件的方法

const create = require('../src/create');

program
  .command('create')
  .description('create a project')
  .action(() => {
    console.log(chalk.green('欢迎使用fishcli,轻松构建react ts项目~🎉🎉🎉'))
    inquirer.prompt(question).then(answer=>{
      if (answer.conf) {
        create(answer)
      } else {
        console.log(chalk.yellow('没关系,希望以后有合作机会!'))
      }
    })

  });

这时候我们fishcli create命令已经完善完毕,可以利用该命令来生成自己的文件

至此我们最基本的脚手架已经构建完成。

结束语

关于文件拷贝部分涉及node的fs知识,这里建议大家抽时间看看这里的内容,我这里也是直接使用了他人写的方法,自己着实没想到啥好的方法来。这篇文章其实介绍功能就是拷贝文件生成文件,终端等命令其实都是工具而已,不过所涉及的工具是大家学习终端操作所避不开的东西。

本文大部分内容借鉴以下俩篇文章,如果大家想深入了解脚手架知识,可以参考下面两篇文章,后续我会继续完善该脚手架,并对文章进行完善,做进一步的了解。

juejin.cn/post/691930…

juejin.cn/post/696611…