一、开发介绍
作者:leo
更新:2019.03.14
项目源码:github
1.开发背景
由于公司 V2项目 需要做组件化升级,但因为 V2项目 项目历史包袱大, 代码和文件非常多,而且嵌套较多,难以全面了解所需要调整的组件的影响范围,所以需要开发这么一个工具,来实现以下几个功能:
- 需要能支持 自定义关键词检索 ,便于按不同的已有组件名搜索;
- 需要能支持检索出该组件的 影响文件范围 ,还有 页面名称路由 等,便于测试按照页面快速测试;
- 需要能支持 数据可视化 ,便于判断所有影响范围的权重;
- 需要能 导出影响范围的路由文件 和 必要数据 ;
基于上面需求,我大概整理思路使用 Nodejs
和 Python
进行需求开发,原因有这几点:
- 需求以操作文件为主,包括读写;
- 需求对数据处理操作比较多,包括过滤,组装数据格式;
- 需求对数据可视化的需求;
起初我准备只使用 Nodejs
完成这个需求,后面开发到一半,发现 数据可视化 方面,实在找不到一个满意的可视化插件,于是想到 Python
的一个2D绘图库—— Matplotlib
,使用起来非常方便,于是便选择了它。
这也是我用 Nodejs 做的第一个作品,还有很多优化空间,欢迎大佬指点哈,感激不尽。
2.工具文档
二、开发环境搭建
1.Nodejs环境搭建
对于 Nodejs
环境搭建,相信对于我们前端开发仔来说,应该是很简单,但这里考虑到可能原生的同学还不太清楚,这里我简单介绍:
- 下载和安装
Nodejs
我们到 Nodejs官网 ,选择对应系统环境进行下载,然后直接打开安装。
- 测试
Nodejs
环境
打开命令行工具,执行 node -v
,看是是否输出对应 Nodejs
版本号,我这显示:
v10.8.0
复制代码
另外在 WIN7 系统下可能会出现下面报错,则需要将 nodejs
安装目录,添加全局路径:
node : 无法将“node”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
复制代码
- 安装完成
2.Python环境搭建
- 下载和安装
Python
在 Python官网 ,选择 3.x 版本下载(由于Python2.x版本已经停止维护,并且即将被淘汰),下载完成直接安装。
- 测试
Python
环境
安装完成,打开命令行工具,执行 python
,看看输出结果是否是版本号和命令行交互模式,我这显示:
PS C:\Users\mi> python
Python 3.6.3 |Anaconda, Inc.| (default, Oct 15 2017, 03:27:45) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>
复制代码
- 安装绘图库
Matplotlib
python
安装其他包是用 pip install packageName
来安装,跟 Nodejs
中的 npm install packageName
是一样的,我们就这么安装 Matplotlib
:
pip install Matplotlib
复制代码
- 安装完成
三、开发过程
首先先介绍下开发的思路:
1.最终效果
最终我实现的效果是,开发 search_current_file.js
和 search_current_file_python.py
两个文件,并通过执行两个命令,来获取对应数据文件:
- 获取 所有包含关键词的文件的路径、所在文件夹内文件数量和所有文件对应页面的路由/参数/标题等数据 统计的文件和表格。
node search_current_file.js
复制代码
- 获取 所有文件夹中文件数量占总文件数的比例 的饼图结果。
python search_current_file_python.py
复制代码
这里需要输入需要生成的指定文件夹的数据,默认不输入则生成所有文件夹下的数据。
2.Nodejs开发部分
首先定义几个下面主要使用的变量,其他没有写在这里的变量和作用,可以查看源码。
var Excel = require('exceljs');
var XLSX = require('xlsx');
var filterFile = ['.html']; // 需要检索的文件类型
var filterDir = ['lib']; // 需要排除的文件夹
var classArray = [ // 需要检索的类名数组
'search-holder','exe-bar-search','输入搜索内容','<exe-search','learn-search','ion-android-search'
];
var resultArray = []; // 最终结果
var resultAlassify = {}; // 最终结果分类
var excelFileArr = []; // excel文件内容数组
复制代码
2.1获取搜索结果
目的: 搜索包含关键词的所有HTML文件,并保存这些数据。
- 核心方法
getCurrenAllFile()
我们通过 fs.readdir
方法,来获取路径下所有文件和文件夹名称作为一个集合;
然后遍历该集合,当 stat.isDirectory()
为 true
则表示该结果为一个文件夹,为 false
则继续使用 getCurrenAllFile()
来读取下一层的文件信息。
/**
* 获取当前项目的所有HTML文件
* @param {string} paths 文件的路径
*/
var getCurrenAllFile = function (paths){
// ... 省略部分
var fileArr = [];// 初始化最终结果分类的对象
fs.readdir(paths, function(err, files){
_.forEach(files, function(item, index){
var c_path = path.join(paths, item);
var stat = fs.lstatSync(c_path);
// TODO 关键
if(stat && stat.isDirectory()){
// .. 省略过滤文件夹的操作
getCurrenAllFile(c_path);
}
}else{
// .. 省略过滤文件夹的操作
getCurrentFile(c_path, item);
}
});
});
return fileArr;
}
复制代码
- 核心方法
getCurrentFile()
读取每个文件的内容,然后再使用 searchCurrentFile()
方法去检索我们要搜索的关键词。
/**
* 获取当前文件内容
* @param {string} paths 文件的路径
* @param {string} filename 文件名
*/
var getCurrentFile = function(paths, filename){
fs.readFile(paths, 'utf8', function(err, data){
// ... 省略部分
if (err) console.log(err);
searchCurrentFile(data, paths);
});
};
复制代码
- 核心方法
searchCurrentFile()
这里遍历我们定义的 classArray
数组,这是包含我们所需要检索的所有关键词,如果检索结果为 true
则将结果保存到 resultArray
数组和 resultAlassify
数组。
/**
* 检索当前文件内容
* @param {object} data 文件的内容
* @param {string} paths 文件的路径
*/
var searchCurrentFile = function(data, paths){
_.forEach(classArray, function(val){
// ... 省略部分
if(data.indexOf(val) >= 0){
resultArray.push(paths);
resultAlassify[val].push(paths); // 保存最终结果(当前关键词下的对象)
}
}
};
复制代码
2.2处理搜索结果
目的: 将获取到的数据,去重,格式化并保存成JSON,作为可视化的数据源。 这里有定义两个简单方法 unique()
用于数据去重,和 setEachDirFileNum()
统计文件数量,不做具体介绍。
这里我们使用 saveDataToJson()
将数据整理成 JSON 格式,并使用 setJSONFile()
方法,将JSON数据保存为 json
文件,用于可视化操作。
- 核心方法
saveDataToJson()
这一步主要只用 loadsh
的分组函数 _.ground
来处理 JSON 数据,我们需要的格式是:
result = {
template: [
home:[ {}, {} ],
my: [ {}, {} ]
// ...
],
view: [
// ...
]
}
复制代码
然后还需要处理成保存 Excel 时所需要的格式,再使用 setJSONFile()
方法保存 JSON 文件。
/**
* 转成JSON数据,用来数据可视化
* @param {*} data 需要处理的数据
*/
var saveDataToJson = function (data){
var result = {};
// 第一层分组 外层文件夹
result = _.groupBy(data, function(item){
item = item.replace(filePath+'\\','');
var list = item.split('\\');
return list[0];
});
// 第二层分组 内层文件夹
for(var k in result){
result[k] = _.groupBy(result[k], function(i){
i = i.replace(filePath+'\\','');
var r = i.split('\\');
return r[1];
});
}
for(var i in result){
for(var m in result[i]){
for(var n in result[i][m]){
var currentPath = result[i][m][n].replace(filePath+'\\','');
currentPath = currentPath.replace(/\\/g, '/');
var current = excelFileObj[currentPath];
result[i][m][n] = {
title : current ? current['路由名称'] : '该文件为模块',
path : current ? current['文件路径'] : currentPath,
url : current ? current['url'] : '该文件为模块',
params: current ? current['路由参数'] : '该文件为模块',
ctrl : current ? current['控制器名称'] : '该文件为模块',
urls : current ? current['url'] : '该文件为模块',
};
}
}
}
setJSONFile(result); // 保存JSON文件
};
复制代码
2.3加入文件标题路由等数据
目的: 解析外部路由Excel表,合并到原有数据
- 核心方法
getExcelFile()
读取 Excel 数据并通过resolve
返回。
/**
* 读取Excel数据
*/
var getExcelFile = function(){
return new Promise(function(resolve, reject){
var excelPath = path.join(__dirname, excelReadName);
fs.exists(excelPath, function(exists){
if(exists){
var workbook = XLSX.readFile(excelPath, {type: 'base64'});// 获取 Excel 中所有表名
var sheetNames = workbook.SheetNames;
resolve({workbook: workbook, sheetNames: sheetNames});
}else{
reject({message:'错误提示:请先获取路由列表文件!(执行node get_router.js)'});
}
});
})
};
复制代码
- 核心方法
getEachSheet()
这里我们需要将 Excel 中的每个表的数据,都保存到 excelFileObj
中,另外需要注意,我们项目的 lodash
不能使用 4.0.0 以上版本的API。
/**
* 解析Excel数据
* @param {object} workbook excel工作区数据
* @param {object} sheetNames excel工作表名数据
*/
var getEachSheet = function(workbook, sheetNames){
_.forEach(sheetNames,function(item,index){
var sheet = workbook.Sheets[sheetNames[index]];
var json = XLSX.utils.sheet_to_json(sheet); // 针对单个表,返回序列化json数据
excelFileArr = excelFileArr.concat(json); // 不能使用lodash的_.concat 因为lodash版本太低
})
_.forEach(excelFileArr, function(val, key){
excelFileObj[val['文件路径']] = val;
});
}
复制代码
2.4生成结果文件
目的: 将处理后的结果生成对应的 Excel/JSON/TXT 文件:
这里生成 JSON/TXT 文件不做介绍,使用的是 Nodejs 内置的文件存储方法fs.write
- 核心方法
setExcelFile()
主要是整理数据为保存 Excel 的数据格式。
/**
* 保存Excel数据
* @param {object} data 需要处理的数据
* return excelFileName.xlsx
*/
var setExcelFile = function(data){
var workbook = new Excel.Workbook();
workbook.creator = 'EXE';
workbook.lastModifiedBy = 'Leo';
workbook.created = new Date();
workbook.modified = new Date();
workbook.lastPrinted = new Date();
for(var item in data){ // 第一层循环 外层文件夹 templates views
for(var list in data[item]){
var worksheet = workbook.addWorksheet(list.toUpperCase()),
rowData = data[item][list];
worksheet.columns = [
{ header: '页面标题' , key: 'title' , width: 40 },
{ header: '文件路径' , key: 'path' , width: 60 },
{ header: '路由地址' , key: 'url' , width: 40 },
{ header: '路由参数' , key: 'params', width: 40 },
{ header: '控制器名称', key: 'ctrl' , width: 40 },
{ header: 'url' , key: 'urls' , width: 40 },
];
for(var row in rowData){
worksheet.addRow({
title : rowData[row].title,
path : rowData[row].path,
url : rowData[row].url,
params: rowData[row].params,
ctrl : rowData[row].ctrl,
urls : rowData[row].urls,
})
}
}
}
workbook.xlsx.writeFile(path.join(__dirname, excelFileName)).then(function() {
// ... 省略部分
});
};
复制代码
到这里我们 Nodejs 程序开发完成,我们最后会有一个文件 search_current_file_json.json
作为 Python 部分的数据源。
3.Python开发部分
Python 部分的内容相对比较简单,做的只有 加载数据,简单处理数据和可视化操作 三部分。
同样在刚开始部分,将几个重要的定义写一下:
# ... 省略一些
import matplotlib.pyplot as plt
keyName = [] # 需要显示的分类图表(按外层文件夹)
selectName = '' # 用户选择的文件夹名称
复制代码
2.1读取数据源
我们通过使用 python
内置的 open
方法来读取文件,并导入内置方法 json
来读取前面 Nodejs
部分生成的 search_current_file_json.json
文件。
file = open('./search_current_file_json.json','r', encoding='utf-8')
file = json.load(file)
复制代码
2.2设置命令行输入项
设置命令行输入项的目的是:让用户通过输入要查看的文件夹名称,来展示对应文件夹的饼图,默认显示所有文件夹饼图。
在设置之前,我们需要先通过 getKeyName()
方法获取到所有第一层文件夹的名称:
def getKeyName():
for name in file:
keyName.append(name)
复制代码
然后才能设置命令行输入项:
getKeyName()
select = ','.join(keyName)
selectName = input('检索到的文件夹有:【' + select + '】,请输入要查看的文件夹名称(默认所有):')
复制代码
2.3绘制单张饼图
接下来绘制单张饼图,这里主要就是设置饼图的参数:
- 核心方法
drawOneChart()
def drawOneChart(name, label, data):
plt_title = name
plt.figure(figsize=(6,9)) # 调节图形大小
labels = label # 定义标签
sizes = data # 每块值
colors = [ # 每块颜色定义 这里省略掉
#...
]
explode = [] # 将某一块分割出来,值越大分割出的间隙越大
max_data = max(sizes)
for i in sizes: # 初始化每块之间间距,最大值分割出来
if i == max_data:
explode.append(0.2)
else:
explode.append(0)
patches,text1,text2 = plt.pie(
sizes, explode = explode, labels = labels, colors = colors,
autopct = lambda pct: pctName(pct, data), # 数值保留固定小数位
frame = 1, # 是否显示饼图的图框,这里设置显示
shadow = True, # 无阴影设置
labeldistance = 1.1, # 图例距圆心半径倍距离
counterclock = False, # 是否让饼图按逆时针顺序呈现;
startangle = 90, # 逆时针起始角度设置
pctdistance = 0.6 # 数值距圆心半径倍数距离
)
plt.xticks(())
plt.yticks(())
plt.axis('equal')
plt.legend()
plt.title(plt_title+'文件夹下文件分布(顺时针)', bbox={'facecolor':'0.8', 'pad':5})
plt.savefig(plt_title+'_'+saveImgName) # 一定放在plt.show()之前
plt.show()
复制代码
2.4绘制多张饼图
最后通过循环调用 drawOneChart()
来生成所有的饼图:
- 核心方法
drawAllChart()
这个方法中需要对之前 JSON 数据再处理,将每个文件夹中文件数量作为饼图的数据,也就是这里的 values
的值。
def drawAllChart(openName):
for name in keyName:
labels = []
values = []
for view_name in file[name]:
labels.append(view_name)
values.append(len(file[name][view_name]))
if openName == '' or openName == name:
drawOneChart(name, labels, values)
else:
print('输入有误')
复制代码
四、总结
1.Nodejs知识点
这部分用得比较多的是 Nodejs 中的:
- 文件读/写操作
- 正则匹配操作
- 数据格式处理操作
因此为了以后开发类似或者其他类型工具,还是需要加强这三方面的知识,这部分的代码可能不够简洁,代码也不够美观,但毕竟作为自己的经验积累,对这类工具开发会有更加清晰的思路。
2.Python知识点
这部分用得比较多的,其实是 Python 中的一些基础语法,这部分代码,其实也是加深自己对 Python 基础语法的使用和理解,练习操作。
3.拓展
接下来会找时间,优化项目代码,然后改造这个项目,将使用 Nodejs 和 Python 分别单独开发一套,并比较两者差距(执行效率/代码量)。 另外 Nodejs 的绘图库还有: node-echarts 和 d3-node。