• 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,
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113

    查看模板配置

    新增 /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,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    删除模板配置项

    新增 /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,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    核心功能

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

    添加 /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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152

    最后再配置下我们的入口文件,新增 /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(' [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 ', 'the template name', '')
    	.option('-t, --type ', 'the template type of git or local', checkTemplateType, '')
    	.option('-p, --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 ', '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 ', 'the project name', '')
    	.option('-t, --templateName ', '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()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

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

    附言

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

    代码仓库地址

    sim-vue

  • 相关阅读:
    【QT进阶】Qt线程与并发之QtConcurrent的简单介绍
    MySQL8.0.28数据库在windows版本下运行宕机问题解决
    PyTorch模型的多种导出方式提供给其他程序使用
    Redis 主从复制+哨兵+集群
    Pandas常见筛选数据的五种方法其一逻辑筛选。看见必懂,懂者必会,会者必加分
    【MySQL基础|第一篇】——谈谈SQL中的DDL语句
    【JAVA-Day43】Java常用类Calendar解析
    手撕代码彻底理解Promise
    vue开发前的入门准备
    跨境物流小包费用怎么计算?
  • 原文地址:https://blog.csdn.net/qq_33673284/article/details/126925382