前言

前端脚手架是前端工程化中一项重要的晋升团队效率的工具,因此构建脚手架对于前端工程师而言是一项不可获取的技能,而业界对于部署方面的脚手架绝对较少,一般来说都是针对于业务的相干模板进行相干的工程化脚手架构建,本文旨在提供一些对前端部署相干的脚手架实际计划,心愿对构建工程链路相干的同学能有所帮忙。

架构

对于一款脚手架而言,不只是单单实现一个部署的计划,因此在脚手架架构设计方面,采纳了插件化的插件模式,通过插件化的机制将所须要的性能局部进行提供

目录

  • packages

    • @pnw

      • cli
      • cli-service
      • cli-shared-utils
      • cli-ui
    • test
  • scripts

    • bootstrap.js
    • dev.js
    • prod.js
    • release.js
  • env.json

案例

应用@pnw/cli脚手架构建,应用命令pnw deploy实现部署目录的构建,后续进行ci/cd流程,其中default.conf次要用于nginx的构建,Dockerfile用于镜像的构建,yaml文件次要用于k8s相干的构建

源码

cli

部署局部的外围在deploy.js中的deployFn函数的调起,而这部分是在cli-plugin-deploy中去实现具体的性能

create.js

const { isDev } = require('../index');console.log('isDev', isDev)const { Creator } = isDev ? require('../../cli-service') : require('@pnw/cli-service');const creator = new Creator();module.exports = (name, targetDir, fetch) => {    return creator.create(name, targetDir, fetch)}

deploy.js

const { isDev } = require('../index');const {    path,    stopSpinner,    error,    info} = require('@pnw/cli-shared-utils');const create = require('./create');const { Service } = isDev ? require('../../cli-service') : require('@pnw/cli-service');const { deployFn } = isDev ? require('../../cli-plugin-deploy') :require('@pnw/cli-plugin-deploy');// console.log('deployFn', deployFn);async function fetchDeploy(...args) {    info('fetchDeploy 执行了')    const service = new Service();    service.apply(deployFn);    service.run(...args);}async function deploy(options) {    // 自定义配置deploy内容 TODO    info('deploy 执行了')    const targetDir = path.resolve(process.cwd(), '.');    return await create('deploy', targetDir, fetchDeploy)}module.exports = (...args) => {    return deploy(...args).catch(err => {        stopSpinner(false)        error(err)    })};

cli-plugin-deploy

实现部署局部的外围局部,其中build、nginx、docker、yaml等次要是用于生成模板内容文件

build.js

exports.build = config => {    return `npm installnpm run build`};

docker.js

exports.docker = config => {    const {        forward_name,        env_directory    } = config;    return `FROM harbor.dcos.ncmp.unicom.local/platpublic/nginx:1.20.1COPY ./dist /usr/share/nginx/html/${forward_name}/COPY ./deploy/${env_directory}/default.conf /etc/nginx/conf.d/EXPOSE 80`}

nginx.js

exports.nginx = config => {    return `client_max_body_size 1000m;server {    listen       80;    server_name  localhost;    location / {        root   /usr/share/nginx/html;        index  index.html;        gzip_static on;    }}`}

yaml.js

exports.yaml = config => {  const {    git_name  } = config;  return `apiVersion: apps/v1kind: Deploymentmetadata:  name: ${git_name}spec:  replicas: 1  selector:    matchLabels:      app: ${git_name}  template:    metadata:      labels:        app: ${git_name}    spec:      containers:        - name: ${git_name}          image: harbor.dcos.ncmp.unicom.local/custom/${git_name}:1.0          imagePullPolicy: Always          resources:            limits:              cpu: 5              memory: 10G            requests:              cpu: 1              memory: 1G          ports:            - containerPort: 80`}

index.js

// const { fs, path } = require('@pnw/cli-shared-utils');const { TEMPLATES } = require('../constant');/** *  * @param {*} template 模板门路 * @param {*} config 注入模板的参数 */function generate(template, config) {    // console.log('template', template);    return require(`./${template}`).generateJSON(config);}function isExitTemplate(template) {    return TEMPLATES.includes(template)}TEMPLATES.forEach(m => {    Object.assign(exports, {        [`${m}`]: require(`./${m}`)    })})exports.createTemplate = (tempalteName, env_dirs) => {    if( isExitTemplate(tempalteName) ) {        return generate(tempalteName, {            // git_name: 'fescreenrj',            // forward_name: 'rj',            env_dirs        });    } else {        return `${tempalteName} is NOT A Template, Please SELECT A correct TEMPLATE`    }}

main.js

const {    createTemplate} = require('./__template__');const { isDev } = require('../index');console.log('isDev', isDev);const {    REPO_REG,    PATH_REG} = require('./reg');const {    TEMPLATES} = require('./constant');const {    path,    fs,    inquirer,    done,    error} = isDev ? require('../../cli-shared-utils') : require('@pnw/cli-shared-utils');/** *  * @param {*} targetDir 指标文件夹的绝对路径 * @param {*} fileName 文件名称 * @param {*} fileExt 文件扩大符 * @param {*} data 文件内容 */const createFile = async (targetDir, fileName, fileExt, data) => {    // console.log('fileName', fileName);    // console.log('fileExt', fileExt);    let file = fileName + '.' + fileExt;    if (!fileExt) file = fileName;    // console.log('file', file)    await fs.promises.writeFile(path.join(targetDir, file), data)            .then(() => done(`创立文件${file}胜利`))            .catch(err => {                if (err) {                    error(err);                    return err;                }            });}/** *  * @param {*} targetDir 须要创立目录的指标门路地址 * @param {*} projectName 须要创立的目录名称 * @param {*} templateStr 目录中的配置字符串 * @param {*} answers 命令行中获取到的参数 */const createCatalogue = async (targetDir, projectName, templateMap, answers) => {    const templateKey = Object.keys(templateMap)[0],        templateValue = Object.values(templateMap)[0];    // console.log('templateKey', templateKey);    // console.log('templateValue', templateValue);    // 获取模板对应的各种工具函数    const {        yaml,        nginx,        build,        docker    } = require('./__template__')[`${templateKey}`];    // 获取环境文件夹    const ENV = templateValue.ENV;    console.log('path.join(targetDir, projectName)', targetDir, projectName)    // 1. 创立文件夹    await fs.promises.mkdir(path.join(targetDir, projectName)).then(() => {            done(`创立工程目录${projectName}胜利`);            return true        })        .then((flag) => {            // console.log('flag', flag);            // 获取build的Options            const buildOptions = templateValue.FILE.filter(f => f.KEY == 'build')[0];            // console.log('buildOptions', buildOptions);                        flag && createFile(path.join(targetDir, projectName), buildOptions[`NAME`], buildOptions[`EXT`], build());        })        .catch(err => {            if (err) {                error(err);                return err;            }        });    ENV.forEach(env => {        fs.promises.mkdir(path.join(targetDir, projectName, env))            .then(() => {                done(`创立工程目录${projectName}/${env}胜利`);                return true;            })            .then(flag => {                // 获取docker的Options                const dockerOptions = templateValue.FILE.filter(f => f.KEY == 'docker')[0];                flag && createFile(path.join(targetDir, projectName, env), dockerOptions[`NAME`], dockerOptions[`EXT`], docker({                    forward_name: answers[`forward_name`],                    env_directory: env                }));                // 获取yaml的Options                const yamlOptions = templateValue.FILE.filter(f => f.KEY == 'yaml')[0];                flag && createFile(path.join(targetDir, projectName, env), yamlOptions[`NAME`], yamlOptions[`EXT`], yaml({                    git_name: answers[`repo_name`]                }));                // 获取nginx的Options                const nginxOptions = templateValue.FILE.filter(f => f.KEY == 'nginx')[0];                flag && createFile(path.join(targetDir, projectName, env), nginxOptions[`NAME`], nginxOptions[`EXT`], nginx());            })            .catch(err => {                if (err) {                    error(err);                    return err;                }            });    });}/** *  * @param {*} projectName 生成的目录名称 * @param {*} targetDir 绝对路径 */ module.exports = async (projectName, targetDir) => {    let options = [];    async function getOptions() {        return fs.promises.readdir(path.resolve(__dirname, './__template__')).then(files => files.filter(f => TEMPLATES.includes(f)))    }    options = await getOptions();    console.log('options', options);    const promptList = [{            type: 'list',            message: '请抉择你所须要部署的利用模板',            name: 'template_name',            choices: options        },        {            type: 'checkbox',            message: '请抉择你所须要部署的环境',            name: 'env_dirs',            choices: [{                    value: 'dev',                    name: '开发环境'                },                {                    value: 'demo',                    name: '演示环境'                },                {                    value: 'production',                    name: '生产环境'                },            ]        },        {            type: 'input',            name: 'repo_name',            message: '倡议以以后git仓库的缩写作为镜像名称',            filter: function (v) {                return v.match(REPO_REG).join('')            }        },        {            type: 'input',            name: 'forward_name',            message: '请应用合乎url门路规定的名称',            filter: function (v) {                return v.match(PATH_REG).join('')            }        },    ];    inquirer.prompt(promptList).then(answers => {        console.log('answers', answers);        const {            template_name        } = answers;        // console.log('templateName', templateName)        // 获取模板字符串        const templateStr = createTemplate(template_name, answers.env_dirs);        // console.log('template', JSON.parse(templateStr));        const templateMap = {            [`${template_name}`]: JSON.parse(templateStr)        }        createCatalogue(targetDir, projectName, templateMap, answers);    })};

cli-service

Service和Creator别离是两个基类,用于提供根底的相干服务

Creator.js

const {    path,    fs,    chalk,    stopSpinner,    inquirer,    error} = require('@pnw/cli-shared-utils');class Creator {    constructor() {    }    async create(projectName, targetDir, fetch) {        // const cwd = process.cwd();        // const inCurrent = projectName === '.';        // const name = inCurrent ? path.relative('../', cwd) : projectName;        // targetDir = path.resolve(cwd, name || '.');        // if (fs.existsSync(targetDir)) {        //     const {        //         action        //     } = await inquirer.prompt([{        //         name: 'action',        //         type: 'list',        //         message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,        //         choices: [{        //                 name: 'Overwrite',        //                 value: 'overwrite'        //             },        //             {        //                 name: 'Merge',        //                 value: 'merge'        //             },        //             {        //                 name: 'Cancel',        //                 value: false        //             }        //         ]        //     }])        //     if (!action) {        //         return        //     } else if (action === 'overwrite') {        //         console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)        //         await fs.remove(targetDir)        //     }        // }        await fetch(projectName, targetDir);    }}module.exports = Creator;

Service.js

const { isFunction } = require('@pnw/cli-shared-utils')class Service {    constructor() {        this.plugins = [];    }    apply(fn) {        if(isFunction(fn)) this.plugins.push(fn)    }    run(...args) {        if( this.plugins.length > 0 ) {            this.plugins.forEach(plugin => plugin(...args))        }    }}module.exports = Service;

cli-shared-utils

共用工具库,将第三方及node相干外围模块收敛到这个里边,对立输入和魔改

is.js

exports.isFunction= fn => typeof fn === 'function';

logger.js

const chalk = require('chalk');const {    stopSpinner} = require('./spinner');const format = (label, msg) => {    return msg.split('\n').map((line, i) => {        return i === 0 ?            `${label} ${line}` :            line.padStart(stripAnsi(label).length + line.length + 1)    }).join('\n')};exports.log = (msg = '', tag = null) => {    tag ? console.log(format(chalkTag(tag), msg)) : console.log(msg)};exports.info = (msg, tag = null) => {    console.log(format(chalk.bgBlue.black(' INFO ') + (tag ? chalkTag(tag) : ''), msg))};exports.done = (msg, tag = null) => {    console.log(format(chalk.bgGreen.black(' DONE ') + (tag ? chalkTag(tag) : ''), msg))};exports.warn = (msg, tag = null) => {    console.warn(format(chalk.bgYellow.black(' WARN ') + (tag ? chalkTag(tag) : ''), chalk.yellow(msg)))};exports.error = (msg, tag = null) => {    stopSpinner()    console.error(format(chalk.bgRed(' ERROR ') + (tag ? chalkTag(tag) : ''), chalk.red(msg)))    if (msg instanceof Error) {        console.error(msg.stack)    }}

spinner.js

const ora = require('ora')const chalk = require('chalk')const spinner = ora()let lastMsg = nulllet isPaused = falseexports.logWithSpinner = (symbol, msg) => {  if (!msg) {    msg = symbol    symbol = chalk.green('✔')  }  if (lastMsg) {    spinner.stopAndPersist({      symbol: lastMsg.symbol,      text: lastMsg.text    })  }  spinner.text = ' ' + msg  lastMsg = {    symbol: symbol + ' ',    text: msg  }  spinner.start()}exports.stopSpinner = (persist) => {  if (!spinner.isSpinning) {    return  }  if (lastMsg && persist !== false) {    spinner.stopAndPersist({      symbol: lastMsg.symbol,      text: lastMsg.text    })  } else {    spinner.stop()  }  lastMsg = null}exports.pauseSpinner = () => {  if (spinner.isSpinning) {    spinner.stop()    isPaused = true  }}exports.resumeSpinner = () => {  if (isPaused) {    spinner.start()    isPaused = false  }}exports.failSpinner = (text) => {  spinner.fail(text)}

总结

前端工程链路不仅仅是前端利用我的项目的构建,同时也须要关注整个前端上下游相干的构建,包含但不限于:ui构建、测试构建、部署构建等,对于前端工程化而言,所有可能形象成模板化的货色都应该能够被工程化,这样能力降本增效,晋升开发体验与效率,共勉!!!

参考

  • vue-cli源码
  • leo:从工程化角度登程的脚手架开源工具