手写脚手架

1 初始化命令

新建工具包初始化自定义命令执行 npm link 设置为全局使用

  1. 新建bin目录,创建 cli.js 文件
  2. 在 cli.js 文件内部设置 #! /usr/bin/env node 指令
  3. 终端切到当前工具包目录,执行 npm link 创建全局链接

2 commander使用

使用 commander 处理自定义帮助命令

  1. 执行 npm i commander -D 安装自定义命令
  2. 按语法设置可选 options 与 自定义命令
const { program } = require('commander')

// 新增自定义的可选属性
program.option('-f --framework <framework>', 'select your framework')
program.option('-d --dest <dest>', 'a destination folder')

// 使用 commander 格式化 argv
program.version(require('../package.json').version).parse(process.argv)

处理帮助信息

// 处理帮助信息
const examples = {
  create: ['sli create|crt <project>'],
  config: [
    'sli config|cfg set <k> <v>',
    'sli config|cfg get <k>'
  ]
}
program.on('--help', () => {
  console.log('Examples: ')
  Object.keys(examples).forEach(function (actionName) {
    examples[actionName].forEach((item) => {
      console.log(' ' + item)
    })
  })
})

自定义命令

program
  .command('create <project> [others...]')
  .alias('crt')
  .description('创建新项目')
  .action((name, args) => {
    console.log(name + '执行了')
    console.log(args)
  })

抽离help函数

  1. 将 --help 需要做的事情抽离到单独的文件当中
  2. 将对应的函数导出供外部进行调用

抽离自定义命令


const {
  createActions
} = require('./actions')

const myCommand = function (program) {
  //? 01 创建项目命令
  program
    .command('create <project> [others...]')
    .alias('crt')
    .description('创建新项目')
    .action(createActions)

  //? 02 配置项目命令
  program
    .command('config <set|get> [others...]')
    .alias('cfg')
    .description('配置项目')
    .action((name, args) => {
      console.log(name + '执行了')
      console.log(args)
    })
}

module.exports = myCommand

3 其它工具使用

chalk 使用

const chalk = require('chalk')

//? 文字颜色
console.log(chalk.green('绿色'))
console.log(chalk.keyword('red')('前端开发'))
console.log(chalk.hex('#fff')('前端开发'))

//? 背景颜色
console.log(chalk.bgGray('带背景'))

//? 格式化输出
console.log(chalk.green.bold`
  {red 从前慢}
  没有前端开发
`)

/**
 * 使用 chalk 包可以修改命令行终端字体的颜色
 * + 提供的关键字设置颜色:green red orange 等等
 * + 提供 keyword 方法接收键字
 * + 提供 hex 方法接收16进制
 * + 可以设置背景颜色
 * + 格式化输出文字内容
 */

ora 使用

import ora from 'ora'

const spinner = ora('正在下载......').start()
spinner.color = 'green'
// spinner.text = 'Loading rainbows'

setTimeout(() => {
  // spinner.succeed('下载成功')
  // spinner.fail('下载失败')
  spinner.info('下载内容')
}, 2000)

/**
 * 当前版本只支持 es6Module,可以将 package.json 文件中添加 type:module 字段
 * ora 实例化对象然后调用 start 
 * color设置文字颜色
 * text 设置文字内容
 * 
 * succeed 成功回调
 * fail 失败回调
 * info
 * ......
 */

inquirer 使用

基本使用

const inquirer = require('inquirer')

//? 定义问题
let quesList = [
  {
    type: 'input',
    name: 'username',
    message: '用户名',
    validate(an) {
      if (!an) {
        return '当前为必填项'
      } else {
        return true
      }
    }
  }
]

//? 获取结果
inquirer.prompt(quesList).then((an) => {
  console.log(an.username)
})
/**
 * 基础字段
 * + type 定义问题类型
 * + name 将来问题的答案会被保存在一个对象当中,这里定义的值就是它的键名
 * + message 设置问题的提示信息
 * + default 设置默认值
 * + validate 函数,接收参数为当前问题的答案,可以添加判断的条件来决定后续走向
 */

递进问题

const inquirer = require('inquirer')

const quesList = [
  {
    type: 'confirm',
    name: 'isLoad',
    message: '是否执行下载'
  },
  {
    type: 'list',
    name: 'method',
    message: '选择下载方式',
    choices: ['npm', 'cnpm', 'yarn'],
    when(preAn) {
      return preAn.isLoad
    }
  }
]

inquirer.prompt(quesList).then((an) => {
  console.log(an)
})

/**
 * confirm 询问型问题,返回 true 或者 false
 * choices 接收一个数组,给问题提供选项
 * when 接收一个参数是一个问题的答案,可用于判断当前问题是否显示
 */

总结

const inquirer = require('inquirer')

// 准备问题
const quesList = [
  {
    type: 'checkbox',
    name: 'feature',
    message: '选择基础功能',
    pageSize: 2,
    choices: ['webpack', 'webpack-cli', 'eslint', 'jest', 'zoe', 'vueRouter', 'React']
  }
]

// 处理问题
inquirer.prompt(quesList).then((an) => {
  console.log(an.feature)
})

/**
 * 问题属性:
 *  + type:input list confirm checkbox
 *  + name: 用于做为答案的链出现
 *  + message: 用于问题的提示信息
 *  + choices: 出现选项时,设置为列表选项
 *  + pageSize: 设置每页显示的问题数量
 * 常见方法:
 *  + validate 校验
 *  + when 判断
 */

4 功能实现

资料

获取组织仓库列表信息
https://api.github.com/orgs/lagoufed/repos

获取个人仓库列表信息
https://api.github.com/users/zcegg/repos

获取指定仓库版本号
https://api.github.com/repos/zcegg/create-nm/tags

查询访问次数

curl -i https://api.github.com/users/octocat

headers={"Authorization":"token "+"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"}


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

步骤分析

已完成:命令、交互工具、业务参数
未完成:
查询远端模板列表(添加交互)
查询选中模板下是否存在多个版本
下载指定模板指定版本
将模板缓存在指定位置
渲染数据写入到指定的目录

请求次数限制

  //! 发送请求
  let ret = await axios('https://api.github.com/users/zcegg/repos')
  console.log(ret.data)

解决次数限制

//! 发送请求
  const headers = { "Authorization": "token " + "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" }
  let { data } = await axios({
    method: 'get',
    url: 'https://api.github.com/users/zcegg/repos',
    headers: headers
  })
  const repos = data.map(item => item.name)
  console.log(repos)

查询模板信息

const createActions = async function (project) {
  //? 定义请求头信息
  const headers = { "Authorization": "token " + "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" }

  var { data } = await axios({
    method: 'get',
    url: 'https://api.github.com/users/zcegg/repos',
    headers: headers
  })
  const repos = data.map(item => item.name)

  //? 01准备问题
  const quesList = [
    {
      type: 'list',
      name: 'tmpRepo',
      message: '选择目标模板',
      choices: repos
    }
  ]
  //? 02 处理问题
  const { tmpRepo } = await inquirer.prompt(quesList)

  //? 03 查询选中模板信息
  var { data } = await axios({
    method: 'get',
    url: 'https://api.github.com/repos/zcegg/create-nm/tags',
    headers: headers
  })

  const tags = data.map(item => item.name)
  console.log(tags, '<----')

}

提取查询方法

const fetchInfo = async function (repoName, tmpName) {
  //? 定义token
  const token = "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"
  const url1 = `https://api.github.com/users/${repoName}/repos`
  const url2 = `https://api.github.com/repos/${repoName}/${tmpName}/tags`
  const headers = { "Authorization": "token " + token }
  const url = !tmpName ? url1 : url2
  let { data } = await axios({
    method: 'get',
    url: url,
    headers: headers
  })
  return data.map(item => item.name)
}

添加耗时及柯理化

//! 工具方法之添加耗时
const addLoading = function (fn) {
  return async function (...args) {
    const spinner = ora('正在查询').start()
    const ret = await fn(...args)
    spinner.succeed('查询成功')
    return ret
  }
}

// const repos = await addLoading(fetchInfo)('zcegg')

版本逻辑处理

let loadUrl = null
  if (tags.length) {
    // 处理版本
    const quesTag = [
      {
        type: 'list',
        name: 'tmpTag',
        message: '选择指定版本',
        choices: tags
      }
    ]
    const { tmpTag } = await inquirer.prompt(quesTag)
  } else {
    console.log('直接执行下载')
  }

处理缓存路径

const toUnixPath = require('../utils/toUnixPath')
// console.log(process.env) // 查询环境变量
// console.log(process.platform) // 查询当平台关键字

console.log(process.env['USERPROFILE'])

console.log(toUnixPath(`${process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME']}` + '/.tmp'))

初始化下载函数

//! 工具方法之下载操作
const downLoadRepo = function (repo, tag) {
  //? 定义缓存目录
  const cacheDir = toUnixPath(`${process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME']}` + '/.tmp')
  //? 处理参数
  let api = `zcegg/${repo}`
  if (tag) api += `#/${tag}`
  console.log(cacheDir)
  console.log(api)
}

实现下载操作

  //* 导出下载函数
  let downloadFn = require('download-git-repo')
  downloadFn = promisify(downloadFn)

  //? 定义缓存路径
  const cacheDir = toUnixPath(`${process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME']}` + '/.tmp')
  //? 处理参数
  let api = `zcegg/${repo}`
  if (tag) api += `#/${tag}`
  //? 定义缓存目录
  const dest = tag ? path.resolve(cacheDir, repo, tag) : path.resolve(cacheDir, repo)
  //? 执行下载操作
  const spinner = ora('正在下载......').start()
  await downloadFn(api, dest)
  spinner.succeed('下载成功')

使用缓存

  //? 执行下载操作(判断缓存中是否存)
  if (!fs.existsSync(dest)) {
    //* 缓存目录中不存在则直接下载
    const spinner = ora('正在下载......').start()
    await downloadFn(api, dest)
    spinner.succeed('下载成功')
  }
  //? 返回地址用于数据读取
  return dest

无数据渲染

//? 05 下载完成之后判断是否需要渲染数据,从而生成本地的项目
  // 如果需要渲染则在 package.json 中使用 ejs 语法,同时提前通过 qus.js 来准备问题
  if (fs.existsSync(path.join(dest, 'que.js'))) {
    console.log('需要渲染')
  } else {
    // 不需要渲染就直接拷贝
    ncp(dest, project)
  }

metalsmith使用

  //? 05 下载完成之后判断是否需要渲染数据,从而生成本地的项目
  // 如果需要渲染则在 package.json 中使用 ejs 语法,同时提前通过 qus.js 来准备问题
  if (fs.existsSync(path.join(dest, 'que.js'))) {
    //! 需要数据渲染
    await new Promise((resolve, reject) => {
      MetalSmith(__dirname)
        .source(dest)
        .destination(path.resolve(project))
        .use((files, metal, done) => {
          //* files 是当前目录下所有的文件信息
          //* 
          console.log(files)
          done()
        })
        .build((err) => {
          if (err) {
            reject()
          } else {
            resolve()
          }
        })
    })

  } else {
    // 不需要渲染就直接拷贝
    ncp(dest, project)
  }

设置问题

if (fs.existsSync(path.join(dest, 'que.js'))) {
    //! 需要数据渲染
    await new Promise((resolve, reject) => {
      MetalSmith(__dirname)
        .source(dest)
        .destination(path.resolve(project))
        .use(async (files, metal, done) => {
          const quesList = require(path.join(dest, 'que.js'))
          const answer = await inquirer.prompt(quesList)
          console.log(answer)
          done()
        })
        .build((err) => {
          if (err) {
            reject()
          } else {
            resolve()
          }
        })
    })

  } else {
    // 不需要渲染就直接拷贝
    ncp(dest, project)
  }

问题数据传递

if (fs.existsSync(path.join(dest, 'que.js'))) {
    //! 需要数据渲染
    await new Promise((resolve, reject) => {
      MetalSmith(__dirname)
        .source(dest)
        .destination(path.resolve(project))
        .use(async (files, metal, done) => {
          const quesList = require(path.join(dest, 'que.js'))
          const answer = await inquirer.prompt(quesList)
          //! 当 answer 的答案我们需要在下一个 use 中进行使用
          //! 利用 metal.metadata() 来保存所有的数据,交给下一个 use 进行使用即可
          let meta = metal.metadata()
          Object.assign(meta, answer)

          // 这步操作完成之后,que.js 文件就没有用了,不需要拷贝至项目的目录
          // delete files['que.js']
          done()
        })
        .use((files, metal, done) => {
          // 获取上一个 use 中拿到的用户数据
          let data = metal.metadata()
          console.log(data)
          done()
        })
        .build((err) => {
          if (err) {
            reject()
          } else {
            resolve()
          }
        })
    })

  } else {
    // 不需要渲染就直接拷贝
    ncp(dest, project)
  }

数据渲染

.use((files, metal, done) => {
  // 获取上一个 use 中拿到的用户数据
  let data = metal.metadata()
  //? 找到那些需要渲染数据的具体文件,找到之后将它们的内容转为字符串
  //? 转为字符串之后,接下来就可以针对于字符串进行替换实现渲染
  Reflect.ownKeys(files).forEach(async (file) => {
    if (file.includes('js') || file.includes('json')) {
      let content = files[file].contents.toString()
      if (content.includes("<%")) {
        content = await render(content, data)
        files[file].contents = Buffer.from(content)
      }
    }
  })
  done()

执行 npm

const { spawn } = require('child_process')

// 执行 npm install 
const commandSpawn = (...args) => {
  return new Promise((resolve, reject) => {
    const childProcess = spawn(...args)
    childProcess.stdout.pipe(process.stdout)
    childProcess.stdout.pipe(process.stderr)
    childProcess.on('close', () => {
      resolve()
    })
  })
}

module.exports = {
  commandSpawn
}

//? 06 执行 npm install 
const run_command = process.platform === 'win32' ? 'npm.cmd' : 'npm'
await commandSpawn(run_command, ['install'], { cwd: `./${project}` })

//?07 执行 run serve
commandSpawn(run_command, ['run', 'serve'], { cwd: `./${project}` })
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容