前言
前端脚手架是前端工程化中一项重要的晋升团队效率的工具,因此构建脚手架对于前端工程师而言是一项不可获取的技能,而业界对于部署方面的脚手架绝对较少,一般来说都是针对于业务的相干模板进行相干的工程化脚手架构建,本文旨在提供一些对前端部署相干的脚手架实际计划,心愿对构建工程链路相干的同学能有所帮忙。
架构
对于一款脚手架而言,不只是单单实现一个部署的计划,因此在脚手架架构设计方面,采纳了插件化的插件模式,通过插件化的机制将所须要的性能局部进行提供
目录
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:从工程化角度登程的脚手架开源工具