溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Node中怎么實現一個自動化部署平臺

發(fā)布時間:2021-07-21 09:34:30 來源:億速云 閱讀:170 作者:Leah 欄目:web開發(fā)

Node中怎么實現一個自動化部署平臺,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。

業(yè)務需求

這個JSSDK,主要作用是在后端了為業(yè)務方分配appKey之后,前端將appKey寫死在JSSDK中,上傳到CDN后,為業(yè)務方提供數據采集服務的腳本。

有的同學可能有疑問,為什么不像一些正常的SDK一樣,appKey是以參數的形式傳入到JSSDK中,這樣就可以統(tǒng)一所有業(yè)務方使用同一個JSSDK,而不需要為每個業(yè)務業(yè)務方都提供一個JSSDK。其實我剛開始也是這么想的,于是我向我的leader提出了我的這個想法,被拒絕了,拒絕原因如下:

  •  appKey如果以參數形式傳入,對業(yè)務方的接入成本有所增加,會出現appKey填錯的問題。

  •  業(yè)務方接入JSSDK之后,希望每次JSSDK版本迭代對業(yè)務方來說是無感知的(也就是版本迭代是覆蓋式發(fā)布),如果所有業(yè)務方使用同一個JSSDK,每次JSSDK的版本迭代,一次發(fā)版會一次性對所有業(yè)務方都有影響,會增加風險。

由于我的leader現在主要是負責產品推廣,經常和業(yè)務方打交道,可能他更能站在業(yè)務方的角度來考慮問題。所以,我的leader選擇犧牲項目的維護成本來降低SDK的接入成本和規(guī)避風險,可以理解。

那既然我們改變不了現狀,那就只能適應現狀。

項目痛點

那么針對原來沒有任何工程化情況的胖腳本,每次新增一個業(yè)務方,我需要做的事情如下:

  •  打開一個胖腳本和JSSDK接入文檔,拷貝一份新的。

  •  找后端要分配好的appKey,找對對應的appKey那一行代碼手動修改。

  •  手動混淆修改完好的腳本并上傳到CDN。

  •  修改JSSDK接入文檔中CDN的地址,保存后發(fā)送給業(yè)務方。

整個過程都需要手動進行,相對來說非常繁瑣,并且一不小心就會填錯,每次都需要對腳本和接入文檔進行檢查。

針對以上情況,得到我們需要解決的問題:

  •  怎樣針對一個新的業(yè)務方快速輸出一份新的JSSDK和接入文檔?

  •  怎樣快速對新的JSSDK進行混淆并上傳到CDN。

自動化方案

介紹方案之前,先上一張平臺截圖,以便先有一個直觀的認識:

Node中怎么實現一個自動化部署平臺

SDK自動化部署平臺主要實現了JSSDK的編譯,發(fā)布測試(在線預覽),上傳CDN功能。

服務端技術棧包括:

  •  框架 Express

  •  熱更新 nodemon

  •  依賴注入 awilix

  •  數據持久化 sequelize

  •  部署 pm2

客戶端技術棧就不介紹了,Vue全家桶 + vue-property-decorator + vuex-class。

項目搭建參考:

Vue+Express+Mysql 全棧初體驗

https://juejin.im/post/5ce96694f265da1bc5523f69

自動化部署平臺主要依賴于 GIT + 本地環(huán)境 + 私有NPM源 + MYSQL,各環(huán)節(jié)之間進行通信交互,完成自動化部署。

Node中怎么實現一個自動化部署平臺

主要達到的效果:本地環(huán)境拉取git倉庫代碼后,進行需求開發(fā),完成后發(fā)布一個帶Rollup的SDK編譯器包到私有NPM倉庫,自動化部署平臺在工程目錄安裝指定版本的SDK,并且備份到本地,在SDK編譯時,選擇特定版本的Rollup的SDK編譯器,并傳參(如appKey,appId等)到編譯器中進行編譯,同時自動生成JSSDK接入文檔等后打包成帶描述文件的Release包,在上傳到CDN時,將描述文件的對應的信息寫入MYSQL中進行保存。

版本管理

由于JSSDK原本只是一個腳本,我們必須實現項目的工程化,從而完成版本管理,方便快速版本切換進行發(fā)布,回滾,進而快速止損。

首先,我們需要將項目工程化,使用Rollup進行模塊管理,并且在發(fā)包NPM包的時候,輸入為各種參數(如appKey)輸出為一個Rollup Complier的函數,然后使用rollup-plugin-replace在編譯時候替換代碼中具體的參數。

lib/build.js,JSSDK中發(fā)包的入口文件,提供給SDK編譯時使用

import * as rollup from 'rollup';  const replace = require('rollup-plugin-replace');  const path = require('path');  const pkgPath = path.join(__dirname, '..', 'package.json');  const pkg = require(pkgPath);  const proConfig = require('./proConfig');  function getRollupConfig(replaceParams) {      const config = proConfig;      // 注入系統(tǒng)變量      const replacereplacePlugin = 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 = async function (params) {      const config = getRollupConfig({          supplyId: params.supplyId || '7102',          appKey: params.appKey      });      const {          input,          plugins      } = config;      const bundle = await rollup.rollup({          input,          plugins      });      const compiler = {          async write(file) {              await bundle.write({                  file,                  format: 'iife',                  sourcemap: false,                  strict: false              });          }      };      return compiler;  };

在自動化部署平臺中,使用shelljs安裝JSSDK包:

import {route, POST} from 'awilix-express';  import {Api} from '../framework/Api';  import * as shell from 'shell';  import * as path from 'path';  @route('/supply')  export default class SupplyAPI extends Api {     // some code      @route('/installSdkVersion')      @POST()      async installSdkVersion(req, res) {          const {version} = req.body;          const pkg = `@baidu/xxx-js-sdk@${version}`;          const registry = 'http://registry.npm.baidu-int.com';          shell.exec(`npm i ${pkg} --registry=${registry}`, (code, stdout, stderr)  => {              if (code !== 0) {                  console.error(stderr);                  res.failPrint('npm install fail');                  return;              }              // sdk包備份路徑              const sdkBackupPath = this.sdkBackupPath;              const sdkPath = path.resolve(sdkBackupPath, version);              shell.mkdir('-p', sdkPath).then((code, stdout, stderr) => {                  if (code !== 0) {                      console.error(stderr);                      res.failPrint(`mkdir \`${sdkPath}\` error.`);                      return;                  }                  const modulePath = path.resolve(process.cwd(), 'node_modules', '@baidu', 'xxx-js-sdk');                  // 拷貝安裝后的文件,方便后續(xù)使用                  shell.cp('-rf', modulePath + '/.', sdkPath).then((code, stdout, stderr) => {                      if (code !== 0) {                          console.error(stderr);                          res.failPrint(`backup sdk error.`);                          return;                      }                      res.successPrint(`${pkg} install success.`);                  });              })          });      }  }

Release包

Release包就是我們在上傳到CDN之前需要準備的壓縮包。因此,打包JSSDK之后,我們需要生成的文件有,接入文檔、JSSDK DEMO預覽頁面、JSSDK編譯結果、描述文件。

首先,打包函數如下:

import {Service} from '../framework';  import * as fs from 'fs';  import path from 'path';  import _ from 'lodash';   export default class SupplyService extends Service {      async generateFile(supplyId, sdkVersion) {          // 數據庫查詢對應的業(yè)務方的CDN文件名          const [sdkInfoErr, sdkInfo] = await this.supplyDao.getSupplyInfo(supplyId);          if (sdkInfoErr) {              return this.fail('服務器錯誤', null, sdkInfoErr);          }          const {appKey, cdnFilename, name} = sdkInfo;          // 需要替換的數據          const data = {              name,             supplyId,              appKey,              'sdk_url': `https://***.com/sdk/${cdnFilename}`          };          try {              // 編譯JSSDK              const sdkResult = await this.buildSdk(supplyId, appKey, sdkVersion);              // 生成接入文檔              const docResult = await this.generateDocs(data);              // 生成預覽DEMO html文件              const demoHtmlResult = await this.generateDemoHtml(data, 'sdk-demo.html', `JSSDK-接入頁面-${data.name}.html`);              // 生成release包描述文件              const sdkInfoFileResult = await this.writeSdkVersionFile(supplyId, appKey, sdkVersion);                        const success = docResult && demoHtmlResult && sdkInfoFileResult && sdkResult;              if (success) {                  // release目標目錄                  const dir = path.join(this.releasePath, supplyId + '');                  const fileName = `${supplyId}-${sdkVersion}.zip`;                  const zipFileName = path.join(dir, fileName);                  // 壓縮所有結果文件                  const zipResult = await this.zipDirFile(dir, zipFileName);                  if (!zipResult) {                      return this.fail('打包失敗');                  }                  // 返回壓縮包提供下載                  return this.success('打包成功', {                      url: `/${supplyId}/${fileName}`                  });              } else {                  return this.fail('打包失敗');              }          } catch (e) {              return this.fail('打包失敗', null, e);          }      }  }

編譯JSSDK

JSSDK的編譯很簡單,只需要加載對應版本的JSSDK的編譯函數,然后將對應的參數傳入編譯函數得到一個Rollup Compiler,然后將 Compiler 結果寫入Release路徑即可。

export default class SupplyService extends Service {      async buildSdk(supplyId, appKey, sdkVersion) {          try {              const sdkBackupPath = this.sdkBackupPath;              // 加載對應版本的備份的JSSDK包的Rollup編譯函數              const compileSdk = require(path.resolve(sdkBackupPath, sdkVersion, 'lib', 'build.js'));              const bundle = await compileSdk({                  supplyId,                  appKey: Number(sdkInfo.appKey)              });              const releasePath = path.resolve(this.releasePath, supplyId, `${supplyId}-sdk.js`);              // Rollup Compiler 編譯結果至release目錄              await bundle.write(releasePath);              return true;          } catch (e) {              console.error(e);              return false;          }      }  }

生成接入文檔

原理很簡單,使用JSZip,打開接入文檔模板,然后使用Docxtemplater替換模板里的特殊字符,然后重新生成DOC文件:

import Docxtemplater from 'docxtemplater';  import JSZip from 'JSZip';  export default class SupplyService extends Service {      async generateDocs(data) {          return new Promise(async (resolve, reject) => {              if (data) {                  // 讀取接入文檔,替換appKey,cdn路徑                  const supplyId = data.supplyId;                  const docsFileName = 'sdk-doc.docx';                  const supplyFilesPath = path.resolve(process.cwd(), 'src/server/files');                  const content = fs.readFileSync(path.resolve(supplyFilesPath, docsFileName), 'binary');                  const zip = new JSZip(content);                  const doc = new Docxtemplater();                  // 替換`[[`前綴和`]]`后綴的內容                  doc.loadZip(zip).setOptions({delimiters: {start: '[[', end: ']]'}});                  doc.setData(data);                  try {                      doc.render();                  } catch (error) {                      console.error(error);                      reject(error);                  }                  // 生成DOC的buffer                  const buf = doc.getZip().generate({type: 'nodebuffer'});                  const releasePath = path.resolve(this.releasePath, supplyId);                  // 創(chuàng)建目標目錄                  shell.mkdir(releasePath).then((code, stdout, stderr) => {                      if (code !== 0 ) {                          resolve(false);                          return;                      }                      // 將替換后的結果寫入release路徑                      fs.writeFileSync(path.resolve(releasePath, `JSSDK-文檔-${data.name}.docx`), buf);                      resolve(true);                  }).catch(e => {                      console.error(e);                      resolve(false);                  });              }          });      }  }

生成預覽DEMO頁面

與接入文檔生成原理類似,打開一個DEMO模板HTML文件,替換內部字符,重新生成文件:

export default class SupplyService extends Service {      generateDemoHtml(data, file, toFile) {          return new Promise((resolve, reject) => {              const supplyId = data.supplyId;              // 需要替換的數據              const replaceData = data;              // 打開文件              const content = fs.readFileSync(path.resolve(supplyFilesPath, file), 'utf-8');              // 字符串替換`{{`前綴和`}}`后綴的內容              const replaceContent = content.replace(/{{(.*)}}/g, (match, key) => {                  return replaceData[key] || match;              });              const releasePath = 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包中,作用很簡單,用來描述當前打包的一些參數,方便上線CDN的時候記錄當前上線的是哪個SDK版本等

export default class SupplyService extends Service {      async writeSdkVersionFile(supplyId, appKey, sdkVersion) {          return new Promise(resolve => {              const writePath = path.resolve(this.releasePath, supplyId, 'version.json');             // Release描述數據              const data = {version: sdkVersion, appKey, supplyId};              try {                  // 寫入release目錄                  fs.writeFileSync(writePath, JSON.stringify(data));                  resolve(true);              } catch (e) {                  console.error(e);                  resolve(false);              }          });      }  }

打包所有文件結果

將之前生成的JSSDK編譯結果、接入文檔、預覽DEMO頁面文件,描述文件使用archive打包起來:

export default class SupplyService extends Service {      zipDirFile(dir, to) {          return new Promise(async (resolve, reject) => {              const output = fs.createWriteStream(to);              const archive = archiver('zip');              archive.on('error', err => reject(err));              archive.pipe(output);              const files = fs.readdirSync(dir);              files.forEach(file => {                  const filePath = path.resolve(dir, file);                  const info = fs.statSync(filePath);                  if (!info.isDirectory()) {                      archive.append(fs.createReadStream(filePath), {                          'name': file                      });                  }              });              archive.finalize();              resolve(true);          });      }  }

CDN部署

大部分上傳到CDN都為像CDN源站push文件,而正好我們運維在我的自動化部署平臺的機器上掛載了NFS,即我只需要本地將JSSDK文件拷貝到共享目錄,就實現了CDN文件上傳。

export default class SupplyService extends Service {      async cp2CDN(supplyId, fileName) {          // 讀取描述文件          const sdkInfoPath = path.resolve(this.releasePath, '' + supplyId, 'version.json');          if (!fs.existsSync(sdkInfoPath)) {              return this.fail('Release描述文件丟失,請重新打包');          }          const sdkInfo = JSON.parse(fs.readFileSync(sdkInfoPath, 'utf-8'));          sdkInfo.cdnFilename = fileName;          // 將文件拷貝至文件共享目錄          const result = await this.cpFile(supplyId, fileName, false);          // 上傳成功          if (result) {              // 將Release包描述文件的數據同步到MYSQL              const [sdkInfoErr] = await this.supplyDao.update(sdkInfo, {where: {supplyId}});              if (sdkInfoErr) {                  return this.fail('JSSDK信息記錄失敗,請重試', null, jssdkInfoResult);              }              return this.success('上傳成功', {url})          }          return this.fail('上傳失敗');      }  }

看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業(yè)資訊頻道,感謝您對億速云的支持。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI