Web团队建设--自定义脚手架

Web团队建设–自定义脚手架

一个脚手架工具可以自动化的帮你完成一些项目基本结构的搭建,从而减轻新项目的初始成本,类似 vue-clicreate-react-app等,在我们的工作过程中总会碰到新项目需要沿用老项目的架构、UI风格等类似的情况,这个时候或许我们就需要自己搞一套属于我们自己的脚手架工具了,接下来便简单解析一个脚手架从0到1的过程。文章最后会附上本文涉及代码仓库地址及 npm 地址。

技术选型

这里由于前端 Node 环境都是现成的,又碰巧的 Node 可以做 IO 操作,所以我们的技术方案便选用 Node 来实现,当然还有很多其他技术也都可以选用,类似 Python 等。

环境要求

packageversion
Node.js>=v12.x
npmlatest

这里其实有好多类库存在更新的版本,但由于历史问题(我这个脚手架创建时间较早,仓库还不能完美支持新语法,也懒得搞)这里还是选择了较老的版本,同学们学习的时候可以使用新版本来搞。

插件安装

packageversiondescription
commander^9.2.0用来处理命令行携带的参数
chalk^4.1.2用于在命令行终端中输出彩色的文字
download-git-repo^3.0.2用于从 git 仓库拉取
fs-extra^10.1.0用于从本地路径复制文件至目标路径
handlebars^4.7.7模板引擎,作用不必多少,大家应该都有所了解
inquirer^8.2.3用于用户与命令行交互
log-symbols^4.1.0提供带颜色的符号
ora^5.4.1实现命令行环境的loading效果, 和显示各种状态的图标等
update-notifier^5.1.0更新通知程序,工具包有更新的话提示

思路整理

开始编码之前先整理一下我们的思路,首先我们想要的功能是从一个已有的模板项目目录拷贝派生出新的项目仓库,那么我们首先需要有一个模板配置的模块

新增模板

创建 /command/add.js 并写入新增模板的逻辑,此处我们将模板配置保存至本地的一个 templates.json 配置文件中

'use strict'
const inquirer = require("inquirer");
const config = require('../templates');
const {checkTemplateNameExit, templateSave, templateList, formatterAnswers} = require("../utils/templateUtils");
const {InvalidArgumentError} = require("commander");
const {listShowTemplates} = require("./list");
const log = require("../utils/log");

const templateObject = {
    name: '', // 配置项名称
    type: 'git', // 模板工程来源
    path: '', // 模板地址
}

/**
 * 新增模板
 * @param template {{path: string, name: string, type: 'git'|'local'}}
 * @param options {{path: string, name: string, type: 'git'|'local'}}
 */
const addTemplate = (template = templateObject, options = templateObject) => {
    const checkedObject = {
        name: options.name || template.name,
        type: options.type || template.type,
        path: options.path || template.path,
        branch: 'master',
        default: true,
    };

    checkTemplateNameExit(checkedObject.name, 'check', true)

    const buildQuestion = (value, key) => {
        return `${value && 'Make sure' || 'Input'} the ${key} for the template`
    }

    const questionList = [
        {
            name: 'name',
            default: checkedObject.name,
            type: 'input',
            message: buildQuestion(checkedObject.name, 'name'),
            validate: (input) => checkTemplateNameExit(input, 'inquirer', true),
        },
        {
            type: 'list',
            choices: ['git', 'local'],
            name: 'type',
            default: checkedObject.type,
            message: buildQuestion(checkedObject.type, 'type'),
        },
        {
            type: 'editor',
            name: 'path',
            default: checkedObject.path,
            message: buildQuestion(checkedObject.path, 'path'),
            validate: (input) => {
                if (!input.trim()) throw new InvalidArgumentError('Template path is required!');
                return true;
            }
        },
        {
            name: 'branch',
            default: 'master',
            message: buildQuestion(checkedObject.branch, 'branch'),
            validate: (input) => {
                if (!input.trim()) throw new InvalidArgumentError('Template branch is required!');
                return true;
            },
            when: (answer) => answer.type === 'git'
        },
        {
            type: 'confirm',
            name: 'default',
            default: true,
            message: 'Do you want to set it as the default option'
        }
    ];

    inquirer
        .prompt(questionList)
        .then(answers => {

            formatterAnswers(answers);

            if (answers.default) {
                templateList().forEach(item => item.default = false);
            }

            config.tpl[answers.name] = {...answers, path: answers.path.replace(/[\u0000-\u0019]/g, '')};

            templateSave(config).then(() => {
                log.log('\n');
                log.success('New template added!\n');
                log.info('The last template list is: \n')
                listShowTemplates();
                log.log('\n')
                process.exit()
            }).catch(() => {
                process.exit()
            })
        })
        .catch((error) => {
            if (error.isTtyError) {
                log.error(`Prompt couldn't be rendered in the current environment`)
            } else {
                log.error(error)
            }
        })
}

module.exports = {
    addTemplate,
}

查看模板配置

新增 /command/list.js 文件,读取并输出模板配置列表

'use strict'
const {showLogo, templateList} = require("../utils/templateUtils");
const log = require("../utils/log");

const listShowTemplates = (withLogo) => {
    withLogo && showLogo();
    log.table(templateList())
    process.exit()
}

module.exports = {
    listShowTemplates,
}

删除模板配置项

新增 /command/delete.js 文件,并写入移除模板的逻辑

'use strict'
const config = require('../templates')
const inquirer = require("inquirer");
const {checkTemplateNameExit, templateSave, templateNameList, formatterAnswers} = require("../utils/templateUtils");
const {listShowTemplates} = require("./list");
const log = require("../utils/log");

const removeTemplate = (name = '') => {
    inquirer
        .prompt([
            {
                type: 'input',
                name: 'name',
                default: name,
                message: `${name ? 'Is' : 'Input'} the template name`,
                validate: (input) => checkTemplateNameExit(input, 'inquirer', false, 'exit')
            },
            {
                type: 'confirm',
                name: 'confirm',
                message: 'Confirm to remove this template',
                default: true
            },
        ])
        .then((answers) => {
            if (answers.confirm) {

                formatterAnswers(answers);

                if (templateNameList().length > 1) {
                    delete config.tpl[answers.name];
                    templateSave(config).then(() => {
                        log.log('\n');
                        log.success('Template removed!\n')
                        log.info('The last template list is: \n')
                        listShowTemplates();
                        log.log('\n');
                        process.exit();
                    }).catch(() => {
                        process.exit();
                    })
                } else {
                    log.error('At least one template needs to be retained!\n');
                    process.exit();
                }
            } else {
                log.warning('The operation was cancelled!\n')
                process.exit()
            }
        })
        .catch((error) => {
            if (error.isTtyError) {
                log.error(`Prompt couldn't be rendered in the current environment`)
            } else {
                log.error(error)
            }
        })
}

module.exports = {
    removeTemplate,
}

核心功能

关于模板的功能我们已经添加过了, 接下来便开始实现我们的核心功能,通过接收用户输入的项目配置参数来通过模板工程创建我们想要的新项目。

添加 /command/init.js 文件,并写入以下内容

'use strict'
const config = require('../templates')
const inquirer = require("inquirer");
const os = require("os");
const fsExtra = require('fs-extra');
const {InvalidArgumentError} = require("commander");
const download = require("download-git-repo");
const handlebars = require("handlebars");
const ora = require("ora");
const {getTemplateDefault, templateNameList, showLogo, formatterAnswers} = require("../utils/templateUtils");
const log = require("../utils/log");

const createSuccess = (spinner, projectConfig) => {
    spinner.succeed();
    // 模板文件列表
    const fileName = [
        `${projectConfig.name}/package.json`,
        `${projectConfig.name}/index.html`
    ];
    const removeFiles = [
        `${projectConfig.name}/.git`
    ]
    const meta = {
        name: projectConfig.name,
        version: projectConfig.version,
        description: projectConfig.description,
        author: projectConfig.author
    }
    fileName.forEach(item => {
        if (fsExtra.pathExistsSync(item)) {
            const content = fsExtra.readFileSync(item).toString();
            const result = handlebars.compile(content)(meta);
            fsExtra.outputFileSync(item, result);
        }
    });
    // 删除多余文件
    removeFiles.forEach(item => {
        fsExtra.removeSync(item);
    })

    showLogo();
    log.success(`Successfully created project ${projectConfig.name}.`)
    log.success('Get started with the following commands:\n')
    log.bash(`cd ${projectConfig.name} && npm install`)
}

const createProjectFromGit = (projectConfig) => {
    const spinner = ora('Download from template...');
    spinner.start();
    download(`direct:${projectConfig.template.path}#${projectConfig.template.branch}`, projectConfig.name, {clone: true}, function (err) {
        if (err) {
            spinner.fail();
            log.error(err)
        } else {
            createSuccess(spinner, projectConfig)
        }
        process.exit();
    })
}

const createProjectFromLocal = (projectConfig) => {
    const spinner = ora('Creating from template...');
    spinner.start();
    fsExtra.copy(projectConfig.template.path.trim(), `./${projectConfig.name}`)
        .then(() => {
            createSuccess(spinner, projectConfig)
            process.exit();
        })
        .catch(err => {
            spinner.fail()
            log.error(err)
            process.exit();
        })
}

const initProjectFromTemplate = (projectName = 'my-app', templateName = '') => {
    inquirer.prompt([
        {
            type: 'input',
            name: 'projectName',
            default: projectName,
            message: 'Is sure the project name',
            validate: (input) => {
                if (!input) throw new InvalidArgumentError('project name is required!');
                if (fsExtra.pathExistsSync(input)) throw new InvalidArgumentError('directory already exists!');
                return true
            }
        },
        {
            name: 'version',
            default: '1.0.0',
            message: 'input the project version'
        },
        {
            name: 'description',
            default: projectName,
            message: 'input the project description'
        },
        {
            name: 'author',
            default: os.userInfo().username,
            message: 'input the project author'
        },
        {
            type: 'confirm',
            name: 'defaultTemplate',
            message: 'use the default template',
            when: !templateName
        },
        {
            type: 'list',
            name: 'templateName',
            default: templateNameList()[0],
            choices: templateNameList(),
            when: (answers) => {
                if (!templateName) return !answers.defaultTemplate;
                else return !templateNameList().some(item => item === templateName);
            },
            message: 'choose the project template',
        },
    ]).then(answers => {

        formatterAnswers(answers);

        const {projectName, version, description, author} = answers;

        let template = answers.templateName || templateName;

        if (answers.defaultTemplate) template = getTemplateDefault().name;

        const projectConfig = {name: projectName, version, description, author, template: config.tpl[template]}

        switch (projectConfig.template.type) {
            case 'git':
                createProjectFromGit(projectConfig);
                break;
            case 'local':
                createProjectFromLocal(projectConfig);
                break;
            default:
                log.warning('Type not supported yet!');
                process.exit();
        }
    }).catch(err => {
        log.error(err)
        process.exit()
    })
}

module.exports = {
    initProjectFromTemplate
}

最后再配置下我们的入口文件,新增 /bin/sim.js 并注册我们的命令项

#!/usr/bin/env node --harmony
'use strict'
const { program } = require('commander');
const pkg = require('../package.json')
const updateNotifier = require('update-notifier')
const {addTemplate, removeTemplate, listShowTemplates, initProjectFromTemplate} = require("../command");
const {checkTemplateType, showLogo} = require("../utils/templateUtils");
updateNotifier({ pkg }).notify({ isGlobal: true })

program
	.version(pkg.version, '-v, --version')
	.option('-a, --add [name...]', 'Create a new project template')
	.option('-r, --remove [name...]', 'Remove a template')
	.option('-l, --list', 'List all of project templates')
	.option('-i, --init [name...]', 'Build a new project from template')
	.usage('<command> [options]')
	.description('Build a new project based on the project template')
	.action((options) => {
		if (options.add) addTemplate({name: options.add[0], type: options.add[1], path: options.add[2]});
		if (options.remove) removeTemplate(options.remove[0]);
		if (options.list) listShowTemplates(true);
		if (options.init) initProjectFromTemplate(options.init[0], options.init[1]);
	})

program
	.command('add')
	.argument('[name]', 'the template name', '')
	.argument('[type]', 'the template type of git or local', checkTemplateType, '')
	.argument('[path]', 'the template path for gitUrl or localPath', '')
	.option('-n, --name <name>', 'the template name', '')
	.option('-t, --type <type>', 'the template type of git or local', checkTemplateType, '')
	.option('-p, --path <path>', 'the template path for git or local', '')
	.description('Create a new project template')
	.action((name, type, path, options) => {addTemplate({name, type, path}, options)})

program
	.command('remove')
	.argument('[name]', 'the template name', '')
	.option('-n, --name <name>', 'the template name', '')
	.description('Remove a template')
	.action((name, options) => {removeTemplate(name || options.name)})

program
	.command('list')
	.description('List all of project templates')
	.action(() => listShowTemplates(true))

program
	.command('init')
	.description('Build a new project from a template')
	.argument('[projectName]', 'the project name', '')
	.argument('[templateName]', 'the template name', '')
	.option('-p, --projectName <name>', 'the project name', '')
	.option('-t, --templateName <name>', 'the template name', '')
	.action((projectName, templateName, options) => {
		initProjectFromTemplate(options.projectName || projectName, options.templateName || templateName)
	})

program.parse(process.argv);

if (!program.args.length && !Object.keys(program.opts()).length) {
	showLogo()
	program.help()
}

接下来便把我们的工程通过 npm publish 命令发布到 npm 服务器然后就可以分享给小伙伴们使用了

附言

附上项目参考资料,以供小伙伴们参考

代码仓库地址

sim-vue

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

增庆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值