此文为已有一个自己的框架为前提,npm角度出发搭建一个脚手架
发布流程不阐述,自行发布
项目地址,npm地址均在文末
步骤
- 创建项目
- 开发脚手架命令
- 控制台与用户交互获取创建信息
- 远程下载模版
- 发布
创建项目(start.js)
目标父级文件夹:
// 创建文件夹
mdkir zzy-react-cli(你的脚手架名称)
// 进入创建的文件夹
cd zzy-react-cli
// npm 初始化
npm init
...一路enter
首先创建下文件夹,npm初始化之后对package.json进行改造
{
"name": "zzy-react-cli",
"version": "1.0.0",
"description": "这里是描述",
"main": "./bin/start.js", // 入口
"bin": {
"zzy-cli": "./bin/start.js", // 配置启动文件路径, zzy-cli 为别名
},
"author": "zzy",
"license": "ISC",
"dependencies": {}
}
测试一下 start.js 文件是否有效
#! /usr/bin/env node
// #! 符号的名称叫 Shebang,用于指定脚本的解释程序
// Node CLI 应用入口文件必须要有这样的文件头
// 如果是Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755
// 具体就是通过 chmod 755 cli.js 实现修改
// 检查入口是否正常执行
console.log('hello start.js')
这里为了调试方便,需要 npm link ,将这个npm包链接到全局上
如果是mac os系统,还需要前置 sudo 来增加权限,也就是 sudo npm link
到这里说明这个npm包运行之后成功执行了这个文件,接着下一步。
开发脚手架命令(start.js)
子步骤:
- 需要依赖 commander 插件来进行实现
- 最初只使用一个 create 命令,如果已有创建目标的文件名,提示是否覆盖
安装 commander 这个插件
yarn add commander -D
处理 start.js
#! /usr/bin/env node
const program = require('commander')
program
// 版本
.version('1.0.0')
// 新建命令 create
.command('create <name>')
// -f or --force 为强制创建,如果创建的目录存在则直接覆盖
.option('-f, --force', '是否强制覆盖')
// 描述
.description('创建项目')
// 回调
.action((name, options) => {
// 打印命令行输入的值
console.log('name:', name, 'options:', options);
})
// 解析用户执行命令传入参数
program.parse(process.argv)
到这里,为创建项目命名 就已经完成了
命令行输入 zzy-cli
这里看到已经加入了 create 这个命令,并赋予了描述,紧接着进行创建
zzy-cli create test-project
在这里我们已经拿到了项目的名称和配置项
紧接着在 bin 文件夹下创建 create.js 在这里进行创建项目的操作
创建文件夹(create.js)
创建之后在 start.js 中使用 create.js
program
.version('1.0.0')
.command('create <name>')
// -f or --force 为强制创建,如果创建的目录存在则直接覆盖
.option('-f, --force', '是否强制覆盖')
.description('创建项目')
.action((name, options) => {
// 调用 create.js 并传入值
require('./create.js')(name, options)
})
创建目录时需要考虑到一个问题,就是当前文件夹内是否存在同名文件夹?
- 当目录存在
- 当 { fouce: true } 时 移除原先文件夹,进行创建
- 当 { fouce: false } 时 询问用户是否进行强制覆盖,是则移除原有进行创建,否则终止进程
- 当目录不存在 直接进行创建
操作系统文件需要用到 fs-extra 插件
yarn add fs-extra -D
fs的使用详情参考 fs模块 Node.js API 文档
接下来对 create.js 这个文件进行编辑:
// bin.create.js
const path = require('path')
const fs = require('fs-extra')
module.exports = async function (name, options) {
// 获取当前目录
const cwd = process.cwd()
// 获取目标文件夹地址
const targetDir = path.join(cwd, name)
// 是否已存在文件夹
if (fs.existsSync(targetAir)) {
// 是否强制创建(覆盖)
if (options.force) {
await fs.remove(targetAir)
} else {
// ...some code 询问用户是否确定要覆盖,下文阐述
}
}
}
控制台与用户交互获取创建信息(create.js)
子步骤:
- 继续之前的流程,询问用户是否进行覆盖操作
- 用户选择项目模版
- 获取对应项目模版的链接
继续之前的流程,询问用户是否进行覆盖操作
这里需要用到 inquirer 插件
插件描述 npm.js、 CSDN
这里不再阐述如何使用
const inquirer = require('inquirer')
// ...some code
// 是否已存在文件夹
if (fs.existsSync(targetDir)) {
// 是否强制创建(覆盖)
if (options.force) {
await fs.remove(targetDir)
} else {
// 异步获取结果
let { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: '目录已存在,请选择一项进行操作:',
choices: [
{
name: '覆盖',
value: 'overwrite'
}, {
name: '取消',
value: false
}
],
}
])
// 根据结果做出相应处理
if (!action) {
return
} else if (action === 'overwrite') {
// 移除已存在的目录
console.log(`\r\nRemoving...`)
await fs.remove(targetDir)
}
}
}
完善相关逻辑之后,测试一波:
当前目录下手动创建 test-project 文件夹,然后执行 create 命令
选择覆盖之后,执行移除文件夹命令。
这里也可以直接使用 -f 或者 --force 强制执行
zzy-cli create test-project --force
这里之所以只移除,是因为线上拉取的模版会直接创建项目目录
获取模版信息
github提供了一些获取用户公开库的API接口
https://api.github.com/users//[github用户名称]/repos
这里建立 bin/https.js 来专门处理模版信息获取
// 远程下载模版
const axios = require('axios')
axios.interceptors.response.use(res => {
return res.data;
})
/**
* 获取模板列表
* @returns Promise
*/
async function getRepoList() {
return axios.get('https://api.github.com/users/Weibienaole/repos')
}
module.exports = {
getRepoList
}
用户选择模版
再在 bin 下新建 Generator.js 来专门处理 模版相关
// bin/Generator.js
const ora = require('ora')
const { getRepoList } = require('./https')
const inquirer = require('inquirer')
class Generator {
constructor(name, targetDir) {
// 文件夹名称
this.name = name
// 位置
this.targetDir = targetDir
}
async create() {
// 1)异步获取模板名称
const repo = await this.getRepo()
console.log(repo);
}
/*
获取用户选择的模版
1.远程拉取模版数据
2.用户选择所要下载的模版名称
3.return用户选择的名称
*/
async getRepo() {
const repoList = await wrapLoading(getRepoList, '获取目标模版中...')
// 空则终止执行
if (!repoList) return
// 筛选指定项目,只要目标模版系列
const repos = repoList.filter(item => item.name.indexOf('zzy-react-project') !== -1)
// 2)用户选择需要下载的模板名称
const { repo } = await inquirer.prompt({
name: 'repo',
type: 'list',
choices: repos.map(item => item.description),
message: '请选择一个模版进行创建'
})
// 3. return用户选择
const selectRepos = repos.filter(item => item.description === repo)[0]
return { name: selectRepos.name, branch: selectRepos.default_branch }
}
}
// 添加加载动画
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 {
// 失败
spinner.fail('请求失败,请重试...')
return null
}
}
module.exports = Generator
在 create.js 中引入并执行
// create.js
const Generator = require('./Generator')
// ...some code
module.exports = async function (name, options) {
// ...some code
// 最后询问完成之后进行调用
const generator = new Generator(name, targetDir)
generator.create()
}
保存执行之后,最终得到以下内容:
选择后者
这里已经拿到了目标模版的名称和分支
下载远程模版(Generator.js)
下载github模版需要download-git-repo插件,但是它不支持promise化,这里需要另一个插件将它转化为promise方式的,它是promisify
yarn add download-git-repo -D
yarn add util -D
对downloadGitRepo promise化处理
// Generator.js
const downloadGitRepo = require('download-git-repo')
const util = require('util')
// ...some code
class Generator {
constructor(name, targetDir) {
// ...some code
// 对download-git-repo 进行promise话改造
this.downloadGitRepo = util.promisify(downloadGitRepo);
}
// ...some code
}
紧接着是下载模版的核心逻辑:
// 远程下载模版
async download({ name, branch }) {
// 拼接链接
const requestUrl = `direct:https://github.com/Weibienaole/${name}/archive/refs/heads/${branch}.zip`
// 2min 之后弹出,显示超时
timer = setTimeout(() => {
clearTimeout(timer)
throw ('模版下载超时,请重试!')
}, 1000 * 60 * 2);
// 调用下载方法,进行远程下载
await wrapLoading(
this.downloadGitRepo,
'正在下载目标模版中...',
requestUrl,
path.resolve(process.cwd(), this.targetDir)
)
}
async create() {
// 1)获取模板名称
const repo = await this.getRepo()
// 同步对指定模版进行下载
await this.download(repo)
console.log('模版下载成功!');
clearTimeout(timer)
}
到这里就在指定目录下远程下载了模版!
下载下的内容:
到这里就结束了,发布npm再次不阐述,自行搜索发布,很简单的