最终效果
- 实现前端脚手架,期望能达到在命令行里初始化工程

背景
- 前端有多套模版,比如:PC端、H5端、小程序、React版本、Vue版本等
- 初始化工程时,需要从模版仓库里
git clone或者fork一份到工程仓库进行开发
问题
- 需要找到对应的模版地址,然后才能初始化工程
git clone后,需要修改仓库地址,还要修改工程的基本信息,比如名字等
- 开发效率低
解决方案
- 前端脚手架命名
create-app,在命令行里执行create-app <project-name>,让用户回答问题最终创建工程

相关依赖
- node 模块
- 文件操作
fs
- 路径操作
path
git命令需要开启子进程child_process
- npm 依赖
具体实现
npm init初始化脚手架工程create-app
- 工程目录结构,各个模块指责单一,易维护

package.json
"bin": {
"create-app": "./bin/www.js"
}
bin/www.js
- 脚本文件,不负责逻辑,主要逻辑在
src/main.js
#!/usr/bin/env node
require('../src/main');
src/main.js
main.js里负责主要逻辑,获取到用户希望创建的工程名称<project-name>,检测是否有同名文件。然后用userAnswers模块,获取工程的基本信息;用downloadRepo模块下载模版;projectUtils负责修改工程的信息,和按照依赖;git模块负责初始化工程的git
- 引入各个模块,把各个模块按照主流程串联起来。流程如上图所述,这里不在赘述
const { Command } = require('commander');
const fs = require('fs');
const { version } = require('../package.json');
const userAnswers = require('./userAnswers');
const projectUtils = require('./project');
const downloadRepo = require('./downloadRepo');
const git = require('./git');
const { message } = require('./utils');
const program = new Command();
program
.version(version)
.arguments('<project-name>')
.description('create a project in <project-name> folder')
.action(async (project) => {
if (fs.existsSync(project)) {
message.error(`${project} 已存在`);
return false;
}
const answers = await userAnswers();
const dowloadRes = await downloadRepo(answers.templateRepo, project);
if (!dowloadRes) return;
const projectInit = projectUtils(project);
await projectInit.setting({
name: answers.projectName,
version: answers.version
});
await git(project, answers.gitRepo);
projectInit.npmInstall();
});
program.parse(process.argv);
src/userAnswers.js
- 通过这一步,可以获得用户选择的模版、工程的名称、版本号以及工程的
git地址等信息
- 引入
inquirer,负责在命令行里询问用户问题,并获取答案
src/tmlRepos 是模版名称和git地址的映射
const inquirer = require('inquirer');
const tmlRepos = require('./tmlRepos');
const userAnswers = async () => {
let answers = await inquirer.prompt([
{
type: 'list',
name: 'template',
message: '请选择模版',
choices: Object.keys(tmlRepos)
},
{
type: 'input',
name: 'projectName',
message: '请输入工程名称'
},
{
type: 'version',
name: 'version',
message: '请输入版本号'
},
{
type: 'input',
name: 'gitRepo',
message: '请输入git仓库的地址'
}
]);
const templateRepo = tmlRepos[answers.template];
answers.templateRepo = templateRepo;
return answers;
};
module.exports = userAnswers;
const tmlRepos = {
H5: 'github.com:JX-Zhuang/create-app.git',
PC: 'github.com:JX-Zhuang/create-app.git',
React:'xxx',
Vue:'xxx'
};
module.exports = tmlRepos;
src/downloadRepo.js
- 获取到模版的
git地址和工程名字后,通过这一步,用downloadRepo方法下载到目标目录
download-git-repo负责下载模版
ora负责在命令行里展示loading的效果,最终改为成功或失败的样式
const ora = require('ora');
const { promisify } = require('util');
const { message } = require('./utils');
const download = promisify(require('download-git-repo'));
const downloadRepo = async (repoUrl, dirName) => {
const spinner = ora('创建模版').start();
try {
await download(repoUrl, dirName, { clone: true });
spinner.succeed();
return true;
} catch (e) {
message.error(e);
spinner.fail();
return false;
}
};
module.exports = downloadRepo;
src/project.js
- 通过之前的操作,工程创建成功。需要设置工程的基本信息,以及安装依赖
setting里是修改工程的名称和版本号,修改package.json的内容,如果模版里没有package-lock.json,可以把设置package-lock.json的逻辑删掉
npmInstall是通过child_process创建子进程,然后安装依赖。processOnClose是处理子进程process.on('close')的逻辑,因为多个地方用到,所以抽离里工具方法
const ora = require('ora');
const spawn = require('child_process').spawn;
const fs = require('fs');
const path = require('path');
const { processOnClose } = require('./utils');
module.exports = (projectName) => {
const npmInstall = async () => {
const spinner = ora('安装依赖').start();
const process = spawn('npm', [ 'install' ], {
cwd: projectName
});
const res = await processOnClose(process);
if (res) {
spinner.succeed();
return true;
}
spinner.fail();
return false;
};
const setting = async (packageSetting) => {
const spinner = ora('设置工程信息').start();
const projectDir = path.join(process.cwd(), `${projectName}`);
const packageJSONPath = path.join(projectDir, 'package.json');
const packageJSONLockPath = path.join(projectDir, 'package-lock.json');
const packageJSON = require(packageJSONPath);
const packageJSONLock = require(packageJSONLockPath);
packageJSON.name = packageSetting.name || packageJSON.name;
packageJSON.version = packageSetting.version || packageJSON.version;
fs.writeFileSync(packageJSONPath, JSON.stringify(packageJSON, null, 2));
packageJSONLock.name = packageJSON.name;
packageJSONLock.version = packageJSON.version;
fs.writeFileSync(packageJSONLockPath, JSON.stringify(packageJSONLock, null, 2));
spinner.succeed();
return true;
};
return {
npmInstall,
setting
};
};
src/git.js
- 通过前面的步骤,工程以及完成,这一步是初始化
git。即git init和git remote add origin
const ora = require('ora');
const { processOnClose } = require('./utils');
const spawn = require('child_process').spawn;
module.exports = async function(projectName, gitRepo) {
const gitRemote = async (repo) => {
const spinner = ora('添加git仓库').start();
const process = spawn('git', [ 'remote', 'add', 'origin', repo ], {
cwd: projectName
});
const res = await processOnClose(process);
if (res) {
spinner.succeed();
return true;
}
spinner.fail();
return false;
};
const spinner = ora('初始化git').start();
const process = spawn('git', [ 'init' ], {
cwd: projectName
});
const res = await processOnClose(process);
if (res) {
spinner.succeed();
if (gitRepo) {
return gitRemote(gitRepo);
}
return true;
}
spinner.fail();
return false;
};
src/utils.js
- 工具方法
message输出成功或失败的信息
processOnClose处理子进程process.on('close')的逻辑
const chalk = require('chalk');
const log = console.log;
const message = {
error: (message) => {
log(chalk.red(message));
},
success: (message) => {
log(chalk.green(message));
}
};
const processOnClose = (process) => {
return new Promise((resolve, reject) => {
process.on('close', function(status) {
if (status === 0) resolve(true);
return resolve(status);
});
});
};
module.exports = {
message,
processOnClose
};
总结
- 通过编写前端脚手架,不仅提高开发效率,还能提高自己的技能库,对之后的开发很有帮助
- 源码