脚手架构建
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 node
const { 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