上一篇文章我们确定了 项目的目标及方案,最后也搭建了一个基础的项目并关联了基础的命令。接下来我们会着手与核心的创建流程的对话设计,并生成逻辑进行封装。
代码地址:github.com/ZhahaSy/sy-... 欢迎Star fork
对话式流程
生成流程大概是怎样的呢?我画了一张简图
如图,整个create 命令可以拆分为三个功能: 生成配置功能
, 创建项目功能
, 下载模板功能
。 而生成配置功能
最主要的功能就是一直在设置选项供用户选择。在上篇文章中我们提到了inquirer
用于做对话,为保证篇幅,不提供详细介绍,大家可以自行学习!
这篇文章讲了inquirer的基础使用。
需要注意的是,最新版本的inquirer只支持 es6 module方案。但我们这个系列选择的是commonjs方案。大家在下载依赖的时候需要把inquirer改为8.x.x
使用之前请执行以下命令。
shell
cd packages/cli
pnpm i inquirer@8
pnpm i chalk@4.1.1
pnpm i fs-extra
基础使用
js
const { prompt } = require('inquirer');
(async() => {
// questions 可以传递对象也可以传递对象数组
const questions = [{
type: 'list',
name: `projectType`,
message: '你要创建什么项目?
choices: [
{ name: 'web', value: 'web' },
{ name: 'service', value: 'service' },
{ name: 'template', value: 'template' },
]
}]
const answers = await prompt(questions);
console.log(answers); // { projectType: 'web' }
})()
代码结构拆分
通过上面流程图可以看出来,整个对话流程存在多个选择对应不同的问题。而inquirer并没有提供分支能力,所以我们需要自己控制问题分支。好在现在的流程不算复杂,我们本章节先实现一个简单版本的分支控制器。
下面我们先实现全流程通用的文件夹重名流程。
1.判断文件名是否存在
还记得在
cli/bin/sy.js
文件的 create命令中的--force
后缀吗?force 翻译过来就是 强制的意思。在我们这里用于项目文件夹重名时强制删除旧文件夹。 如果没有设置force则会询问用户是否要替换。
下面是代码实现:
js
const cwd = options.cwd || process.cwd() // 获取当前工作目录
const targetDir = path.resolve(cwd, projectName || '.') // 解析目标目录路径
// 检查目标目录是否已存在
if (fs.existsSync(targetDir)) {
// 使用逻辑运算符简化判断
if (!options.force) {
const { isOverwrite } = await ask({
type: 'confirm',
name: 'isOverwrite',
message: `当前路径下,项目 ${chalk.cyan(projectName)} 已存在,是否覆盖?`,
});
if (!isOverwrite) return; // 早期返回减少嵌套
}
console.log(`\nRemoving ${chalk.cyan(targetDir)}...`);
await fs.remove(targetDir);
console.log(`\nRemove ${chalk.cyan(targetDir)} success!`);
}
2. 询问项目类型
在上面的流程图中,我们期望脚手架提供
web
,service
,template
三种项目类型,而不同的项目类型还对应着不同的对话分支,所以我们还是选择单独创建一个对话来控制分支
下面是代码实现:
js
// 询问用户要创建的项目类型
const { projectType } = await ask([{
type: 'list',
name: 'projectType',
message: '你要创建什么项目?',
choices: [
{ name: 'web', value: 'web' },
{ name: 'service', value: 'service' },
{ name: 'template', value: 'template' },
]
}]);
3. 创建不同项目类型的promptOption
接下来就得创建不同项目类型的问题配置了。透过流程图可以看到,后面的问题都是插拔或者注入了,那么就可以把每个项目类型的问题配置都是单独的文件用于设置问题,公共的问题就抽出来一个公共的问题配置。
web
js
const defaultPrompt = require('./default')
module.exports = [
{
type: 'list',
name: `frameName`,
message: '你要使用什么框架',
choices: [
{ name: 'vue', value: 'vue' },
{ name: 'react', value: 'react' },
]
},
...defaultPrompt,
]
service
js
const defaultPrompt = require('./default')
module.exports = [
{
type: 'list',
name: `frameName`,
message: '你要使用什么框架',
choices: [
{ name: 'express', value: 'express' },
{ name: 'koa', value: 'koa' },
]
},
...defaultPrompt,
]
default
js
module.exports = [
{
type: 'confirm',
name: `isUseTs`,
message: '是否使用Ts',
},
{
type: 'confirm',
name: `isLinter`,
message: '是否接入tslint,eslint,commitLint',
},
]
template
tips: 当前 git下载并非最终版本,还需要鉴权,登录git 等操作。这只是一个简单的模板。并非最终版本,只是为了理清思路
js
module.exports = [
{
type: 'list',
name: `templateUrl`,
message: '请选择要下载的模板',
choices: [
{ name: 'vue + mobile', value: 'vue-m' },
{ name: 'react + mobile', value: 'react-m' },
]
},
...defaultPrompt,
]
调用map
最终我们应该通过一个对象或map整理所有的项目配置。projectType是我们在询问项目类型 的时候得到的。接下来就可以调用inquire.prompt方法来链式询问了。
js
// 根据用户选择的项目类型,获取相应的配置选项并显示
const result = await ask(promptOptions[projectType]);
console.log('配置', { projectType, ...result });
最终对话结果
最终我们会得到如下图的一个配置对象。至此,本章要实现的功能已经实现了!
总结一下
开发中不要过分关注代码封装问题。先保做最基础的封装,不要过度设计,增加心理负担。