背景
鉴于当前开发项目越来越多,随之而来带来一个新的问题,每初始化一个新的项目都要把之前老的项目copy下来,删除git文件,以及一大堆毫无关系的业务文件,工作量大不说还有可能出错,工作效率及其低下,为了提高工作效率以及省去不必要的重复性工作,开发一个前端脚手架势在必行。
实现以下功能
- 选择对应的模版并下载
- 自动安装项目模版依赖
工具准备
说白了开发一个脚手架其实就是各种工具库的应用,站在前人的肩膀上,加速开发效率
- Commander 完整的node js命令行解决方案
- Inquirer 通用的交互式命令行用户界面
- Ora 命令行加载效果
- Download-git-repo 从gitlab或者github下载代码
- Shelljs Node js 扩展,用于实现 Unix shell 命令执行
- Chalk 美化终端字体样式
- cross-spawn 执行项目安装依赖
- fs 读取文件
- log-symbols node下终端展示图标
脚手架实现
项目结构
├── dist
├── ... //生成文件
├── commands
├── init.js // 脚手架初始化
├── config
└── index.js // 常量配置
├──utils
├── clone.js // 下载模版
├── install.ts // 安装依赖
├── constant.js // 常量文件
├── .babelrc //babel配置文件
└── package.json //包管理
- 初始化项目 npm init ,如下配置并安装下列依赖
{
"name": "du-template-cli", // 脚手架发布名称
"version": "1.0.0", // 发布版本,每发布一次,version必须更改
"description": "前端脚手架工具",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"type": "module",
"author": "jikaibo",
"license": "ISC",
"bin": {
"du-template-cli": "index.js" // 定义命令名和关联的执行文件,通过bin字段添加
},
"dependencies": {
"chalk": "^4.1.2",
"commander": "^8.2.0",
"cross-spawn": "^7.0.3",
"download-git-repo": "^3.0.2",
"fs": "0.0.1-security",
"inquirer": "^8.2.0",
"log-symbols": "^5.0.0",
"ora": "^6.0.1",
"shelljs": "^0.8.4"
}
}
- 项目根目录下新建index.js文件,并将index.js设置为可执行文件
#!/usr/bin/env node
// 注意:这并不是注释,而是声明在node环境下执行此文件,这行必须添加
import Commander from 'commander';
import { VERSION } from './utils/constants.js'
import { initAction } from './commands/init.js'
Commander.usage('<command> [options]');
Commander.version(VERSION).option('-v,--version','查看版本号'); // 查看版本号
Commander
.command('init <name>') // 定义init子命令,<name>为必需参数可在action的function中接收,如需设置非必需参数,可使用中括号
.option('-d, --dev', '获取开发版') // 配置参数,简写和全写中使用,分割
.description('创建项目') // 命令描述说明
.action(initAction)
// 利用commander解析命令行输入,必须写在所有内容最后面
Commander.parse(process.argv);
- 新建commands并在此下面新建init.js文件,用于初始化脚手架
import logSymbols from 'log-symbols';
import shell from 'shelljs';
import {clone} from '../utils/clone.js';
import inquirer from 'inquirer'; // 获取用户输入内容
import fs from 'fs'
import chalk from 'chalk'
import { BRANCH, remoteUrlArr } from '../utils/constants.js'
import { questions,installTool } from '../config/index.js'
import { install } from '../utils/install.js'
import path from 'path'
let branch = BRANCH;
let remote = '';
const initAction = async (name, option) => {
// 0. 检查控制台是否可以运行`git `,
if (!shell.which('git')) {
console.log(logSymbols.error, '对不起,git命令不可用!');
shell.exit(1);
}
// 1. 验证输入name是否合法
if (fs.existsSync(name)) {
console.log(logSymbols.warning,`已存在项目文件夹${name}!`);
return;
}
if (name.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) {
console.log(logSymbols.error, '项目名称存在非法字符!');
return;
}
// 2. 获取option,确定模板类型(分支)
if (option.dev) branch = 'develop';
// 3 模版操作指令
const answers = await inquirer.prompt(questions)
const { type } = answers;
remoteUrlArr.forEach(item => {
if(item.type === type) {
remote = item.url;
}
})
let confirm = await inquirer.prompt([
{
type: 'confirm',
message: '确认创建?',
default: 'Y',
name: 'isConfirm'
}
]);
if (!confirm.isConfirm) return false;
// 4. 下载模板
await clone(`direct:${remote}#${branch}`, name, { clone: true });
// 5. 填写模版中的package.json文件基础信息
const fileName = `${name}/package.json`;
if(fs.existsSync(fileName)) {
const data = fs.readFileSync(fileName).toString();
let json = JSON.parse(data);
json.name = answers.name;
json.author = answers.author;
json.description = answers.description;
json.stageCode = answers.stageCode
//修改项目文件夹中 package.json 文件
fs.writeFileSync(fileName, JSON.stringify(json, null, '\t'), 'utf-8');
}
// 6. 安装依赖文件
const installAnswers = await inquirer.prompt(installTool)
await install({
cwd: path.join(process.cwd(), name),
package: installAnswers.package,
}).then(() => {
console.log(chalk.cyan('依赖安装完成'))
console.log(chalk.cyan('cd'), name)
console.log(`${chalk.cyan(`${installAnswers.package} start`)}`)
})
// 7. 清理文件
const deleteDir = ['.git', 'docs']; // 需要清理的文件
const pwd = shell.pwd();
deleteDir.map(item => shell.rm('-rf', pwd + `/${name}/${item}`));
};
export {
initAction
}
总结:此文件大概做了以下几件事
- 判断控制台是否安装了git,并对输入的一些字符做了校验。
- 确定模版分支,由于本项目的前端模版是放在远端的git上,所以确定模版分支,还有另一种做法是将模版放到本工程中,这样做法的优点可以加快模版速度,但不好维护,模版文件修改脚手架要重新发布,这里并不推荐,而是将前端模版单独放到远程git上方便维护。
- 通过inquirer读取模版操作指令,通过交付命令选择对应的前端模版并下载,目前提供两种前端模版,即React-Admin 和 Vue-Element-Admin。
- 通过clone方法下载前端模版。
- 模版下载成功后,向模版中的package.json文件中写入基础信息,比如项目名称(name)、项目描述(description)、作者(author)、项目code(stageCode)
- 模版文件写入成功后,这时候可以安装模版所需要的依赖文件了
- 清理文件:清理模版文件中的git信息及docs信息。
- 新建utils文件夹并新建clone文件,用于从远端下载模版文件
import download from 'download-git-repo';
import symbols from 'log-symbols'; // 用于输出图标
import ora from 'ora'; // 用于输出loading
import chalk from 'chalk'; // 用于改变文字颜色
const clone = function (remote, name, option) {
const downSpinner = ora('正在下载模板...').start();
return new Promise((resolve, reject) => {
download(remote, name, option, err => {
if (err) {
downSpinner.fail();
console.log(symbols.error, chalk.red(err));
reject(err);
return;
};
downSpinner.succeed(chalk.green('模板下载成功!'));
resolve();
});
});
};
export {clone}
- 在utils下新建install文件,用于下载模版项目依赖
import spawn from 'cross-spawn'
export const install = async (options) => {
const cwd = options.cwd
return new Promise((resolve, reject) => {
const command = options.package
const args = ['install', '--save', '--save-exact', '--loglevel', 'error']
const child = spawn(command, args, {
cwd,
stdio: ['pipe', process.stdout, process.stderr],
})
child.once('close', (code) => {
if (code !== 0) {
reject({ command: `${command} ${args.join(' ')}`,})
return
}
resolve();
})
child.once('error', reject)
})
}
- 在utils下新建constants文件,用于保存方法中用到的常量
import { readFile } from 'fs/promises';
const json = JSON.parse(await readFile(new URL('../package.json', import.meta.url)));
const { version } = json;
//当前 package.json 的版本号
export const VERSION = version;
// 拉去模版分支
export const BRANCH = 'master';
// 远端git地址
export const remoteUrlArr = [
{
url:'git@github.com:marmelab/react-admin.git',
type:'react',
},
{
url:'git@github.com:PanJiaChen/vue-element-admin.git',
type:'vue',
},
];
- 新建一个config文件夹并在此下面新建一个index文件,用于单独保存交互式命令
// 定义需要询问的问题
const questions = [
{
type: 'input',
message: '请输入项目名称:',
name: 'name',
validate(val) {
if (!val) return '模板名称不能为空!';
if (val.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) return '模板名称包含非法字符,请重新输入';
return true;
}
},
{
type: 'input',
message: '请输入项目简介:',
name: 'description'
},
// 项目编码可以不用,此 stageCode 是本公司统一系统注册用的
{
type: 'input',
message: '请输入项目编码:',
name: 'stageCode'
},
{
type: 'input',
message: '请输入项目作者:',
name: 'author'
},
{
type: 'list',
message: '请选择项目类型:',
choices: ['react','vue'],
name: 'type'
}
];
const installTool = {
name: 'package',
type: 'list',
message: '请选择安装工具',
choices: ['npm', 'yarn'],
default: 'npm',
}
export {
questions,
installTool
}
到此,一个脚手架基本开发完成了,上述的两个功能已经完成,可以本地测试一下,在项目根目录下执行 npm link可以将du-template-cli命令链接到全局环境中,就可以用du-template-cli来执行相关命令了。
发布到npm
-
npm adduser: 输入Username、Password、Email完成注册信息
-
npm login 完成登陆认证
-
npm publish 完成发布(注意:每次发布版本信息必须修改,即version信息)
-
发布完成后就可以通过全局安装命令来安装了,此脚手架已经上传到npm了(du-template-cli)