3
头图

foreword

Front-end scaffolding is an important tool for improving team efficiency in front-end engineering. Therefore, building scaffolding is an inaccessible skill for front-end engineers. However, there are relatively few deployment scaffolds in the industry. Generally speaking, they are aimed at Based on the relevant templates of the business, the related engineering scaffolding is constructed. This article aims to provide some scaffolding practices related to front-end deployment, hoping to help students who are related to building engineering links.

Architecture

For a scaffold, it is not just to implement a deployment solution. Therefore, in the design of the scaffolding architecture, a plug-in model is adopted, and the required functional parts are provided through the plug-in mechanism.

content

  • packages

    • @pnw

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

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

case

Use @pnw/cli scaffolding to build, use the command pnw deploy to build the deployment directory, and follow up with the ci/cd process, where default.conf is mainly used for nginx construction, Dockerfile is used for image construction, and yaml file is mainly used for k8s related build

source code

cli

The core of the deployment part is the deployment of the deployFn function in deploy.js, and this part is to implement specific functions in 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

Implement the core part of the deployment part, in which build, nginx, docker, yaml, etc. are mainly used to generate template content files

build.js

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

docker.js

exports.docker = config => {
    const {
        forward_name,
        env_directory
    } = config;
    return `FROM harbor.dcos.ncmp.unicom.local/platpublic/nginx:1.20.1
COPY ./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/v1
kind: Deployment
metadata:
  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 and Creator are two base classes, used to provide basic related services

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

Shared tool library, converging third-party and node-related core modules into this, unified output and magic modification

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 = null
let isPaused = false

exports.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)
}

Summarize

The front-end engineering link is not only the construction of front-end application projects, but also needs to pay attention to the entire front-end upstream and downstream related construction, including but not limited to: ui construction, test construction, deployment construction, etc. For front-end engineering, all abstractions that can be Everything that is templated should be able to be engineered, so as to reduce costs and increase efficiency, improve development experience and efficiency, and encourage each other! ! !

refer to


维李设论
1.1k 声望4k 粉丝

专注大前端领域发展