创建自定义脚手架

news/2024/12/26 0:42:42/

需求描述

每次开新项目时都需要从头搭建架构,或者就是把之前的项目直接复制粘贴过来修修改改。
当然有时稍微勤奋一点的就会弄个基础模板放在本地或者放在 github,需要的时候直接 clone 过来。
但是其便捷性和通用性极差,就开始想为什么不做一个类似 vue-cli 的命令行工具呢(刚好现在有时间~)

脚手架基本功能

1、通过命令行交互式的询问用户问题
2、根据用户的答复选择不同的模版或者生成不同的文件

脚手架构建用到的基本工具

commander 可以自定义一些命令行指令,在输入自定义的命令行的时候,会去执行相应的操作
npm install commanderinquirer 可以在命令行询问用户问题,并且可以记录用户回答选择的结果
npm install inquirerfs-extra 是fs的一个扩展,提供了非常多的便利API,并且继承了fs所有方法和为fs方法添加了promise的支持。
npm install fs-extrachalk 可以美化终端的输出
npm install chalk@4.1.0figlet 可以在终端输出logo
npm install figletora 控制台的loading样式
npm install oradownload-git-repo 下载远程模板
npm install download-git-repo

构建过程

1、首先创建一个文件夹,初始化 package.json 文件

mkdir zyq_fronted_clicd zyq_fronted_clinpm init

2、创建文件夹 bin ,用于放置程序的入口文件

zyq_fronted_cli      
├─ bin        
└─ package.json 

3、创建文件夹 lib ,用来放一些工具函数

zyq_fronted_cli      
├─ bin
├─ lib        
└─ package.json 

4、在 bin 文件夹中创建 cli.js 文件

zyq_fronted_cli      
├─ bin
│  ├─ cli.js   
├─ lib        
└─ package.json 

5、在 package.json 文件中指定程序的入口文件为 bin 文件夹下的 cli.js 文件

package.json 文件{"name": "zyq_fronted_cli","version": "1.0.0","description": "","main": "index.js","bin": {"zyq_fronted": "./bin/cli.js"},"scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"author": "","license": "ISC","dependencies": {"chalk": "^4.1.0","commander": "^10.0.1","figlet": "^1.6.0","fs-extra": "^11.1.1"}
}

6、下载安装 commander 自定义命令行指令包

npm install commander

7、在 bin 文件夹下的 cli.js 中引入 commander
注意:文件开头如果带有 #!的话,那么这个文件就会被当做一个执行文件来执行,执行文件也即是可以由操作系统进行加载执行的文件。如果带有这个符号,说明这个文件可以当做脚本来运行。
/usr/bin/env node 的意思是这个文件用 node 来执行,(会去用户的安装根目录下的 env 环境变量里面去寻找 node(/usr/bin/env node)然后用node 来执行整个的脚本文件)

#! /usr/bin/env nodeconst commander = require('commander')commander
.version('0.1.0')
.command('create <project name>')
.discription('create a new project')
.action(res => {console.log(res)
})commander.parse()

8、将当前项目(zyq_fronted_cli) 链接到全局

npm link

9、安装 chalk 和 figlet,在 bin 文件夹的 cli.js 中引入 ,用于自定义字体和颜色

npm install chalk@4.1.0
npm install figlet
#! /usr/bin/env nodeconst commander = require('commander') // 自定义指令// 自定义指令
commander.version('0.1.0').command('create <project_name>').description('create a new project').action(res => {console.log(res)})const chalk = require('chalk') // chalk 改变颜色
const figlet = require('figlet') // figlet 改变字体commander.on('--help', () => { // 监听 --help 执行console.log('\r\n' + figlet.textSync('ZYQ_FRONTED_CLI', {font: 'Ghost',horizontalLayout: 'default',verticalLayout: 'default',width: 500,whitespaceBreak: true}))// 新增说明信息console.log(`\r\nRun ${chalk.cyan(`zyq_fronted <command> --help`)} for detailed usage of given command\r\n`)})commander.parse()

10、在 lib 文件夹中创建 create.js 文件,用于编写创建文件所需的逻辑

zyq_fronted_cli      
├─ bin
│  ├─ cli.js   
├─ lib        
│  ├─ create.js  
└─ package.json 
create.js 文件
module.exports =  async function (name, option){console.log('项目名称以及配置项:', name, option)
}

11、 创建项目时,询问用户是否需要强制覆盖已有的文件(在 cli.js 文件中加入 --force 选项来实现该需求,修改 create.js 文件的逻辑)
创建项目时(zyq_fronted create myproject --force 或者 zyq_fronted create myproject -f),询问用户是否需要强制覆盖已有的文件

cli.js 文件#! /usr/bin/env nodeconst commander = require('commander') // 自定义指令
const create = require('../lib/create.js')commander.version('0.1.0').command('create <project_name>').description('create a new project').option('-f --force', 'overwrite target directory if it exist').action((name, option) => {console.log(name, option)create(name, option)})const chalk = require('chalk') // chalk 改变颜色const figlet = require('figlet') // figlet 改变字体commander.on('--help', () => { // 监听 --help 执行console.log('\r\n' + figlet.textSync('ZYQ_FRONTED_CLI', {font: 'Ghost',horizontalLayout: 'default',verticalLayout: 'default',width: 500,whitespaceBreak: true}))// 新增说明信息console.log(`\r\nRun ${chalk.cyan(`zyq_fronted <command> --help`)} for detailed usage of given command\r\n`)})commander.parse()
create.js 文件module.exports = async function (name, option){const path = require('path')const fs = require('fs-extra') // npm install fs-extraconst cwd = process.cwd() // 当前命令行选择的目录const targeCwd = path.join(cwd, name) // 需要创建的目录地址// 判断是否存在该目录if (fs.existsSync(targetCwd)) { // 目录存在if (options.force) { // 是否强制创建console.log('进行强制创建')} else {console.log('询问用户是否强制创建')}} else { // 目录不存在console.log('目录不存在,进行强制创建')}console.log('项目名称以及配置项:', name, options)
}

12、安装 inquirer ,使用 inquirer 获取终端与用户的交互信息
inquirer 可以在命令行询问用户问题,也可以记住用户在命令行的选择

npm install --save inquirer@^8.0.0

13、修改 create.js 文件的逻辑(当非强制性创建项目的时候,项目存在的话就询问用户要不要覆盖项目;当非强制性创建项目的时候,项目不存在的话直接创建项目;当强制性创建项目的时候,项目存在或不存在都强制创建项目;)

create.js 文件module.exports =  async function (name, options){const path = require('path')const fs = require('fs-extra')const inquirer = require('inquirer')const cwd  = process.cwd() // 当前命令行选择的目录const targetCwd = path.join(cwd, name) // 需要创建的目录地址console.log(cwd,targetCwd)// 判断是否存在该目录if (fs.existsSync(targetCwd)) { // 目录存在if (options.force) { // 是否强制创建console.log('进行强制创建')// 移除原来存在的项目await fs.remove(targetCwd)} else {console.log('询问用户是否强制创建')// 询问用户是否强制创建项目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} else {console.log('移除存在的文件')await fs.remove(targetCwd)}}} else { // 目录不存在console.log('目录不存在,进行强制创建')}console.log('项目名称以及配置项:', name, options)
}

14、在 lib 文件夹下创建 factory.js 文件,用于负责 创建目录、拉取模版等逻辑

zyq_fronted_cli      
├─ bin
│  ├─ cli.js   
├─ lib
│  ├─ create.js
│  ├─ factory.js 
└─ package.json

15、编辑 factory.js 文件内容,并在 create.js 文件中引入

factory.js 文件module.exports = class Factory{constructor(name, targetCwd){this.name = name // 目录名称this.targetCwd = targetCwd // 目录所在地址console.log(this.name, this.targetCwd)}// 创建create() {}
}
create.js 文件module.exports =  async function (name, options){const path = require('path')const fs = require('fs-extra')const inquirer = require('inquirer')const cwd  = process.cwd() // 当前命令行选择的目录const targetCwd = path.join(cwd, name) // 需要创建的目录地址console.log(cwd,targetCwd)// 判断是否存在该目录if (fs.existsSync(targetCwd)) { // 目录存在if (options.force) { // 是否强制创建console.log('进行强制创建')// 移除原来存在的项目await fs.remove(targetCwd)} else {console.log('询问用户是否强制创建')// 询问用户是否强制创建项目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} else {console.log('移除存在的文件')await fs.remove(targetCwd)}}} else { // 目录不存在console.log('目录不存在,进行强制创建')}// 创建项目  const Factory = require('./factory')const factory = new Factory(name, targetCwd)factory.create()console.log('项目名称以及配置项:', name, options)
}

16、接着来写询问用户选择模版的逻辑
github 提供了接口可以获取模板,你可以事先准备好了两个模板发布到 github 上
在 lib 文件夹中创建 http.js 文件,专门用来管理接口,创建好后整个目录结构如下

zyq_fronted_cli      
├─ bin
│  ├─ cli.js   
├─ lib
│  ├─ create.js
│  ├─ factory.js
│  ├─ http.js 
└─ package.json 

17、安装 axios ,编写 http.js 文件内容

npm install axios
http.js 文件const axios = require('axios')axios.interceptors.response.use(res => {return res.data
})// 获取模版列表
async function getRepoList(myGithub = 'vue3-0-cli-yd'){return axios.get('https://api.github.com/orgs/vue3-0-cli-yd/repos') // 更换自己的 github 项目 `https://api.github.com/orgs/wangml-gitbub/repos`
}// 获取版本信息
async function getTagList(repo) {return axios.get(`https://api.github.com/repos/vue3-0-cli-yd/${repo}/tags`) // https://api.github.com/orgs/wangml-gitbub/repos
}module.exports = {getRepoList,getTagList
}

18、安装 ora,用于显示加载中的效果;
安装 util , util 可以让没有 node 环境的宿主(如:浏览器)拥有 node 的 util模块;
安装 download-git-repo,用于下载 git 存储库

npm install ora@5.4.1
npm install util
npm install download-git-repo

19、编写 factory.js 文件内容, 添加加载动画、获取用户选择的模版、获取模版的 tag 列表、下载远程模版、创建项目 的逻辑

factory.js 文件const { getRepoList, getTagList } = require('./http')
const ora = require('ora') // 显示加载中的效果
const util = require('util') // 让没有 node 环境的宿主拥有 node 的 util 模块
const downloadGitRepo = require('download-git-repo') // 下载 git 存储库
const inquirer = require('inquirer')
const path = require('path')
const chalk = require('chalk')module.exports = class Factory{constructor(name, targetCwd){this.name = name // 目录名称this.targetCwd = targetCwd // 目录所在地址this.downloadGitRepo = util.promisify(downloadGitRepo) // 对 download-git-repo 进行 promise 化改造console.log(this.name, this.targetCwd)}// 加载动画async loading(fn, message, ...args) {const spinning = ora(message) // 初始化 ora,传入提示信息 message spinning.start() // 开始加载动画try {const result = await fn(...args) // 执行 fn 方法spinning.succeed() // 将状态改为成功return result} catch (err){spinning.fail('Request failed, refetch ...')}}// 获取用户选择的模版async getRepo(){// 从远程拉取模板数据const repoList = await this.loading(getRepoList, 'waiting fetch template')if(!repoList) return// 过滤需要的模板名称const repos = repoList.map(item => item.name) console.log(repos)// 让用户选择模版const { repo } = await inquirer.prompt({name: 'repo',type: 'list',choices: repos,message: 'Please choose a template to create project'})// 返回用户选择的名称return repo }// 获取模版的 tag 列表async getTag(repo){// 从远程拉取模板 tag 列表const tags = await this.loading(getTagList, 'waiting fetch tag', repo)if(!tags) return// 过滤需要的 tag 名称const tagList = tags.map(item => item.name)console.log(tagList)// 让用户选择 tagconst { tag } = await inquirer.prompt({name: 'tag',type: 'list',choices: tagList,message: 'Place choose a tag to create project'})// 返回用户选择的 tagreturn tag}// 下载远程模版async download(repo, tag){const requestUrl = `vue3-0-cli-yd/${repo}${tag ? '#' + tag : ''}` // 拉取模版的地址const createUrl =  path.resolve(process.cwd(), this.targetCwd) // 创建项目的地址// 下载方法调用await this.loading(this.downloadGitRepo, 'waiting download template', requestUrl, createUrl)}// 创建项目async create() {console.log('创建项目---', this.name, this.targetCwd)try {// 获取用户选择的模版名称const repo = await this.getRepo()// 获取用户选择的 tagconst tag = await this.getTag(repo)await this.download(repo, tag)// 4)模板使用提示console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)console.log(`\r\n  cd ${chalk.cyan(this.name)}`)console.log(`\r\n  npm install`)console.log("\r\n  npm run dev\r\n")} catch (error) {console.log(error);}}
}

20、到此就结束啦,可以使用这个自定义的脚手架进行拉取相应的模版

zyq_fronted create my_project
选择模版及 tag
cd my_project
npm install
npm run dev

21、代码地址
cli.js 文件代码

#! /usr/bin/env nodeconst commander = require('commander') // 自定义指令
const create = require('../lib/create.js')commander.version('0.1.0').command('create <project_name>').description('create a new project').option('-f --force', 'overwrite target directory if it exist').action((name, option) => {console.log(name, option)create(name, option)})const chalk = require('chalk') // chalk 改变颜色const figlet = require('figlet') // figlet 改变字体commander.on('--help', () => { // 监听 --help 执行console.log('\r\n' + figlet.textSync('ZYQ_FRONTED_CLI', {font: 'Ghost',horizontalLayout: 'default',verticalLayout: 'default',width: 500,whitespaceBreak: true}))// 新增说明信息console.log(`\r\nRun ${chalk.cyan(`zyq_fronted <command> --help`)} for detailed usage of given command\r\n`)})commander.parse()

create.js 文件代码

module.exports =  async function (name, options){const path = require('path')const fs = require('fs-extra')const inquirer = require('inquirer')const cwd  = process.cwd() // 当前命令行选择的目录const targetCwd = path.join(cwd, name) // 需要创建的目录地址console.log(cwd,targetCwd)// 判断是否存在该目录if (fs.existsSync(targetCwd)) { // 目录存在if (options.force) { // 是否强制创建console.log('进行强制创建')// 移除原来存在的项目await fs.remove(targetCwd)} else {console.log('询问用户是否强制创建')// 询问用户是否强制创建项目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} else {console.log('移除存在的文件')await fs.remove(targetCwd)}}} else { // 目录不存在console.log('目录不存在,进行强制创建')}// 创建项目  const Factory = require('./factory')const factory = new Factory(name, targetCwd)factory.create()console.log('项目名称以及配置项:', name, options)
}

http.js 文件代码

const axios = require('axios')axios.interceptors.response.use(res => {return res.data
})// 获取模版列表
async function getRepoList(){return axios.get('https://api.github.com/orgs/vue3-0-cli-yd/repos') // https://api.github.com/orgs/wangml-gitbub/repos
}// 获取版本信息
async function getTagList(repo) {return axios.get(`https://api.github.com/repos/vue3-0-cli-yd/${repo}/tags`) // https://api.github.com/orgs/wangml-gitbub/repos
}module.exports = {getRepoList,getTagList
}

factory.js 文件代码

const { getRepoList, getTagList } = require('./http')
const ora = require('ora') // 显示加载中的效果
const util = require('util') // 让没有 node 环境的宿主拥有 node 的 util 模块
const downloadGitRepo = require('download-git-repo') // 下载 git 存储库
const inquirer = require('inquirer')
const path = require('path')
const chalk = require('chalk')module.exports = class Factory{constructor(name, targetCwd){this.name = name // 目录名称this.targetCwd = targetCwd // 目录所在地址this.downloadGitRepo = util.promisify(downloadGitRepo) // 对 download-git-repo 进行 promise 化改造console.log(this.name, this.targetCwd)}// 加载动画async loading(fn, message, ...args) {const spinning = ora(message) // 初始化 ora,传入提示信息 message spinning.start() // 开始加载动画try {const result = await fn(...args) // 执行 fn 方法spinning.succeed() // 将状态改为成功return result} catch (err){spinning.fail('Request failed, refetch ...')}}// 获取用户选择的模版async getRepo(){// 从远程拉取模板数据const repoList = await this.loading(getRepoList, 'waiting fetch template')if(!repoList) return// 过滤需要的模板名称const repos = repoList.map(item => item.name) console.log(repos)// 让用户选择模版const { repo } = await inquirer.prompt({name: 'repo',type: 'list',choices: repos,message: 'Please choose a template to create project'})// 返回用户选择的名称return repo }// 获取模版的 tag 列表async getTag(repo){// 从远程拉取模板 tag 列表const tags = await this.loading(getTagList, 'waiting fetch tag', repo)if(!tags) return// 过滤需要的 tag 名称const tagList = tags.map(item => item.name)console.log(tagList)// 让用户选择 tagconst { tag } = await inquirer.prompt({name: 'tag',type: 'list',choices: tagList,message: 'Place choose a tag to create project'})// 返回用户选择的 tagreturn tag}// 下载远程模版async download(repo, tag){const requestUrl = `vue3-0-cli-yd/${repo}${tag ? '#' + tag : ''}` // 拉取模版的地址const createUrl =  path.resolve(process.cwd(), this.targetCwd) // 创建项目的地址// 下载方法调用await this.loading(this.downloadGitRepo, 'waiting download template', requestUrl, createUrl)}// 创建项目async create() {console.log('创建项目---', this.name, this.targetCwd)try {// 获取用户选择的模版名称const repo = await this.getRepo()// 获取用户选择的 tagconst tag = await this.getTag(repo)await this.download(repo, tag)// 4)模板使用提示console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)console.log(`\r\n  cd ${chalk.cyan(this.name)}`)console.log(`\r\n  npm install`)console.log("\r\n  npm run dev\r\n")} catch (error) {console.log(error);}}
}

package.json 内容

{"name": "zyq_fronted_cli","version": "1.0.0","description": "","main": "index.js","bin": {"zyq_fronted": "./bin/cli.js"},"scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"author": "","license": "ISC","dependencies": {"axios": "^1.3.5","chalk": "^4.1.0","commander": "^10.0.1","download-git-repo": "^3.0.2","figlet": "^1.6.0","fs-extra": "^11.1.1","inquirer": "^8.2.5","ora": "^5.4.1","util": "^0.12.5"}
}

http://www.ppmy.cn/news/65971.html

相关文章

matplotlib的安装和使用教程:中文字体及语言参数设置

matplotlib是一个常用的数据可视化库&#xff0c;广泛应用于科学研究、工程设计、金融分析等领域。由于其强大的功能和易用性&#xff0c;matplotlib已经成为了广大科研工作者和数据分析师的必备工具之一。本文将重点介绍matplotlib的安装和允许中文及几种字体的方法。 一、mat…

springboot整合邮箱功能二(普通邮件, html邮件, thymleaf邮件)

【SpringBoot整合Email发送邮件】_ζั͡ ั͡空 ั͡ ั͡白&#xfffd;的博客-CSDN博客 https://www.cnblogs.com/erlou96/p/16878192.html#_label1_5 1. 准备工作 1.1 qq邮箱设置 本文默认使用qq邮箱来发送邮件,然后使用一个在线临时邮箱来接收邮件。为了让程序能够通过…

react初始化配置rem,less,@,本地代理,通配符,视口单位等

初始化项目之后&#xff0c;项目配置中默认配置的是scss 想用less就需要单独配置了&#xff0c;在做一个完整的项目情况下create-react-app搭出来架子的配置往往是不够的至少需要简单配置以下信息 暴露webpack之后会增加很多文件和依赖配置&#xff0c;有些时候并不想把它暴露出…

【Java零基础入门篇】第 ⑤ 期 - 抽象类和接口(二)

博主&#xff1a;命运之光 专栏&#xff1a;Java零基础入门 学习目标 1.了解什么是抽象类&#xff0c;什么是接口&#xff1b; 2.掌握抽象类和接口的定义方法&#xff1b; 3.理解接口和抽象类的使用场景&#xff1b; 4.掌握多态的含义和用法&#xff1b; 5.掌握内部类的定义方法…

C++使用中需要避免的10个常见错误

C使用中需要避免的10个常见错误 常见C错误与注意事项一、不使用命名空间二、不正确使用头文件三、没有检查数组越界四、忘记释放动态分配的内存五、不使用const关键字六、指针误用七、类型强制转换错误八、浮点数比较不准确九、忘记判断函数返回值十、不使用引用传参 常见C错误…

rk3568 修改开机logo

rk3568 修改开机显示logo Android 显示 logo 的作用是为了标识应用程序或设备的品牌和身份。在应用程序中&#xff0c;logo 可以帮助用户快速识别应用程序&#xff0c;并与其他应用程序区分开来。在设备中&#xff0c;logo 可以帮助用户识别设备的品牌和型号&#xff0c;以及与…

怎么给pdf文件添加水印

怎么给pdf文件添加水印&#xff1f;PDF文件的应用比较广泛&#xff0c;大家喜欢将各种办公资料和文档转换成PDF格式&#xff0c;为什么呢&#xff1f;因为pdf文件具有比较强的稳定性而且不利于编辑修改&#xff0c;所以更利于保存和转发。有时候我们会在工作中花费大量时间制作…

stream笔记

1、 创建流stream 1.1、 Stream 的操作三个步骤 1.2、 stream中间操作 1.2.1 、 limit、skip、distinct 1.2.2、 map and flatMap 1.2.3、 sort 自然排序和定制排序 1.3、 add and andAll difference: 1.4、 终止操作 1.4.1、 allmatch、anyMatch、noneMatch、max、min…