👽 概论
前边和大家讲了很多的脚手架构思、设计上的知识,今天正式进入代码开发阶段。如果你对理论文字不感兴趣,那么从这一篇开始跟进也可以完成属于自己的脚手架!
👽 项目初始化
创建好项目文件夹之后,在其中执行初始化命令npm init -y,依次建立如下的目录结构:
│ .babelrc --babel配置文件
│ package.json
│ README.md
│
├─bin --cmd命令文件夹
│ cmd --关键cmd
│
├─src --脚手架逻辑文件夹
│ main.js --核心代码
│
└─template --模板仓库
然后依次安装如下依赖:
npm i babel-cli babel-preset-env chalk commander fs-extra inquirer log-symbols ora shelljs
这些依赖的作用已经在上篇文章介绍过了,感兴趣的可以查看了解。
👽 打包配置
在package.json中写入打包命令和bash执行命令,同时更改脚手架名称:
"name": "cccli",
"scripts": {
"build": "babel src -d dist",
"watch": "npm run build -- --watch"
},
"bin": {
"cccli": "./bin/cmd"
},
在.babelrc中写入打包配置:
{
"presets": [
[
"env",
{
"targets": {
"node": "current"
}
}
]
]
}
👽 脚本引入
在cmd中做两件事情:(1)指定运行环境;(2)引入逻辑js。
#!/usr/bin/env node
require('../dist/main.js');
完成后,可以在main.js中随便写点语句,执行打包命令npm run watch。
console.log('脚手架测试!')
然后在项目根目录下执行命令npm link,可以将项目中的bash命令临时注册到全局环境,方便测试。
弹出以下命令即为注册成功:
之后我们执行bash命令:cccli,即可看到main.js中输出语句。
👽 定义脚手架主界面
不太明白主界面是什么的同学,可以在安装vue-cli后,输入命令vue,显示的第一个页面即为主界面。主界面往往包含脚手架版本介绍、功能列表展示等内容。
此处的核心是commander这个库,因为官方文档有中文,而且说明也很详细,此处就不多介绍了。
/*
* @FileName: main.js
* @Description:脚手架入口文件
*/
const program = require('commander');
const chalk = require('chalk');
//注册版本
program.version(require('../package').version);
//注册create功能
program
.command('create <appName>')
.description('创建一个新项目')
.action((name) => {
if (process.argv.slice(3).length > 1) {
console.log(chalk.yellow('\n 文件名中请勿包含空格!'));
}
//为方便管理,create功能的详细逻辑单独放在如下文件中管理
const create = require('./command/create');
create(name);
});
program.parse(process.argv);
完成后再执行cccli,就可以看到界面已经不一样了:
👽 create功能的逻辑
建立对应的文件,在其中主要做两件事:(1)检查文件名是否重复,并做相应处理;(2)根据用户选择,生成相应模板。
🚩文件名检查
/*
* @FileName: create.js
* @Description:cli-create命令
*/
const fs = require('fs-extra');
const chalk = require('chalk');
const inquirer = require('inquirer');
async function create(appName) {
//判断名称是否重复
const nameRepeat = fs.existsSync(`./${appName}`);
//判断是与文件名还是文件夹名重复
const isFile = nameRepeat ? fs.statSync(`./${appName}`).isFile() : false;
//定义交互项
const newPrompts = {
fileRepeat: {
name: 'fileRepeat',
type: 'list',
message: `当前路径中已存在${isFile ? '文件' : '文件夹'}${chalk.bgBlue(
' ' + appName + ' '
)},请确认操作:`,
choices: [
{
name: '覆盖',
value: 1,
},
{
name: '退出',
value: 0,
},
],
},
};
//检查名称是否重复
if (nameRepeat) {
const { fileRepeat } = await inquirer.prompt(newPrompts['fileRepeat']);
//选择退出后中断程序
if (!fileRepeat) return;
}
}
//将create作为函数导出
module.exports = (...args) => {
return create(...args).catch(err => {
console.log(chalk.red('项目创建出错:', err));
});
};
完成后测试结果如图:
🚩询问模板类型
接下来我们设计模板类型选择界面,此处将模板分为Web/H5和小程序两个大类,Web/H5下又分后台管理系统模板和基础模板两个小类。
/*
* @FileName: create.js
* @Description:cli-create命令
*/
...
async function create(appName) {
···
//定义交互项
const newPrompts = {
···
appType: {
name: 'appType',
type: 'list',
message: `请选择应用类型:`,
choices: [
{
name: 'Web/H5应用',
value: 'web',
},
{
name: `小程序应用`,
value: 'wx',
},
],
},
scaffoldType: {
web: {
name: 'scaffoldType',
type: 'list',
message: `请选用预设模板类型:`,
choices: [
{
name: `${chalk.bold(
'后台管理系统模板'
)} (Vue2 + Vuex + Vue-Router + axios + less + ele + dayjs)`,
value: 'admin',
},
{
name: `${chalk.bold('基础模板')} (Vue2 + Vuex + Vue-Router + axios + less)`,
value: 'basic',
},
],
},
wx: {
name: 'scaffoldType',
type: 'list',
message: `请选用预设模板类型:`,
choices: [
{
name: `UniApp(Vue2 + Vuex + Vue-Router + uView)${chalk.red(
'推荐使用HbuilderX运行发包'
)}`,
value: 'uni',
},
],
},
},
};
···
//询问应用类型
const { appType } = await inquirer.prompt(newPrompts['appType']);
//询问模板类型
const { scaffoldType } = await inquirer.prompt(newPrompts['scaffoldType'][appType]);
//输出结果以便测试
console.log('appType: ', appType);
console.log('scaffoldType : ', scaffoldType);
}
//将create作为函数导出
module.exports = (...args) => {
return create(...args).catch(err => {
console.log(chalk.red('项目创建出错:', err));
});
};
完成后执行,可以看到输出了用户选择的值:
🚩 代码拆分
到这一步我们发现,如果所有的代码都写在这一个文件里,代码整体定会十分混杂。为阅读维度清晰,我们做一定拆分:(1)交互项对象newPrompts作为配置文件拆分至src/config/prompts中;(2)后续如有功能函数,我们将其放入src/utils中。
重新组织代码如下:
/*
* @FileName: create.js
* @Description:cli-create命令
*/
const fs = require('fs-extra');
const shell = require('shelljs');
const chalk = require('chalk');
const inquirer = require('inquirer');
const prompts = require('../config/prompts');
async function create(appName) {
const nameRepeat = fs.existsSync(`./${appName}`);
const isFile = nameRepeat ? fs.statSync(`./${appName}`).isFile() : false;
const newPrompts = prompts(isFile, appName);
//检查名称是否重复
if (nameRepeat) {
const { fileRepeat } = await inquirer.prompt(newPrompts['fileRepeat']);
if (!fileRepeat) return;
}
//询问应用类型
const { appType } = await inquirer.prompt(newPrompts['appType']);
//询问模板类型
const { scaffoldType } = await inquirer.prompt(newPrompts['scaffoldType'][appType]);
}
module.exports = (...args) => {
return create(...args).catch(err => {
console.log(chalk.red('项目创建出错:', err));
});
};
/*
* @FileName: prompts.js
* @Description:交互项
*/
const chalk = require('chalk');
const PROMPTS = (isFile, appName) => {
return {
fileRepeat: {
name: 'fileRepeat',
type: 'list',
message: `当前路径中已存在${isFile ? '文件' : '文件夹'}${chalk.bgBlue(
' ' + appName + ' '
)},请确认操作:`,
choices: [
{
name: '覆盖',
value: 1,
},
{
name: '退出',
value: 0,
},
],
},
appType: {
name: 'appType',
type: 'list',
message: `请选择应用类型:`,
choices: [
{
name: 'Web/H5应用',
value: 'web',
},
{
name: `小程序应用`,
value: 'wx',
},
],
},
scaffoldType: {
web: {
name: 'scaffoldType',
type: 'list',
message: `请选用预设模板类型:`,
choices: [
{
name: `${chalk.bold(
'后台管理系统模板'
)} (Vue2 + Vuex + Vue-Router + axios + less + ele + dayjs)`,
value: 'admin',
},
{
name: `${chalk.bold('基础模板')} (Vue2 + Vuex + Vue-Router + axios + less)`,
value: 'basic',
},
],
},
wx: {
name: 'scaffoldType',
type: 'list',
message: `请选用预设模板类型:`,
choices: [
{
name: `UniApp(Vue2 + Vuex + Vue-Router + uView)${chalk.red(
'推荐使用HbuilderX运行发包'
)}`,
value: 'uni',
},
],
},
},
};
};
module.exports = PROMPTS;
🚩 模板生成
在src/utils/generateFile中定义文件、文件夹生成函数:
/*
* @FileName: generateFile.js
* @Description:文件、文件夹生成函数
*/
const fs = require('fs-extra');
const ora = require('ora');
//加载等待效果
const oraIns = ora({
text: '努力处理中···',
spinner: 'dots',
color: 'yellow',
interval: 150,
});
function generateFile(templatePath, appName, isFile) {
//解析模板地址
const resourcePath = process.argv[1].replace('\\bin\\cmd', templatePath);
//定义输出地址
const outputPath = process.cwd() + '/' + appName;
//加载动画开始
oraIns.start();
//存在重名文件时先将其删除
isFile && fs.removeSync(outputPath);
//克隆文件夹
fs.copySync(resourcePath, outputPath);
//加载动画开始
oraIns.stop();
}
module.exports = generateFile;
注入create功能中。
/*
* @FileName: create.js
* @Description:cli-create命令
*/
···
const generateFile = require('../utils/generateFile');
async function create(appName) {
···
//据询问结果拼接模板路径
const templatePath = `/template/${appType}/${scaffoldType}`;
//克隆模板至本地
generateFile(templatePath, appName, isFile);
}
···
至此脚手架的核心功能就已经完成了!下期我们再讲怎么把现在已经做好的脚手架发布到NPM,敬请期待!