1. 程式人生 > >初識TypeScript:查詢指定路徑下的檔案按型別生成json

初識TypeScript:查詢指定路徑下的檔案按型別生成json

如果開發過node.js的話應該對js(javascript)非常熟悉,TypeScript(以下簡稱ts)是js的超集。

下面是ts的官網:

https://www.tslang.cn/

 

1.環境配置(如果已經進行過環境配置,可以跳過此步)

開發ts需要先簡單的配置開發環境,如果使用的是Visual Studio,只需要簡單裝一個node.js的元件包即可:

 

 該元件包主要包含Node.js開發工具,js和ts語言支援;除了該工具包外,還需要額外安裝ts sdk:

 

但如果使用的是VS Code的話,這些就需要自己手動安裝和配置了,node.js開發工具的下載地址為:

https://nodejs.org/en/download/

安裝完node就可以執行npm指令了,npm是Node.js包管理器(node package manager),你可以認為它是一個巨大的雲端資料庫,其中集成了大量js或ts開發中需要的包和程式碼模組,當你在專案中需要引用這些包或模組時,隨時可以利用npm指令進行快速下載使用,這樣不被引用的模組不必佔用過多專案空間。比如,可以直接利用npm來安裝ts,開啟cmd輸入:

> npm install -g typescript

其中-g表示全域性安裝,在npm指令中,install也可以簡寫為i:

> npm i -g typescript

ts安裝完成後,就可以直接建立一個空資料夾作為工程目錄了,但這時建立的ts檔案並不能編譯,因為一個新的ts工程還需要先初始化npm和ts配置檔案,可以在VS Code中直接呼叫新的終端:

> npm init -y
> tsc -init

執行完這兩條指令後,我們會發現工程中生成了兩個json檔案,它們分別是package.json和tsconfig.json;引數-y表示按照預設方式生成,tsc即為type script config的縮寫。

package.json中記錄了整個工程的基本資訊,簡化的命令列指令,以及當前工程的依賴模組和庫等;開發者可以自行在該檔案的scripts塊中新增自定義的指令,例如:

    "start": "tsc main.ts && node main.js",
    "build": "tsc main.ts && pkg -t win main.js"

這樣便可以直接呼叫:

> npm run start
> npm run build

來代替執行自定義新增的命令列內容;在第一次build時,系統一般會告訴你它蠢蠢的沒有找到pkg,這時,你只需要執行安裝它的指令即可:

> npm i -g pkg

同樣的,之後在編譯過程中遇到了引用的模組或庫找不到的情況,可以先考慮該模組是否安裝,如果沒有,都可以執行類似的安裝指令,但需要區分是否全域性安裝。

回過頭來說下pkg是什麼東西,這是將Node.js專案打包為可執行檔案的一個工具,引數-t win 表示生成的目標(target)平臺為windows,更多詳情瞭解可見github:

https://github.com/vercel/pkg

另外,為了更方便的通過ts來引用一些常用的node.js庫,可以考慮提前執行以下指令:

> npm i @types/node --save-dev

完成後,工程目錄的node_modules下會自動新增對應安裝的庫。引數-save意思是在package.json中儲存並寫入該依賴庫,-dev指的是僅在開發階段需要依賴該庫,編譯部署後則不再依賴。

 

2.正式編寫

在正式開始編寫之前,需要明確的是,ts並非強封裝型別的語言,和很多面向物件的程式語言有一定的區別,也不需要程式入口一樣的main函式,而是從上到下,從左到右依次讀取程式中的每一行;

當然了,這並不代表ts不能實現封裝,你依然可以將固定的程式碼塊封裝為函式或類,但這並非是強制性的。

 

為了對檔案和路徑進行操作,需要提前引用一些模組,類似於C#中的using,ts中的格式則類似於:

import * as fs from 'fs';
import * as path from 'path';

因為之前已經安裝過@types/node, 所以這裡不會出現找不到引用的報錯。當然了,還可以用另一種方式來引用模組:

const fs = require('fs');
const path = require('path');

順便提一句ts中宣告的幾個關鍵字const,var,let;const和var在C#也有,分別用於宣告常量與區域性變數,而let是我之前沒有見過的,在網上查閱之後,發現let和var很多地方都是類似的,但有以下幾點區別:

1.var宣告的變數會自動提升到該語句所在程式碼塊的開頭(但注意初始化的賦值並不會),這種現象稱為變數提升;而let不具備變數提升的特性

造成的影響便是,var可以先使用後宣告,不會有任何報錯,而是會輸出未定義型別undefined,但let這麼做就會直接報錯(迷)

2.var允許重複宣告同一變數,會覆蓋之前變數的值,但let則不能重複宣告同一變數(迷)

3.var重複宣告變數時內部程式碼塊的值可以覆蓋外部值(什麼還有這種操作?!),但let則表現為不同的兩個變數,

主要因為var與let宣告的變數作用範圍不同,var的作用範圍包含子塊以及它所在的函式的任何位置(迷),而let只在當前塊(不包含子塊)中有效

emm...感覺和C#裡的var完全不一樣啊,作為新手如果為了保險起見,可以均使用let來宣告區域性變數。

 

下面的方法為查詢指定路徑下的檔案,並將所有檔案的絕對路徑儲存到一個臨時的陣列中:

 1 let temp: string[] = new Array();
 2 function fileDisplay(filePath: string) {
 3     // 根據檔案路徑讀取檔案,返回一個檔案列表
 4     const files = fs.readdirSync(filePath);
 5     // 遍歷讀取到的檔案列表
 6     for (let filename of files) {
 7         // path.join得到當前檔案的絕對路徑
 8         const filepath = path.join(filePath, filename);
 9         // 根據檔案路徑獲取檔案資訊
10         const stats = fs.statSync(filepath);
11         const isFile = stats.isFile(); // 是否為檔案
12         const isDir = stats.isDirectory(); // 是否為資料夾
13         if (isFile) {
14             temp.push(filepath);
15         }
16         if (isDir) {
17             fileDisplay(filepath); // 遞迴,如果是資料夾,就繼續遍歷該資料夾裡面的檔案
18         }
19     };
20 }

注意在上述的方法中需要需要同步讀取檔案(Sync),而不應該採取預設的非同步讀取,這樣之後的程式碼中取到temp陣列時才會得到正確的值,如果非要非同步讀取,則需要用回撥的方式來寫json。

 

為了獲得命令列中輸入的引數,可以使用下面的語句:

let argument = process.argv.splice(2);

process.argv()為node.js中返回當前命令列引數的方法,其中2代表的是實際輸入的引數陣列,如果輸入0的話則代表獲取node,1的話返回執行的js的完整路徑

之後直接將命令列輸入的第一個引數,也就是使用者鍵入的資料夾路徑作為引數傳遞給fileDisplay方法即可:

fileDisplay(argument[0]);

 

得到所有的檔案路徑後,接下來就是按照檔案的型別寫入json中了

首先我們需要先遍歷所有的檔案路徑,通過路徑字串可以得到檔案的一些基本資訊,例如檔案的拓展名,檔案的基本名稱等,通過檔案的副檔名可以對檔案資源的型別重定義和分類:

1 for (let item of temp)
2 {
3     let extname = path.extname(item);//獲取檔案的副檔名,帶.
4     let basename = path.basename(item, extname);//獲取檔案的基本名稱,第二個引數為需要剔除的副檔名
5     //...
6 }

當然了,如果你不想用path模組的方法,也可以直接用字串的方式來擷取:

    let fileExtension = item.substring(item.lastIndexOf('.'));
//
    let fileName = item.substring(item.lastIndexOf('\\') + 1, item.lastIndexOf('.'));

需要注意的是,在ts中遍歷元素內容的方式為of而非in(習慣C#了這裡被坑了一把),in只能遍歷出索引...

另外,匹配[\]時需要用兩個[\\]才可以,因為一個[\]代表的大多為轉義字元。 

 

根據檔案的副檔名返回自定義的檔案型別:

 1 function GetType(extension: string): string {
 2     switch (extension) {
 3         case ".png":
 4         case ".jpg":
 5             return "image";
 6         case ".fnt":
 7             return "bitmapFont";
 8         case ".TTF":
 9         case ".ttf":
10             return "font";
11         case ".spine":
12         case ".particle":
13             return "particle";
14         case ".mp3":
15             return "audio";
16         default:
17             return "null";
18     }
19 }

 篩選過濾檔案:

1     let type = GetType(extname);
2     //過濾非指定型別檔案
3     if (type == "null")
4         continue;
5     //過濾重名檔案
6     if (resources[basename]) {
7         console.log(`錯誤!!!該檔名已存在【${basename}】`);
8         continue;
9     }

定義json基礎資料結構:

1 let outjson: any = {}
2 let resources: any = {};
3 let d: any = {};
4 
5 d.tye = type;
6 d.url = item;
7 
8 resources[basename] = d;
9 outjson.resources = resources;

上面是為了更方便讀者理解而將這三個變數放在一起,實際上變數d是在迴圈體內部宣告的區域性變數,any型別是ts中的一種特殊型別,它可以被定義為任何一種其他型別,這裡將它定義為了一種大括號型別的資料結構,代表它的內部還有一些其他的任意成員變數。

如果是在C#中書寫json的資料結構,將是一件非常麻煩的事,需要嚴格的定義為一個新的類或結構體,但ts中似乎相當自由,只需要用一個變數來代替即可,甚至直接在賦值初始化的時候來確定鍵值。

但網上關於大括號型別的any講解並不多,所以做了一點額外的測試:

 1 let a: any = {};
 2 let b: any = {};
 3 let c: any = {};
 4 a.b = "c";
 5 a.c = 5.6;
 6 a.a = a.b;
 7 a["b"] = a.b;
 8 b["c"] = a.c;
 9 b["a"] = a;
10 c.a = b;
11 c[a.b] = a;
12 console.log(c);

大家可以推導下會打印出什麼結果;好,接下來公佈答案:

 1 {
 2     a: {
 3         c: 5.6,
 4         a: {
 5             b: 'c',
 6             c: 5.6,
 7             a: 'c'
 8         }
 9     },
10     c: {
11         b: 'c',
12         c: 5.6,
13         a: 'c'
14     }
15 }

下面來進行一個簡單的梳理:

測試第四行 代表a中有一個鍵(變數名)為b的成員,它的值為字串c

測試第五行 代表a中有一個鍵(變數名)為c的成員,它的值為數字型別5.6(ts中所有的數字型別均為浮點型,省去了很多其他程式語言中值型別資料的繁瑣分類)

測試第六行 代表a中有一個鍵(變數名)為a的成員,它的值初始化為a中鍵為b的那個成員的值,也即是同樣的字串c

測試第七行 實際意義與第四行相同,但這裡是為了測試[key]這種書寫形式所存在的意義,實際上結合第十一行就能得出結論,那就是——當我們需要一個字串變數而非常量來作為鍵時就不能直接用“.成員名”的方式了,因為這樣的方式只能生成固定的字串名,

可以再比較以下例子:

 1 let x1: any;
 2 let x2: any = {};
 3 x1 = "x2";
 4 x2.x1 = x1;
 5 x2[x1] = x1;
 6 console.log(x2);
 7 x1 = "6";
 8 x2.x1 = x1;
 9 x2[x1] = x1;
10 console.log(x2);

大家可以再推導一下會打印出什麼結果;好,現在公佈答案:

1 {
2     x1: 'x2',
3     x2: 'x2'
4 } {
5     '6': '6',
6     x1: '6',
7     x2: 'x2'
8 }

是不是讓人有些驚訝,實話說,第二次的列印結果筆者也沒有做對,我沒有想到它竟然能打印出3個值...原因就在於在第一次中x2[x1]中x1對應的字串x2這一鍵並沒有被修改或刪除,而x2.x1中鍵x1是一個固定的變數名,所以它的值理所當然的被改變為了後面的字串6,又因為x1的值已經發生了改變,所以x2[x1]已經不再是原來的任何一個鍵,從而又生成了一個新的鍵值對。

 

經過上面的對比測試,應該已經可以很好的區分什麼時候用".成員名",什麼時候用[變數]了,返回前面的json的資料結構;因為檔名這一鍵是根據檔案的不同隨時都會變化的值,所以採用中括號的形式,而typ,url等為固定的字串,每一個檔案都具備這類成員名,所以直接用點的形式即可。

 

接下來只需要將json寫入到指定的路徑即可:

1 //寫入json檔案選項
2 function writeJson(data: any, jsonFilePath: string) {
3     fs.writeFileSync(jsonFilePath, JSON.stringify(data, null, 4).replace(/\\\\/g, "/"), 'utf-8');
4 }
5 writeJson(outjson, "./default.res.json");

我在寫入json時遇到了一個問題,就是路徑的\總是在寫入時實際檔案時變為\\,但在控制檯列印字串時又是正常的(迷),所以沒辦法就用正則表示式全域性匹配\\替換為\,至於出現這個問題的原因到現在還沒有弄清楚,如果有大佬發現是什麼原因歡迎告知筆者。

 

3.生成可執行檔案和批處理檔案

在環境配置時已經說了pkg安裝與執行指令,這裡直接在命令列中呼叫:npm run build即可,因為已經設定了平臺為win,build後文件夾中就會出現exe檔案。

此時直接點選這個應用程式沒有任何效果,因為程式中設定的是需要得到使用者輸入的命令列引數——搜尋的資料夾路徑才行,當然了,你可以直接開啟cmd來執行該exe並設定引數,但每次都要設定引數未免有些難受,這是就可以寫一個批處理來執行當前exe所在路徑下的檔案查詢和生成json,這樣即使是程式白痴也能用了。

main.exe .\
pause

 

開啟看一下生成的json是否讓人滿意:(只截取了一部分)

&n