前言
前端脚手架项目其实在掘金网上已经被写得很多了,脚手架项目实现起来确实也比较简单。但是作为我的低代码提效工具中不可缺少的一部分,还是用一篇文档进行总结,看完此文便可以了解一个简单的脚手架如何编写。
一句话总结:脚手架就是获取用户指令下载指定模板到指定位置。
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 添加颜色、样式和背景色等效果到终端文本中。
把里面的模板进行替换就是你自己的脚手架。
如果有帮助,请点赞收藏,非常感谢。