前端部署脚手架专网项目实践

前言 前端脚手架是前端工程化中一项重要的提升团队效率的工具,因而构建脚手架对于前端工程师而言是一项不可获取的技能,而业界对于部署方面的脚手架相对较少,一般来说都是针对于业务的相关模板进行相关的工程化脚手架构建,本文旨在提供一些对前端部署相关的脚手架实践方案,希望对构建工程链路相关的同学能有所帮助。
架构 对于一款脚手架而言,不只是单单实现一个部署的方案,因而在脚手架架构设计方面,采用了插件化的插件模式,通过插件化的机制将所需要的功能部分进行提供
前端部署脚手架专网项目实践
文章图片

目录

  • 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 { listen80; server_namelocalhost; location / { root/usr/share/nginx/html; indexindex.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 = https://www.it610.com/article/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 = 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:从工程化角度出发的脚手架开源工具

    推荐阅读