本文作者:IMWeb dekuchen 原文出处:IMWeb社区 未经同意,禁止转载
badjs
基本的架构如下。
然后,从宏观上的看一下BadJs都干了些什么。
这一部分,主要是badjs-report,他的任务是捕捉js的报错,并把报错进行上报。这一部分,主要是要在页面中引入js,并配置,这一部分并不属于二次开发的范畴中,所以,不详述了。
在服务端,整套badjs包括接收端,存储端和管理端共三个部分,这三个部分都是基于express的框架。
其中,accepter
负责接收badsjs-report
上报过来的数据,也就是页面上报过来的数据。然后将数据整理一下,校验一下是否是有效数据,然后通过zmq
组件将数据传递给badjs-storage
,badjs-storage
则负责将传递过来的数据进行存储,这里使用了mongoDB
作为主存储,file
作为辅助cache
,在badjs-storage
中使用了map-stream
作为其数据流的管理。而badjs-web
则是将badjs-storage
的数据用一种更人性化的形式呈现出来,这里用到了mysql
作为存储。嗯,整个体系比较简单的看就是这样的。
主要捕获两种错误
1、JS脚本里边存着语法错误; 2、JS脚本在运行时发生错误。
那么我们如何来捕获这种异常呢,有两种方法,
由于try.catch
没法捕捉到全局的错误事件,也即是说 只有try,catch的块里边运行出错才会被你捕捉到。所以我们这里是属于主动上报方案,监控采用第二种方法,也就是window.onerror
方法。
部分过时的浏览器只能提供部分数据。它的用法如下:
window.onerror = function (message, url, lineNo, columnNo, error)
五个参数的含义如下: 1、message {String} 错误信息。直观的错误描述信息,不过有时候你确实无法从这里面看出端倪,特别是压缩后脚本的报错信息,可能让你更加疑惑。 2、url {String} 发生错误对应的脚本路径,比如是你的http://a.js
报错了还是http://b.js
报错了。 3、lineNo {Number} 错误发生的行号。 4、columnNo {Number} 错误发生的列号。 5、error {Object} 具体的 error 对象,包含更加详细的错误调用堆栈信息,这对于定位错误非常有帮助。
不同浏览器对同一个错误的 message
是不一样的。 IE10
以下浏览器只能获取到 message
,url
和 lineNo
这三个参数,获取不到columnNo
和 error
不过 window.event
对象提供了 errorLine
和 errorCharacter
,以此来对应相应的行列号信息。 在使用onerror的时候,我们可以使用arguments.callee.caller 来递归出调用堆栈,这一类信息是最直接的错误信息信息,所以是必须要捕获并上报的。
var orgError = global.onerror;
// rewrite window.oerror
global.onerror = function(msg, url, line, col, error) {
var newMsg = msg;
if (error && error.stack) {
newMsg = _processStackMsg(error);
}
if (_isOBJByType(newMsg, "Event")) {
newMsg += newMsg.type ? ("--" + newMsg.type + "--" + (newMsg.target ? (newMsg.target.tagName + "::" + newMsg.target.src) : "")) : "";
}
//其他处理
};
相关代码可以在这里看到 badjs-report
前端上报badjs-report
改造 1)badjs.init 的时候,上报 关键字 !#imweb-badjs-pv#! 2)切换路由 的时候,业务需要执行 BJ_REPORT.pv 上报
acceptor
用来对消息进行过滤,分拣,并通过message queue
进行分发。
acceptor
提供了四种message
队列的组件,axon
,nodeNet
,redis
,zmq
,这为之后存储的扩容和单机向多机的拓展提供了可能性。
badjs-mq 即接收,也发送。 接受来自 acceptor 发送来的消息, 发送给 storage 要存储的消息。
默认使用axon-zmq
架构图
基于mongodb
的存储,使用zmq
来dispatch
消息队列,接受mq
传输的插入数据,写入mongodb
。这个组件的服务是基于node的,所以,使用了express作为其整体的骨架,嗯,文件的目录结构就是这样的。
.
├── LICENSE
├── Readme.md
├── acceptor/
├── app.js
├── benchmarks/
├── cache/
├── service/
├── storage/
├── test/
嗯,从文件上来看,主要是acceptor
,service
,storage
这几个部分。嗯,就像我们都知道的那样,express的入口文件当然使我们的app.js,看看它都干了什么。并不是很长,我就直接贴出来吧。
//...此处略去若干
var dispatcher = require(GLOBAL.pjconfig.acceptor.module)
, save = require('./storage/MongodbStorage');
var cacheCount = require('./service/cacheErrorCount');
// use zmq to dispatch
dispatcher()
.pipe(save());
logger.log('badjs-storage start ...');
setTimeout(function (){
require('./service/query')();
cacheCount.insertAuto();
require('./service/autoClear')();
},1000);
其实前面有一大堆都是进行环境判断的,在启动的时候,附加不同的参数,将会使用不同的配置文件。
在app.js的第3行,这里使用了调度器,而这个调度器指向的是acceptor\zmq.js
,在这个zmp中,拉起了一个和badjs-accepter的tcp连接。这里通过map-stream来保证每个tcp数据流能以一个队列的形式来执行map定义的函数。。。不知道理解的对不对啊。可以直接看这里,你也可以看这里来理解。而调度器指向的存储是storage里面的mongostorage,这个文件将得到的数据存入分布式的mongoDB中。注意哦,这里使用是分布式的mongoDB,所以在使用一些函数的时候,要注意是不是支持分布式。
存储逻辑比较好理解,因为为了保证高并发的数据流能够得到很好的处理,所以,越是简单的设计,越是可靠~!然后,我们来看看复杂的查询逻辑。这里的查询逻辑是交给了service的query.js来处理。嗯,这个文件有点长,我们不直接贴了,看一下折叠后的代码,理解下逻辑。 代码图 嗯,很直白的express的用法,connect中间件将请求分流导向不同的处理函数,在处理函数里处理自己的逻辑即可。处理逻辑中,比较建议的写法是只在函数中处理请求检查,函数response填充处理。将具体的逻辑处理抽象成一个函数放在exports的外部,如果是比较重的逻辑,则可以当初写成一个service来执行。
会有一台前置机,负责如何是分配存储和读取,在处理的时候,请注意mongo命令中对分布式的支持。 嗯,说两个比较复杂的,其他的就很好理解的。一个是在数据插入的时候。也就是storage里面的MongoStorage。
var insertDocuments = function(db , model) {
var collectionName = 'badjslog_' + model.id;
var collection = db.collection(collectionName); //获取数据集
collection.insert([ //插入数据
model.model
] , function (err , result){
if(err){
logger.debug('err,err is'+err);
return;
}
if (hadCreatedCollection[collectionName]) {
return ;
}
collection.indexExists('date_-1_level_1' , function (err , result ){ //判断数据集合索引
if(!result){
collection.createIndex( {date : -1 , level : 1 } , function (err , result){//建立索引,其中-1表示降序,1表示升序,前端的key表示对象。
});
if (global.MONGO_SHARD) {
shardCollection(db, collection, collectionName);
}
}
hadCreatedCollection[collectionName] = true;
})
});
logger.debug("save one log : " + JSON.stringify(model.model));
};
// shard new collection when created
var shardCollection = function (db, collection, collectionName) {
collection.createIndex({_id: "hashed"}, function (err, result) {
if (err) {
logger.info("failed to create hashed index");
} else {
logger.info("hashed index created");
var adminDb = db.admin();
if (global.MONGO_ADMIN_USER && global.MONGO_ADMIN_PASSWORD) {
adminDb.authenticate(global.MONGO_ADMIN_USER, global.MONGO_ADMIN_PASSWORD, function (err, result) { //权限认证,因为这里使用的是admin,要执行命令
if (err) {
logger.info("failed to access adminDB");
} else {
adminDb.command({ //执行mongo命令
shardcollection: "badjs." + collectionName,
key: {_id: "hashed"}
}, function (err, info) {
if (err) {
logger.info("failed to shardcollection " + collectionName);
} else {
logger.info(collectionName + " shard correctly");
}
});
}
});
}
}
});
};
这个函数的作用是,插入数据的时候看一下是不是有分布式,如果是,则使用分布式。 嗯,另一个例子看起来简单一点。
var cursor = collection.aggregate([
{$match: {'date': {$lt: endDate, $gt: startDate}}},
{
$group: {
_id: {
time: {$dateToString: {format: "%Y-%m-%d %H:%M", date: '$date'}}
},
count: {$sum: 1}
}
},
{$sort: {"_id": 1}}
]);
其实这里一点都不简单,这里使用的是一个聚合查询,同时使用了聚合通道,具体的话,可以参考官方的说明文档,这里做一个说明,group,mapReduce这两个都是聚合查询的,但是group是不支持分布的,mapReduce使用的是map-reduce框架,但是实现的比较复杂,而且性能比聚合通道低。英文看不懂的话,可以看这篇博文。
badjs-web是一个典型的几年前的web管理平台,具有使用express搭建的后台,mysql做数据持久化存储,前端使用jquery和bootstrap。整体的代码在放在几年前写的是非常优雅的。整个架构也非常清晰明了
同时通过一个进程池维护websocket
连接。
对数据库的增删查改等操作,对后台接口的修改,都可以查看这里的说明文档readme
实际上整体的结构很复杂。。。这里画的比较简单,把worker和service放在了一起,整体说明。 在和mysql连接这块使用了orm,用数据岛的模式来做对象化的数据处理。简单的说,就是可以像操作对象一下操作数据库。
.
├── app.js
├── controller/ 包含所有数据库操作文件
├── dao/ orm对象模型
├── db/ 数据库相关
├── model/ 数据库模型
├── oos/ 登录相关
├── plugin/ 一些插件,主要是oa登录等
├── service/ 封装了对数据库的增删查改
├── sh/ 一些脚本
├── static/ 打包后的静态资源
├── test/ 测试文件
├── views/ 打包后的前端页面
├── webpack.config.js
├── workflow/ 工作流程文件
├── workflow.config.json
请求逻辑是指从url过来的请求解析逻辑,包括请求html,接口请求,还有静态请求。
这个最简单,通过express框架,直接指向相应的资源文件。单独拿出来,是因为,这个地方的js是使用的模块化开发,webpack打包。每个页面的逻辑代码在static\module下的对应文件夹
里,这里的文件其实是source文件,因为在发布前,要执行webpack命令,通过webpack将页面的内容打包到module下的接口文件
(entry.*.js)里。主页面的逻辑是基于事件的,因为,渲染逻辑在请求html的时候就已经走完了。所有的事件都是委托给document.body去执行的。具体的实现方式是这样的。
//页面中
<a data-event-click="xxx">
//js中
$(document.body).on('click','xxx',function(){})
还有一部分是websocket的,因为自己不是很懂,就不详述了。
嗯,实话实说,这个页面渲染的逻辑相对比较简单,在badjs-web中,使用的页面渲染引擎是一个内部人员自行开发的micro-tpl引擎,说明文档嘛,看这个吧。请求走的是express工作流,从router出来,简单的没有复杂的页面逻辑的请求,直接渲染模板,并返回,又复杂页面渲染逻辑的,则会通过action调用不同的service来实现逻辑获取,并渲染模板。
这里着重讲一下我们对于既有的二次开发的接口。原有的接口请求是这样的。
app.use("/",function(req, res , next){
//controller 请求action
//....此处略去若干
//根据不同actionName 调用不同action
try{
switch(action){
case "user": UserAction[operation](params,req, res);break;
case "apply": ApplyAction[operation](params,req, res);break;
case "approve": ApproveAction[operation](params,req, res);break;
case "log" : LogAction[operation](params,req, res); break;
case "userApply": UserApplyAction[operation](params,req, res);break;
case "statistics" : StatisticsAction[operation](params, req, res); break;
default : next();
}
}catch(e){
res.send(404, 'Sorry! can not found action.');
}
return;
});
这个请求是通过正则匹配来获取不同的请求参数,进行请求分流。嗯,相对而言,我做的就比较简单粗暴了。直接的监听,get请求。
app.get('/xxx',function(){})
捕捉到相应的请求之后,将请求的参数传递给相应的action去处理。例如,log页面的请求交给LogAction,在action中,对不同动作,如果需要数据流,则会调用不同的底层服务(service)来实现和数据流的交互,那么,在logAction中,我们使用的就是LogService。在service中发出http请求去拉去badjs-storage的数据,或者,通过数据岛(DAO)来实现和mysql的交互。
整理一下就是,action处理动作,service处理数据流,dao负责和数据库(mysql)交互。
传统的jquery+bootstrap的代码。使用webpack构建。
badjs-mobile ui using react redux
为badjs写的一套移动端UI
登录页与展示页
设置页面
+-- build/ ---打包的文件目录
+-- config/ ---npm run eject 后的配置文件目录
+-- node_modules/ ---npm下载文件目录
+-- public/
| --- index.html ---首页入口html文件
+-- src/ ---核心代码目录
| +-- axios ---http请求存放目录
| | --- index.js
| +-- components ---各式各样的组件存放目录
| | +-- pages ---页面组件
| | | +-- HistoryTable.jsx ---页面组件
| | | +-- Login.jsx ---页面组件
| | | +-- NotFound.jsx ---页面组件
| | | +-- NotFound.jsx ---页面组件
| | | --- ...
| | +-- dashboard ---首页组件
| | | --- ...
| | +-- forms ---表单组件
| | | --- ...
| | +-- pages ---页面组件
| | | --- ...
| | +-- tables ---表格组件
| | | --- ...
| | +-- ui ---ui组件
| | | --- ...
| | --- BreadcrumbCustom.jsx ---面包屑组件
| | --- HeaderCustom.jsx ---顶部导航组件
| | --- Page.jsx ---页面容器
| | --- SiderCustom.jsx ---左边菜单组件
| +-- style ---项目的样式存放目录,主要采用less编写
| +-- utils ---工具文件存放目录
| --- App.js ---组件入口文件
| --- index.js ---项目的整体js入口文件,包括路由配置等
--- .env ---启动项目自定义端口配置文件
--- .eslintrc ---自定义eslint配置文件,包括增加的react jsx语法限制
--- package.json
技术栈:react+redux+react-router
全家桶。
值得说的几个点:
axios
代码封装为了提高http请求的复用性,在src/axios
文件夹下封装了get
,post
请求。通过config
配置请求的接口。httpdata
的reducer,只有receiveData
和requestData
两种action
,对于异步请求添加了isFetching
的状态封装,代码见src/rereducer
fetchdata
(异步)和receiveData
的action creator
使用样例
const mapStateToProps = state => {
const { useritems = { data: { items: [] } },logConfig={ data: {}} ,logging={data:{logging:false}}} = state.httpData;
return { items: useritems.data.items ,logConfig, logging: logging.data.logging}
}
const mapDispatchToProps = dispatch => ({
fetchData: bindActionCreators(fetchData, dispatch),
receiveData: bindActionCreators(receiveData, dispatch),
getState: bindActionCreators(receiveData,dispatch)
})
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(PanelMweb));
其实mapstateToProps
以及mapDispatchToProps
都是标准的样板代码,是不是可以用高阶组件进一步抽象出来。
使用到的工具
bindActionCreators
:用来减少代码重复度,减少写dispatch
的次数withRouter
:将redux
注入组件redux-thunk
处理异步redux action
具体这么做的原因是:
举例子
规则:1、ke.qq.com,2、ke.qq.com/A,3、ke.qq.com/B
数据:
ke.qq.com/A PV = 1000,Error = 10
ke.qq.com/A PV = 100,Error = 5
ke.qq.com PV = 1100,Error = 15
这个是否是个问题,目前没办法区分首页,这个问题是否需要解决
ssh haigecao@sng.mnet2.com -p 36000 http://iaas.isd.com/password/select 查询root密码 ssh root@10.185.18.34
BJ_REPORT.init({id : 1}); // id 申请的业务ID
BJ_REPORT.report('level 4') // 上报错误
BJ_REPORT.info('level 2') // 上报日志
BJ_REPORT.info('!#imweb-badjs-pv#!') // 测试统计PV
1、owner 表结构
- error message 保存错误信息
- total 数据汇总【后期可以干掉】,total 中有 2种 命名格式
- 日期_index (一个下划线) —— 记录了 汇总的数据
- appid 下对应的 所有错误数量,以及所有错误内容
```
数据结构
{
1361 : { // appkey
total: num, // 错误数量
errorMap: {
error1: { // message 的 md5 值
total: num, // 这个错误数量
msg: "error message" // 这个错误内容
}
}
}
}
```
- 日期__index 两个下划线
本地环境 和 dev 环境都是可以及时生效的。 正式环境,需要一天同步一次规则。
PV:
for (var i = 0 ;i < 10; i++) {
BJ_REPORT.info('!#imweb-badjs-pv#!');
}
Error:
for (var i = 0 ;i < 10; i++) {
BJ_REPORT.report('!#imweb-bad123213js-pv#!');
}