手刃前端監控系統
為什麼要做前端監控
我們為什麼要做前端系統呢,可以明顯地從下表看出來,前端的效能對於產品的價值提升還是蠻有幫助的,但是這些資訊如果我們能實時的採集到,並且實施以監控和報警,讓整個產品在產品線上一直保持高效的運作,這是我們的目標,做前端監控只是為了達到這個目標的手段。
效能 | 收益 |
---|---|
Google 延遲 400ms | 搜尋量下降 0.59% |
Bing 延遲 2s | 收入下降 4.3% |
Yahoo 延遲 400ms | 流量下降 5-9% |
Mozilla 頁面開啟減少 2.2s | 下載量提升 15.4% |
Netflix 開啟 Gzip | 效能提升 13.25% 頻寬減少50% |
其次,前端監控能讓我們即使發現問題(頁面載入過慢等)或者錯誤(js錯誤,資源載入失敗等),我們總不可能等待使用者的反饋投訴,到那個時候花兒都謝了。也能夠在我們改進前端程式碼效能或者相關措施後,對於效能的提升有多少,有一個清晰地資料前後對比,這樣子也比較好寫報告(KPI)。
於是擼起袖子,說幹就幹,自己參照了市面上的各鍾前端監控系統,搞一個貼合公司需求的前端監控系統。並把其接入了內部系統做了進行測試。參與了從產品設計,前後端開發,SDK開發的過程,學習到了很多東西,下面開始分享。
技術選型
- 前端:
React
,echarts
,axios
,webpack
,antd
,typescript
等; - 後端:
egg
,typescript
等; - 資料庫:
mysql
,opentsdb
; - 訊息佇列:
kafka
;
本來公司內部使用的全都是vue
,為什麼在這裡我用了react
,一是因為自己一直對react
就持有興趣,二來則是vue
實在用的有點多了。總的感覺來說就是react
通過jsx
和render
函式可以做到高自由度的封裝,而vue
則需要需要花更多的精力在封裝上;但是react
對於狀態的管理比較花精力,一個不注意就會無限迴圈觸發render
函式,vue
系統簡介
監控了什麼東西
通過埋下SDK,上報資料,監控了以下兩大型別資料:
1.頁面載入效能資料
效能資料的上報使用了opentsdb
時序資料庫(時序資料庫非常適合監控類的資料),先看一下上報的具體資料,是一個數組,如下圖所示:
有關
opentsdb
時序資料庫的介紹可以看一下這篇文章。
來看看每個欄位的具體含義:
欄位 | 含義 |
---|---|
endpoint | 專案ID |
metric | view(檢視).service(服務).topic(主題)_uri(識別符號) |
tasg | 記錄一些非數值型別的值,類似打上一些標籤 |
timestamp | 時間戳 |
step | 資料上報週期 |
counterType | 資料型別,預設是GAUGE型別(瞬時值),還有COUNTER型別(累加值) |
value | 在metric條件下,這條資料的具體數值 |
我這裡的metric
填的其中一條是frontMonitor.perf.time_dns
指的是:前端監控系統-效能-時間-dns。
我們可以從metric
中提取出效能數值型別的指標:
指標 | 含義 |
---|---|
load | 頁面完全載入時間 |
ready | HTML 載入完成時間,DOM ready時間 |
fpt | 首次渲染時間,白屏時間 |
tti | 首次可互動時間 |
dom | DOM解析耗時 |
dns | DNS解析耗時 |
tcp | TCP解析耗時 |
ssl | SSL安全連線耗時,只在HTTPS存在 |
ttfb | 首位元組(time to first byte) |
trans | 資料傳輸耗時 |
res | 頁面同步資源載入耗時 |
還記錄了些字串型別指標: 如作業系統型別
,瀏覽器型別
,解析度
,頁面path
,域名
,sdk版本
等,都可以在tags
裡面找到。
根據以上指標可以做成如下頁面:
效能總覽:
頁面效能:
2.資源載入資料
同樣也是用opentsdb
,為了節省空間,這裡我只展示其中陣列的一條資料,如下圖:
我這裡的metric
填的其中一條是frontMonitor.perf.resource_size
指的是:前端監控系統-效能-資源-資源大小。
資源載入的資料我們可以用performance.getEntriesByType('resource')
獲得:
metric
中提取出效能數值型別的指標:
指標 | 含義 |
---|---|
size | 資源大小(decodedBodySize) |
parseSize | 壓縮後資源大小(transferSize) |
request | 請求時間(responseStart - requestStart) |
response | 響應時間(responseEnd - responseStart) |
還記錄了些字串型別指標: 如資源名字
,資源型別
,域名
,協議
等,都可以在tags
裡面找到。
根據以上指標可以做成資源載入頁:
3.錯誤資料
前端錯誤主要分為三類:
3.1指令碼錯誤
import BaseError from './base'
import EventUtil from '../../utils/event'
export default class ScriptError extends BaseError {
constructor () {
super('script')
}
start () {
this.attachEvent()
}
attachEvent () {
// 普通指令碼你錯誤
EventUtil.add(window, 'error', (e) => {
this.handleError(e)
}, false)
// promise之類的錯誤
EventUtil.add(window, 'unhandledrejection', (e) => {
this.handleError(e)
}, false)
}
handleError (e) {
const {
message,
filename,
lineno,
colno,
reason,
type,
error
} = e
if (!message) {
this.send({
type,
message: reason.message,
stack: reason.stack
})
} else {
const lowMsg = message.toLowerCase()
if (lowMsg.includes('script error')) {
this.send({
message
})
} else {
this.send({
message,
filename,
lineno,
colno,
type,
stack: error.stack
})
}
}
}
}
複製程式碼
如果引用的指令碼跨域,則需要另行設定:
<script type="rexr/javascript" src="https://crossorigin.com/app.js" crossorigin="anonymous"></script>
要在引用的script
標籤中加上crossorigin="anonymous"
- 伺服器要返回的頭資訊包括:
Access-Control-Allow-Origin: *
3.2資源載入錯誤
可以捕獲資源訪問失敗的錯誤,如img,script,style等。
import BaseError from './base'
import EventUtil from '../../utils/event'
import DOMReady from '../../utils/ready' // 相容IE8
export default class DocumentError extends BaseError {
constructor () {
super('document')
}
start () {
this.attachEvent()
}
attachEvent () {
DOMReady(() => {
EventUtil.add(document, 'error', (e) => {
const el = EventUtil.getTarget(e)
const tag = el.tagName.toLowerCase()
const src = el.src
this.send({
el,
tag,
src
})
}, true)
})
}
}
複製程式碼
對於此型別錯誤的捕獲,需要滿足一下兩個條件:
- 事件需要設定在捕獲階段
- 資源必須在dom樹上
3.3 ajax
請求錯誤
這裡需要對原生xhr
進行打補丁,從而攔截ajax
請求
import BaseError from "./base";
// 過濾自身伺服器上報時發生錯誤
const urlWhiteList = [
'//api.b1anker.com/msg',
'//api.b1anker.com/d.gif/',
'//api.b1anker.com/form/push'
]
export default class AjaxError extends BaseError {
constructor () {
super('ajax')
}
start () {
this.patch()
}
patch () {
if (!XMLHttpRequest && !window.ActiveXObject) {
return
}
// patch
const XHR = XMLHttpRequest || window.ActiveXObject
const open = XHR.prototype.open
let METHOD = ''
let URL = ''
try {
XHR.prototype.open = function (method, url) {
// 儲存請求方法和請求連結
METHOD = method
URL = url
open.call(this, method, url, true)
}
} catch (err) {
console.log(err)
}
const send = XHR.prototype.send
const self = this
XHR.prototype.send = function (data = null) {
// 獲取剛剛暫存的請求連結
let CURRENT_URL = URL
try {
this.addEventListener('readystatechange', () => {
if (this.readyState === 4) {
if (this.status !== 200 && this.status !== 304) {
// 不上報自身的報錯,如上報伺服器出錯等
if (urlWhiteList.some((url) => CURRENT_URL.includes(url))) {
return
}
const name = this.statusText
const reponse = this.responseText
const url = this.responseURL
const status = this.status
const withCredentials = this.withCredentials
self.send({
name,
reponse,
url,
status,
withCredentials,
data,
method: METHOD
})
}
}
}, false)
send.call(this, data)
} catch (err) {
console.log(err)
}
}
}
}
複製程式碼
3.4 fetch錯誤
這裡也對原生fetch進行了hook:
import BaseError from './base'
export default class FetchError extends BaseError {
constructor() {
super('fetch')
}
start () {
this.patch()
}
patch() {
if (!window.fetch) {
return null
}
let _fetch = fetch
const self = this
window.fetch = function() {
const params = self.parseArgs(arguments)
return _fetch
.apply(this, arguments)
.then(self.checkStatus)
.catch(async (err) => {
const { response } = err
if (response) {
const data = await response.text()
self.send({
name: response.statusText,
type: response.type,
data,
status: response.status,
url: response.url,
redirected: response.redirected,
method: params.method,
credentials: params.credentials,
mode: params.mode
})
} else {
self.send({
name: err.message,
method: params.method,
credentials: params.credentials,
mode: params.mode,
url: params.url
})
}
return err
})
}
}
checkStatus (response) {
if (response.status >= 200 && response.status < 300) {
return response
} else {
var error = new Error(response.statusText)
error.response = response
throw error
}
}
parseArgs (args) {
const parms = {
method: 'GET',
type: 'fetch',
mode: 'cors',
credentials: 'same-origin'
}
args = Array.prototype.slice.apply(args)
if (!args || !args.length) {
return parms
}
try {
if (args.length === 1) {
if (typeof args[0] === 'string') {
parms.url = args[0]
} else if (typeof args[0] === 'object') {
this.setParams(parms, args[0])
}
} else {
parms.url = args[0]
this.setParams(parms, args[1])
}
} catch (err) {
throw err
} finally {
return parms
}
}
setParams (params, newParams) {
params.url = newParams.url || params.url
params.method = newParams.method
params.credentials = newParams.credentials || params.credentials
params.mode = newParams.mode || params.mode
return params
}
}
複製程式碼
4.自定義資料上報
有時候使用者需要監控自己頁面上的一些資料,比如說直播視訊中,監控啟動這個播放器的時間,又或者是播放器的播放幀率等。基於此需求,我們簡單地來擴充套件一波sdk
:
// customReport.js
import BaseReport from './baseReport'
import throttle from 'lodash/throttle'
import isEmpty from 'lodash/isEmpty'
// 暫時只支援數值型別的上報
const defaultOptions = {
type: 'number'
}
export default class CustomReport extends BaseReport {
constructor (options = {
delay: 5000
}) {
super('custom');
this.skynetQuque = [];
// 使用者上報有可能是多次上報,所以做了個防抖,把資料快取起來然後再統一上報
this.sendToSkynetThrottled = throttle(this.sendToSkynet.bind(this), options.delay, {
leading: false,
trailing: true
})
}
upload (options = defaultOptions, data) {
const { type } = options;
if (type === 'number') {
// 數值型別的上報
this.uploadToSkynet(data);
}
}
uploadToSkynet (data) {
this.skynetLoop(data);
}
// 把資料快取到佇列裡,等時間到了,統一上報
skynetLoop (data) {
this.skynetQuque.push(this.formatSkynetData(data));
this.sendToSkynetThrottled(this.skynetQuque)
}
// 把資料格式化成opentsdb的上報格式
formatSkynetData (data) {
const { module, metric, tags, value } = data;
const result = {
metric: `frontMonitor.custom.${module}_${metric}`,
endpoint: `${window.__HBI.id}`,
counterType: "GAUGE",
step: 1,
value,
timestamp: parseInt((new Date()).getTime() / 1000)
};
if (!isEmpty(tags)) {
// 如果tags不是空,則需要做一些轉換處理,處理成k1=v1,k2=v2形式的字串
result.tags = Object.entries(tags).map(([key, value]) => `${key}=${value}`).join(',')
}
return result
}
// 上報資料,並把佇列清空
sendToSkynet (data) {
this.sender.doSendToSkynet(data)
this.skynetQuque = []
}
}
複製程式碼
這樣子,開發者就可以用如下程式碼進行上報:
if (window.__CUSTOM_REPORT__) {
const data = {
module: 'player',
metric: 'openTime',
value: 100,
tags: {
browser: 'Chrome69',
op: 'mac'
}
}
c.upload({
type: 'number'
}, data)
}
複製程式碼
遇到了什麼問題
1.上報跨域問題
由於每個網站引用sdk
的時候,sdk
上報的地址是固定的(專門用來做上報資料處理,跟目標網站非同源),就會發生跨域問題,可以利用form
表單和iframe
結合解決跨越問題:
class FormPost {
postData (url, data) {
let formId = this.getId('form');
let iframeId = this.getId('iframe');
let form = this.initForm(formId, iframeId, url, data);
let ifr = this.initIframe(iframeId);
return this.doPost(ifr, form);
}
doPost (ifr, form) {
return new Promise(resolve => {
let target = document.head || document.getElementsByTagName('head')[0];
!target && (target = document.body);
target.appendChild(form);
target.appendChild(ifr);
ifr.onload = () => {
// iframe載入完成後解除安裝form和iframe
form.parentNode.removeChild(form);
ifr.parentNode.removeChild(ifr);
resolve();
}
form.submit();
});
}
getId (prefix) {
!prefix && (prefix = '');
return `${prefix}${new Date().getTime()}${parseInt(Math.random() * 10000)}`;
}
initForm (id, ifrId, url, data) {
let fo = document.createElement('form');
fo.setAttribute('method', 'post');
fo.setAttribute('action', url);
fo.setAttribute('id', id);
fo.setAttribute('target', ifrId);// 在iframe中載入
fo.style.display = 'none';
for (let k in data) {
let d = data[k];
let inTag = document.createElement('input');
inTag.setAttribute('name', k);
inTag.setAttribute('value', d);
fo.appendChild(inTag);
}
return fo;
}
initIframe (id) {
let ifr = (/MSIE (6|7|8)/).test(navigator.userAgent) ?
document.createElement(`<iframe name="${id}">`) :
document.createElement('iframe')
ifr.setAttribute('id', id);
ifr.setAttribute('name', id);
ifr.style.display = 'none';
return ifr;
}
}
export default new FormPost();
複製程式碼
2.資料採集維度指標爆炸
由於使用的是opentsdb
時序資料庫,一開始設計上報資源載入資料的時候,想著把uri
設為資源名字,然後把request
,response
, size
, parseSize
等資訊放到tags
裡,value
則隨便填個數字就好,一條資源只用上報一條資料即可。這樣子上報是可以正常上報的,但是由於在tags
裡存數值型別的值(數值的具體指太多了),導致資料組合爆炸,資料根本就查不出來。
優化前上報資料格式:
{
"metric": "frontMonitor.perf.resource_app.js",
"value": 0,
"endpoint": "3",
"timestamp": 1539068028,
"tags": "size=177062,parseSize=300,request=200,response=300,type=script,origin=huya.com,protocol=h2",
"counterType": "GAUGE",
"step": 1
}
複製程式碼
所以只好把uri
設為request
,response
, size
, parseSize
等,把資源名字存到tags
裡,這樣子每條資源就要上報多條資料。雖然會增加上報內容體積,但是這樣可以有效地降低維度,使得資料可以快速查出來。
優化後上報資料格式:
{
"metric": "frontMonitor.perf.resource_size",
"value": 177062,
"endpoint": "3",
"timestamp": 1539068028,
"tags": "name=app.js,type=script,origin=huya.com,protocol=h2",
"counterType": "GAUGE",
"step": 1
}
複製程式碼
3.上報併發量大
考慮如果把系統接入使用者量大的網站中,就會遇到同一秒收到多條資料的情況。當遇到這種情況,opentsdb
就會出現一個覆蓋問題,具體原因就是上報的資料中除了value
欄位,其他欄位都一樣的話,opentsdb
就會把這一秒內的最後一條資料覆蓋掉前面的資料。當時一個解決辦法就是給tags
欄位裡新增unique
欄位,並通過一些簡單的演算法讓它去到唯一值,這樣就可以解決覆蓋問題。
但是這樣並不完美,主要有兩個原因,第一個原因是在畫出來的圖表中會出現在x軸上的同一個點上會出現多個y值,所以只能對圖表做些適應,在前端聚合這些資料(在服務端做會增加服務端壓力);第二個原因是資料量太大,會對伺服器造成壓力的同時也讓查詢效率變慢,於是利用kafak
做了佇列處理,對這些資料做分鐘維度的歸併,再上報到opentsdb
,這樣一箭雙鵰,即解決了覆蓋問題,也能減少伺服器壓力並提高查詢效率。
4.部署的坑
4.1前端構建
因為專案的釋出是要通過公司的統一發布系統進行釋出,並且後端用的是egg
框架,所以需要先把前端專案構建到後端專案的app/public
資料夾下 :
即需要修改前端的構建專案為後端專案中的app/public
下:
4.2後端構建
由於使用了egg
+ typescript
,所以使用生產環境程式碼的時候需要多一個用tsc
編譯成js
的步驟,不然會報錯,以下是構建指令碼命令:
"scripts": {
"start": "egg-scripts start --daemon --title=egg-server-monitor-backend --port=8088",
"stop": "egg-scripts stop --title=egg-server-monitor-backend --port=8088",
"dev": "egg-bin dev -r egg-ts-helper/register --port=8088",
"debug": "egg-bin debug -r egg-ts-helper/register",
"test-local": "egg-bin test -r egg-ts-helper/register",
"test": "npm run lint -- --fix && npm run test-local",
"cov": "egg-bin cov -r egg-ts-helper/register",
"tsc": "ets && tsc -p tsconfig.json",
"ci": "npm run lint && npm run cov && npm run tsc",
"autod": "autod",
"lint": "tslint --project . -c tslint.json",
"clean": "ets clean",
"pack": "npm run tsc && rm -rf ./node_modules && npm i --production && tar -zcvf ../ROOT.tgz ./ && npm run reDevEnv && npm run clean",
"reDevEnv": "rm -rf ./node_modules && npm i",
"zip": "node ./zip.js"
}
複製程式碼
我們構建的時候,用的是pack
指令,即使用npm run pack
或者yarn run pack
即可,其實就是執行npm run tsc && rm -rf ./node_modules && npm i --production && tar -zcvf ../ROOT.tgz ./ && npm run reDevEnv && npm run clean
。執行這條指令發生瞭如下幾個步驟:
- 先用
tsc
編譯成js
程式碼; - 刪掉
node_modules
程式碼; - 安裝生產環境的
node_modules
程式碼; - 把專案壓縮成
.tgz
格式; - 刪掉
node_modules
程式碼; - 重新安裝開發環境的
node_modules
程式碼; - 刪掉
tsc
編譯成的js
程式碼;
4.3後端使用前端靜態資源
由於是前後端分離專案,並沒有用到egg
提供的模板功能,所以需要寫一箇中間件,因為egg是基於koa來寫的,所以koa的一些中介軟體是也是可以用的,來指定訪問路由時引用的頁面:
// kstatic.ts
import * as KoaStatic from 'koa-static';
import * as path from 'path';
export default (options) => {
// 使用koa-static中介軟體
return KoaStatic(path.join(__dirname, '../public'), options);
};
複製程式碼
然後再config/config.default.ts
中新增程式碼config.middleware = ['kstatic']
即可
4.4修復路由指向
由於前端頁面使用react-router-dom
,並且使用的是history
模式,當訪問根頁面時是可以正常載入頁面和js等檔案的,但是當我們需要訪問二級甚至三級路由或者重新整理頁面時,如xxx.huya.com/test/100
時,就可能會出現js載入失敗的情況,從而導致頁面渲染失敗。
所以我們需要修復這些本地靜態資源的訪問路徑,當訪問的時候,讓他們從根目錄上去找,因此我們再新增一箇中間件:
// historyApiFaalback.ts
import * as url from 'url';
export default (options) => {
return function historyApiFallback(ctx, next) {
options.logger = ctx.logger;
const logger = getLogger(options);
logger.info(ctx.url);
// 如果不是get請求或者非html則跳過
if (ctx.method !== 'GET' || !ctx.accepts(options.accepts || 'html')) {
return next();
}
const parsedUrl = url.parse(ctx.url);
let rewriteTarget;
options.rewrites = options.rewrites || [];
// 根據規則進行url跳轉處理
for (let i = 0; i < options.rewrites.length; i++) {
const rewrite = options.rewrites[i];
let match;
if (parsedUrl && parsedUrl.pathname) {
match = parsedUrl.pathname.match(rewrite.from);
} else {
match = '';
}
if (match !== null) {
rewriteTarget = evaluateRewriteRule(parsedUrl, match, rewrite.to, ctx);
ctx.url = rewriteTarget;
return next();
}
}
const pathname = parsedUrl.pathname;
if (
pathname &&
pathname.lastIndexOf('.') > pathname.lastIndexOf('/') &&
options.disableDotRule !== true
) {
return next();
}
rewriteTarget = options.index || '/index.html';
logger('Rewriting', ctx.method, ctx.url, 'to', rewriteTarget);
ctx.url = rewriteTarget;
return next();
};
};
function evaluateRewriteRule(parsedUrl, match, rule, ctx) {
if (typeof rule === 'string') {
return rule;
} else if (typeof rule !== 'function') {
throw new Error('Rewrite rule can only be of type string or function.');
}
return rule({ parsedUrl, match, ctx });
}
function getLogger(_options) {
if (_options && _options.verbose) {
return console.log.bind(console);
} else if (_options && _options.logger) {
return _options.logger;
}
}
複製程式碼
然後在config/config.default.ts
裡之前的中介軟體程式碼中新增:config.middleware = ['historyApiFallback', 'kstatic'];
,注意要按順序。
並且再新增選項程式碼:
config.historyApiFallback = {
ignore: [/.*\..+$/, /api.*/],
rewrites: [{ from: /.*/, to: '/' }]
};
複製程式碼
5sdk版本釋出管理
一開始為了方便,就把編譯後的sdk直接丟到cdn上,然後各個系統直接引用這個指令碼即可。但是這個的風險比較大,主要有兩點原因,第一點是當sdk沒有做好充分測試就上傳到cdn上的話,sdk如果出現bug,則所有系統都會受到影響。第二點就是對於不同的系統對於sdk的功能需求是不一樣的,所以用同一個sdk的話,維護起來就比較困難。考慮這兩點,於是做了sdk版本釋出管理的功能,以下是具體流程;
5.1 sdk編譯:
向服務獲取當前最新版本號,並更新一個版本號;構建多入口,根據功能模組將sdk切割成多個檔案,如:sdk.perf.js
和sdk.error.js
(分別是效能監控,錯誤監控)。然後將幾個檔案合併成一個檔案,並加上各模組之間加上切割符,以備後續分離sdk;
const axios = require('axios')
const webpack = require('webpack')
const webpackConfig = require('../webpack.config.prod.js')
const fs = require('fs')
const path = require('path')
const OUTPUT_DIR = '../dist/'
const resolve = (dir) => path.join(__dirname, OUTPUT_DIR, dir)
const combineFiles = (bases, error, target) => {
// 合併sdk
let data = ''
// 合併公共模組
bases.forEach((file) => {
data += fs.readFileSync(resolve(file))
fs.unlinkSync(resolve(file))
})
// 新增錯誤監控切割符,合併錯誤監控程式碼
data += '/*HBI-SDK-ERROR-MONITOR*/'
data += fs.readFileSync(resolve(error))
fs.unlinkSync(resolve(error))
fs.writeFileSync(resolve(target), data)
}
async function build () {
// 獲取sdk最新版本號,新更新版本號
const version = await axios.get('https://api.b1anker.com/api/v0/systemVariable/list?name=SDK_VERSION')
.then(({data: { data }}) => {
return data[0].value;
});
webpack(webpackConfig({
version
}), (err, stats) => {
if (err || stats.hasErrors()) {
console.error('構建失敗')
throw err
} else {
// 合併sdk模組
combineFiles([
'hbi.vendor.js',
'hbi.commons.js',
'hbi.performance.js'
], 'hbi.error.js', 'hbi.js')
console.error('構建成功: v' + version);
}
});
}
build()
複製程式碼
5.2 sdk上傳:
sdk上傳到伺服器本地,當釋出的時候獲取相應版本進行後續操作中。其中sdk的上傳操作應該由人手動操作,這樣可以記錄相應的資訊,以便出問題或有需求的時候回滾:
並且多系統釋出:在進行釋出的時候,後端從本地找出對應版本的sdk,並且查出系統對應的sdk配置,從而決定給sdk配置什麼功能,也就是切割sdk;在生成相應sdk的時候,給sdk以專案的flag(建立的時候設定)來命名sdk的名字(如b1anker.sdk.js),這樣子就可以做到sdk的釋出只會作用到使用了這個flag的系統;
export default class SDK extends Service {
// 釋出sdk
public async pulishSDK (projects: string[], version: string) {
const success: any[] = [];
const error: any[] = [];
for (let i = 0; i < projects.length; i++) {
const id: number = Number(projects[i]);
try {
// 獲取專案相應資訊
const { flag } = await this.service.project.getProject(id);
// 根據專案flag和sdk版本生成對應的sdk
await this.uploadSDKToCDN(flag, version);
// 上傳至cdn
await this.service.sdk.updateSdkInfo(id, version);
success.push(id);
} catch (err) {
error.push(id);
this.logger.error(err);
}
}
return {
success,
error
};
}
public async uploadSDKToCDN (flag: string, version: string) {
// 從資料庫中查找出專案的錯誤配置資訊
const error = await this.app.mysql.query(`select error from project a inner join project_sdk_setting b where a.id = b.pid and a.flag = '${flag}';`)
// 預設關閉錯誤監控
let enableError = false;
// 處理錯誤配置
try {
if (JSON.parse(error[0].error).length) {
enableError = true;
}
} catch (err) {
throw err;
}
const sdkPath = path.join(os.homedir(), 'sdk', `b1anker-${version}.js`);
const cdnPath = `b1anker/${flag}.sdk.js`;
// 根據專案的sdk配置來生成最終sdk
if (enableError) {
// 沒有開啟錯誤監控則修改下名字就可以直接上傳到cdn
await this.service.util.uploadFileToCdn(sdkPath, cdnPath);
} else {
const sdkData = fs.readFileSync(sdkPath).toString();
// 根據切割符切割sdk,然後生成新的sdk
const withoutErrorMonitor = sdkData.split('/*HBI-SDK-ERROR-MONITOR*/')[0];
// 上傳到cdn
await this.service.util.uploadBufferToCdn(cdnPath, new Buffer(withoutErrorMonitor));
}
}
}
複製程式碼
總結
通過這個專案,個人接觸到了很多前端以外的知識,系統構思,原型設計,後端邏輯處理,mysql
關係型資料庫,opentsdb
時序資料庫,kafak
訊息佇列等,也讓自己對一個完整的系統有了較為清晰的認識,也能更好理解不同技術上的瓶頸,尤其是前端和後端的關注方向。也擴充套件了自己的前端技術棧,對react
有了一定的認識。