美文网首页
手写脚手架

手写脚手架

作者: A_走在冷风中 | 来源:发表于2022-04-14 20:48 被阅读0次

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}` })

相关文章

网友评论

      本文标题:手写脚手架

      本文链接:https://www.haomeiwen.com/subject/odiqertx.html