内容简介:我们为什么要做前端系统呢,可以明显地从下表看出来,前端的性能对于产品的价值提升还是蛮有帮助的,但是这些信息如果我们能实时的采集到,并且实施以监控和报警,让整个产品在产品线上一直保持高效的运作,这是我们的目标,做前端监控只是为了达到这个目标的手段。其次,前端监控能让我们即使发现问题(页面加载过慢等)或者错误(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开发的过程,学习到了很多东西,下面开始分享。
技术选型
- 前端:
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)); } } } 复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 自己动手打造前端性能监控系统
- 转转商业前端错误监控系统 (Sentry) 策略升级
- zanePerfor前端性能监控系统高可用之Mongodb副本集读写分离架构
- zanePerfor 一款完整,高性能,高可用的前端性能监控系统,不要错过
- 【监控系统】配合Graphite使用的报警系统
- WGCLOUD 监控系统更新,集成 ES 在线监控工具
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
网络机器人Java编程指南
美 Heaton J. / 电子工业出版社 / 2002-7 / 44.00元
这是一本研究如何实现具有Web访问能力的网络机器人的书。该书从Internet编程的基本原理出发,深入浅出、循序渐进地阐述了网络机器人程序Spider、Bot、Aggregator的实现技术,并分析了每种程序的优点及适用场合。本书提供了大量的有效源代码,并对这些代码进行了详细的分析。通过本书的介绍,你可以很方便地利用这些技术,设计并实现网络蜘蛛或网络信息搜索器等机器人程序。 适合于具有一起来看看 《网络机器人Java编程指南》 这本书的介绍吧!