手刃前端监控系统

栏目: 服务器 · 发布时间: 5年前

内容简介:我们为什么要做前端系统呢,可以明显地从下表看出来,前端的性能对于产品的价值提升还是蛮有帮助的,但是这些信息如果我们能实时的采集到,并且实施以监控和报警,让整个产品在产品线上一直保持高效的运作,这是我们的目标,做前端监控只是为了达到这个目标的手段。其次,前端监控能让我们即使发现问题(页面加载过慢等)或者错误(js错误,资源加载失败等),我们总不可能等待用户的反馈投诉,到那个时候花儿都谢了。也能够在我们改进前端代码性能或者相关措施后,对于性能的提升有多少,有一个清晰地数据前后对比,这样子也比较好写报告(KPI

我们为什么要做前端系统呢,可以明显地从下表看出来,前端的性能对于产品的价值提升还是蛮有帮助的,但是这些信息如果我们能实时的采集到,并且实施以监控和报警,让整个产品在产品线上一直保持高效的运作,这是我们的目标,做前端监控只是为了达到这个目标的手段。

性能 收益
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开发的过程,学习到了很多东西,下面开始分享。

技术选型

  • 前端: Reactecharts , axioswebpackantd , typescript 等;
  • 后端: egg , typescript 等;
  • 数据库: mysql , opentsdb
  • 消息队列: kafka

本来公司内部使用的全都是 vue ,为什么在这里我用了 react ,一是因为自己一直对 react 就持有兴趣,二来则是 vue 实在用的有点多了。总的感觉来说就是 react 通过 jsxrender 函数可以做到高自由度的封装,而 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 设为资源名字,然后把 requestresponse , 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 设为 requestresponse , 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.jssdk.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));
    }
  }
}
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Weaving the Web

Weaving the Web

Tim Berners-Lee / Harper Paperbacks / 2000-11-01 / USD 15.00

Named one of the greatest minds of the 20th century by Time , Tim Berners-Lee is responsible for one of that century's most important advancements: the world wide web. Now, this low-profile genius-wh......一起来看看 《Weaving the Web》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换