前端部署腳手架專網專案實踐

野林發表於2022-01-17

前言

前端腳手架是前端工程化中一項重要的提升團隊效率的工具,因而構建腳手架對於前端工程師而言是一項不可獲取的技能,而業界對於部署方面的腳手架相對較少,一般來說都是針對於業務的相關模板進行相關的工程化腳手架構建,本文旨在提供一些對前端部署相關的腳手架實踐方案,希望對構建工程鏈路相關的同學能有所幫助。

架構

對於一款腳手架而言,不只是單單實現一個部署的方案,因而在腳手架架構設計方面,採用了外掛化的外掛模式,通過外掛化的機制將所需要的功能部分進行提供

目錄

  • 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 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和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 = 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)
}

總結

前端工程鏈路不僅僅是前端應用專案的構建,同時也需要關注整個前端上下游相關的構建,包括但不限於:ui構建、測試構建、部署構建等,對於前端工程化而言,所有能夠抽象成模板化的東西都應該可以被工程化,這樣才能降本增效,提升開發體驗與效率,共勉!!!

參考

相關文章