一步一步打造属于自己的脚手架

预备知识

  • 本地安装卸载包

    在项目根目录下运行下面命令

    # 安装
    npm link
    
    # 卸载
    npm unlink
    
  • 命令行命令指定运行文件

    package.json文件中bin字段指定

项目依赖包

  • chalk 打印出彩色字

  • commander 完整的 node.js 命令行解决方案

  • download-git-repo 下载仓库代码

  • handlebars 编译文件

  • inquirer 交互式命令行界面

  • ora 加载动画

  • update-notifier 更新通知

  • figlet 炫酷文字

    字体示例

支持的命令

da --help
da -version
da update
da add
da delete
da list
da create
da site add
da site delete
da site list

命令详情介绍以及项目地址点击传送门

动起来

1. 新建文件夹da-cli
2. 打开命令行工具,进入文件夹,运行初始化仓库
cd ./da-cli
npm init -y
3. 安装依赖
npm i chalk commander download-git-repo handlebars inquirer ora update-notifier figlet -S
  • handlebars 不是一定要用,可以使用node读写文件,本项目使用是多介绍一种修改文件内容方案,项目中会使用两种:原生node读写文件、使用handlebars修改文件内容。
  • figlet 不是一定要用,主要为了炫酷,装一下
  • ora 不是一定要用,为了体验优化
4. 修改package.json
  • 脚手架会有多个命令,每个命令会有对应的处理逻辑,一般是会分模块写,这样增加可读性、方便维护

  • 命令执行文件,可以由命令根据已经配置的一个文件自己匹配文件,也可以自己注册

    例如: da initda add两个命令(da是自定义的,必须和bin中键保持一致)

    匹配文件模式:(自己编的模式)

    // package.json文件
    {
        ...
        "bin": {
            "da": "./bin/da.js"
        },
        ...
    }
    

    ./bin/da.js文件中不会写 da initda add两个命令处理逻辑

    当运行da init时,会去bin文件夹下面找da-init.js文件(也可以在bin中增加da-init键指定执行文件)

    当运行da add时,会去bin文件夹下面找da-add.js文件

    自己注册模式:(自己编的模式)

    // package.json文件
    {
        ...
        "bin": {
            "da": "./bin/index.js"
        }
        ...
    }
    

    ./bin/index.js文件中会写 da initda add两个命令处理逻辑

    本项目采用自己注册模式

  • package.json内容

    {
      "name": "da-cli",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "bin": {
        "da": "./bin/index.js"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
    }
    
5. 新建文件夹da-cli/binda-cli/lib,新建文件da-cli/bin/index.js
6. 编写da-cli/bin/index.js,首先处理兼容问题
#!/usr/bin/env node 
  • 惊叹、有病,环境是node方便记忆
7.实现da -version,编写da-cli/bin/index.js
#!/usr/bin/env node

const program = require('commander')

program.name('da').version(require('../package').version, '-v -version', 'output the current version').usage('<command> [options]')

program.parse(process.argv)
  • program.parse(process.argv)必须有这个,一直放到最后一行用于解析命令行参数

  • <>包裹的参数一般是必传的

  • .name('da')可以去除

  • 此时可以使用以下命令(执行命令前,根项目下运行npm link,这样可以全局使用)

    da -v
    da -version
    
    da -h
    da -help
    
8. 实现da update

新建编写da-cli/lib/update/update.js

const updateNotifier = require('update-notifier')
const chalk = require('chalk')
const pkg = require('../../package.json')

const notifier = updateNotifier({
	pkg,
	updateCheckInterval: 1000,
})

function updateVer() {
	if (notifier.update) {
		console.log(`New version ${chalk.green(notifier.update.latest)} found, please update`)
		notifier.notify()
	} else {
		console.log('There is no updatable version')
	}
}

module.exports = updateVer

编写da-cli/bin/index.js

#!/usr/bin/env node

const program = require('commander')

program.name('da').version(require('../package').version, '-v -version', 'output the current version').usage('<command> [options]')

// update
program
	.command('update')
  .alias('u')
	.description('update version')
	.action(() => {
		updateVer()
	})

program.parse(process.argv)
9. 增加配置文件,方便下面命令实现

新增文件夹da-cli/config

增加da-cli/config/codebase.json

// 存放模板仓库
{
  "loading": {
    "tplOwner": "lydxwj", // 项目仓库所有者
    "tplName": "vue-loading", // 项目仓库名
    "site": "gitee" // 项目仓库地址类型
  }
}

增加da-cli/config/config.json

{
  "sites": { // 全部项目仓库地址类型,支持用户自定义扩展
    "github": {
      "name": "github",
      "value": "github",
      "short": "github",
      "url": "github:{tplOwner}/{tplName}"
    },
    "gitlab": {
      "name": "gitlab",
      "value": "gitlab",
      "short": "gitlab",
      "url": "gitlab:{tplOwner}/{tplName}"
    },
    "gitee": {
      "name": "gitee",
      "value": "gitee",
      "short": "gitee",
      "url": "direct:https://gitee.com/{tplOwner}/{tplName}"
    }
  },
  "frames": { // 创建项目框架选项,以及对应url,不支持用户自定义扩展
    "vue": {
      "url": "direct:https://gitee.com/lydxwj/vue-create"
    }
  }
}
10. 实现da add(增加模板)

新增文件夹da-cli/lib/template

增加da-cli/lib/template/add.js

const inquirer = require('inquirer')
const chalk = require('chalk')
const fs = require('fs')
// 现有模板,用于判断是否重复添加和新增后的数据拼接
const codeBase = require(`${__dirname}/../../config/codebase`)
// 用于生成模板URL地址类型选择
const sites = require(`${__dirname}/../../config/config`).sites

// 生成模板URL地址类型
const sitesChoices = [];
Object.keys(sites).forEach(item => {
  sitesChoices.push(sites[item]);
})
function add() {
  // 交互式命令
  let question = [
    {
      name: "name",
      type: 'input',
      message: "请输入模板名称(英文字母)",
      validate (val) {
        if (val === '') {
          return 'Name is required!'
        } else if (!/^[a-zA-Z]+$/.test(val)) {
          return 'It can only contain English letters!'
        } else if (codeBase[val]) {
          return 'Template has already existed!'
        } else {
          return true
        }
      }
    },
    {
      type: 'list',
      message: '请选择模板仓库地址类型:',
      name: 'site',
      choices: sitesChoices,
    },
    {
      name: "tplOwner",
      type: 'input',
      message: "请输入仓库所属",
      validate (val) {
        if (val === '') return 'The tplOwner is required!'
        return true
      }
    },
    {
      name: "tplName",
      type: 'input',
      message: "请输入仓库名",
      validate (val) {
        if (val === '') return 'The tplName is required!'
        return true
      }
    }
  ]
  
  inquirer
    .prompt(question).then(answers => {
      let { name, tplOwner, tplName, site } = answers;
      // 拼接增加新的模板
      codeBase[name] = {
        tplOwner,
        tplName,
        site
      };
      // 写文件,更新模板
      fs.writeFile(`${__dirname}/../../config/codebase.json`, JSON.stringify(codeBase, null, 2), 'utf-8', err => {
        if (err) {
          console.log(chalk.red('Error\n'))
          console.log(err)
          return
        }
        console.log('\n')
        console.log(chalk.green('Added successfully!\n'))
        console.log(chalk.grey('The latest template list is: \n'))
        // 更新成功并打印最新的模板列表
        Object.keys(codeBase).forEach(item => {
          console.log(chalk.green('   ' + item + ':  \n'));
          console.log('     仓库地址类型:' + sites[codeBase[item].site].name + ' \n');
          console.log('     仓库所属:' + codeBase[item].tplOwner + ' \n');
          console.log('     仓库名:' + codeBase[item].tplName + ' \n');
        })
        console.log('\n')
      })
    })
}
module.exports = add

编写da-cli/bin/index.js

...
const tplAdd = require('../lib/template/add')

...
// add a new template
program
  .command('add')
  .description('add a new template')
  .alias('a')
  .action(() => {
    tplAdd()
  })
...
11. 实现da delete(删除模板)

本命令的实现和上一个十分类似,只是把变量增加键值对变成删除键

增加da-cli/lib/template/delete.js

const inquirer = require('inquirer')
const chalk = require('chalk')
const fs = require('fs')
const codeBase = require(`${__dirname}/../../config/codebase`)
const sites = require(`${__dirname}/../../config/config`).sites
function tplDelete() {
  let question = [{
    name: "name",
    message: "请输入要删除的模板名称",
    validate(val) {
      if (val === '') {
        return 'Name is required!'
      } else if (!codeBase[val]) {
        return 'Template does not exist!'
      } else {
        return true
      }
    }
  }]
  
  inquirer
    .prompt(question).then(answers => {
      let {
        name
      } = answers;
      delete codeBase[name]
      fs.writeFile(`${__dirname}/../../config/codebase.json`, JSON.stringify(codeBase, null, 2), 'utf-8', err => {
        if (err) {
          console.log(chalk.red('Error\n'))
          console.log(err)
          return
        }
        console.log('\n')
        console.log(chalk.green('Deleted successfully!\n'))
        console.log(chalk.grey('The latest template list is: \n'))
        Object.keys(codeBase).forEach(item => {
          console.log(chalk.green('   ' + item + ':  \n'));
          console.log('     仓库地址类型:' + sites[codeBase[item].site].name + ' \n');
          console.log('     仓库所属:' + codeBase[item].tplOwner + ' \n');
          console.log('     仓库名:' + codeBase[item].tplName + ' \n');
        })
        console.log('\n')
      })
    })
}

module.exports = tplDelete

编写da-cli/bin/index.js

...
const tplDelete = require('../lib/template/delete')

...
// delete a template
program
  .command('delete')
  .description('delete a template')
  .alias('d')
  .action(() => {
    tplDelete()
  })
...
12. 实现da list(模板列表)

此功能在增加和删除命令中已经实现

增加da-cli/lib/template/list.js

const chalk = require('chalk')
const codeBase = require(`${__dirname}/../../config/codebase`)
const sites = require(`${__dirname}/../../config/config`).sites
function tplList() {
  Object.keys(codeBase).forEach(item => {
    console.log(chalk.green('   ' + item + ':  \n'));
    console.log('     仓库地址类型:' + sites[codeBase[item].site].name + ' \n');
    console.log('     仓库所属:' + codeBase[item].tplOwner + ' \n');
    console.log('     仓库名:' + codeBase[item].tplName + ' \n');
  })
}
module.exports = tplList

编写da-cli/bin/index.js

...
const tplList = require('../lib/template/list')

...
// list all the templates
program
  .command('list')
  .description('list all the templates')
  .alias('l')
  .action(() => {
    tplList()
  })
...
13. 实现da init(初始化项目)

有了模板,现在就是用起来

增加da-cli/lib/template/init.js

const program = require('commander')
const chalk = require('chalk')
const ora = require('ora')
const download = require('download-git-repo')
const inquirer = require('inquirer')
const path = require('path')
const fs = require('fs')
const codeBase = require(`${__dirname}/../../config/codebase`)
const sites = require(`${__dirname}/../../config/config`).sites

const promptList = [
  {
    type: 'input',
    message: '请输入项目简介',
    name: 'description',
    default: '项目简介'
  },
  {
    type: 'input',
    message: '请输入项目版本',
    name: 'version',
    default: '1.0.0'
  }
]
// 读写文件,修改package的name,description,version
function setProjectDescription(projectName, options) {
  const packagePath = `${projectName}/package.json`
  const packageJson = fs.readFileSync(packagePath, 'utf-8')
  const packageResult = JSON.stringify(Object.assign({}, JSON.parse(packageJson), options), null, 2)
  fs.writeFileSync(packagePath, packageResult)
}
function tplInit(templateName, projectName) {
  // 判断模板是否存在
  if (!codeBase[templateName]) {
    console.log(chalk.red('\n Template name does not exit! \n '))
    return;
  }
  if (!projectName) {
    console.log(chalk.red('\n Project name should not be empty! \n '))
    return program.help();
  }
  if (program.args.length < 2) return program.help()
  // 判断是否已经存在这个文件夹,存在则报错(不判断会造成仓库代码下载不成功)
  try {
    const stat = fs.statSync(path.resolve('./', projectName));
    if (stat.isDirectory()) {
      console.log(chalk.red('\n The folder already exists! Please change the project name! \n '))
      return;
    };
  } catch (err) {}
  const tplObj = codeBase[templateName];
  if (!sites[tplObj.site]) {
    console.log(chalk.red('\n Code base address type does not exist! \n '))
    return;
  }
  // 拼接仓库地址
  let url = sites[tplObj.site].url;
  url = url.replace('{tplOwner}', tplObj.tplOwner);
  url = url.replace('{tplName}', tplObj.tplName);
  const spinner = ora("Downloading...");
  inquirer.prompt(
    promptList
  ).then(answers => {
    console.log(chalk.white('\n Start generating... \n'))
    spinner.start()
    // 下载仓库代码
    download(
      url,
      projectName,
      {
        clone: url.startsWith('direct:') ? true : false,
      },
      err => {
        if (err) {
          spinner.fail()
          console.log(chalk.red(`Generation failed. ${err}`))
          return
        }
        const { description, version } = answers
        setProjectDescription(projectName, { name: projectName, description, version })
        spinner.succeed()
        console.log(chalk.cyan('\n Generation completed!'))
        console.log(chalk.cyan('\n To get started'))
        console.log(chalk.cyan(`\n    cd ${projectName}`))
        console.log(chalk.cyan(`\n    npm install \n`))
      }
    )
  });
}
module.exports = tplInit

编写da-cli/bin/index.js

...
const tplInit = require('../lib/template/init')

...
// generate a new project from a template
program
  .command('init')
  .arguments('<templateName> <projectName>')
  .description('generate a new project from a template', {
    templateName: 'existing template name',
    projectName: 'folder that does not exist'
  })
  .alias('i')
  .action((templateName, projectName) => {
    tplInit(templateName, projectName)
  })
...
14. 实现da site list(URL地址类型列表),da site add(URL地址类型增加),da site delete(URL地址类型删除)

因为上面已经实现了类似的,这里就一次搞定三个

新建文件夹da-cli/lib/site

增加da-cli/lib/site/list.js

const chalk = require('chalk')
const sites = require(`${__dirname}/../../config/config`).sites
function siteList() {
  Object.keys(sites).forEach(item => {
    console.log(chalk.green('   ' + item + ':  \n'));
    console.log('     仓库地址类型名:' + sites[item].name + ' \n');
    console.log('     仓库地址URL:' + sites[item].url + ' \n');
  })
  console.log('\n')
}
module.exports = siteList

增加da-cli/lib/site/add.js

const chalk = require('chalk')
const inquirer = require('inquirer')
const fs = require('fs')
const configJson = require(`${__dirname}/../../config/config`)
const sites = configJson.sites
function siteAdd() {
  let question = [
    {
      name: "siteName",
      type: 'input',
      message: "请输入仓库地址类型名",
      validate (val) {
        if (val === '') {
          return 'siteName is required!'
        } else {
          return true
        }
      }
    },
    {
      name: "siteKey",
      type: 'input',
      message: "请输入存储仓库地址类型的键(key)",
      validate (val) {
        if (val === '') return 'siteKey is required!'
        if (sites[val]) return 'siteKey has already existed!'
        if (!/^[a-zA-Z]+$/.test(val)) {
          return 'It can only contain English letters!'
        }
        return true
      }
    },
    {
      name: "siteUrl",
      type: 'input',
      message: "请输入仓库地址URL",
      suffix: '例如:https://my.sitecode.com',
      validate (val) {
        if (val === '') return 'The siteUrl is required!'
        if (!/(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/.test(val)) return 'Please enter the correct URL!'
        return true
      }
    }
  ]
  inquirer.prompt(question).then(answers => {
    let { siteName, siteKey, siteUrl } = answers;
    const newSite = {
      name: siteName,
      value: siteKey,
      short: siteName,
      url: 'direct:' + siteUrl + '/{tplOwner}/{tplName}.git'
    }
    sites[siteKey] = newSite
    fs.writeFile(`${__dirname}/../../config/config.json`, JSON.stringify({ ...configJson, sites }, null, 2), 'utf-8', err => {
      if (err) {
        console.log(chalk.red('Error\n'))
        console.log(err)
        return
      }
      console.log('\n')
      console.log(chalk.green('Added successfully!\n'))
      console.log(chalk.grey('The latest sites list is: \n'))
      Object.keys(sites).forEach(item => {
        console.log(chalk.green('   ' + item + ':  \n'));
        console.log('     仓库地址类型名:' + sites[item].name + ' \n');
        console.log('     仓库地址URL:' + sites[item].url + ' \n');
      })
      console.log('\n')
    })
  })
}
module.exports = siteAdd

增加da-cli/lib/site/delete.js

const chalk = require('chalk')
const inquirer = require('inquirer')
const fs = require('fs')
const configJson = require(`${__dirname}/../../config/config`)
const sites = configJson.sites
function siteDelete() {
  let question = [
    {
      name: "siteKey",
      type: 'input',
      message: "请输入要删除的存储仓库地址类型键(key)",
      validate (val) {
        if (val === '') {
          return 'siteKey is required!'
        } else if (!sites[val]) {
          return 'siteKey does not exist!'
        } else if (val == 'github' || val == 'gitlab' || val == 'gitee') {
          return 'no permission!'
        }
        return true
      }
    },
  ]
  inquirer.prompt(question).then(answers => {
    let { siteKey } = answers;
    delete sites[siteKey]
    fs.writeFile(`${__dirname}/../../config/config.json`, JSON.stringify({ ...configJson, sites }, null, 2), 'utf-8', err => {
      if (err) {
        console.log(chalk.red('Error\n'))
        console.log(err)
        return
      }
      console.log('\n')
      console.log(chalk.green('Deleted successfully!\n'))
      console.log(chalk.grey('The latest sites list is: \n'))
      Object.keys(sites).forEach(item => {
        console.log(chalk.green('   ' + item + ':  \n'));
        console.log('     仓库地址类型名:' + sites[item].name + ' \n');
        console.log('     仓库地址URL:' + sites[item].url + ' \n');
      })
      console.log('\n')
    })
  })
}
module.exports = siteDelete

编写da-cli/bin/index.js

...
const siteList = require('../lib/site/list')
const siteAdd = require('../lib/site/add')
const siteDelete = require('../lib/site/delete')

...
// 由于是两个指令参数,所以要配合addCommand
// configure site
function makeSiteCommand() {
  const site = new program.Command('site');

  // list all the sites
  site
    .alias('s')
    .usage('<command>')
    .command('list')
    .alias('l')
    .description('list all the sites')
    .action(() => {
      siteList()
    });

  // add a new site
  site
    .command('add')
    .alias('a')
    .description('add a new site')
    .action(() => {
      siteAdd()
    });

  // delete a site
  site
    .command('delete')
    .alias('d')
    .description('delete a site')
    .action(() => {
      siteDelete()
    });
  return site;
}
program.addCommand(makeSiteCommand())
...
15. 实现da create

这是最后一个命令,是定制化的生成项目,需要自己建一个用于生成定制的项目。本项目使用的是https://gitee.com/lydxwj/vue-create,没有什么特别的只是需要修改的文件含有模板引擎的代码,所以本命令使用了模板引擎插件,并且使用了figlet 炫酷文字

新建文件夹da-cli/lib/create

增加da-cli/lib/create/create.js

const program = require('commander')
const chalk = require('chalk')
const figlet = require('figlet')
const inquirer = require('inquirer')
const ora = require('ora')
const download = require('download-git-repo')
const handlebars = require('handlebars')
const fs = require('fs')
const path = require('path')
const frames = require(`${__dirname}/../../config/config`).frames

// 生成框架选择数组
const framesChoices = [];
Object.keys(frames).forEach(item => {
  framesChoices.push(item);
})

const questions = [
  {
    type: 'list',
    message: '请选择框架:',
    name: 'frame',
    choices: framesChoices,
  },
  {
    type: 'input',
    message: '请输入项目简介',
    name: 'description',
    default: '项目简介'
  },
  {
    type: 'input',
    message: '请输入项目版本',
    name: 'version',
    default: '1.0.0'
  }
]

// 修改文件
function renderFiles(targetPath, globalData) {
  // 需要修改的文件列表
  const fileList = [
    `${targetPath}/package.json`,
    `${targetPath}/README.md`,
  ]
  return new Promise(async (resolve, reject) => {
    for (let i = 0; i < fileList.length; i++) {
      try {
        const fileContent = await fs.readFileSync(fileList[i], 'utf8')
        // 使用模板引擎编译文件
        const fileRendered = await handlebars.compile(fileContent)(globalData)
        await fs.writeFileSync(fileList[i], fileRendered)
        resolve()
      } catch (err) {
        chalk.red('\n craete project failed. \n')
        chalk.red(`\n ${err} \n`)
        reject(err)
      }
    }

  })
}

function createPro(projectName) {
  if (!projectName) {
    console.log(chalk.red('\n Project name should not be empty! \n '))
    return program.help()
  }
  if (program.args.length < 1) return program.help()
  try {
    const stat = fs.statSync(path.resolve('./', projectName));
    if (stat.isDirectory()) {
      console.log(chalk.red('\n The folder already exists! Please change the project name! \n '))
      return;
    };
  } catch (err) {}
  // 生成炫酷文字
  const text = figlet.textSync('da cli', {
    font: 'isometric1',
    horizontalLayout: 'default',
    verticalLayout: 'default',
    width: 80,
    whitespaceBreak: true
  })
  console.log(chalk.green(text));
  const spinner = ora("Generating...");
  inquirer.prompt(questions).then(answers => {
    console.log(chalk.white('\n Start generating... \n'))
    spinner.start()
    let { frame, description, version } = answers;
    const url = frames[frame].url;

    download(
      url,
      projectName,
      {
        clone: url.startsWith('direct:') ? true : false,
      },
      err => {
        if (err) {
          spinner.fail()
          console.log(chalk.red(`Generation failed. ${err}`))
          return
        }
        renderFiles(projectName, { projectName, description, version }).then(() => {
          spinner.succeed()
          console.log(chalk.cyan('\n Generation completed!'))
          console.log(chalk.cyan('\n To get started'))
          console.log(chalk.cyan(`\n    cd ${projectName}`))
          console.log(chalk.cyan(`\n    npm install \n`))
        }).catch(() => {
          process.exit()
        })
      }
    )
  })
}
module.exports = createPro

编写da-cli/bin/index.js

...
const createPro = require('../lib/create/create')

...
// generate an official project
program
  .command('create')
  .arguments('<projectName>')
  .description('generate an official project', {
    projectName: 'folder that does not exist'
  })
  .alias('c')
  .action((projectName) => {
    createPro(projectName)
  })
...
16. 最后增加帮助附加

编写da-cli/bin/index.js

...
program.addHelpText('after', `

Example call:
  $ da --help
  $ da -version
  $ da update
  $ da add
  $ da delete
  $ da list
  $ da create
  $ da site add
  $ da site delete
  $ da site list`);
...

完结

以上是整个项目的实现步骤,代码中有少量注释,没有具体详细讲解,如果有什么不懂得欢迎关注公众号询问。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值