从0到1手撸前端脚手架

672 阅读2分钟

最终效果

  • 实现前端脚手架,期望能达到在命令行里初始化工程 image.png

背景

  • 前端有多套模版,比如:PC端、H5端、小程序、React版本、Vue版本等
  • 初始化工程时,需要从模版仓库里git clone或者fork一份到工程仓库进行开发

问题

  • 需要找到对应的模版地址,然后才能初始化工程
  • git clone后,需要修改仓库地址,还要修改工程的基本信息,比如名字等
  • 开发效率低

解决方案

  • 前端脚手架命名create-app,在命令行里执行create-app <project-name>,让用户回答问题最终创建工程

相关依赖

具体实现

  • npm init初始化脚手架工程create-app
  • 工程目录结构,各个模块指责单一,易维护 image.png
  • package.json
    • 添加create-app命令,映射到脚本文件
"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;
  • src/tmlRepos.js
// 添加模版
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 initgit 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();
        //remote add origin
        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
};

总结

  • 通过编写前端脚手架,不仅提高开发效率,还能提高自己的技能库,对之后的开发很有帮助
  • 源码