当前位置: 首页 > 科技观察

前端赋能业务:Node实现自动化部署平台

时间:2023-03-14 18:59:26 科技观察

前言有很多人和我一样有这样的烦恼,每天需求无穷无尽,Bug无穷无尽,业务代码重复繁琐?,担心自己的技术成长。其实从另一个角度来说,我们所学的前端技术都是为业务服务的,那么我们为什么不想办法利用前端技术为业务做点什么呢?这样不仅可以解决业务问题,还可以让自己摆脱每天只写重复繁琐代码的烦恼。本文主要是作者针对当前团队中的一些业务问题,实现的一个自动化部署平台的技术解决方案。背景去年年初,因为团队里没有前端,我恰好是这里招的第一个也是唯一一个FE,于是接手了一个一直由后端维护的JSSDK项目。一个2000多行代码的胖脚本,没有任何工程痕迹。JSSDK针对业务需求的主要功能是在后端将appKey分配给业务方后,前端将appKey写入JSSDK中,上传到CDN,并提供数据采集服务的脚本用于商务派对。可能有同学会有疑惑,为什么不像一些普通的SDK,将appKey作为参数传入JSSDK,这样所有业务方就可以使用同一个JSSDK,而不需要每个业务方都提供一个JSDK。其实我一开始也是这么想的,所以我向领导提出了自己的想法,被拒绝了。拒绝的原因如下:如果将appKey作为参数传入,会增加业务方的接入成本,并且会出现appKey填写错误的问题。业务方接入JSSDK后,希望每次JSSDK版本迭代对业务方无敏感(即版本迭代为覆盖发布)。如果所有业务方都使用同一个JSSDK,每个JSSDK版本迭代,一旦版本发布,就会一次性影响到所有业务方,这样会增加风险。由于我领导现在主要负责产品推广,经常和业务方打交道,所以他可能更能站在业务方的角度考虑问题。所以,我领导选择牺牲项目维护成本来降低SDK接入成本,规避风险,无可厚非。既然我们无法改变现状,那么我们只能适应现状。项目的痛点所以对于没有任何工程情况的胖脚本,每次添加业务方,我需要做的是:打开一个胖脚本和JSSDK接入文档,复制一个新的。找到后台分配的appKey,手动修改appKey对应的代码行。手动混淆修改后的脚本并上传到CDN。修改JSSDK接入文档中的CDN地址,保存后发送给业务方。整个过程需要手动完成,比较繁琐,而且一不小心就会出错。每次都需要检查脚本和访问文档。针对以上情况,我们得到了需要解决的问题:如何为新的业务方快速输出新的JSSDK和接入文档?如何快速混淆新的JSSDK并上传到CDN。在介绍自动化方案之前,先对平台进行截图,以便有一个直观的认识:SDK自动化部署平台主要实现了JSSDK的编译、发布测试(在线预览)、上传CDN功能。服务端技术栈包括:frameworkExpress热更新nodemon依赖注入awilix数据持久化sequelize部署pm2客户端技术栈就不介绍了,Vue全家桶+vue-property-decorator+vuex-class。项目搭建参考:Vue+Express+Mysql全栈初体验https://juejin.im/post/5ce96694f265da1bc5523f69自动化部署平台主要依赖GIT+本地环境+私有npm源+MYSQL,各个环节之间的通信和交互,完成自动化部署。实现的主要效果:本地环境从git仓库拉取代码后,开发需求,完成后用Rollup发布一个SDK编译包到私有npm仓库,自动部署平台安装指定版本的SDK在项目目录并备份到本地,编译SDK时,选择特定版本的RollupSDK编译器,将参数(如appKey、appId等)传递给编译器进行编译,自动生成JSSDK访问文档等,打包成带有描述文件的Release包,上传到CDN时,将描述文件的相应信息写入MYSQL保存。版本管理由于JSSDK本来只是一个脚本,所以我们必须通过项目工程来完成版本管理,方便发布、回滚、快速止损的快速版本切换。首先,我们需要工程化工程,使用Rollup进行模块管理,在发送npm包时,输入各种参数(如appKey)输出为RollupComplier的函数,然后使用rollup-plugin-replace进行编译替换代码中的具体参数。lib/build.js,JSSDK下包入口文件,编译时提供给SDK=path.join(__dirname,'..','package.json');constpkg=require(pkgPath);constproConfig=require('./proConfig');functiongetRollupConfig(replaceParams){constconfig=proConfig;//注入系统变量constreplacereplacePlugin=replace({'__JS_SDK_VERSION__':JSON.stringify(pkg.version),'__SUPPLY_ID__':JSON.stringify(replaceParams.supplyId||'7102'),'__APP_KEY__':JSON.stringify(replaceParams.appKey)});return{input:config.input,output:config.output,plugins:[...config.plugins,replacePlugin]};};module.exports=asyncfunction(params){constconfig=getRollupConfig({supplyId:params.supplyId||'7102',appKey:params.appKey});const{input,plugins}=config;constbundle=awaitrollup.rollup({input,plugins});constcompiler={asyncwrite(file){awaitbundle.write({文件,格式:'iife',sourcemap:false,strict:false});}};returncompiler;};在自动化部署平台,使用shelljs安装JSSDK包:import{route,POST}from'awilix-express';import{Api}from'../framework/Api';import*asshellfrom'shell';import*aspathfrom'path';@route('/supply')exportdefaultclassSupplyAPIextendsApi{//somecode@route('/installSdkVersion')@POST()asyncinstallSdkVersion(req,res){const{version}=req.body;constpkg=`@baidu/xxx-js-sdk@${version}`;constregistry='http://registry.npm.baidu-int.com';shell.exec(`npmi${pkg}--registry=${registry}`,(code,stdout,stderr)=>{if(code!==0){console.error(stderr);res.failPrint('npminstallfail');return;}//sdk包备份路径constsdkBackupPath=this.sdkBackupPath;constsdkPath=path.resolve(sdkBackupPath,version);shell.mkdir('-p',sdkPath).then((code,stdout,stderr)=>{if(code!==0){console.error(stderr);res.failPrint(`mkdir\`${sdkPath}\`error.`);return;}constmodulePath=path.resolve(process.cwd(),'node_modules','@baidu','xxx-js-sdk');//选择安装后的文件,方方便后继续使用shell.cp('-rf',modulePath+'/.',sdkPath).then((code,stdout,stderr)=>{if(code!==0){console.error(stderr);res.failPrint(`backupsdkerror.`);return;}res.successPrint(`${pkg}installsuccess.`);});})});}}发布包发布包就是我们要上传的CDN之前需要准备的压缩包所以打包完JSSDK之后,我们需要生成的文件包括访问文档、JSSDKDEMO预览页面、JSSDK编译结果、描述文件。一、打包函数如下:import{Service}from'../framework';import*asfsfrom'fs';importpathfrom'path';import_from'lodash';exportdefaultclassSupplyServiceextendsService{asyncgenerateFile(supplyId,sdkVersion){//对应到数据库查询业务方CDN文件名const[sdkInfoErr,sdkInfo]=awaitthis.supplyDao.getSupplyInfo(supplyId);if(sdkInfoErr){returnthis.fail('ServerError',null,sdkInfoErr);}const{appKey,cdnFilename,name}=sdkInfo;//待替换的数据constdata={name,supplyId,appKey,'sdk_url':`https://***.com/sdk/${cdnFilename}`};try{//CompileJSSDKconstsdkResult=awaitthis.buildSdk(supplyId,appKey,sdkVersion);//生成访问文档constdocResult=awaitthis.generateDocs(data);//生成预览DEMOhtml文件constdemoHtmlResult=awaitthis.generateDemoHtml(data,'sdk-demo.html',`JSSDK-访问页面-${data.name}.html`);//生成发布包描述文件constsdkInfoFileResult=awaitthis.writeSdkVersionFile(supplyId,appKey,sdkVersion);constsuccess=docResult&&demoHtmlResult&&sdkInfoFileResult&&sdkResult;if(success){//发布目标目录constdir=path.join(this.releasePath,ssupplyId+'');constfileName=`${supplyId}-${sdkVersion}.zip`;constzipFileName=path.join(dir,fileName);//压缩所有结果文件constzipResult=awaitthis.zipDirFile(dir,zipFileName);if(!zipResult){returnthis.fail('packagefailed');}//返回下载的压缩包returnthis.success('packagesuccessful',{url:`/${supplyId}/${fileName}`});}else{returnthis.fail('packagingfailed');}}catch(e){returnthis.fail('packagingfailed',null,e);}}}编译JSDKJSSDK编译很简单,加载对应版本的JSSDK即可编译函数,然后将相应的参数传入编译函数得到一个RollupCompiler,然后将Compiler结果写入Release路径exportdefaultclassSupplyServiceextendsService{asyncbuildSdk(supplyId,appKey,sdkVersion){try{constsdkBackupPath=this.sdkBackupPath;//Rollup编译函数加载对应版本的备份JSSDK包constcompileSdk=require(path.resolve(sdkBackupPath,sdkVersion,'lib','build.js'));constbundle=awaitcompileSdk({supplyId,appKey:Number(sdkInfo.appKey)});constreleasePath=path.resolve(this.releasePath,supplyId,`${supplyId}-sdk.js`);//RollupCompiler将结果编译到release目录awaitbundle.write(releasePath);returntrue;}catch(e){console.error(e);returnfalse;}}}生成access文件的原理很简单。使用JSZip打开access文档模板,然后使用Docxtemplater替换模板中的特殊字符,然后重新生成DOC文件:resolve,reject)=>{if(data){//读取访问文档,替换appKey,cdn路径constupplyId=data.supplyId;constdocsFileName='sdk-doc.docx';constupplyFilesPath=path.resolve(process.cwd(),'src/server/files');constcontent=fs.readFileSync(path.resolve(supplyFilesPath,docsFileName),'binary');constzip=newJSZip(content);constdoc=newDocxtemplater();//替换`[[`前缀和`]]`后缀内容doc.loadZip(zip).setOptions({delimiters:{start:'[[',end:']]'}});doc.setData(data);try{doc.render();}catch(error){console.error(error);reject(error);}//生成DOCbufferconstbuf=doc.getZip().generate({type:'nodebuffer'});constreleasePath=path.resolve(this.releasePath,supplyId);//创建目标目录shell.mkdir(releasePath)。then((code,stdout,stderr)=>{if(code!==0){resolve(false);return;}//将替换结果写入release路径fs.writeFileSync(path.resolve(releasePath,`JSSDK-Document-${data.name}.docx`),buf);resolve(true);}).catch(e=>{console.error(e);resolve(false);});}});}}生成预览DEMO页面的原理和access文档类似。打开一个DEMO模板HTML文件,替换内部字符,重新生成文件:exportdefaultclassSupplyServiceextendsService{generateDemoHtml(data,file,toFile){returnnewPromise((resolve,reject)=>{constsupplyId=data.supplyId;//要替换的数据constreplaceData=data;//打开文件constcontent=fs.readFileSync(path.resolve(supplyFilesPath,file),'utf-8');//字符字符串替换`{{`前缀和`}}`后缀内容constreplaceContent=content.replace(/{{(.*)}}/g,(match,key)=>{returnreplaceData[key]||match;});constreleasePath=path.resolve(this.releasePath,supplyId);//写入文件fs.writeFile(path.resolve(releasePath,toFile),replaceContent,err=>{if(err){console.error(err);resolve(false);}else{resolve(true);}});});}}生成Release包描述文件,将当前打包的一些参数存放在一个文件中,一起打包到Release包中,即非常有用简单,用来描述当前打包的一些参数,方便上线时记录当前上线的是哪个SDK版本。exportdefaultclassSupplyServiceextendsService{asyncwriteSdkVersionFile(supplyId,appKey,sdkVersion){returnnewPromise(resolve=>{constwritePath=path.resolve(this.releasePath,supplyId,'version.json');//发布描述数据constdata={version:sdkVersion,appKey,supplyId};try{//写入释放目录fs.writeFileSync(writePath,JSON.stringify(data));resolve(true);}catch(e){console.error(e);resolve(false);}});}}Packageallfilesresults将之前生成的JSSDK编译结果,access文档,预览DEMO页面文件,描述文件打包archive:exportdefaultclassSupplyServiceextendsService{zipDirFile(dir,to){returnnewPromise(async(resolve,reject)=>{constoutput=fs.createWriteStream(to);constarchive=archiver('zip');archive.on('error',err=>reject(err));archive.pipe(output);constfiles=fs.readdirSync(dir);files.forEach(file=>{constfilePath=path.resolve(dir,file);constinfo=fs.statSync(filePath);if(!info.isDirectory()){archive.append(fs.createReadStream(filePath),{'name':file});}});archive.finalize();resolve(true);});}}CDN部署上传到CDN的大部分都是像CDN源站一样的推送文件,而恰好我们运维在我的自动化部署平台的机器上挂载了NFS,也就是我只需要将JSSDK文件复制到本地的共享目录即可实现CDN文件上传exportdefaultclassSupplyServiceextendsService{asyncp2CDN(supplyId,fileName){//读取描述文件constsdkInfoPath=path.resolve(this.releasePath,''+supplyId,'version.json');if(!fs.existsSync(sdkInfoPath)){returnthis.fail('发布描述文件丢失,请重新打包');}constsdkInfo=JSON.parse(fs.readFileSync(sdkInfoPath,'utf-8'));sdkInfo.cdnFilename=fileName;//复制文件到文件共享directoryconstresult=awaitthis.cpFile(supplyId,fileName,false);//上传成功if(result){//将Release包描述文件的数据同步到MYSQLconst[sdkInfoErr]=awaitthis.supplyDao.update(sdkInfo,{where:{supplyId}});if(sdkInfoErr){returnthis.fail('JSSDK信息记录失败,请重试',null,jssdkInfoResult);}returnthis.success('上传成功',{url})}returnthis.fail('上传失败');}}项目结果项目的收益还是很明显的,本质上解决了我们需要解决的问题:完成了项目的工程化,自动生成了JSSDK和access文档。编译时自动混淆,一键上传CDN。节省了手动上传粘贴代码的时间,大大提高了工作效率。这个项目是我2019年上半年业余时间完成的一个工具项目,后来被Leader看重,工具正式升级为平台,在平台上集成了很多业务相关的配置。2019年上半年我的KPI是这样出来的是的哈~~~总结还是这套思路更适合每个业务了解业务背景发现业务痛点找到解决方案并积极促进问题的实现其实每个项目中的痛点一般都是XX的性能低,XX的效率很低,也比较容易找出来。这时候,你只需要积极寻找解决方案并推动其实施即可。前端技术离不开业务,技术永远为业务服务。没有业务的技术是根本没有立足之地的技术,是完全没有意义的技术。因此,除了编写页面,利用前端页面实现工具化、自动化,向平台化进阶也是一个不错的选择。