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

前言

创新互联建站服务项目包括杜集网站建设、杜集网站制作、杜集网页制作以及杜集网络营销策划等。多年来,我们专注于互联网行业,利用自身积累的技术优势、行业经验、深度合作伙伴关系等,向广大中小型企业、政府机构等提供互联网行业的解决方案,杜集网站推广取得了明显的社会效益与经济效益。目前,我们服务的客户以成都为中心已经辐射到杜集省份的部分城市,未来相信会继续扩大服务区域并继续获得客户的支持与信任!

是否有很多人跟我一样有这样的一个烦恼,每天有写不完的需求、改不完的BUG,每天撸着重复、繁琐的业务代码,担心着自己的技术成长。

其实换个角度,我们所学的所有前端技术都是服务于业务的,那我们为什么不想办法使用前端技术为业务做点东西?这样既能解决业务的困扰,也能让自己摆脱每天只能写重复繁琐代码的困扰。

本文主要为笔者针对当前团队内的一些业务问题,实现的一个自动化部署平台的技术方案。

背景

去年年初,由于团队里没有前端,刚好我是被招过来的第一个,也是唯一一个FE,于是我接手了一个一直由后端维护的JSSDK项目,其实也说不上项目,接手的时候它只是一个2000多行代码的胖脚本,没有任何工程化痕迹。

业务需求

这个JSSDK,主要作用是在后端了为业务方分配appKey之后,前端将appKey写死在JSSDK中,上传到CDN后,为业务方提供数据采集服务的脚本。

有的同学可能有疑问,为什么不像一些正常的SDK一样,appKey是以参数的形式传入到JSSDK中,这样就可以统一所有业务方使用同一个JSSDK,而不需要为每个业务业务方都提供一个JSSDK。其实我刚开始也是这么想的,于是我向我的leader提出了我的这个想法,被拒绝了,拒绝原因如下:

  •  appKey如果以参数形式传入,对业务方的接入成本有所增加,会出现appKey填错的问题。
  •  业务方接入JSSDK之后,希望每次JSSDK版本迭代对业务方来说是无感知的(也就是版本迭代是覆盖式发布),如果所有业务方使用同一个JSSDK,每次JSSDK的版本迭代,一次发版会一次性对所有业务方都有影响,会增加风险。

由于我的leader现在主要是负责产品推广,经常和业务方打交道,可能他更能站在业务方的角度来考虑问题。所以,我的leader选择牺牲项目的维护成本来降低SDK的接入成本和规避风险,可以理解。

那既然我们改变不了现状,那就只能适应现状。

项目痛点

那么针对原来没有任何工程化情况的胖脚本,每次新增一个业务方,我需要做的事情如下:

  •  打开一个胖脚本和JSSDK接入文档,拷贝一份新的。
  •  找后端要分配好的appKey,找对对应的appKey那一行代码手动修改。
  •  手动混淆修改完好的脚本并上传到CDN。
  •  修改JSSDK接入文档中CDN的地址,保存后发送给业务方。

整个过程都需要手动进行,相对来说非常繁琐,并且一不小心就会填错,每次都需要对脚本和接入文档进行检查。

针对以上情况,得到我们需要解决的问题:

  •  怎样针对一个新的业务方快速输出一份新的JSSDK和接入文档?
  •  怎样快速对新的JSSDK进行混淆并上传到CDN。

自动化方案

介绍方案之前,先上一张平台截图,以便先有一个直观的认识:

SDK自动化部署平台主要实现了JSSDK的编译,发布测试(在线预览),上传CDN功能。

服务端技术栈包括:

  •  框架 Express
  •  热更新 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编译时,选择特定版本的Rollup的SDK编译器,并传参(如appKey,appId等)到编译器中进行编译,同时自动生成JSSDK接入文档等后打包成带描述文件的Release包,在上传到CDN时,将描述文件的对应的信息写入MYSQL中进行保存。

版本管理

由于JSSDK原本只是一个脚本,我们必须实现项目的工程化,从而完成版本管理,方便快速版本切换进行发布,回滚,进而快速止损。

首先,我们需要将项目工程化,使用Rollup进行模块管理,并且在发包NPM包的时候,输入为各种参数(如appKey)输出为一个Rollup Complier的函数,然后使用rollup-plugin-replace在编译时候替换代码中具体的参数。

lib/build.js,JSSDK中发包的入口文件,提供给SDK编译时使用

 
 
 
 
  1. import * as rollup from 'rollup'; 
  2. const replace = require('rollup-plugin-replace'); 
  3. const path = require('path'); 
  4. const pkgPath = path.join(__dirname, '..', 'package.json'); 
  5. const pkg = require(pkgPath); 
  6. const proConfig = require('./proConfig'); 
  7. function getRollupConfig(replaceParams) { 
  8.     const config = proConfig; 
  9.     // 注入系统变量 
  10.     const replacereplacePlugin = replace({ 
  11.         '__JS_SDK_VERSION__': JSON.stringify(pkg.version), 
  12.         '__SUPPLY_ID__': JSON.stringify(replaceParams.supplyId || '7102'), 
  13.         '__APP_KEY__': JSON.stringify(replaceParams.appKey) 
  14.     }); 
  15.     return { 
  16.         input: config.input, 
  17.         output: config.output, 
  18.         plugins: [ 
  19.             ...config.plugins, 
  20.             replacePlugin 
  21.         ] 
  22.     }; 
  23. }; 
  24. module.exports = async function (params) { 
  25.     const config = getRollupConfig({ 
  26.         supplyId: params.supplyId || '7102', 
  27.         appKey: params.appKey 
  28.     }); 
  29.     const { 
  30.         input, 
  31.         plugins 
  32.     } = config; 
  33.     const bundle = await rollup.rollup({ 
  34.         input, 
  35.         plugins 
  36.     }); 
  37.     const compiler = { 
  38.         async write(file) { 
  39.             await bundle.write({ 
  40.                 file, 
  41.                 format: 'iife', 
  42.                 sourcemap: false, 
  43.                 strict: false
  44.              }); 
  45.         } 
  46.     }; 
  47.     return compiler; 
  48. };

在自动化部署平台中,使用shelljs安装JSSDK包:

 
 
 
 
  1. import {route, POST} from 'awilix-express'; 
  2. import {Api} from '../framework/Api'; 
  3. import * as shell from 'shell'; 
  4. import * as path from 'path'; 
  5. @route('/supply') 
  6. export default class SupplyAPI extends Api {
  7.     // some code 
  8.     @route('/installSdkVersion') 
  9.     @POST() 
  10.     async installSdkVersion(req, res) { 
  11.         const {version} = req.body; 
  12.         const pkg = `@baidu/xxx-js-sdk@${version}`; 
  13.         const registry = 'http://registry.npm.baidu-int.com'; 
  14.         shell.exec(`npm i ${pkg} --registry=${registry}`, (code, stdout, stderr)  => { 
  15.             if (code !== 0) { 
  16.                 console.error(stderr); 
  17.                 res.failPrint('npm install fail'); 
  18.                 return; 
  19.             } 
  20.             // sdk包备份路径 
  21.             const sdkBackupPath = this.sdkBackupPath; 
  22.             const sdkPath = path.resolve(sdkBackupPath, version); 
  23.             shell.mkdir('-p', sdkPath).then((code, stdout, stderr) => { 
  24.                 if (code !== 0) { 
  25.                     console.error(stderr); 
  26.                     res.failPrint(`mkdir \`${sdkPath}\` error.`); 
  27.                     return; 
  28.                 } 
  29.                 const modulePath = path.resolve(process.cwd(), 'node_modules', '@baidu', 'xxx-js-sdk'); 
  30.                 // 拷贝安装后的文件,方便后续使用 
  31.                 shell.cp('-rf', modulePath + '/.', sdkPath).then((code, stdout, stderr) => { 
  32.                     if (code !== 0) { 
  33.                         console.error(stderr); 
  34.                         res.failPrint(`backup sdk error.`); 
  35.                         return; 
  36.                     } 
  37.                     res.successPrint(`${pkg} install success.`); 
  38.                 }); 
  39.             }) 
  40.         }); 
  41.     } 
  42. }

Release包

Release包就是我们在上传到CDN之前需要准备的压缩包。因此,打包JSSDK之后,我们需要生成的文件有,接入文档、JSSDK DEMO预览页面、JSSDK编译结果、描述文件。

首先,打包函数如下:

 
 
 
 
  1. import {Service} from '../framework'; 
  2. import * as fs from 'fs'; 
  3. import path from 'path'; 
  4. import _ from 'lodash';  
  5. export default class SupplyService extends Service { 
  6.     async generateFile(supplyId, sdkVersion) { 
  7.         // 数据库查询对应的业务方的CDN文件名 
  8.         const [sdkInfoErr, sdkInfo] = await this.supplyDao.getSupplyInfo(supplyId); 
  9.         if (sdkInfoErr) { 
  10.             return this.fail('服务器错误', null, sdkInfoErr); 
  11.         } 
  12.         const {appKey, cdnFilename, name} = sdkInfo; 
  13.         // 需要替换的数据 
  14.         const data = { 
  15.             name,
  16.             supplyId, 
  17.             appKey, 
  18.             'sdk_url': `https://***.com/sdk/${cdnFilename}` 
  19.         }; 
  20.         try { 
  21.             // 编译JSSDK 
  22.             const sdkResult = await this.buildSdk(supplyId, appKey, sdkVersion); 
  23.             // 生成接入文档 
  24.             const docResult = await this.generateDocs(data); 
  25.             // 生成预览DEMO html文件
  26.              const demoHtmlResult = await this.generateDemoHtml(data, 'sdk-demo.html', `JSSDK-接入页面-${data.name}.html`); 
  27.             // 生成release包描述文件 
  28.             const sdkInfoFileResult = await this.writeSdkVersionFile(supplyId, appKey, sdkVersion);          
  29.              const success = docResult && demoHtmlResult && sdkInfoFileResult && sdkResult; 
  30.             if (success) { 
  31.                 // release目标目录 
  32.                 const dir = path.join(this.releasePath, supplyId + ''); 
  33.                 const fileName = `${supplyId}-${sdkVersion}.zip`; 
  34.                 const zipFileName = path.join(dir, fileName); 
  35.                 // 压缩所有结果文件 
  36.                 const zipResult = await this.zipDirFile(dir, zipFileName); 
  37.                 if (!zipResult) { 
  38.                     return this.fail('打包失败'); 
  39.                 } 
  40.                 // 返回压缩包提供下载 
  41.                 return this.success('打包成功', { 
  42.                     url: `/${supplyId}/${fileName}` 
  43.                 }); 
  44.             } else { 
  45.                 return this.fail('打包失败'); 
  46.             } 
  47.         } catch (e) { 
  48.             return this.fail('打包失败', null, e); 
  49.         } 
  50.     }
  51.  }

编译JSSDK

JSSDK的编译很简单,只需要加载对应版本的JSSDK的编译函数,然后将对应的参数传入编译函数得到一个Rollup Compiler,然后将 Compiler 结果写入Release路径即可。

 
 
 
 
  1. export default class SupplyService extends Service { 
  2.     async buildSdk(supplyId, appKey, sdkVersion) { 
  3.         try { 
  4.             const sdkBackupPath = this.sdkBackupPath; 
  5.             // 加载对应版本的备份的JSSDK包的Rollup编译函数 
  6.             const compileSdk = require(path.resolve(sdkBackupPath, sdkVersion, 'lib', 'build.js')); 
  7.             const bundle = await compileSdk({ 
  8.                 supplyId, 
  9.                 appKey: Number(sdkInfo.appKey) 
  10.             }); 
  11.             const releasePath = path.resolve(this.releasePath, supplyId, `${supplyId}-sdk.js`); 
  12.             // Rollup Compiler 编译结果至release目录 
  13.             await bundle.write(releasePath); 
  14.             return true; 
  15.         } catch (e) { 
  16.             console.error(e); 
  17.             return false; 
  18.         } 
  19.     } 
  20. }

生成接入文档

原理很简单,使用JSZip,打开接入文档模板,然后使用Docxtemplater替换模板里的特殊字符,然后重新生成DOC文件:

 
 
 
 
  1. import Docxtemplater from 'docxtemplater'; 
  2. import JSZip from 'JSZip'; 
  3. export default class SupplyService extends Service { 
  4.     async generateDocs(data) { 
  5.         return new Promise(async (resolve, reject) => { 
  6.             if (data) { 
  7.                 // 读取接入文档,替换appKey,cdn路径 
  8.                 const supplyId = data.supplyId;
  9.                  const docsFileName = 'sdk-doc.docx'; 
  10.                 const supplyFilesPath = path.resolve(process.cwd(), 'src/server/files'); 
  11.                 const content = fs.readFileSync(path.resolve(supplyFilesPath, docsFileName), 'binary'); 
  12.                 const zip = new JSZip(content); 
  13.                 const doc = new Docxtemplater(); 
  14.                 // 替换`[[`前缀和`]]`后缀的内容
  15.                  doc.loadZip(zip).setOptions({delimiters: {start: '[[', end: ']]'}}); 
  16.                 doc.setData(data); 
  17.                 try { 
  18.                     doc.render(); 
  19.                 } catch (error) { 
  20.                     console.error(error); 
  21.                     reject(error); 
  22.                 } 
  23.                 // 生成DOC的buffer 
  24.                 const buf = doc.getZip().generate({type: 'nodebuffer'}); 
  25.                 const releasePath = path.resolve(this.releasePath, supplyId); 
  26.                 // 创建目标目录 
  27.                 shell.mkdir(releasePath).then((code, stdout, stderr) => { 
  28.                     if (code !== 0 ) { 
  29.                         resolve(false); 
  30.                         return; 
  31.                     } 
  32.                     // 将替换后的结果写入release路径 
  33.                     fs.writeFileSync(path.resolve(releasePath, `JSSDK-文档-${data.name}.docx`), buf); 
  34.                     resolve(true); 
  35.                 }).catch(e => { 
  36.                     console.error(e); 
  37.                     resolve(false); 
  38.                 }); 
  39.             } 
  40.         }); 
  41.     } 
  42. }

生成预览DEMO页面

与接入文档生成原理类似,打开一个DEMO模板HTML文件,替换内部字符,重新生成文件:

 
 
 
 
  1. export default class SupplyService extends Service { 
  2.     generateDemoHtml(data, file, toFile) { 
  3.         return new Promise((resolve, reject) => { 
  4.             const supplyId = data.supplyId; 
  5.             // 需要替换的数据 
  6.             const replaceData = data; 
  7.             // 打开文件 
  8.             const content = fs.readFileSync(path.resolve(supplyFilesPath, file), 'utf-8'); 
  9.             // 字符串替换`{{`前缀和`}}`后缀的内容 
  10.             const replaceContent = content.replace(/{{(.*)}}/g, (match, key) => { 
  11.                 return replaceData[key] || match; 
  12.             }); 
  13.             const releasePath = path.resolve(this.releasePath, supplyId); 
  14.             // 写入文件 
  15.             fs.writeFile(path.resolve(releasePath, toFile), replaceContent, err => { 
  16.                 if (err) { 
  17.                     console.error(err); 
  18.                     resolve(false); 
  19.                 } else { 
  20.                     resolve(true); 
  21.                 } 
  22.             }); 
  23.         }); 
  24.     } 
  25. }

生成Release包描述文件

将当前打包的一些参数存在一个文件中的,一并打包到Release包中,作用很简单,用来描述当前打包的一些参数,方便上线CDN的时候记录当前上线的是哪个SDK版本等

 
 
 
 
  1. export default class SupplyService extends Service { 
  2.     async writeSdkVersionFile(supplyId, appKey, sdkVersion) { 
  3.         return new Promise(resolve => { 
  4.             const writePath = path.resolve(this.releasePath, supplyId, 'version.json');
  5.             // Release描述数据 
  6.             const data = {version: sdkVersion, appKey, supplyId}; 
  7.             try { 
  8.                 // 写入release目录 
  9.                 fs.writeFileSync(writePath, JSON.stringify(data)); 
  10.                 resolve(true); 
  11.             } catch (e) { 
  12.                 console.error(e); 
  13.                 resolve(false); 
  14.             } 
  15.         }); 
  16.     } 
  17. }

打包所有文件结果

将之前生成的JSSDK编译结果、接入文档、预览DEMO页面文件,描述文件使用archive打包起来:

 
 
 
 
  1. export default class SupplyService extends Service { 
  2.     zipDirFile(dir, to) { 
  3.         return new Promise(async (resolve, reject) => { 
  4.             const output = fs.createWriteStream(to); 
  5.             const archive = archiver('zip'); 
  6.             archive.on('error', err => reject(err)); 
  7.             archive.pipe(output); 
  8.             const files = fs.readdirSync(dir); 
  9.             files.forEach(file => { 
  10.                 const filePath = path.resolve(dir, file); 
  11.                 const info = fs.statSync(filePath); 
  12.                 if (!info.isDirectory()) { 
  13.                     archive.append(fs.createReadStream(filePath), { 
  14.                         'name': file 
  15.                     }); 
  16.                 } 
  17.             }); 
  18.             archive.finalize(); 
  19.             resolve(true); 
  20.         }); 
  21.     } 
  22. }

CDN部署

大部分上传到CDN都为像CDN源站push文件,而正好我们运维在我的自动化部署平台的机器上挂载了NFS,即我只需要本地将JSSDK文件拷贝到共享目录,就实现了CDN文件上传。

 
 
 
 
  1. export default class SupplyService extends Service { 
  2.     async cp2CDN(supplyId, fileName) {
  3.          // 读取描述文件 
  4.         const sdkInfoPath = path.resolve(this.releasePath, '' + supplyId, 'version.json'); 
  5.         if (!fs.existsSync(sdkInfoPath)) { 
  6.             return this.fail('Release描述文件丢失,请重新打包'); 
  7.         } 
  8.         const sdkInfo = JSON.parse(fs.readFileSync(sdkInfoPath, 'utf-8')); 
  9.         sdkInfo.cdnFilename = fileName; 
  10.         // 将文件拷贝至文件共享目录 
  11.         const result = await this.cpFile(supplyId, fileName, false); 
  12.         // 上传成功 
  13.         if (result) {
  14.              // 将Release包描述文件的数据同步到MYSQL 
  15.             const [sdkInfoErr] = await this.supplyDao.update(sdkInfo, {where: {supplyId}}); 
  16.             if (sdkInfoErr) { 
  17.                 return this.fail('JSSDK信息记录失败,请重试', null, jssdkInfoResult); 
  18.             } 
  19.             return this.success('上传成功', {url}) 
  20.         } 
  21.         return this.fail('上传失败'); 
  22.     } 
  23. }

项目成效

项目效益还是很明显,从本质上解决了我们需要解决的问题:

  •  完成了项目的工程化,自动化生成JSSDK和接入文档。
  •  编译过程中自动化进行混淆,并实现了一键上传至CDN。

节省了人工上传粘贴代码的时间,大大地提高了工作效率。

这个项目还是19年前半年个人花业余时间完成的工具项目,后来得到了Leader的重视,将工具正式升级为平台,集成了很多业务相关的配置在平台,我19年的前半年KPI就这么来的,哈~~~

总结

或者这一套思路对每个业务都比较适用

  1.  了解业务的背景
  2.  发现业务的痛点
  3.  寻找解决方案并主动推进实现
  4.  解决问题

其实每个项目中的痛点都一般都是XX的性能低下、XX非常低效,还是比较容易发现的,这个时候只需要主动的寻找方案并推进实现就OK了。

前端技术离不开业务,技术永远服务于业务,离开了业务的技术,那是完全没有落脚点的技术,完全没有意义的技术。所以,除了写写页面,利用前端页面实现工具化、自动化,从而推进到平台化也是一个不错的落脚点选择。

文章题目:前端赋能业务:Node实现自动化部署平台
浏览路径:http://www.csdahua.cn/qtweb/news21/255121.html

网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网