前端部署脚手架专网项目实践
前言
前端脚手架是前端工程化中一项重要的提升团队效率的工具,因而构建脚手架对于前端工程师而言是一项不可获取的技能,而业界对于部署方面的脚手架相对较少,一般来说都是针对于业务的相关模板进行相关的工程化脚手架构建,本文旨在提供一些对前端部署相关的脚手架实践方案,希望对构建工程链路相关的同学能有所帮助。
架构
对于一款脚手架而言,不只是单单实现一个部署的方案,因而在脚手架架构设计方面,采用了插件化的插件模式,通过插件化的机制将所需要的功能部分进行提供
文章图片
目录
- packages
- @pnw
- cli
- cli-service
- cli-shared-utils
- cli-ui
- test
- @pnw
- 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:从工程化角度出发的脚手架开源工具
推荐阅读
- Beego打包部署到Linux
- 私有化轻量级持续集成部署方案--03-部署web服务(下)
- Jsr303做前端数据校验
- 7、前端--jQuery简介、基本选择器、基本筛选器、属性选择器、表单选择器、筛选器方法、节点操作、绑定事件
- 前端代码|前端代码 返回顶部 backToTop
- Spring|Spring Boot部署到Resin遇到的问题
- 如何在阿里云linux上部署java项目
- 前端|web前端dya07--ES6高级语法的转化&render&vue与webpack&export
- 前端自学笔记01
- js保留自定义小数点