2019-03-03
需求介绍:
1. 系统权限管理,数据系统管理,各个系统权限联通。
2. 可以对不同用户设置角色,赋予不同权限,显示看到不同的页面。
3. 需要在不改代码的前提下,增加外部链接菜单页面。
4. 决策系统数据系统部分迁移到天眼。
项目命名
项目命名为bigdata-admin-vue
技术栈
1. vue-cli3;
2. vue-router、vuex、element、axios;
3. 权限基于RBAC权限管理模型,即用户→角色→权限管理模型;
系统目录
[站外图片上传中...(image-87a9ff-1575879535075)]
1路由控制和菜单显示
vue权限路由实现方式:
* 视图(View) 使用xtpl模板引擎
##路由解析,根据URL寻找对应的控制器和行为
```javascript
app.post('/template|/templatePC|/user-defined|/user-defined-pc',control.postRander); //post入口,页面编辑、报表、热图、abtest报表页面渲染
app.get('/modules/*/index',control.moduleRander); //用于本地单个无线模块开发调试
app.get('/modules_pc/*/index',control.moduleRander);//用于本地单个pc模块开发调试
app.get('/template/design(_second)?|design_pc(_second)?',control.pagePreview); // 所有iframe的入口,预览
app.get(/personalized$|personalized\/[^\/]*?$/,control.personalized); //个性化
app.get('/AB_RedirectServer?',control.abtest); //ab
//保存页面请求
app.post('/savePage|savePagePc/index',control.pageShow);
//js combo
app.get('/jscombo/',combo.jsCombo);
app.get('*',control.errorPage);
3.渲染部分:
编辑页渲染父级通过调用function dataSend(tag,viewModule,callback) 里的ajax将数据发送给cms postDataProcessing函数处理,
returnData = self.dataRender(data,equipment,xtplPath+data.template_file+'.xtpl',moduleName);
if(data.flagForPage == 0 || data.mustDocumentWrite == true){
xtpl.renderFile('./views/'+xtplPath+data.template_file+'.xtpl',returnData,function(error,content){
res.writeHead(200, {"Content-Type": "text/plain"});
res.write(content);
});
}else{
res.send(returnData);
}
iframe在展示页面之初,默认模板是index.xtpl,如果要使用用户自定义的xtpl模板,因为页面结构是默认的index.xtpl的结构,需要对页面进行重新写入。
1. 通过dataRender将处理好的返回数据,赋值给returnData
2. if语句里的代码对模板进行的渲染,得到的数据是字符串,并且通过res.write将数据返回个浏览器对应的浏览器部分代码处理如下
document.open(); //打开文档流
document.write(data); //将默认的iframe里页面内容替换
document.close(); //关闭文档流
3. else语句里将returnData直接返回给浏览器
对应代码处理如下:
$('#css-box').html(data.css);
$('#components-box').html(data.xtplHTml);
$("#body-content-box").attr({
"data-page-style": data.style,
"data-is-publish": data.isModule,
"ppcc-id": data.pageId_peopleId_childPageId_childPeopleId,
"idsite": data.idsite,
"page-id": (data['digital'] ? data['digital'].page_group_id : null) || data.id,
"is-personalized": data.isPersonalized,
"people-id": data.peopleId,
"data-view-module": (data.viewModule ? data.viewModule : "")
});
company_id = !!data.tenant_id ? data.tenant_id : (!!data.company_id ? data.company_id : 0);
//设置页面背景
(function () {
var style = $('body').attr('data-page-style');
if (style != '') {
$('body').css('background', style);
}
})();
$(getIframe()).attr("isSuccess", "true");
$('#components-box').append("<script>" + data.jsContainer + "</\script>");
if ($(".module_bottom-nav").length === 0) {
$("body").css("padding", 0);
}
if (data.config.length > 0 || data.show_user_defined) {
$(".prompts").hide();
} else {
$(".prompts").show();
}
if (tag.c_data && tag.r_data) {
reportClickData(tag);
}
('#components-box').html(data.xtplHTml);将iframe里的html替换
$('#components-box').append("<script>" + data.jsContainer + "</script>");将iframe里的js替换
4.个性化
个性化是一个比较复杂的系统,但是cms只做了渲染,复杂的逻辑都在我们系统编辑部分做了处理,个性化页面没有生成静态的文件,通过接口获取数据,并有dataRender渲染展示页面
5.Abtest
函数abtest对其做的处理
目前有两种情况is_out 为 0 和is_out 为 1
为0的时候表示内部页面,也就是我们站点生成的页面,在调用dataRender方法做处理展示页面
为1的时候表示外部页面,也就是客户的页面,直接res.write渲染
6.页面生成机制
当页面保存的时候触发,将接收的数据里的配置信息解析,查找对应的xtpl模板,
在dist查找对应模块css和js并导入数据,生成个一个字符串的页面数据\
- pageSave函数执行;
- saveToMakeFile普通页面保存,就生成一个html,以页面id为命名
- makeImageFiles函数来生成图片文件
- separateFiles函数做文件分离处理,将html、css、js、图片剥离开,变成单个文件
- getlistening函数将监听文件添加到页面
- tongjiFn函数设置统计参数
7.日志管理:
require log4js
log4js.configure({
appenders: [
{ type: 'console' }, //控制台输出
{
type: 'dateFile', //文件输出
filename: './log/', //文件保存在log目录下
maxLogSize: 20480,
backups:3,
category: 'log_date',
alwaysIncludePattern: true,
pattern: "yyyy_MM_dd.log" //文件命名规范
}
],
replaceConsole: true
});
var logger = log4js.getLogger('log_date');
logger.setLevel(log4js.levels.INFO);
8.pm2多进程服务
pm2 是一个带有负载均衡功能的Node应用的进程管理器,把独立代码利用全部的服务器上的所有CPU,并保证进程永远都存活,0秒的重载;
线上服务在运行的时候会出现异常,虽然代码里已经用try catch 来捕获异常,但是还是会有一些情况会导致服务崩溃,pm2开启多进程,可以保证代码运行稳定
9.Redis
只要用于用户预览的时候数据存储,在用户点击预览的时候将保存在redis中,以时间戳和随机6位随机数字为key存储数据,用户可以通过手机扫描二维码访问到页面,pc预览页面可以通过新窗口打开,目前页面预览有效时间设置为20分钟,预览的时候通过key来读取数据数据
10.监听部分:listening
1. 监听包括piwik和sdk,piwik已不再使用。页面滚动手机pv数据、页面到达率数据,点击搜集点击数据,热图数据;
2. 页面编辑状态下,预览状态下不会添加监听代码,发布的页面、ab页面、个性化页面会引入监听代码,
11.打包工具:gulp
- watch监听了模块里的js和css文件,当模块里的js或css做修改时,自动打包压缩到dist目录下
- gulp bulid可以生成打包后的代码,用于部署客户服务器上
12.模板引擎:xtpl
- xtpl是淘宝kissy架构里的模板渲染引擎,介绍:https://github.com/xtemplate/xtemplate/blob/master/docs/tutorial/introduce.md
- 在系统开发之初,我是想用ejs作为渲染模板引擎,由于项目的时间紧迫,我对xtpl比较熟悉些,经过商议还是决定用xtpl。
- xtpl的语法功能并不强,但也是可以满足我们的渲染需求,目前在项目里xtpl运行稳定。
npm包链接:https://www.npmjs.com/package/xtpl
gitbub链接:https://github.com/xtemplate/xtemplate
13.模块(组件)
- 页面是由一个或者多个模块构成的,模块是构成页面基础单位
- 模块js代码基础框架
(function() { //闭包函数自执行
function Mod() { //函数定义 Mod构造函数
this.init.apply(this, arguments);
}
Mod.prototype = { //原型链
init: function(container) {
var self = this;
self._node = $(container); // 模块容器
self.data = self._node.find('.J-dataJson').html(); // 模块数据
self.data = JSON.parse(self.data);
}
};
new Mod("#clickplus-module-box"); //don't modify this line of code
// new一个构造函数
})();
基础框架不允许改动,当页面里有多个相同模块的时候,构造函数就发挥作用了,我会new多个Mod
new Mod("#clickplus-module-1496965951574")
new Mod("#clickplus-module-1496965951575")
.......
后面跟上模块创建的时间戳,保证唯一性,这样就避免了重复写入相同的模块代码,减少页面的代码量
- 模块xtpl
精简代码示例:
<textarea class="J-dataJson" style="display: none">{{newTextarea}}</textarea>
<div class="area-choose">
<div class="main" style="{{#if(config.data[0].height)}}margin-bottom:{{config.data[0].height/64}}rem;{{/if}}">
</div>
</div>
当你页面里需要cms返回的模块数据时可以添加textarea的代码
class="main"也是固定的
- 模块样式
精简代码示例:
@r: 64rem;
.area-choose {
min-width: 320px;
width: 640/@r;
margin: 0 auto;
.main {
width: 640/@r;
min-width: 320px;
}
}
@r: 64rem;作为页面适配基础单位
用实际的尺寸除以@r得到适配的rem尺寸,就可以适配各个手机
14.nginx应用
主要做了8080端口的代理,转发到80,设置了cms的
server {
listen 80;
server_name node.clickplus.cn;
server_name cms.clickplus.cn;
server_name cms-test.clickplus.cn;
location / {
proxy_pass http://127.0.0.1:8080;
fastcgi_connect_timeout 60;
fastcgi_send_timeout 180;
fastcgi_read_timeout 180;
fastcgi_buffer_size 128k;
fastcgi_buffers 256 16k;
client_body_buffer_size 4096k;
}
}
15.报表部分
- 先前版本是将报表的渲染处理放在controller.js里的,带来一个很大的问题就是,当切换有效点击或者无效点击、选着日期等操作的时候其实要重新渲染整个页面
- 现在的逻辑是页面只渲染一次,做切换数据的操作时页面不再刷新,只将数据刷新
footSettings.xtpl文件里:
reportClickData、
reportCalculate、
reportForNav、
reportForNavGoods、
reportForGoods
函数对报表的数据做了处理
16.页面适配原理
对font-size进行了重新定义,以rem作为一个单位,64为基数,
!function(e) {
function t() {
var t = n.clientWidth,
r = "}";
!navigator.userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile/i) && t > 1024 && (t = 640, r = ";max-width:" + t + "px;margin-right:auto!important;margin-left:auto!important;}"), e.rem = t / 10, /ZTE U930_TD/.test(navigator.userAgent) && (e.rem = 1.13 * e.rem), /Android\s+4\.4\.4;\s+M351\s/.test(navigator.userAgent) && (e.rem = e.rem / 1.05), i.innerHTML = "html{font-size:" + e.rem + "px!important;}body{font-size:" + 12 * (t / 320) + "px" + r
}
var n = document.documentElement,
i = document.createElement("style");
n.firstElementChild.appendChild(i), e.addEventListener("resize", function() {
t()
}, !1), e.addEventListener("pageshow", function(e) {
e.persisted && t()
}, !1), t()
}(window);
17.调试
- 可以通过webstorm来调试
- 可用node debug index.js 打debugger断点
- 基于Chrome浏览器的调试器 npm install -g node-inspector 导入安装路径到环境变量。 node-inspector 默认情况下node-inspector的端口是8080,可以通过参数--web-port=[port]来设置端口。 node-debug --web-port=8000 index.js启动程序 node --debug-brk
18.环境安装
- 在cms根目录里执行npm install,安装依赖包
- 安装全局pm2模块 sudo npm install -g pm2 用于开启多进程,保证node服务不停止工作,线上环境一点要安装并执行 pm2 start index.js -i 4
- 安装全局gulp模块 sudo npm install -g gulp
- 安装全局nginx sudo npm install -g nginx 用于将8080端口发送到80端口
- 打开sudo gedit /etc/nginx/etc/nginx ,找到http,在里面加上如下代码,server_name 为你本地host设置的127.0.0.1对应的名称 ,将8080的端口请求指向80
server {
listen 80;
server_name node.clickplus.cn;
server_name cms.clickplus.cn;
server_name cms-test.clickplus.cn;
location / {
proxy_pass http://127.0.0.1:8080;
}
}
nginx的buffer设定 开发中可能会发现post请求传输问题net::ERR_INCOMPLETE_CHUNKED_ENCODING 请在server添加buffer设定,并且设置gzip为on;
server {
listen 80;
server_name node.clickplus.cn;
server_name cms.clickplus.cn;
server_name cms-test.clickplus.cn;
location / {
proxy_pass http://127.0.0.1:8080;
fastcgi_connect_timeout 60;
fastcgi_send_timeout 180;
fastcgi_read_timeout 180;
fastcgi_buffer_size 128k;
fastcgi_buffers 256 16k;
client_body_buffer_size 4096k;
}
}
19.数据结构
模块数据以对象数组形式存放在在config里,动态的配置信息存放在模块的config里
1.普通页面数据结构
{
"app_url": null, //页面app链接
"body_foot": null, //用户自定尾部
"body_head": "", //用户自定头部
"cdn_url": "https://huodong.clickplus.cn", //cdn地址,用户域名
"children": null,
"component_num": null,
"config": [ //存放页面模板的数据
{
"id": 546,
"img_path": "http://huodong.clickplus.cn/image/tuwenrequ.jpg",
"title": "图文",
"stick": false,
"basic": 1,
"timeIndex": 1496288530354,
"create_time": 1477459545000,
"device": 0,
"path": "/modules/area-choose-text",
"config": { //动态配置信息
"data": [
{
"wrapHeight": 640,
"wrapWidth": 640,
"image": "http://huodong.clickplus.cn/image/hjhg.jpg",
"iHeight": 640,
"iWidth": 640
}
],
"areaData": []
},
"tenant_id": 3,
"type": 12
}
],
"create_time": 1495789835000,
"custom_body": "<script src=\"//res.wx.qq.com/open/js/jweixin-1.2.0.js\"></script>\n<script type=\"text/javascript\">\nvar shareURL = window.location.href;\nvar sTitle = document.title;\nvar sDesc = document.getElementsByTagName('meta')['description'].content;\nvar imgUrl = $(\"#body-content-box > div:nth-child(1) > img\")[0].src;\n$.ajax({\n url: '//weixintest.clickplus.cn/sign/0',\n type: 'get',\n dataType: 'jsonp',\n async:false,\n data: {\n url: location.href.split('#')[0]\n },\n success: function(data) {\n wx.config({\n debug: false,\n appId: data.appid,\n timestamp: data.timestamp,\n nonceStr: data.nonceStr,\n signature: data.signature,\n jsApiList: [\n // 所有要调用的 API 都要加到这个列表中\n 'onMenuShareTimeline',\n 'onMenuShareAppMessage'\n ]\n });\n wx.ready(function(){\n wx.onMenuShareTimeline({\n title: sTitle, // 分享标题\n desc: sDesc, // 分享描述\n link: shareURL, // 分享链接\n imgUrl: imgUrl, // 分享图标\n success: function () {\n },\n cancel: function () {\n }\n });\n wx.onMenuShareAppMessage({\n title: sTitle, // 分享标题\n desc: sDesc, // 分享描述\n link: shareURL, // 分享链接\n imgUrl: imgUrl, // 分享图标\n type: '', // 分享类型,music、video或link,不填默认为link\n dataUrl: '', // 如果type是music或video,则要提供数据链接,默认为空\n success: function () {\n },\n cancel: function () {\n }\n }); \n wx.error(function(res){\n console.log(\"errorMSG-1:\"+res);\n });\n })\n },\n error:function(e){\n console.log(e)\n }\n});\n</script>",
"custom_head": "",
"data_collection_type": 1,
"default_page_id": null,
"description": "adadasddasd",
"device": 0,
"file_name": "zhuxiyu_1028",
"hav_app": 0,
"id": 5228,
"idsite": 31,
"im_position": "0",
"im_text": "",
"is_deleted": null,
"is_group": 0,
"keywords": null,
"pageMainWidth": "auto",
"path": null,
"relation": null,
"release_time": null,
"setting": null,
"share_img": "https://huodong.clickplus.cn/image/1496891043699.png",
"site_type": 1,
"smart_component_setting": 0,
"stat_position": "0",
"stat_text": "<script>\nvar _hmt = _hmt || [];\n(function() {\n var hm = document.createElement(\"script\");\n hm.src = \"https://hm.baidu.com/hm.js?b5e1a9760cd46dbf098ad3e8db6f15a2\";\n var s = document.getElementsByTagName(\"script\")[0]; \n s.parentNode.insertBefore(hm, s);\n})();\n</script>",
"status": 1,
"style": "#eeeeee",
"templateSettingItem": null,
"template_file": "index",
"template_id": 0,
"tenant_id": 3,
"tenant_logo": "https://huodong.clickplus.cn/image/1495612416087.jpg",
"thumbnail": "http://huodong.clickplus.cn/file/1496909741687.jpg",
"title": "zhuxiyu_1028",
"type": 0,
"type_name": null,
"update_time": 1496909741000,
"url": "https://huodong.clickplus.cn/html/zhuxiyu_1028.html",
"wx_app_id": "",
"only": "149691787773263078",
"isEdit": true,
"mustDocumentWrite": false,
"flagForPage": "2"
}
2.个性化页面数据结构
{
"config": [ //组件数据
{
"basic": 1,
"config": {
"data": [
{
"image": "http://huodong.clickplus.cn/image/hjhg.jpg",
"wrapHeight": 640,
"wrapWidth": 640,
"iWidth": 640,
"iHeight": 640
}
],
"areaData": []
},
"create_time": 1477459545000,
"device": 0,
"id": 546,
"img_path": "http://huodong.clickplus.cn/image/tuwenrequ.jpg",
"path": "/modules/area-choose-text",
"stick": false,
"tenant_id": 0,
"title": "图文",
"type": 12,
"timeIndex": 1496918752643,
"fromComponent": "component1496918752643",
"fromText": "default1496918752643"
}
],
"id": "4454",
"img_path": "",
"path": "",
"tenant_id": 3,
"title": "",
"style": "#eeeeee",
"company_id": "3",
"isEdit": true,
"template_file": "index",
"device": 0,
"only": "149691875264302242",
"mustDocumentWrite": false,
"flagForPage": "2"
}
参考文献
pm2:https://www.douban.com/note/314200231/
《深入浅出 node.js》作者:朴灵