提高效率,記一個內部工具的開發經歷

pingan8787發表於2019-03-14

一、開發介紹

作者:leo
更新:2019.03.14
專案原始碼:github

1.開發背景

由於公司 V2專案 需要做元件化升級,但因為 V2專案 專案歷史包袱大, 程式碼和檔案非常多,而且巢狀較多,難以全面瞭解所需要調整的元件的影響範圍,所以需要開發這麼一個工具,來實現以下幾個功能:

  • 需要能支援 自定義關鍵詞檢索 ,便於按不同的已有元件名搜尋;
  • 需要能支援檢索出該元件的 影響檔案範圍 ,還有 頁面名稱路由 等,便於測試按照頁面快速測試;
  • 需要能支援 資料視覺化 ,便於判斷所有影響範圍的權重;
  • 需要能 匯出影響範圍的路由檔案必要資料

基於上面需求,我大概整理思路使用 NodejsPython 進行需求開發,原因有這幾點:

  • 需求以操作檔案為主,包括讀寫;
  • 需求對資料處理操作比較多,包括過濾,組裝資料格式;
  • 需求對資料視覺化的需求;

起初我準備只使用 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
複製程式碼
  • 安裝完成

三、開發過程

首先先介紹下開發的思路:

20190311share4

1.最終效果

最終我實現的效果是,開發 search_current_file.jssearch_current_file_python.py 兩個檔案,並通過執行兩個命令,來獲取對應資料檔案:

  • 獲取 所有包含關鍵詞的檔案的路徑所在資料夾內檔案數量所有檔案對應頁面的路由/引數/標題等資料 統計的檔案和表格。
node search_current_file.js
複製程式碼

20190311share3

  • 獲取 所有資料夾中檔案數量佔總檔案數的比例 的餅圖結果。
python search_current_file_python.py
複製程式碼

這裡需要輸入需要生成的指定資料夾的資料,預設不輸入則生成所有資料夾下的資料。

20190311share2

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-echartsd3-node

bg

相關文章