🚁一起来封装脚手架吧![第4天:看完必会的脚手架开发!]

979 阅读4分钟

👽 概论

前边和大家讲了很多的脚手架构思、设计上的知识,今天正式进入代码开发阶段。如果你对理论文字不感兴趣,那么从这一篇开始跟进也可以完成属于自己的脚手架!

👽 项目初始化

创建好项目文件夹之后,在其中执行初始化命令npm init -y,依次建立如下的目录结构:

│  .babelrc         --babel配置文件
│  package.json
│  README.md
│
├─bin               --cmd命令文件夹
│      cmd          --关键cmd
│
├─src               --脚手架逻辑文件夹
│      main.js      --核心代码
│
└─template          --模板仓库

然后依次安装如下依赖:

    npm i babel-cli  babel-preset-env  chalk commander fs-extra inquirer log-symbols ora shelljs

这些依赖的作用已经在上篇文章介绍过了,感兴趣的可以查看了解。

👽 打包配置

package.json中写入打包命令和bash执行命令,同时更改脚手架名称:

  "name": "cccli",
  "scripts": {
    "build": "babel src -d dist", 
    "watch": "npm run build -- --watch"
  },
  "bin": {
    "cccli": "./bin/cmd"
  },

.babelrc中写入打包配置:

{
  "presets": [
    [
      "env",
      {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

👽 脚本引入

cmd中做两件事情:(1)指定运行环境;(2)引入逻辑js。

#!/usr/bin/env node
require('../dist/main.js');

完成后,可以在main.js中随便写点语句,执行打包命令npm run watch

console.log('脚手架测试!')

然后在项目根目录下执行命令npm link,可以将项目中的bash命令临时注册到全局环境,方便测试。 弹出以下命令即为注册成功:

image.png

之后我们执行bash命令:cccli,即可看到main.js中输出语句。

image.png

👽 定义脚手架主界面

不太明白主界面是什么的同学,可以在安装vue-cli后,输入命令vue,显示的第一个页面即为主界面。主界面往往包含脚手架版本介绍、功能列表展示等内容。

此处的核心是commander这个库,因为官方文档有中文,而且说明也很详细,此处就不多介绍了。

/*
 * @FileName: main.js
 * @Description:脚手架入口文件
 */
const program = require('commander');
const chalk = require('chalk');

//注册版本
program.version(require('../package').version);

//注册create功能
program
  .command('create <appName>')
  .description('创建一个新项目')
  .action((name) => {
    if (process.argv.slice(3).length > 1) {
      console.log(chalk.yellow('\n 文件名中请勿包含空格!'));
    }
    //为方便管理,create功能的详细逻辑单独放在如下文件中管理
    const  create  = require('./command/create');
    create(name);
  });

program.parse(process.argv);

完成后再执行cccli,就可以看到界面已经不一样了:

image.png

👽 create功能的逻辑

建立对应的文件,在其中主要做两件事:(1)检查文件名是否重复,并做相应处理;(2)根据用户选择,生成相应模板。

🚩文件名检查

 /*
 * @FileName: create.js
 * @Description:cli-create命令
 */
const fs = require('fs-extra');

const chalk = require('chalk');
const inquirer = require('inquirer');

async function create(appName) {
  //判断名称是否重复
  const nameRepeat = fs.existsSync(`./${appName}`);
  //判断是与文件名还是文件夹名重复
  const isFile = nameRepeat ? fs.statSync(`./${appName}`).isFile() : false;

  //定义交互项
  const newPrompts = {
    fileRepeat: {
      name: 'fileRepeat',
      type: 'list',
      message: `当前路径中已存在${isFile ? '文件' : '文件夹'}${chalk.bgBlue(
        ' ' + appName + ' '
      )},请确认操作:`,
      choices: [
        {
          name: '覆盖',
          value: 1,
        },
        {
          name: '退出',
          value: 0,
        },
      ],
    },
  };
  //检查名称是否重复
  if (nameRepeat) {
    const { fileRepeat } = await inquirer.prompt(newPrompts['fileRepeat']);
    //选择退出后中断程序
    if (!fileRepeat) return;
  }
}

//将create作为函数导出
module.exports = (...args) => {
  return create(...args).catch(err => {
    console.log(chalk.red('项目创建出错:', err));
  });
};

完成后测试结果如图:

image.png

🚩询问模板类型

接下来我们设计模板类型选择界面,此处将模板分为Web/H5小程序两个大类,Web/H5下又分后台管理系统模板基础模板两个小类。

/*
* @FileName: create.js
* @Description:cli-create命令
*/

...

async function create(appName) {

···

 //定义交互项
 const newPrompts = {
   ···
     
   appType: {
     name: 'appType',
     type: 'list',
     message: `请选择应用类型:`,
     choices: [
       {
         name: 'Web/H5应用',
         value: 'web',
       },
       {
         name: `小程序应用`,
         value: 'wx',
       },
     ],
   },

   scaffoldType: {
     web: {
       name: 'scaffoldType',
       type: 'list',
       message: `请选用预设模板类型:`,
       choices: [
         {
           name: `${chalk.bold(
             '后台管理系统模板'
           )} (Vue2 + Vuex + Vue-Router + axios + less + ele + dayjs)`,
           value: 'admin',
         },
         {
           name: `${chalk.bold('基础模板')} (Vue2 + Vuex + Vue-Router + axios + less)`,
           value: 'basic',
         },
       ],
     },
     wx: {
       name: 'scaffoldType',
       type: 'list',
       message: `请选用预设模板类型:`,
       choices: [
         {
           name: `UniApp(Vue2 + Vuex + Vue-Router + uView)${chalk.red(
             '推荐使用HbuilderX运行发包'
           )}`,
           value: 'uni',
         },
       ],
     },
   },
 };
 ···
 
 //询问应用类型
 const { appType } = await inquirer.prompt(newPrompts['appType']);
 //询问模板类型
 const { scaffoldType } = await inquirer.prompt(newPrompts['scaffoldType'][appType]);
 
 //输出结果以便测试
 console.log('appType: ', appType);
 console.log('scaffoldType : ', scaffoldType);
}

//将create作为函数导出
module.exports = (...args) => {
 return create(...args).catch(err => {
   console.log(chalk.red('项目创建出错:', err));
 });
};

完成后执行,可以看到输出了用户选择的值:

image.png

🚩 代码拆分

到这一步我们发现,如果所有的代码都写在这一个文件里,代码整体定会十分混杂。为阅读维度清晰,我们做一定拆分:(1)交互项对象newPrompts作为配置文件拆分至src/config/prompts中;(2)后续如有功能函数,我们将其放入src/utils中。

重新组织代码如下:

/*
* @FileName: create.js
* @Description:cli-create命令
*/
const fs = require('fs-extra');

const shell = require('shelljs');
const chalk = require('chalk');
const inquirer = require('inquirer');

const prompts = require('../config/prompts');

async function create(appName) {
 const nameRepeat = fs.existsSync(`./${appName}`);
 const isFile = nameRepeat ? fs.statSync(`./${appName}`).isFile() : false;

 const newPrompts = prompts(isFile, appName);

 //检查名称是否重复
 if (nameRepeat) {
   const { fileRepeat } = await inquirer.prompt(newPrompts['fileRepeat']);
   if (!fileRepeat) return;
 }

 //询问应用类型
 const { appType } = await inquirer.prompt(newPrompts['appType']);
 //询问模板类型
 const { scaffoldType } = await inquirer.prompt(newPrompts['scaffoldType'][appType]);
}

module.exports = (...args) => {
 return create(...args).catch(err => {
   console.log(chalk.red('项目创建出错:', err));
 });
};
/*
* @FileName: prompts.js
* @Description:交互项
*/
const chalk = require('chalk');

const PROMPTS = (isFile, appName) => {
return {
  fileRepeat: {
    name: 'fileRepeat',
    type: 'list',
    message: `当前路径中已存在${isFile ? '文件' : '文件夹'}${chalk.bgBlue(
      ' ' + appName + ' '
    )},请确认操作:`,
    choices: [
      {
        name: '覆盖',
        value: 1,
      },
      {
        name: '退出',
        value: 0,
      },
    ],
  },

  appType: {
    name: 'appType',
    type: 'list',
    message: `请选择应用类型:`,
    choices: [
      {
        name: 'Web/H5应用',
        value: 'web',
      },
      {
        name: `小程序应用`,
        value: 'wx',
      },
    ],
  },

  scaffoldType: {
    web: {
      name: 'scaffoldType',
      type: 'list',
      message: `请选用预设模板类型:`,
      choices: [
        {
          name: `${chalk.bold(
            '后台管理系统模板'
          )} (Vue2 + Vuex + Vue-Router + axios + less + ele + dayjs)`,
          value: 'admin',
        },
        {
          name: `${chalk.bold('基础模板')} (Vue2 + Vuex + Vue-Router + axios + less)`,
          value: 'basic',
        },
      ],
    },
    wx: {
      name: 'scaffoldType',
      type: 'list',
      message: `请选用预设模板类型:`,
      choices: [
        {
          name: `UniApp(Vue2 + Vuex + Vue-Router + uView)${chalk.red(
            '推荐使用HbuilderX运行发包'
          )}`,
          value: 'uni',
        },
      ],
    },
  },
};
};
module.exports = PROMPTS;

🚩 模板生成

src/utils/generateFile中定义文件、文件夹生成函数:

 /*
 * @FileName: generateFile.js
 * @Description:文件、文件夹生成函数
 */
 
const fs = require('fs-extra');
const ora = require('ora');

//加载等待效果
const oraIns = ora({
  text: '努力处理中···',
  spinner: 'dots',
  color: 'yellow',
  interval: 150,
});

function generateFile(templatePath, appName, isFile) {
  //解析模板地址
  const resourcePath = process.argv[1].replace('\\bin\\cmd', templatePath);
  //定义输出地址
  const outputPath = process.cwd() + '/' + appName;
  //加载动画开始
  oraIns.start();
  
  //存在重名文件时先将其删除
  isFile && fs.removeSync(outputPath);
  //克隆文件夹
  fs.copySync(resourcePath, outputPath);
  
  //加载动画开始
  oraIns.stop();
}

module.exports = generateFile;

注入create功能中。

/*
* @FileName: create.js
* @Description:cli-create命令
*/
···

const generateFile = require('../utils/generateFile');

async function create(appName) {
  ···

 //据询问结果拼接模板路径
 const templatePath = `/template/${appType}/${scaffoldType}`;
 //克隆模板至本地
 generateFile(templatePath, appName, isFile);
}
···

至此脚手架的核心功能就已经完成了!下期我们再讲怎么把现在已经做好的脚手架发布到NPM,敬请期待!

👽 ‘一起来封装脚手架’系列文章

🚁第1天:脚手架模板的构思

🚁第2天:项目规范的定义

🚁第3天:脚手架开发的前置知识

🚁第4天:看完必会的脚手架开发!

🚁第5天:NPM包的发布