搭建入门级脚手架

748 阅读7分钟

前言

什么是脚手架?

     关于脚手架的定义,我推荐看码农王小胖的文章《前端工程化系列之闲谈脚手架》,由浅入深,从多维度总结了脚手架的定义。在这里,我就直接引用他的定义:“快速且可配置化的,避免重复工作,统一技术规范化的构建项目初始化文件结构的工具。”

为什么要使用脚手架?

     为什么我们要使用脚手架,它可以给我们带来什么好处,并解决什么痛点问题呢?

  • 降低项目配置成本
  • 统一项目规范,为构建可复用的物料库奠定基础
  • 降低开发人员的沟通和学习成本
  • 降低项目开发和维护成本
  • 降低新技术选型风险

第三方依赖

脚手架搭建

创建项目

     先创建项目后执行yarn init 或 yarn init -y 完成项目初始化,然后安装上述的第三方依赖工具,执行 yarn add commander download-git-repo chalk Inquirer ora copy-templete-dir

创建命令入口

     创建bin文件,作为可执行命令的入口文件。
     文件开头#!/usr/bin/env node是必要的,这种用法是为了防止操作系统用户没有将node装在默认的/usr/bin路径里。当系统看到这一行的时候,首先会到env设置里查找node的安装路径,再调用对应路径下的解释器程序完成操作。

#!/usr/bin/env node
const program = require('commander')
const packageJson = require('../package.json')

// 获取版本
program
  .version(packageJson.version)
  .usage('<command> [options]')

// 初始化项目命令
program
  .command('create')
  .description('create a new project')
  .action(() => {
    require('../lib/create')()
  })

// 解析传入的命令参数
program.parse(process.argv)

// 输出帮助信息
if (program.args.length === 0) {
  program.help();
}  

     以上的命令入口文件中创建了create初始化项目命令,命令执行后会在lib目录下找对应的create文件,下面就来详细说说create文件中的内容。

初始化项目

     创建 ../lib/create文件,用于初始化项目,包含命令交互,下载模版,写入模版等操作。其中,下载模版尝试了远程下载和本地复制两种方式,远程下载先通过download-git-repo包下载模版,再通过fs包读写模版,将用户配置选项写入模版中,最后完成项目初始化;本地复制方式是通过copy-template-dir包复制模版,并将用户配置写入其中,完成初始化,但是该方法有个坑点,favicon.ico 文件复制后不能展示出来,所以该方法只是一个参考啦,可以忽略~

const {prompt} = require('inquirer')
const copy = require('copy-template-dir')
const path = require('path')
const {exec} = require('child_process')
const download = require('download-git-repo')
const templetes = require('../templetes/templete-config.json')
let ora = require('ora')
let fs = require('fs')
let chalk = require('chalk')
module.exports = async () => {
  // 命令交互,获取用户配置
  let defaultName = process.argv.slice(2)[1] || 'vue-demo'
  let question = [
    {
      type: 'input',
      name: 'projectName',
      message: 'project name:',
      default: defaultName,
      filter(val) {
        return val.trim()
      },
      transformer(val) {
        return val
      }
    },
    {
      type: 'input',
      name: 'description',
      message: 'description:',
      default: 'new project'
    },
    {
      type: 'list',
      name: 'templeteWay',
      message: 'the way of get templtet:',
      choices: ['remote', 'local'],
      default: 'remote',
    },
    {
      type: 'list',
      name: 'templete',
      message: 'templete:',
      choices: ['vue', 'react'],
      default: 'vue',
      when(val) {
        if (val.templeteWay === 'remote') {
          return true
        }
      }
    }
  ]
  prompt(question).then(({projectName, description, templeteWay, templete}) => {
    let inDir = path.join(__dirname, '../templetes', 'vue-templete')
    let outDir = path.join(process.cwd(), projectName)
    let vars = {
      projectName,
      description
    }
    let spinner = ora('Downloading please wait....')
    spinner.start()
    if (templeteWay === 'local') {
      // 本地拷贝模版
      copy(inDir, outDir, vars, (err, createdFiles) => {
        if (err) {
          process.exit(1)
        } else {
          exec(`cd ${projectName} && yarn`, err => {
            if (err) process.exit(1)
            spinner.stop()
            console.log(chalk.green('project create successfully!'))
            console.log(`
              ${chalk.yellow(`cd ${projectName}`)}
              ${chalk.yellow(templete === 'react' ? 'yarn start' : 'yarn serve')}
            `)
          })
        }
      })
    } else {
      // 远程下载模版
      let repo = templetes[templete].repo
      let branch = templetes[templete].branch
      download(`${repo}${branch}`, outDir, function(err) {
        if (err) {
          console.log('download err', err)
          spinner.stop()
        } else {
          fs.readFile(path.join(outDir, 'package.json'), function(err, res) {
            if (err) {
              console.log('readFile err', err)
              spinner.stop()
            } else {
              let data = JSON.parse(res.toString())
              data.name = projectName
              data.description = description
              fs.writeFile(path.join(outDir, 'package.json'), JSON.stringify(data, null, 2), function(err) {
                if (err) {
                  console.log('writeFile err', err)
                }
                spinner.stop() 
                console.log(chalk.green('project create successfully!'))
                console.log(`
                  ${chalk.yellow(`cd ${projectName}`)}
                  ${chalk.yellow(`yarn`)}
                  ${chalk.yellow(templete === 'react' ? 'yarn start' : 'yarn serve')}
                `)
              })
            }
          })
        }
      })
    }
  })
}

命令行交互 prompt

  • question 数组为交互命令配置,数组中每一个对象都对应一个执行命令时候的一个问题
  • type 为该提问的类型,包含input,list,checkbox,password等
  • name 为该问题的值,表示用户输入答案
  • message 为显示的问题
  • default 则为用户没输入时的默认为其提供一个答案
  • validate 方法可以校验用户输入的内容,返回true时校验通过,若不正确可以返回对应的字符串提示文案
  • transformer 为用户输入问题答案后将对应的答案展示到问题位置,需要有返回值,返回到字符串为展示内容

下载模版 download

  • 第一个参数为下载模版的仓库地址,可以表示为 'Janehuhuhu/mini-cli#vue-project',#后面表示分支,也可以写为 'direct:github.com/Janehuhuhu/…'
  • 第二个参数表示模版要下载的位置
  • 第三个参数如果不写,表示用http的方式下载代码。也可以通过设置参数{clone: true} 表示使用git clone的方式下载代码,虽然这可能会慢一些,但是如果设置了适当的SSH密钥,它允许使用私有存储库。这里要说明一点,使用SSH可以生成一个公钥-私钥对,我们会把公钥添加到Git的服务器,把私钥放在本地。提交文件的时候Git服务器会用公钥和客户端提交私钥做验证,如果验证通过则提交成功。
  • 第四个参数表示回调函数

拷贝模版 copy

  • 第一个参数表示模版存放位置
  • 第二个参数表示模版要下载的位置
  • 第三个参数可以替换模版中的{{}}变量信息 注意:_package.json,开头的文件通过拷贝后会自动删除

package.json下bin字段配置

     bin配置内部命令对应的可执行文件的位置,配置命令后,npm会找到对应的可执行文件,并建立对应的符号链接。
     关于bin属性,参考文章做以下说明:假如你发布了一个npm包,其中带有执行脚本,你希望用户安装你的npm包的时候,把可执行的脚本文件也安装下来,就会用到这个bin字段。如果在项目中局部安装该npm包后,npm会在项目中的node_modules/.bin目录下创建一条符号链接,点击这个文件,就会链接到bin字段中定义的文件。如果是全局安装,npm会在环境变量路径/usr/local/bin目录下(MAC)创建一个symbolic,指向bin字段中声明的文件,这样在当前用户任意目录下,都可以使用bin属性中定义的命令了。

  "bin": {
    "mini-cli": "./bin/index.js"
  }   

本地测试

     有两种本地测试的方式,第一种直接使用node命令,node ./bin/index.js create [项目名称]。第二种方式采用yarn link,在mini-cli项目中输入yarn link,在其它命令窗口,cd [项目目录],输入yarn link mini-cli,即可链接mini-cli包,输入mini-cli create [项目名称] 即可初始化项目。

坑点

  1. 使用download-git-repo时设置了{clone: true},却报错 git clone failed with status 128?
    答:因为这种写法是通过ssh的方式克隆项目,需要将本机的公钥加入github。
  2. 执行 mini-cli create 命令报错 Permission Denied(Mac)?
    答:文件没有可执行权限,执行命令chmod a+x [文件位置]

具体mini-cli脚手架代码可以看我的 github地址

参考文档