本篇文章用来为大家提供一个搭建简易前端脚手架的思路。
先来看一眼实现的效果。
从图上来看这个脚手架的功能非常的简单只有一个创建的命令,其他都是帮助和显示版本号的。
也就是上图这句,创建一个新项目,只需要输入create 项目名便可使用,在创建时执行了一系列的操作,这一块的思路很简单,就是将git仓库中的项目模板拷贝下来再依据使用者的不同操作对复制下来的模板的部分文件进行修改就可以了,大致思路便介绍到这里,接下来我们便来详细的讲讲如何实现,以及会用到的依赖。
脚手架目录结构
了解搭建的脚手架
脚手架就是在启动的时候询问一些简单的问题,并且通过用户回答的结果去渲染对应的模板文件,我们接下来的流程亦是如此
脚手架的初始化
由于它是一个npm的包,因此我们需要使用npm的初始化命令,随意新建一个文件夹打开命令行,输入npm init
,会出现以下情况。
名称 | 意思 | 默认值 |
---|---|---|
package name | 包的名称 | 创建文件夹时的名称 |
version | 版本号 | 1.0.0 |
description | 包的描述 | 创建文件时的名称 |
entry point | 入口文件 | index.js |
test command | 测试命令 | — |
git repository | git仓库地址 | — |
keywords | 关键词,上传到npm官网时在页面中展示的关键词 | — |
author | 作者信息,对象的形式,里面存储一些邮箱、作者名、url | — |
license | 执照 | MIT |
这就是输入初始化命令时会询问的东西,回答完这些后就会生成一个 package.json
的文件,这个文件就是记录包的信息。
如果想要了解更多,可查看如下地址:
package.json详解
脚手架依赖安装
用到如下依赖请安装。
npm i path
npm i chalk@4.1.0
npm i fs-extra
npm i inquirer@8.2.4
npm i commander
npm i axios
npm i download-git-repo
询问用户问题
创建入口文件
在询问问题前我们需要先创建一个入口文件,创建完成后在package.json中添加bin项,并且将入口文件路径写进去
填写完入口文件路径后在入口文件内随便输出一句, 但必须在入口文件顶层声明文件执行方式为node。
声明代码:#! /usr/bin/env node
写完后我们需要测试一下我们是否可以正常的访问的我们的脚手架,在本文件夹打开命令行,输入 npm link
,该命令会创建一个全局访问的包的快捷方式,这个是临时的就是本地测试的时候用的,这个在命令行输入你的脚手架的名称可以看到入口文件输出的内容。
最基本的交互命令
在完成上一步后我们就要开始与用户进行交互了,这个时候我们就需要用到一个用于自定义命令行指令的依赖 commander。
引入依赖:
const program = require('commander')
简单介绍一下commander依赖常用的方法
command
命令。.command()
的第一个参数为命令名称。命令参数可以跟在名称后面,也可以用.argument()
单独指定。
参数可为必选的(尖括号表示)、可选的(方括号表示)或变长参数(点号表示,如果使用,只能是最后一个参数)。
例如:
// 创建一个create命令
.command('create <app-name>')
parse
解析。.parse()
的第一个参数是要解析的字符串数组,也可以省略参数而使用process.argv
,这里我们也是用process.argv用来解析node的参数。
例如:
// 解析用户执行命令传入参数
program.parse(process.argv);
option
选项。option()
可以附加选项的简介。第一个参数可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(–后面接一个或多个单词),使用逗号、空格或|
分隔。第二个参数为该选项的简介。
例如:
.option('-f, --force', '如果存在的话强行覆盖')
action
处理函数。用command创建的自定义命令
的处理函数,action携带的实参顺序就是命令上的参数的顺序。
例如:
program
.command('create <app-name>')
// 这个name 就代表第一个必填参数 options就代表其余, 如果有第二个就在写一个,最后一个永远是剩余参数
.action((name, options) => {
console.log(name)
// 打印执行结果
// require("../lib/create")(name, options)
})
编写交互命令 create
入口文件
#! /usr/bin/env node
const program = require('commander');
const chalk = require('chalk');
// 定义命令和参数
// create命令
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)
})
// 解析用户执行命令传入参数
program.parse(process.argv);
这里我们创建了一个叫 create
的自定义指令,这个命令有着必填的项目名、可以选择的强制覆盖的选项 -f
,有着处理函数action
。
我们在action中接收并打印了用户输入的项目名称。
接下来我们再次运行一下自己的脚手架并带上create命令,我的叫test
test-cli create app
出现如下就说明第一个命令创建成功了
这里请注意 解析用户命令参数的操作一定要在最后一行否则什么都不会出现。
program.parse(process.argv)
到这里为止我们成功为我们脚手架创建了第一个交互命令,想查看更多关于 commander
的请点击这里commander。
创建第一个模板项目
在创建了一个基本命令 create
后我们就要开始创建一个模板并在用户使用该命令时复制并修改我们所创建的模板。
创建一个模板
我们在复制模板前需要一个模板,现在的我们随便创建一个文件夹并取名为template里面创建一个html。
像这样创建好后,我们就有了一个模板,但我们依然需要让模板有一个可被下载、查询的地方,这里我选择的是使用 git
组织仓库,因为这样可以直接通过git
提供的接口进行文件下载,包括选择不同的模板等。
上传模板
我们先去 git
的官网中新建一个存放模板的组织仓库。
点击图中的位置进入组织,并点击下图的创建
会进入到付费的位置,没有大需求就选免费
填写信息完基本就算创建成功了
接下来在组织中创建一个储存库
这里我们暂且选择可见的仓库,千万不要选择私人仓库,否则git
接口会找不该仓库
创建好后的仓库,就直接将模板代码提交至也本次创建的仓库中就可以了,我们在vscode
中进行演示。
先点击推送
如果没有推送的仓库则会提示是否添加推送仓库,我们点击推送远程仓库,并从中找到自己的仓库
择完成后输入仓库名称,然后会报错,报错原因就是因为暂无推送的内容,这个使用,正常的在 vscode
中提交代码就行了,然后查看自己的仓库,会出现上传的内容
增加一个新的版本标签
跟着下列图操作
点击发布发行版后就可以了。
下载模板
我们上传模板后可以通过 git
提供的接口来完成下载模板的功能,首先我们先去询问用户要下载的模板名称然后在用依赖包来进行下载:
https://api.github.com/orgs/geeksTest/repos 获取该组织下的所有模板
create命令后续操作
上传模板后,我们就可以继续完成create
命令的后续操作了。
create命令下使用创建函数
program
.command('create <app-name>')
.description(chalk.cyan('create a new project'))
// -f or --force 为强制创建,如果创建的目录存在则直接覆盖
.option('-f, --force', 'overwrite target directory if it exist')
.action((name, options) => {
// 打印执行结果
require("../lib/create")(name, options)
})
创建create文件
创建 create 文件用来回应用户的 create 命令。
这里用到的依赖
// lib/create.js
const path = require('path')
// fs-extra 是对 fs 模块的扩展,支持 promise 语法
const fs = require('fs-extra')
// 用于交互式询问用户问题
const inquirer = require('inquirer')
// 导出Generator类
const Generator = require('./Generator')
//1. 抛出一个方法用来接收用户要创建的文件夹(项目)名 和 其他参数
module.exports = async function (name, options) {
// 当前命令行选择的目录
const cwd = process.cwd();
// 需要创建的目录地址
const targetAir = path.join(cwd, name)
//2 判断是否存在相同的文件夹(项目)名
// 目录是否已经存在?
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 Pick an action:',
choices: [
{
name: 'Overwrite',
value: 'overwrite'
},{
name: 'Cancel',
value: false
}
]
}
])
// 如果用户拒绝覆盖则停止剩余操作
if (!action) {
return;
} else if (action === 'overwrite') {
// 移除已存在的目录
console.log(`\r\nRemoving...`)
await fs.remove(targetAir)
}
}
}
//3 新建generator类
const generator = new Generator(name, targetAir);
generator.create();
}
创建generator类
// lib/Generator.js
const { getRepoList, getTagList } = require('./http')
const ora = require('ora')
const inquirer = require('inquirer')
const util = require('util')
const downloadGitRepo = require('download-git-repo') // 不支持 Promise
const chalk = require('chalk')
const path = require('path');
const fs = require("fs-extra");
// 添加加载动画
async function wrapLoading(fn, message, ...args) {
// 使用 ora 初始化,传入提示信息 message
const spinner = ora(message);
// 开始加载动画
spinner.start();
try {
// 执行传入方法 fn
const result = await fn(...args);
// 状态为修改为成功
spinner.succeed();
return result;
} catch (error) {
// 状态为修改为失败
spinner.fail('Request failed, refetch ...');
}
}
class Generator {
constructor (name, targetDir){
// 目录名称
this.name = name;
// 创建位置
this.targetDir = targetDir;
// 对 download-git-repo 进行 promise 化改造
this.downloadGitRepo = util.promisify(downloadGitRepo);
}
// 获取用户选择的模板
// 1)从远程拉取模板数据
// 2)用户选择自己新下载的模板名称
// 3)return 用户选择的名称
async getRepo() {
// 1)从远程拉取模板数据
const repoList = await wrapLoading(getRepoList, 'waiting fetch template');
if (!repoList) return;
// 过滤我们需要的模板名称
const repos = repoList.map(item => item.name);
// 2)用户选择自己新下载的模板名称
const { repo } = await inquirer.prompt({
name: 'repo',
type: 'list',
choices: repos,
message: 'Please choose a template to create project'
})
// 3)return 用户选择的名称
return repo;
}
// 获取用户选择的版本
// 1)基于 repo 结果,远程拉取对应的 tag 列表
// 2)自动选择最新版的 tag
async getTag(repo) {
// 1)基于 repo 结果,远程拉取对应的 tag 列表
const tags = await wrapLoading(getTagList, 'waiting fetch tag', repo);
if (!tags) return;
// 过滤我们需要的 tag 名称
const tagsList = tags.map(item => item.name);
// 2)return 用户选择的 tag
return tagsList[0]
}
// 下载远程模板
// 1)拼接下载地址
// 2)调用下载方法
async download(repo, tag){
// 1)拼接下载地址
const requestUrl = `geeksTest/${repo}${tag ? '#'+tag : ''}`;
// 2)调用下载方法
await wrapLoading(
this.downloadGitRepo, // 远程下载方法
'waiting download template', // 加载提示信息
requestUrl, // 参数1: 下载地址
path.resolve(process.cwd(), this.targetDir) // 参数2: 创建位置
)
}
// 核心创建逻辑
// 1)获取模板名称
// 2)获取 tag 名称
// 3)下载模板到模板目录
// 4) 对uniapp模板中部分文件进行读写
// 5) 模板使用提示
async create(){
// 1)获取模板名称
const repo = await this.getRepo()
// 2) 获取 tag 名称
const tag = await this.getTag(repo)
// 3)下载模板到模板目录
await this.download(repo, tag)
// 5)模板使用提示
console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)
console.log(`\r\n cd ${chalk.cyan(this.name)}`)
console.log(`\r\n 启动前请务必阅读 ${chalk.cyan("README.md")} 文件`)
}
}
module.exports = Generator;
创建http文件
新建一个http.js的文件用来存放要请求的接口,我们用axios去请求.
依赖安装 npm i commander
// lib/http.js
// 通过 axios 处理请求
const axios = require('axios')
axios.interceptors.response.use(res => {
return res.data;
})
/**
* 获取模板列表
* @returns Promise
*/
async function getRepoList() {
return axios.get('https://api.github.com/orgs/geeksTest/repos')
}
/**
* 获取版本信息
* @param {string} repo 模板名称
* @returns Promise
*/
async function getTagList(repo) {
return axios.get(`https://api.github.com/repos/geeksTest/${repo}/tags`)
}
module.exports = {
getRepoList,
getTagList
}
最后导出了两个方法, 模板列表、模板tag列表。
这个时候的api接口是可以直接在浏览器中访问到的,如果不想被人随意访问读取数据则可以在git中增加双因素验证,然后每次访问api时都会要求带上git的访问token否则会访问不到,查看双因素详情
搭建完成
完成这一步后我们再去进行test-cli create app
命令,会看到下图。
会询问要创建的模板项目,我这里的远程组织模板叫做test,大家选择自己的模板回车,稍等一下就会创建成功,并看到在你使用命令的路径上多出一个项目名的文件夹,就成功了。
如果有对模板在下载后进行操作的需求可以使用fs
依赖进行操作,到这里为止我们已经完成了一个简易的脚手架搭建,感谢大家耐心观看。