Node中怎么實(shí)現(xiàn)一個自動化部署平臺,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。
創(chuàng)新互聯(lián)建站專注為客戶提供全方位的互聯(lián)網(wǎng)綜合服務(wù),包含不限于成都網(wǎng)站設(shè)計(jì)、做網(wǎng)站、成都外貿(mào)網(wǎng)站建設(shè)公司、阿圖什網(wǎng)絡(luò)推廣、微信平臺小程序開發(fā)、阿圖什網(wǎng)絡(luò)營銷、阿圖什企業(yè)策劃、阿圖什品牌公關(guān)、搜索引擎seo、人物專訪、企業(yè)宣傳片、企業(yè)代運(yùn)營等,從售前售中售后,我們都將竭誠為您服務(wù),您的肯定,是我們最大的嘉獎;創(chuàng)新互聯(lián)建站為所有大學(xué)生創(chuàng)業(yè)者提供阿圖什建站搭建服務(wù),24小時服務(wù)熱線:18982081108,官方網(wǎng)址:www.cdcxhl.com
業(yè)務(wù)需求
這個JSSDK,主要作用是在后端了為業(yè)務(wù)方分配appKey之后,前端將appKey寫死在JSSDK中,上傳到cdn后,為業(yè)務(wù)方提供數(shù)據(jù)采集服務(wù)的腳本。
有的同學(xué)可能有疑問,為什么不像一些正常的SDK一樣,appKey是以參數(shù)的形式傳入到JSSDK中,這樣就可以統(tǒng)一所有業(yè)務(wù)方使用同一個JSSDK,而不需要為每個業(yè)務(wù)業(yè)務(wù)方都提供一個JSSDK。其實(shí)我剛開始也是這么想的,于是我向我的leader提出了我的這個想法,被拒絕了,拒絕原因如下:
appKey如果以參數(shù)形式傳入,對業(yè)務(wù)方的接入成本有所增加,會出現(xiàn)appKey填錯的問題。
業(yè)務(wù)方接入JSSDK之后,希望每次JSSDK版本迭代對業(yè)務(wù)方來說是無感知的(也就是版本迭代是覆蓋式發(fā)布),如果所有業(yè)務(wù)方使用同一個JSSDK,每次JSSDK的版本迭代,一次發(fā)版會一次性對所有業(yè)務(wù)方都有影響,會增加風(fēng)險。
由于我的leader現(xiàn)在主要是負(fù)責(zé)產(chǎn)品推廣,經(jīng)常和業(yè)務(wù)方打交道,可能他更能站在業(yè)務(wù)方的角度來考慮問題。所以,我的leader選擇犧牲項(xiàng)目的維護(hù)成本來降低SDK的接入成本和規(guī)避風(fēng)險,可以理解。
那既然我們改變不了現(xiàn)狀,那就只能適應(yīng)現(xiàn)狀。
項(xiàng)目痛點(diǎn)
那么針對原來沒有任何工程化情況的胖腳本,每次新增一個業(yè)務(wù)方,我需要做的事情如下:
打開一個胖腳本和JSSDK接入文檔,拷貝一份新的。
找后端要分配好的appKey,找對對應(yīng)的appKey那一行代碼手動修改。
手動混淆修改完好的腳本并上傳到CDN。
修改JSSDK接入文檔中CDN的地址,保存后發(fā)送給業(yè)務(wù)方。
整個過程都需要手動進(jìn)行,相對來說非常繁瑣,并且一不小心就會填錯,每次都需要對腳本和接入文檔進(jìn)行檢查。
針對以上情況,得到我們需要解決的問題:
怎樣針對一個新的業(yè)務(wù)方快速輸出一份新的JSSDK和接入文檔?
怎樣快速對新的JSSDK進(jìn)行混淆并上傳到CDN。
自動化方案
介紹方案之前,先上一張平臺截圖,以便先有一個直觀的認(rèn)識:
SDK自動化部署平臺主要實(shí)現(xiàn)了JSSDK的編譯,發(fā)布測試(在線預(yù)覽),上傳CDN功能。
服務(wù)端技術(shù)棧包括:
框架 Express
熱更新 nodemon
依賴注入 awilix
數(shù)據(jù)持久化 sequelize
部署 pm2
客戶端技術(shù)棧就不介紹了,Vue全家桶 + vue-property-decorator + vuex-class。
項(xiàng)目搭建參考:
Vue+Express+MySQL 全棧初體驗(yàn)
https://juejin.im/post/5ce96694f265da1bc5523f69
自動化部署平臺主要依賴于 GIT + 本地環(huán)境 + 私有NPM源 + MYSQL,各環(huán)節(jié)之間進(jìn)行通信交互,完成自動化部署。
主要達(dá)到的效果:本地環(huán)境拉取git倉庫代碼后,進(jìn)行需求開發(fā),完成后發(fā)布一個帶Rollup的SDK編譯器包到私有NPM倉庫,自動化部署平臺在工程目錄安裝指定版本的SDK,并且備份到本地,在SDK編譯時,選擇特定版本的Rollup的SDK編譯器,并傳參(如appKey,appId等)到編譯器中進(jìn)行編譯,同時自動生成JSSDK接入文檔等后打包成帶描述文件的Release包,在上傳到CDN時,將描述文件的對應(yīng)的信息寫入MYSQL中進(jìn)行保存。
版本管理
由于JSSDK原本只是一個腳本,我們必須實(shí)現(xiàn)項(xiàng)目的工程化,從而完成版本管理,方便快速版本切換進(jìn)行發(fā)布,回滾,進(jìn)而快速止損。
首先,我們需要將項(xiàng)目工程化,使用Rollup進(jìn)行模塊管理,并且在發(fā)包NPM包的時候,輸入為各種參數(shù)(如appKey)輸出為一個Rollup Complier的函數(shù),然后使用rollup-plugin-replace在編譯時候替換代碼中具體的參數(shù)。
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之前需要準(zhǔn)備的壓縮包。因此,打包JSSDK之后,我們需要生成的文件有,接入文檔、JSSDK DEMO預(yù)覽頁面、JSSDK編譯結(jié)果、描述文件。
首先,打包函數(shù)如下:
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) { // 數(shù)據(jù)庫查詢對應(yīng)的業(yè)務(wù)方的CDN文件名 const [sdkInfoErr, sdkInfo] = await this.supplyDao.getSupplyInfo(supplyId); if (sdkInfoErr) { return this.fail('服務(wù)器錯誤', null, sdkInfoErr); } const {appKey, cdnFilename, name} = sdkInfo; // 需要替換的數(shù)據(jù) 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); // 生成預(yù)覽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目標(biāo)目錄 const dir = path.join(this.releasePath, supplyId + ''); const fileName = `${supplyId}-${sdkVersion}.zip`; const zipFileName = path.join(dir, fileName); // 壓縮所有結(jié)果文件 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的編譯很簡單,只需要加載對應(yīng)版本的JSSDK的編譯函數(shù),然后將對應(yīng)的參數(shù)傳入編譯函數(shù)得到一個Rollup Compiler,然后將 Compiler 結(jié)果寫入Release路徑即可。
export default class SupplyService extends Service { async buildSdk(supplyId, appKey, sdkVersion) { try { const sdkBackupPath = this.sdkBackupPath; // 加載對應(yīng)版本的備份的JSSDK包的Rollup編譯函數(shù) 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 編譯結(jié)果至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(); // 替換`[[`前綴和`]]`后綴的內(nèi)容 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)建目標(biāo)目錄 shell.mkdir(releasePath).then((code, stdout, stderr) => { if (code !== 0 ) { resolve(false); return; } // 將替換后的結(jié)果寫入release路徑 fs.writeFileSync(path.resolve(releasePath, `JSSDK-文檔-${data.name}.docx`), buf); resolve(true); }).catch(e => { console.error(e); resolve(false); }); } }); } }
生成預(yù)覽DEMO頁面
與接入文檔生成原理類似,打開一個DEMO模板HTML文件,替換內(nèi)部字符,重新生成文件:
export default class SupplyService extends Service { generateDemoHtml(data, file, toFile) { return new Promise((resolve, reject) => { const supplyId = data.supplyId; // 需要替換的數(shù)據(jù) const replaceData = data; // 打開文件 const content = fs.readFileSync(path.resolve(supplyFilesPath, file), 'utf-8'); // 字符串替換`{{`前綴和`}}`后綴的內(nèi)容 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包描述文件
將當(dāng)前打包的一些參數(shù)存在一個文件中的,一并打包到Release包中,作用很簡單,用來描述當(dāng)前打包的一些參數(shù),方便上線CDN的時候記錄當(dāng)前上線的是哪個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描述數(shù)據(jù) const data = {version: sdkVersion, appKey, supplyId}; try { // 寫入release目錄 fs.writeFileSync(writePath, JSON.stringify(data)); resolve(true); } catch (e) { console.error(e); resolve(false); } }); } }
打包所有文件結(jié)果
將之前生成的JSSDK編譯結(jié)果、接入文檔、預(yù)覽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文件,而正好我們運(yùn)維在我的自動化部署平臺的機(jī)器上掛載了NFS,即我只需要本地將JSSDK文件拷貝到共享目錄,就實(shí)現(xiàn)了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包描述文件的數(shù)據(jù)同步到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('上傳失敗'); } }
看完上述內(nèi)容是否對您有幫助呢?如果還想對相關(guān)知識有進(jìn)一步的了解或閱讀更多相關(guān)文章,請關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝您對創(chuàng)新互聯(lián)的支持。