手刃前端监控系统

栏目: 服务器 · 发布时间: 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));
    }
  }
}
复制代码

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

查看所有标签

猜你喜欢:

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

The Creative Curve

The Creative Curve

Allen Gannett / Knopf Doubleday Publishing Group / 2018-6-12

Big data entrepreneur Allen Gannett overturns the mythology around creative genius, and reveals the science and secrets behind achieving breakout commercial success in any field. We have been s......一起来看看 《The Creative Curve》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具