如何搭建一款简易脚手架

什么是脚手架?

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

市面常见的脚手架?

  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('<command> [option]')

// 定义指令  create
program
  .command('create <app-name>')
  .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 <command> --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

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