2024年前端提效项目盘点之:前端脚手架

1,478 阅读5分钟

前言

前端脚手架项目其实在掘金网上已经被写得很多了,脚手架项目实现起来确实也比较简单。但是作为我的低代码提效工具中不可缺少的一部分,还是用一篇文档进行总结,看完此文便可以了解一个简单的脚手架如何编写。

一句话总结:脚手架就是获取用户指令下载指定模板到指定位置。

1. 为什么需要脚手架?

和我们经常使用的vue-cli等脚手架工具一样,脚手架本质就是把一套模板按通过终端指令下载到你本地,从而能把前端的一些基础建设如,代码风格检测、接口封装,代码编写模式,以及打包配置等直接一键生成。减少重复工作,建立团队中统一的开发模式。

2. 我为什么需要一个脚手架?

由于在我们前端开发团队中遵循mvp模式的开发结构。同时接口封装,代码风格检测 以及最重要的低代码插件配置和模板等需要固定下来的。于是我就需要一个脚手架工具去减少这些重复的工作。

3. 怎么去实现一个脚手架?

3.1 我们需要实现什么功能?

  • 获取要下载的项目列表
  • 用户选择具体要下载的项目模板
  • 下载项目到指定路径
  • 提示成功的交互

3.2 声明自定义指令

如果你用过vue-lci 那么你肯定知道输入 vue create myapp 命令来创建一个 Vue 工程。

但是如果我们没有运行 npm install -g vue-cli 安装 vue-cli 脚手架,而直接在命令行窗口中直接运行 vue create myapp,会报错。

所以类比vue-cli 我们要实现一个自定义指令,并在全局去注册。

  • 执行npm init ,创建初始项目,生成package.json
  • 在生成的package.json里面配置指令:"bin": "bin/teteCli.js"
{
  "name": "tete-cli",
  "version": "1.0.0",
  "description": "a cli for lowlow project",
  "main": "bin/teteCli.js", 
  "bin": "bin/teteCli.js",//主要这里
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "aleeeeex",
  "license": "ISC",
  "dependencies": {
    "axios": "^1.7.7",
    "chalk": "^4.1.0",
    "commander": "^10.0.0",
    "download-git-repo": "^3.0.2",
    "fs-extra": "^11.2.0",
    "inquirer": "^8.2.4",
    "ora": "^5.1.0",
    "path": "^0.12.7"
  }
}

3.3 创建bin文件夹及入口文件

在根目录创建bin文件夹,并且在bin文件夹中新建一个teteCli.js文件,和package.json里面的mian字段对应。 这是脚手架的入口文件。(注意在头部要加这一行:#!/usr/bin/env node,否则报错)

#! /usr/bin/env node

const program = require('commander')
program
  .command('create <app-name>')
  .description('create a new project')
  // -f or --force 为强制创建,如果创建的目录存在则直接覆盖
  .option('-f, --force', 'overwrite target directory if it exist')
  .action((name, options) => {
    // 打印执行结果
    console.log('项目名称', name)
    require('../lib/create')(name, options)
  })

// 解析用户执行命令传入参数
program.parse(process.argv)

到此一个基本的脚手架命令就生成了,如果要本地调试,那么执行npm link后输入 tete-cli create xxx便可以成功调用指令了。

3.4 具体功能实现

如tete-cli.js,我们通过commander包来调用program.command去注册指令。指令在执行的时候将会调用require('../lib/create')(name, options) 也就是create.js的内容

//create.js
const path = require('path') 
const fs = require('fs-extra')
const inquirer = require('inquirer') //和用户交互用
const Generator = require('./generator')

module.exports = async function (name, options) {
  // 判断项目是否存在
  const cwd = process.cwd() //当前命令行所在的路径
  const targetAir = path.join(cwd, name)
  // 目录是否存在
  if (fs.existsSync(targetAir)) {
    // 是否为强制创建
    if (options.force) {
      await fs.remove(targetAir)
    } else {
      // 询问用户是否确定要覆盖
      let { action } = await inquirer.prompt([
        {
          name: 'action',
          type: 'list',
          message: 'Target directory already exists',
          choices: [
            {
              name: 'Overwrite',
              value: 'overwrite'
            },
            {
              name: 'Cancel',
              value: false
            }
          ]
        }
      ])
      // 如果用户拒绝覆盖则停止生于操作
      if (!action) {
        return
      } else if (action === 'overwrite') {
        await fs.remove(targetAir)
      }
    }
  }

  // 新建模板
  const generator = new Generator(name, targetAir)
  generator.create()
}

直接看到最后一句:generator.create() 核心是这个方法,我们在这个地方去生成我们的代码模板。

少废话,先看代码,generator.js全部代码如下:

//generator.js
const inquirer = require('inquirer')
const util = require('util')
const downloadGitRepo = require('download-git-repo')
const path = require('path')
const chalk = require('chalk')
const ora = require('ora')
const { getRepoList, getTagList } = require('./http')

// 封装loading外壳
async function wrapLoading(fn, message, ...args) {
  const spinner = ora(message)

  spinner.start()

  try {
    const result = await fn(...args)

    spinner.succeed()
    return result
  } catch (error) {
    spinner.fail('Requiest failed, please refetch...')
  }
}

class Generator {
  constructor(name, targetDir) {
    this.name = name
    this.targetDir = targetDir
    this.downloadGitRepo = util.promisify(downloadGitRepo)
  }

  // 获取用户选择的模版
  // 1. 从远端拉模版数据
  // 2. 用户去选择自己已有下载的模版名称
  // 3. 返回用户选择的模版
  async getRepo() {
    const repoList = await wrapLoading(
      getRepoList,
      'waiting for fetch template'
    )

    if (!repoList) return

    const repos = repoList.map((item) => item.name)

    // 让用户去选择自己新下载的模版名称
    const { repo } = await inquirer.prompt({
      name: 'repo',
      type: 'list',
      choices: repos,
      message: 'Please choose a template to create project'
    })

    return repo
  }

  // 获取用户选择的版本
  // 1. 基于repo的结果,远程拉版本列表
  // 2. 自动选择最新的tag
  async getTag(repo) {
    const tags = await wrapLoading(getTagList, 'waiting for fetch tag', repo)
    if (!tags) return

    const tagsList = tags.map((item) => item.name)

    return tagsList[0]
  }

  // 下载远程模版
  // 1. 拼接下载地址
  // 2. 调用下载方法
  async download(repo, tag) {
    const requestUrl = `FEcourseZone/${repo}${tag ? '#' + tag : ''}`

    await wrapLoading(
      this.downloadGitRepo,
      'waiting download template',
      requestUrl,
      path.resolve(process.cwd(), this.targetDir)
    )
  }

  // 核心创建逻辑
  async create() {
    // 1. 获取模版名称
    const repo = await this.getRepo()

    // 2. 获取tag名称
    const tag = await this.getTag(repo)

    // 3. 下载模版到目录
    await this.download(repo, tag)

    console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)
  }
}

module.exports = Generator

generate.create主要做了几件事:

  // 1. 获取模版名称
    const repo = await this.getRepo()

    // 2. 获取tag名称
    const tag = await this.getTag(repo)

    // 3. 下载模版到目录
    await this.download(repo, tag)

    console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)
  }

getRepo: 获取远端代码模板列表,并通过inquirer.prompt函数去和用户进行交互,当用户选择某个模板,获得用户选择要下载的模板名称repo并返回。

getTag:获取用户选择的版本,基于repo的结果,远程拉版本列表,自动选择最新的tag返回

download:拼接下载地址,和 通过path.resolve(process.cwd(), this.targetDir)获取到要下载到的目标文件夹(process.cwd()是代表当前命令执行的绝对路径),然后把下载地址,和要下载到的目标文件夹地址作为参数传递给wrapLoading函数

wrapLoading:使用downloadGitRepo 包来负责模板的下载。

最后,使用chalk来在终端提示生成成功的文案。

chalk 是一个用于在终端中美化输出文本的 npm 包,允许开发者通过简单的 API 添加颜色、样式和背景色等效果到终端文本中。

项目地址:github.com/aleeeeexx/t…

把里面的模板进行替换就是你自己的脚手架。

如果有帮助,请点赞收藏,非常感谢。