如何搭建一款简易脚手架

什么是脚手架?

简而言之它就是一个工具,方便我们新建项目用的,通过这个工具创建的项目之后我们可以直接开发了。

市面常见的脚手架?

  1. vue-cli 提供vue开发的webpack,pwa等模板
  2. create-react-app React团队官方出的一个构建React单页面应用的脚手架工具
  3. Yeoman 通用型脚手架,过于通用,不够专注,使用麻烦

为什么要自己搭建?

  1. 专心快速的完成业务
  2. 代码更加规范化
  3. 少造轮子,少拷贝代码,简化流程

如何搭建一款简易脚手架?

一、目录搭建

  1. 创建一个文件夹,取名为lang-cli
  2. 在该目录下执行 npm init -y,就会生成一个package.json,在packjson中写入以下依赖并执行npm install安装。
"dependencies": {
    "axios": "^0.24.0",
    "chalk": "^4.1.2",
    "commander": "^8.3.0",
    "download-git-repo": "^3.0.2",
    "fs-extra": "^10.0.0",
    "inquirer": "^8.2.0",
    "ora": "^5.4.1"
  }
  1. 新建一个bin文件夹,并在bin目录下新建一个无后缀的文件,取名为lang(这个文件将作为我们整个脚手架的入口文件, 用node ./bin/lang也可以运行),并写入以下内容,这个语句的意思就是为了让系统看到这一行的时候,沿着该路径去查找node并执行。
#! /usr/bin/env node
console.log('hello lang')
  1. 由于一直在本地用node ./bin/lang运行起来很麻烦,所以可以挂载到全局。在package.json中加入如下一行,在根目录下执行npm link,之后每次输入lang,就可以直接运行了。
{
  "name": "lang",
  "bin": "./bin/lang", // 默认取的name的名字
}
{
  "name": "lang",
  "bin": {
      'lang-cli': './bin/lang'
  }
}

二、编写具体指令(配置可执行命令)

commander用来编写指令和处理命令行的一个工具。chalk 是用来修改控制台输出内容样式的,比如颜色啊,具体用法如下:

const program = require("commander")
const chalk = require("chalk")
// 定义当前版本
// 定义使用方法
program
  .version(`lang-cli@${require("../package.json").version}`)
  .usage(' [option]')

// 定义指令  create
program
  .command('create ')
  .description('create a new project')
  .option('-f,--force', 'overwrite target directory if it exsit')
  .action((name, cmd) => {
      console.log(name, cmd)
      //每个功能放单独模块中写,调用create模块去创建
      require('../lib/create.js')(name, cmd)
  })
 
//vue config set/get 等指令 这里就不详述了

// 监听 "--help命令输入"
program
  .on('--help', function () {
    console.log()
    console.log(`Run ${chalk.cyan('lang  --help')} show details`)
    console.log()
  })
// 解析命令行参数
program.parse(process.argv)

三、编写创建逻辑

  1. 创建一个lib文件夹,在此文件中创建一个create.js,具体代码如下:
const path = require('path')
const fs = require('fs-extra')
const inquirer = require('inquirer')
const Creator = require('./Creator')
module.exports = async function (projectName, option) {
  // 创建项目
  const cwd = process.cwd(); // 获取当前命令执行时的工作目录
  const targetDir = path.join(cwd, projectName) // 目标目录

  if (fs.existsSync(targetDir)) {
    if (option.force) {//如果强制创建,删除已有的
      await fs.remove(targetDir)
    } else {
      // 提示用户是否要覆盖
      let { action } = await inquirer.prompt([ //配置询问的方式
        {
          name: 'action',
          type: 'list', //类型各种
          message: 'Target directory already exists Pick an action: ',
          choices: [
            { name: 'overwrite', value: 'overwrite' },
            { name: 'cancel', value: false },
          ]
        }
      ])
      if (!action) return;
      if (action === 'overwrite') {
        console.log('removing.....')
        await fs.remove(targetDir)
      }
    }
  }

  // 创建项目(单独提出来一个类去做)
  const creator = new Creator(projectName, targetDir)
  creator.create()
}

Creator.js类中的代码如下:

const { fetchRepoList, fetchTagList } = require('./request')
const { wrapLoading } = require('./util')
const inquirer = require('inquirer')
const downloadGitRepo = require('download-git-repo') //不支持promise
const util = require('util') //node自带的
const path = require('path') //node自带的
class Creator {
  constructor(projectName, targetDir) {
    this.name = projectName;
    this.target = targetDir;
    this.downloadGitRepo = util.promisify(downloadGitRepo) //转换成promise方法
  }
  async fetchRepo() {
    // 失败要重新拉取(网慢的话)
    let repos = await wrapLoading(fetchRepoList, 'waiting fetch template')
    if (!repos) return;
    repos = repos.map(i => i.name)
    let { repo } = await inquirer.prompt({
      name: 'repo',
      type: 'list',
      message: 'please choose a template to create project',
      choices: repos,
    })
    return repo;
  }
  async fetchTag(repo) {
    let tags = await wrapLoading(fetchTagList, 'waiting fetch tag', repo)
    if (!tags) return;
    tags = tags.map(i => i.name)
    let { tag } = await inquirer.prompt({
      name: 'tag',
      type: 'list',
      message: 'please choose a tag to create project',
      choices: tags,
    })
    return tag;
  }
  async download(repo, tag) {
    // 1.拼接出下载路径  https://github.com/wave1994
    let requestUrl = `wave1994/${repo}${tag ? '#' + tag : ''}`
    // 2.把资源下载到某个路径上
    // await this.downloadGitRepo(requestUrl, this.target)
    await this.downloadGitRepo(
        requestUrl, 
        path.resolve(process.cwd(), `${repo}@${tag}`)
    )
  }
  async create() {
    //开始创建
    // 1. 采用远程拉取的方式
    // 1). 先去拉取当前组织下的模板
    let repo = await this.fetchRepo()
    // 2). 再通过模板找到版本号
    let tag = await this.fetchTag(repo)
    // 3). 下载
    let downloadUrl = await this.download(repo, tag)
  }
}
module.exports = Creator

request.js中代码:

// 通过axios获取结果
const axios = require('axios')
axios.interceptors.response.use(res => res.data)

async function fetchRepoList() {
  return axios.get('https://api.github.com/users/wave1994/repos')
}

async function fetchTagList(repo) {
  return axios.get(`https://api.github.com/repos/wave1994/${repo}/tags`)
}
module.exports = {
  fetchRepoList,
  fetchTagList
}

util.js文件中代码:

const ora = require('ora')

async function sleep(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(), n);
  })
}
// 等待loading函数
async function wrapLoading(fn, msg, ...args) {
  const spinner = ora(msg)
  spinner.start(); // 开始加载
  try {
    let repos = await fn(...args);
    spinner.succeed() //成功
    return repos;
  } catch (error) {
    spinner.fail('fetch failed, refetch....')
    await sleep(1000)
    return wrapLoading(fn, msg, ...args)
  }

}
module.exports = {
  wrapLoading
}

四、执行命令

lang create app-name

总结

脚手架是前端工程化领域的基本项,个人认为掌握前端脚手架的开发是十分重要的,本文旨在提供一个大概思路及样板,目前只包含了命令行、模板拉取,相对于成熟的脚手架如vue-cli、create-react-app等来说,还有很多很多工作要做,包括本地服务、打包构建、集成部署、周边其他等都还需要完善,想要在工程化领域有所建树的同学,不妨在这几个方面多下下功夫。

参考文档:https://vleedesigntheory.github.io/tech/front/cli20200701.html#%E5%89%8D%E8%A8%80

你可能感兴趣的:(如何搭建一款简易脚手架)