1. 引言
背景介绍:
前端脚手架的目的在于提高前端开发的效率和一致性 ,节约一些重复性工作,例如:编译、打包、代码检查,避免了从零开始搭建项目框架的繁琐工作。
需求分析:
- 项目创建与初始化:
- 创建新的项目目录结构。
- 初始化基本的项目文件,如
index.html
、package.json
等。
- 依赖安装:
- 自动安装项目所需的核心依赖。
- 支持选择性安装额外的依赖或插件。
- 用户交互:
- 提供命令行界面,让用户能够输入项目配置信息。
- 根据用户输入生成定制化的项目配置。
- 模板生成:
- 根据用户的选择生成基础的项目模板。
- 支持多种预设模板以适应不同的项目类型。
- 架构设计:
- 包括命令行解析模块、模板管理模块、文件生成模块等,并解释每个模块的作用和它们之间的交互。
2. 相关工具介绍
(📗代表核心工具,📒代表辅助工具)
命令行交互
- 📗 commander:Commander.js 中文网
- 📗 inquirer:选项命令行交互工具
- 📒 chalk:美化命令行输出
- 📒 figlet:创建 ASCII 艺术字
- 📒 ora:加载指示器
下载模板
- 📗 git-clone:用于创建任何可通过网络访问的 Git 仓库的副本
- 📗 npminstall: 可以用来安装依赖包,将安装的依赖包名称及版本信息添加到项目的 package.json 文件中
文件操作
- 📗 fs-extra:扩展 Node.js 原生 fs 模块功能的库,支持 Promise、防止 EMFILE 错误,并包含复制、移动、确保目录存在等常用操作
4. 实现脚手架
以下是一个简单的脚手架结构示例:
scaffold/
├── package.json
├── jsconfig.json
├── babel.config.js
└── bin/├── index.js
└── src/├── commands├── create.js└── ...├── lib├── consts.js├── index.js├── logger.js├── spinner.js└── ...
创建 package.json
在 package.json
文件中,你需要定义脚手架的元数据和依赖项:
{"name": "demo-cli","version": "1.0.0","description": "","main": "index.js","bin": "./bin/index.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"keywords": [],"author": "","license": "ISC","dependencies": {"chalk": "^4.0.0","commander": "^12.1.0","figlet": "^1.7.0","fs-extra": "^11.2.0","git-clone": "^0.2.0","inquirer": "^8.0.0","npminstall": "^7.6.0","ora": "^5.0.0"},"devDependencies": {"@babel/cli": "^7.23.4","@babel/core": "^7.25.2","@babel/plugin-proposal-object-rest-spread": "^7.20.7","@babel/preset-env": "^7.24.7"}
}
实现 index.js
index.js
是脚手架的入口文件,它将处理用户输入和脚手架的逻辑流程:
#!/usr/bin/env nodeconst { program } = require('commander');
const pkg = require('../package.json');
const chalk = require('chalk');
const inquirer = require('inquirer');
const ora = require('ora');
const figlet = require('figlet');
const { info, warn, error } = require('../src/lib/logger');
const { startSpinner, succeedSpiner, failSpinner } = require('../src/lib/spinner');
const createAction = require('../src/commands/create');// figlet('KFC Crazy Thursday V Me 50 !!!', (err, data) => {
// if (err) {
// console.log('Something went wrong...');
// console.dir(err);
// return;
// }
// console.log(chalk.green(data));
// });// const spinner = ora('loading ...').start();program.name('demo-cli').usage('<command> [options]');
program.version(`v${pkg.version}`);
program.on('--help', () => {console.log();console.log(' Examples:');console.log();console.log(' $ demo-cli create <project-name>');console.log(' $ demo-cli <command> [options]');
});
program.command('create <name>').description('create a new project').action((name) => {createAction(name);});program.parse(process.argv);// const options = program.opts();
// console.log(options);// console.log('肯德基疯狂星期四,V我50!');
实现 create.js
create.js
它将处理用户输入和脚手架的逻辑流程:
const path = require('path');
const fs = require('fs-extra');
const inquirer = require('inquirer');
const chalk = require('chalk');
const { startSpinner, succeedSpiner, failSpinner } = require('../lib/spinner');
const { info, warn, error } = require('../lib/logger');
const { cwd, reactRepo, vueRepo } = require('../lib/consts');
const gitCLone = require('git-clone');const checkProjectExist = async (targetDir) => {if (fs.existsSync(targetDir)) {const answer = await inquirer.prompt({type: 'list',name: 'checkExist',message: `\n仓库路径${targetDir}已存在,请选择`,choices: ['覆盖', '取消'],});if (answer.checkExist === '覆盖') {warn(`删除${targetDir}...`);fs.removeSync(targetDir);} else {return true;}}return false;
};const getQuestions = async (projectName) => {return await inquirer.prompt([{type: 'input',name: 'name',message: `project name: (${projectName})`,default: projectName,},{type: 'rawlist',name: 'frame',message: '选择所用框架:',choices: ['React', 'Vue'],default: 'React',},{type: 'checkbox',name: 'enosDependencies',message: '请选择所用依赖:',choices: [{ name: 'antd', value: 'antd' },{ name: '***SDK', value: '***SDK' },],},// {// type: 'confirm',// name: 'antd',// message: '是否集成antd:',// default: false,// },]);
};const createProject = async (targetDir, projectInfo) => {console.log(projectInfo);const repo = projectInfo.frame === 'React' ? reactRepo : vueRepo;startSpinner('下载react仓库...');gitCLone(repo, targetDir, { checkout: 'main' }, (err) => {if (err) {failSpinner(err);return;}succeedSpiner(`项目创建完成 ${chalk.yellow(projectInfo.name)}\n👉 输入以下命令进入项目:`);info(`$ cd ${projectInfo.name}\n `);});
};const action = async (projectName, cmdArgs) => {try {const targetDir = path.join((cmdArgs && cmdArgs.context) || cwd, projectName);if (!(await checkProjectExist(targetDir))) {const projectInfo = await getQuestions(projectName);await createProject(targetDir, projectInfo);}} catch (err) {failSpinner(err);return;}
};module.exports = action;
5. 测试脚手架
- 本地测试:
- 使用
npm link
进行本地测试,以便在全局环境中测试你的脚手架。 - 然后,你可以使用以下命令来创建新项目:
- 使用
demo-cli create test-project