写一个自用的前端脚手架

在工作中我们会用到很多便捷的脚手架工具,比如Vue的vue-cli,React的create-react-app等。极大的提高了我们的工作效率,那么今天我们就来学学怎么制作一款自用的前端脚手架。

核心依赖

项目结构

图片

项目搭建

在一个空文件下执行npm init 将以上依赖全部安装,执行npm install commander ... validate-npm-package-name -S

初始化

在根目录下新建bin/mkimq.js文件,并添加以下代码

#!/usr/bin/env node

// Check node version before requiring/doing anything else
// The user may be on a very old node version

const chalk = require('chalk')
const program = require('commander')

program
  .version(require('../package').version)
  .usage('<command> [options]')

program.parse(process.argv)

if (!process.argv.slice(2).length) {
  program.outputHelp()
}

首先文件第一行表示该文件运行于node环境,接着引入commander。最后的program.parse方法用于解析命令行中传入的参数。

添加第一个指令

command命令有两种用法,官方示例如下:

// Command implemented using action handler (description is supplied separately to `.command`)
// Returns new command for configuring.
program
  .command('clone <source> [destination]')
  .description('clone a repository into a newly created directory')
  .action((source, destination) => {
    console.log('clone command called');
  });

// Command implemented using separate executable file (description is second parameter to `.command`)
// Returns top-level command for adding more commands.
program
  .command('start <service>', 'start named service')
  .command('stop [service]', 'stop named service, or all if no name supplied');

其中参数对应的<>, [ ]分别代表必填和选填。这里我们使用第一种,添加如下代码:

program
  .command('create <app-name>')
  .description('  Create a project with template already created.')
  .action((name, cmd) => {
    require('../lib/create')(name)
  })

添加监听--help事件

// add some useful info on help
program.on('--help', () => {
  console.log()
  console.log(`  Run ${chalk.cyan('mkimq <command> --help')} for detailed usage of given command.`)
  console.log()
})

mkimq create --help

交互说明

在根目录下创建lib文件,并添加create.js文件。

module.exports = async function create (projectName) { }

校验包名

const path = require('path')
const fs = require('fs-extra')

const inquirer = require('inquirer')
const chalk = require('chalk')
const validateProjectName = require('validate-npm-package-name')

module.exports = async function create (projectName) {
  const cwd = process.cwd()
  const targetDir = path.resolve(cwd, projectName)
  const name = path.relative(cwd, projectName)

  const result = validateProjectName(name)
  if (!result.validForNewPackages) {
    console.error(chalk.red(`Invalid project name: "${name}"`))
    result.errors && result.errors.forEach(err => {
      console.error(chalk.red.dim('Error: ' + err))
    })
    result.warnings && result.warnings.forEach(warn => {
      console.error(chalk.red.dim('Warning: ' + warn))
    })
    process.exit(1)
  }

  if (fs.existsSync(targetDir)) {
    const { action } = await inquirer.prompt([
      {
        name: 'action',
        type: 'list',
        message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,
        choices: [
          { name: 'Overwrite', value: 'overwrite' },
          { name: 'Cancel', value: false }
        ]
      }
    ])
    if (!action) {
      return
    } else if (action === 'overwrite') {
      console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)
      await fs.remove(targetDir)
    }
  }

  // ...
}

inquirer.js 处理命令交互

const inquirer = require('inquirer')

module.exports = async function create (projectName) {
 	// ...

  const { bolierplateType, author, description, version } = await inquirer.prompt([
    {
      name: 'bolierplateType',
      type: 'list',
      default: 'vue',
      choices: [
        {
          name: 'Vue',
          value: 'vue'
        },
        {
          name: 'React',
          value: 'react'
        }
      ],
      message: 'Select the boilerplate type.'
    }, {
      type: 'input',
      name: 'description',
      message: 'Please input your project description.',
      default: 'description',
      validate (val) {
        return true
      },
      transformer (val) {
        return val
      }
    }, {
      type: 'input',
      name: 'author',
      message: 'Please input your author name.',
      default: 'author',
      validate (val) {
        return true
      },
      transformer (val) {
        return val
      }
    }, {
      type: 'input',
      name: 'version',
      message: 'Please input your version.',
      default: '0.0.1',
      validate (val) {
        return true
      },
      transformer (val) {
        return val
      }
    }
  ])

  // ...
}

封装下载文件lib/downloadFromRemote.js

const download = require('download-git-repo')

module.exports = function downloadFromRemote (url, name) {
  return new Promise((resolve, reject) => {
    download(`direct:${url}`, name, { clone: true }, function (err) {
      if (err) {
        reject(err)
        return
      }
      resolve()
    })
  })
}

添加下载操作

const fs = require('fs-extra')
const chalk = require('chalk')
const logSymbols = require('log-symbols')
const downloadFromRemote = require('../lib/downloadFromRemote')

module.exports = async function create (projectName) {
  // ...
  downloadFromRemote(remoteUrl, projectName).then(res => {
    fs.readFile(`./${projectName}/package.json`, 'utf8', function (err, data) {
      if (err) {
        spinner.stop()
        console.error(err)
        return
      }
      const packageJson = JSON.parse(data)
      packageJson.name = projectName
      packageJson.description = description
      packageJson.author = author
      packageJson.version = version
      var updatePackageJson = JSON.stringify(packageJson, null, 2)
      fs.writeFile(`./${projectName}/package.json`, updatePackageJson, 'utf8', function (err) {
        spinner.stop()
        if (err) {
          console.error(err)
        } else {
          console.log(logSymbols.success, chalk.green(`Successfully created project template of ${bolierplateType}\n`))
          console.log(`${chalk.grey(`cd ${projectName}`)}\n${chalk.grey('yarn install')}\n${chalk.grey('yarn serve')}\n`)
        }
        process.exit()
      })
    })
  }).catch((err) => {
    console.log(logSymbols.error, err)
    spinner.fail(chalk.red('Sorry, it must be something error,please check it out. \n'))
    process.exit(-1)
  })
}

运行

本项目没有发布到npm上,仅作学习研究之用,可以自己拉取项目然后执行npm link,在本地体验。为了可以全局使用,我们需要在package.json里面设置一下,这样就可以执行luchx命令开头的指令了。

"bin": {
  "mkimq": "bin/mkimq.js"
},

详细代码

bin/mkimq.js
#!/usr/bin/env node

// Check node version before requiring/doing anything else
// The user may be on a very old node version

const chalk = require('chalk')
const semver = require('semver')
const requiredVersion = require('../package.json').engines.node
const didYouMean = require('didyoumean')

// Setting edit distance to 60% of the input string's length
didYouMean.threshold = 0.6

function checkNodeVersion (wanted, id) {
  if (!semver.satisfies(process.version, wanted)) {
    console.log(chalk.red(
      'You are using Node ' + process.version + ', but this version of ' + id +
      ' requires Node ' + wanted + '.\nPlease upgrade your Node version.'
    ))
    process.exit(1)
  }
}

checkNodeVersion(requiredVersion, '@mkimq/cli')

const program = require('commander')

program
  .version(require('../package').version)
  .usage('<command> [options]')

program
  .command('create <app-name>')
  .description('  Create a project with template already created.')
  .action((name, cmd) => {
    require('../lib/create')(name)
  })

// output help information on unknown commands
program
  .arguments('<command>')
  .action((cmd) => {
    program.outputHelp()
    console.log('  ' + chalk.red(`Unknown command ${chalk.yellow(cmd)}.`))
    console.log()
    suggestCommands(cmd)
  })

// add some useful info on help
program.on('--help', () => {
  console.log()
  console.log(`  Run ${chalk.cyan('mkimq <command> --help')} for detailed usage of given command.`)
  console.log()
})

// enhance common error messages
const enhanceErrorMessages = require('../lib/util/enhanceErrorMessages')

enhanceErrorMessages('missingArgument', argName => {
  return `Missing required argument ${chalk.yellow(`<${argName}>`)}.`
})

enhanceErrorMessages('unknownOption', optionName => {
  return `Unknown option ${chalk.yellow(optionName)}.`
})

enhanceErrorMessages('optionMissingArgument', (option, flag) => {
  return `Missing required argument for option ${chalk.yellow(option.flags)}` + (
    flag ? `, got ${chalk.yellow(flag)}` : ''
  )
})

program.parse(process.argv)

if (!process.argv.slice(2).length) {
  program.outputHelp()
}

function suggestCommands (cmd) {
  const availableCommands = program.commands.map(cmd => {
    return cmd._name
  })

  const suggestion = didYouMean(cmd, availableCommands)
  if (suggestion) {
    console.log('  ' + chalk.red(`Did you mean ${chalk.yellow(suggestion)}?`))
  }
}
lib/util/enhanceErrorMessages.js
const program = require('commander')
const chalk = require('chalk')

module.exports = (methodName, log) => {
    program.Command.prototype[methodName] = function (...args) {
        if (methodName === 'unknownOption' && this._allowUnknownOption) {
            return
    }
    this.outputHelp()
    console.log('  ' + chalk.red(log(...args)))
    console.log()
    process.exit(1)
  }
}
lib/util/enum.js
module.exports = {
  vue: 'https://github.com/mankeung/mk-vue.git',
  react: 'https://github.com/mankeung/mk-react.git'
}
lib/create.js
const path = require('path')
const fs = require('fs-extra')

const inquirer = require('inquirer')
const Ora = require('ora')
const chalk = require('chalk')
const logSymbols = require('log-symbols')
const validateProjectName = require('validate-npm-package-name')
const TPL_TYPE = require('../lib/util/enum')
const downloadFromRemote = require('../lib/downloadFromRemote')

module.exports = async function create (projectName) {
  const cwd = process.cwd()
  const targetDir = path.resolve(cwd, projectName)
  const name = path.relative(cwd, projectName)

  const result = validateProjectName(name)
  if (!result.validForNewPackages) {
    console.error(chalk.red(`Invalid project name: "${name}"`))
    result.errors && result.errors.forEach(err => {
      console.error(chalk.red.dim('Error: ' + err))
    })
    result.warnings && result.warnings.forEach(warn => {
      console.error(chalk.red.dim('Warning: ' + warn))
    })
    process.exit(1)
  }

  if (fs.existsSync(targetDir)) {
    const { action } = await inquirer.prompt([
      {
        name: 'action',
        type: 'list',
        message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,
        choices: [
          { name: 'Overwrite', value: 'overwrite' },
          { name: 'Cancel', value: false }
        ]
      }
    ])
    if (!action) {
      return
    } else if (action === 'overwrite') {
      console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)
      await fs.remove(targetDir)
    }
  }

  const { bolierplateType, author, description, version } = await inquirer.prompt([
    {
      name: 'bolierplateType',
      type: 'list',
      default: 'vue',
      choices: [
        {
          name: 'Vue',
          value: 'vue'
        },
        {
          name: 'React',
          value: 'react'
        }
      ],
      message: 'Select the boilerplate type.'
    }, {
      type: 'input',
      name: 'description',
      message: 'Please input your project description.',
      default: 'description',
      validate (val) {
        return true
      },
      transformer (val) {
        return val
      }
    }, {
      type: 'input',
      name: 'author',
      message: 'Please input your author name.',
      default: 'author',
      validate (val) {
        return true
      },
      transformer (val) {
        return val
      }
    }, {
      type: 'input',
      name: 'version',
      message: 'Please input your version.',
      default: '0.0.1',
      validate (val) {
        return true
      },
      transformer (val) {
        return val
      }
    }
  ])

  const remoteUrl = TPL_TYPE[bolierplateType]
  console.log(logSymbols.success, `Creating template of project ${bolierplateType} in ${targetDir}`)
  const spinner = new Ora({
    text: `Download template from ${remoteUrl}\n`
  })

  spinner.start()
  downloadFromRemote(remoteUrl, projectName).then(res => {
    fs.readFile(`./${projectName}/package.json`, 'utf8', function (err, data) {
      if (err) {
        spinner.stop()
        console.error(err)
        return
      }
      const packageJson = JSON.parse(data)
      packageJson.name = projectName
      packageJson.description = description
      packageJson.author = author
      packageJson.version = version
      var updatePackageJson = JSON.stringify(packageJson, null, 2)
      fs.writeFile(`./${projectName}/package.json`, updatePackageJson, 'utf8', function (err) {
        spinner.stop()
        if (err) {
          console.error(err)
        } else {
          console.log(logSymbols.success, chalk.green(`Successfully created project template of ${bolierplateType}\n`))
          console.log(`${chalk.grey(`cd ${projectName}`)}\n${chalk.grey('yarn install')}\n${chalk.grey('yarn serve')}\n`)
        }
        process.exit(1)
      })
    })
  }).catch((err) => {
    console.log(logSymbols.error, err)
    spinner.fail(chalk.red('Sorry, it must be something error,please check it out. \n'))
    process.exit(1)
  })
}
lib/downloadFromRemote.js
const download = require('download-git-repo')

module.exports = function downloadFromRemote (url, name) {
  return new Promise((resolve, reject) => {
    download(`direct:${url}`, name, { clone: true }, function (err) {
      if (err) {
        reject(err)
        return
      }
      resolve()
    })
  })
}
贡献者: mankueng