从零开始搭建前端脚手架(四)-- [命令动态加载及文件功能模块划分与优化]

178 阅读6分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

一、前言

本篇文章主要讲述脚手架的多命令动态加载以及命令模块的划分和文件功能模块的划分与优化。因为随着越来越多的命令需求与交互都放到同一文件夹中,会造成代码的堆积,因此我们要将不同的功能按照相应的业务进行划分。

二、本篇文章的设计思路

  1. 命令行交互的动态加载
  2. 动态命令对应其功能的划分
  3. 命令与交互实现的相结合

三、实现步骤

拆分后的目录结构

在上期文章中,我们新增了添加模板、删除模板、获取模板列表的命令,再加上原有的下载模板命令,我们已经有四个命令了,后期可能还会增加新的命令,所以我们现在将这四个命令拆解出来,做成动态引入的方式。 拆分后的目录结构如图所示

├─bin 
│  ├─index.js # air 全局命令
├─libs
│  ├─command # 各个命令模板
│  │  ├─add.js # 添加模板命令
│  │  ├─create.js # 下载命令
│  │  ├─index.js # 所有命令出口文件
│  │  ├─remove.js # 删除模板命令
│  │  └─list.js # 展示所有模板命令
│  ├─data # 动态数据存储位置
│  │  ├─command.json # 动态命令数据集
│  │  └─record.json # 动态模板数据集
│  └─constant.js # 常量

image.png

常量及动态数据的封装

  1. 在这之前,我们先将常用变量拆分出来,放到libs/constant.js中,代码如下所示
import fs from 'fs-extra'
import path from 'path'
import { fileURLToPath } from 'url'
const __filenameNew = fileURLToPath(import.meta.url)
const __dirnameNew = path.dirname(__filenameNew)

// 根目录
export const rootPath = __dirnameNew.slice(0, __dirnameNew.length - 4)

// 获取package.json文件内容
export const packageJsonData = JSON.parse(fs.readFileSync(rootPath + 'package.json', 'utf8'))

// 获取当前命令列表
export const commandList = JSON.parse(fs.readFileSync(rootPath + 'libs/data/command.json', 'utf8'))

// 获取当前模板列表
export const templateList = JSON.parse(fs.readFileSync(rootPath + 'libs/data/record.json', 'utf8'))
  1. 将多个命令做成json动态数据,放到libs/data/command.json
[
  {
    "command": "create <projectName>",
    "alias": "c",
    "description": "下载模板项目",
    "action": "create"
  },
  {
    "command": "add",
    "alias": "a",
    "description": "新增模板项目",
    "action": "add"
  },
  {
    "command": "list",
    "alias": "ls",
    "description": "获取模板项目列表",
    "action": "list"
  },
  {
    "command": "remove",
    "alias": "d",
    "description": "删除某一模板项目",
    "action": "remove"
  }
]

这里面的action要对应到command文件夹中的命令。

  1. 模板的json数据上一期咱们已经说过了,将它换个文件夹存放即可。变换后的文件路径如下libs/data.record.json
[
  {
    "name": "electron",
    "value": "electron",
    "description": "electron+vue2(桌面端项目)",
    "link": "https://gitee.com/airdark/first-electron-1.git"
  },
  {
    "name": "web",
    "value": "web",
    "description": "vue2+webpack+iview(浏览器项目)",
    "link": "https://gitee.com/airdark/vue2-demo.git"
  }
 ]

实现命令的拆分

我们按照目录结构一个一个的将命令拆分出来

1.添加模板命令

这里我们将之前下载模板的相关代码拆分出来常量从constant中获取,最后将所有的添加模板逻辑通过一个方法导出,如下所示

// libs/command/add.js

import inquirer from 'inquirer'
import chalk from 'chalk'
import fs from 'fs-extra'
import { rootPath, templateList } from '../constant.js' 

let addQuestion = [
  {
    name: 'name',
    type: 'input',
    message: '请输入模板名称',
    validate(val) {
      if (!val) {
        return '名称为必填项!'
      } else if (templateList.some(item => item.name === val)) {
        return '名称重复!'
      } else {
        return true
      }
    }
  },
  {
    name: 'description',
    type: 'input',
    message: '请输入模板描述'
  },
  {
    name: 'link',
    type: 'input',
    message: '请输入模板地址',
    validate(val) {
      return !val ? '模板地址为必填项' : true
    }
  }
]

const add = () => {
  console.clear()
  inquirer.prompt(addQuestion).then(answer => {
    let template = { value: answer.name, ...answer }
    templateList.push(template)
    fs.writeJson(rootPath + 'libs/data/record.json', templateList).then(() => {
      console.log(chalk.greenBright('添加成功'))
      console.table(templateList)
    }).catch(err => {
      console.log(chalk.redBright('添加失败'))
      console.log(err)
    })
    
  })
}

export default add

2.下载模板命令

这里我们将下载模板的相关插件和命令拆分出来,放到单独文件中,最后将所有下载逻辑通过一个方法导出。如下所示

// libs/command/create.js

import download from 'download-git-repo'
import inquirer from 'inquirer'
import ora from 'ora'
import chalk from 'chalk'
import fs from 'fs-extra'
import path from 'path'
import { templateList } from '../constant.js' 

/*** 判断是否有同名文件夹  start */
const isHas = (projectName) => {
  const targetDir = path.join(process.cwd(), projectName)
  return fs.existsSync(targetDir)
}
/*** 判断是否有同名文件夹  end */

// 设置让用户选择模版的问题项
const question = [
  {
    name: "features", // 选项名称
    message: "请选择要创建的项目模板", // 选项提示语
    type: "list", // 选项类型 另外还有 confirm check 等
    choices: templateList
  }
]

// 设置检测重名的问题交互
const duplicateName = [
  {
    name: "duplicateName", // 选项名称
    message: "当前目录已存在,选择", // 选项提示语
    type: "list", // 选项类型 另外还有 confirm check 等
    choices: [ // 具体的选项
      {
        name: "覆盖", // 选项展示的名称
        value: "overWrite", // 用户最终选择的值
      },
      {
        name: "重命名",
        value: "rename"
      }
    ]
  },
  {
    name: 'newName',
    when: answers => answers.duplicateName === 'rename',
    type: 'input',
    message: '目录名称为:'
  }
]
// 检查重名--递归
const checkName = (name) => {
  // 清空控制台
  console.clear()
  // 这里我们就可以对用户输入的命令进行进行操作
  console.log(`用户想创建一个名为${chalk.cyan(name)}的项目`)
  if (isHas(name)) {
    inquirer.prompt(duplicateName).then(res => {
      if (res.duplicateName === 'overWrite') {
        const targetDir = path.join(process.cwd(), name)
        const loadingMsg = ora(`正在删除 ${chalk.cyan(targetDir)}...`)
        loadingMsg.start()
        fs.remove(targetDir).then(val => {
          loadingMsg.succeed()
          loadingMsg.stop()
          letDownload(name)
        })
      } else {
        checkName(res.newName)
      }
    })
  } else {
    letDownload(name)
  }
}
// 下载对应模板
const letDownload = (projectName) => {
  inquirer.prompt(question).then(res => {
    /*** 初始化loading图标文字 start */
    const spinner = ora('模版下载中 ...')
    /*** 初始化loading图标文字 end */
    spinner.start()
    // 获取到第一项中,用户选择的值,这里偷了个懒,大家可以根据答案和问题获取对应下载链接。
    let info = question[0].choices.find(item => item.value === res.features)
    download(`direct:${info.link}`, `./${projectName}`, {clone: true}, (err) =>{
      if (err) {
        spinner.fail()
        console.log(chalk.red(err))
        console.log(chalk.red('获取模版失败'))
      } else {
        spinner.succeed()
        console.log(chalk.green('Success!'))
      }
      spinner.stop()
    })
  })
}
export default checkName

3.展示模板列表命令

这里我们将展示列表模板的命令拆分出来,放到单独文件中,最后将所有展示逻辑通过一个方法导出。该方法简单,展示列表,通过console.table()来实现就好了,如下所示

// libs/command/list.js

import { templateList } from '../constant.js' 

const list = () => {
  console.clear()
  console.table(templateList)
}

export default list

4.删除模板命令

这里我们将删除模板的相关插件和命令拆分出来,放到单独文件中,最后将所有删除逻辑通过一个方法导出,如下所示

// libs/command/remove.js

import inquirer from 'inquirer'
import chalk from 'chalk'
import fs from 'fs-extra'
import { rootPath, templateList } from '../constant.js' 

let removeQuestion = [
  {
    name: 'name',
    type: 'input',
    message: '请输入要删除的模板名称',
    validate(val) {
      if (!val) {
        return '名称为必填项!'
      } else if (!templateList.some(item => item.name === val)) {
        return '名称不存在!'
      } else {
        return true
      }
    }
  }
]

const remove = () => {
  console.clear()
  inquirer.prompt(removeQuestion).then(answer => {
    let index = templateList.findIndex(item => item.name === answer.name)
    templateList.splice(index, 1)
    fs.writeJson(rootPath + 'libs/data/record.json', templateList).then(() => {
      console.log(chalk.greenBright('删除成功'))
      console.table(templateList)
    }).catch(err => {
      console.log(chalk.redBright('删除失败'))
      console.log(err)
    })
    
  })
}

export default remove

最终将这些命令放到统一的出口文件中,做统一引入

// libs/command/index.js
import create from './create.js'
import add from './add.js'
import remove from './remove.js'
import list from './list.js'

export default {
  create,
  add,
  remove,
  list
}

实现命令的动态加载

最终我们将所有的命令都拆分出去了,我们来看下我们全局命令的主界面(bin/index.js)还剩下些什么,并动态引入所有命令

#! /usr/bin/env node

import { program } from 'commander'
import { packageJsonData, commandList } from '../libs/constant.js' 
import commands from '../libs/command/index.js'

// 加载所有的命令
commandList.forEach(element => {
  program.command(element.command)
    .alias(element.alias)
    .action((projectName) => { 
      // 这里就是咱们在整理命令的动态数据时候约定的action相对应,对应命令导出方法。
      commands[element.action](projectName)
    })
});

program.version(packageJsonData.version, '-v, --version')
program.parse(process.argv);

三、后记

这样我们就实现了命令的动态加载,以及功能文件模块的拆分,这样我们的代码可读性变得更高了,拆分后逻辑也更加的清晰了。

本篇完结! 撒花! 感谢观看! 希望能帮助到你!