Зачем нужен внешний мониторинг
Зачем нам нужна фронтенд-система?Из приведенной ниже таблицы ясно видно, что производительность фронтенда весьма полезна для стоимости продукта, но если мы сможем собрать эту информацию в режиме реального времени и внедрить Мониторинг и оповещение, пусть Весь продукт работает эффективно на продуктовой линейке Это наша цель Front-end мониторинг является лишь средством для достижения этой цели.
представление | доход |
---|---|
Задержка Google 400 мс | Объем поиска упал на 0,59% |
Задержка Bing 2 с | Выручка упала на 4,3% |
Задержка Yahoo 400 мс | Падение трафика на 5-9% |
Время открытия страницы в Mozilla сократилось на 2,2 с. | Увеличение загрузки на 15,4% |
Netflix включает Gzip | На 13,25% выше производительность и на 50% меньше пропускная способность. |
Во-вторых, front-end мониторинг позволяет нам находить проблемы (слишком медленная загрузка страниц и т.д.) или ошибки (ошибки 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 | Идентификатор проекта |
metric | вид (представление). сервис (сервис). тема (тема) _uri (идентификатор) |
tasg | Запишите некоторые нечисловые значения, похожие на маркировку |
timestamp | отметка времени |
step | Цикл представления данных |
counterType | Тип данных, по умолчанию используется тип GAUGE (мгновенное значение) и тип COUNTER (накопленное значение). |
value | В метрических условиях удельное значение этих данных |
я здесьmetric
Одна из записейfrontMonitor.perf.time_dns
Относится к: внешнему мониторингу system-performance-time-dns.
мы можем начать сmetric
Показатели результативности числового типа извлекаются из:
показатель | имея в виду |
---|---|
load | время полной загрузки страницы |
ready | Время завершения загрузки HTML, время готовности DOM |
fpt | время первого рендера, время белого экрана |
tti | Интерактив в первый раз |
dom | Парсинг DOM требует времени |
dns | Разрешение DNS требует времени |
tcp | Отнимающий много времени анализ TCP |
ssl | Безопасное соединение SSL занимает много времени и существует только в HTTPS. |
ttfb | время до первого байта |
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 ошибка выборки
Нативная выборка здесь тоже зацеплена:
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
framework, поэтому вам нужно сначала собрать интерфейсный проект во внутренний проект.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
Функция шаблона предоставлена, поэтому вам нужно написать промежуточное программное обеспечение, потому что яйцо написано на основе 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 не удалась, она может появиться, в результате чего выходит сбой рендеринга страницы.
Итак, нам нужно исправить пути доступа к этим локальным статическим ресурсам, чтобы при доступе они находили их из корневого каталога, поэтому мы добавляем еще один middleware:
// 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 есть ошибка, это повлияет на все системы. Второй момент заключается в том, что разные системы имеют разные функциональные требования к sdk, поэтому его сложнее поддерживать, если используется один и тот же sdk. Учитывая эти два момента, я сделал функцию управления выпуском версии sdk.Ниже приведен конкретный процесс;
Компиляция SDK 5.1:
Получите текущий номер последней версии из службы и обновите номер версии; создайте несколько записей и разделите 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()
Загрузка SDK 5.2:
SDK загружается на сервер локально, и соответствующая версия получается при его выпуске для последующих операций. Среди них операция загрузки sdk должна выполняться людьми вручную, чтобы соответствующая информация могла быть записана, чтобы ее можно было откатить при возникновении проблемы или необходимости:
И мультисистемный релиз:При публикации бэкэнд находит соответствующую версию sdk локально, и выясняет конфигурацию sdk, соответствующую системе, чтобы решить, какой функцией настроить sdk, то есть обрезать sdk, при генерации соответствующего sdk дать sdk a project Флаг (установленный во время создания) для именования имени sdk (например, b1anker.sdk.js), так что выпуск sdk затронет только ту систему, которая использует этот флаг;
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
иметь определенное понимание.