用 Node.js 實作一個 Apache HTTP Server (三)

前一篇用 Node.js 實作一個 Apache HTTP Server (二),完成了大部分 Apache 目錄頁面的功能,最後提到還有一些部分可以做優化:

  • 取得檔案大小、創建時間、修改時間資訊,判斷檔案類型 icon
  • 點擊目錄內的檔案名,可以直接造訪該檔案
  • 大標題路徑與請求路徑一致
  • 當造訪的目錄內有 index 為名的檔案時,不顯示目錄,而是該檔案
  • 優化程式碼

目前進度

取得檔案大小、創建時間、修改時間資訊,判斷檔案類型 icon

fs.statSync() - 取得資源詳細資訊

Node.js 提供一個 API fs.statSync(),它接受一個路徑參數,回傳該路徑檔案或資料夾的詳細資訊(包含了檔案大小、創建時間、修改時間的資訊)。前面已經透過 fs.readdir 取得資料夾列表的所有成員的名稱字串,接著只需要將請求路徑字串成員的名稱字串連接再當作參數帶入 fs.statSync() 即可取得目錄頁面所有成員的詳細資訊

這邊任意將一個資源路徑帶入 fs.statSync(),看看輸出的資料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Stats {
dev: 447961964,
mode: 33206,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: undefined,
ino: 16888498603848728,
size: 22,
blocks: undefined,
atimeMs: 1568171004525.9443,
mtimeMs: 1568171004525.9443,
ctimeMs: 1568171004525.9443,
birthtimeMs: 1568170980574.5933,
atime: 2019-09-11T03:03:24.526Z,
mtime: 2019-09-11T03:03:24.526Z,
ctime: 2019-09-11T03:03:24.526Z,
birthtime: 2019-09-11T03:03:00.575Z }
2019-09-11T03:03:24.526Z
2019-09-11T03:03:24.526Z

其中我們需要的資訊為

  • size 檔案大小(Byte)
  • ctime 創建時間
  • mtime 修改時間

另外需要注意 fs.statSync() 輸出的時間格式為 RFC3999Date 時間格式,我們需要處理一下將它轉為像是 2019-01-01 11:11 這樣的格式

透過函式處理

1
2
3
4
5
var timeFormater = function(RFCdate) {
var millisecond = Date.parse(RFCdate) // RFC3999Date格式轉毫秒
var dateObj = new Date(millisecond)  // 轉時間物件
return `${dateObj.toLocaleDateString()} ${dateObj.getHours()}:${dateObj.getMinutes()}`
}

目前每個資料夾目錄成員須具備的資訊有

  • 檔名(或資料夾名)
  • 檔案大小
  • 創建時間
  • 修改時間

將所有成員需要的資訊一一整理成物件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
if (fs.statSync(fullPath).isDirectory()) {
// 路徑為資料夾時,渲染抓資料夾內容,並渲染阿帕契目錄頁面
console.log('路徑為資料夾')
// fs.readdir => 取資料夾內的成員
fs.readdir(fullPath, function (err, dirFiles) {
if (err) {
return res.end('404 Not Found.')
}
fs.readFile('./template.html', function (err, templateFile) {
if (err) {
return res.end('404 Not Found.')
}
// ********************Start********************
var filesDetailInfo = dirFiles.map((file) => {
// fs.statSync => 取得檔案或資料夾的詳細資料
var statSync = fs.statSync(fullPath + '/' + file)
var fileDetail = {
fileName: file, // 檔名
size: statSync.size, // 檔案大小
createTime: timeFormater(statSync.ctime), // 創建時間
mutateTime: timeFormater(statSync.mtime), // 修改時間
}
return fileDetail
})
// 將資料帶入模板
var htmlStr = template.render(templateFile.toString(), {
files: filesDetailInfo
})
res.end(htmlStr)
// ********************End***********************
})
})
}
1
2
3
4
5
6
7
8
9
10
<tbody id="tbody">
{{ each files }}
<tr>
<td><a class="icon file" href="#">{{ $value.fileName }}</a></td>
<td class="detailsColumn">{{ $value.size }} Byte</td>
<td class="detailsColumn">{{ $value.createTime }}</td>
<td class="detailsColumn">{{ $value.mutateTime }}</td>
</tr>
{{/each}}
</tbody>

目錄頁面成員類型icon (資料夾或檔案)

目前目錄頁面的成員無論是什麼檔案類型,一律都是顯示檔案的 icon,現在我們需要做判斷,資料夾就渲染資料夾 icon,其他的就渲染檔案 icon。
我們可以利用 art-templateif 功能配合正規式判斷,正規式的判斷方式很簡單,只要成員的名稱出現 . 我們可以直接判定它是具附檔名的一般檔案,檔名沒有出現 . 則一律當作資料夾處理。

1
2
3
4
5
6
7
8
9
10
11
12
13
{{ each files }}
<tr>
<!-- 判斷 icon 是資料夾或是一般檔案 -->
{{ if (/\./.test($value.fileName))}}
<td><a class="icon file" href="#">{{ $value.fileName }}</a></td>
{{ else }}
<td><a class="icon dir" href="#">{{ $value.fileName }}</a></td>
{{ /if }}
<td class="detailsColumn">{{ $value.size }} Byte</td>
<td class="detailsColumn">{{ $value.createTime }}</td>
<td class="detailsColumn">{{ $value.mutateTime }}</td>
</tr>
{{/each}}

點擊檔案名即可造訪該檔案

目前可以透過輸入網址造訪到單一檔案,但是點擊目錄頁面的檔案是沒有反應的,這裡可以透過 art-template 將請求路徑資料帶入 HTML a 標籤的 href 屬性即可。

請求路徑port 號加上檔名字串接起來,傳入模板並帶入 href 屬性

1
2
3
4
5
6
var htmlStr = template.render(templateFile.toString(), {
path: filePath, // **
port: 9000, // **
files: filesDetailInfo
})
res.end(htmlStr)

修改模板

1
<td><a class="icon file" href="http://127.0.0.1:{{ port + path + '/' + $value.fileName }}">{{ $value.fileName }}</a></td>

大標題路徑與請求路徑一致

將請求路徑及帶入模板即可

1
<h1 id="header">Index Of C:/nodejs/www{{ path }}</h1>

當造訪的目錄內有 index 為名的檔案時,不顯示目錄,而是該檔案

dirFiles 為使用 fs.readdir() 所取出的資料夾成員陣列,透過迴圈及正規式判斷使否有成員名稱吻合 index.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (fs.statSync(fullPath).isDirectory()) {
// 路徑為資料夾時,渲染抓資料夾內容,並渲染阿帕契目錄頁面
console.log('路徑為資料夾')
// fs.readdir => 取資料夾內的成員
fs.readdir(fullPath, function (err, dirFiles) {
if (err) {
return res.end('404 Not Found.')
}
// ********************Start********************
// 檢查該資料夾內是否有名為index的檔案,有則建立一個indexItem物件
var indexItem = {}
dirFiles.forEach((file) => {
if (/index\./.test(file)) {
indexItem = {
hasIndexFile: true,
fileName: file
}
}
})
// ********************End***********************
}

如果請求資料夾內有 index 為名的檔案,則直接回傳該檔案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 檢查該資料夾內是否有名為index的檔案,有則建立一個indexItem物件
var indexItem = {}
dirFiles.forEach((file) => {
if (/index\./.test(file)) {
indexItem = {
hasIndexFile: true,
fileName: file
}
}
})
// ********************Start********************
// 如果有index,則回傳該檔案
if (indexItem.hasIndexFile) {
var indexPathLocation = fullPath + '/' + indexItem.fileName
fs.readFile(indexPathLocation, function(err, indexFile) {
if (err) {
return res.end('404 Not Found.')
}
res.end(indexFile)
})
// ********************End**********************
// 若沒有則印出資料夾成員頁面
} else {
fs.readFile('./template.html', function (err, templateFile) {
if (err) {
return res.end('404 Not Found.')
}
var filesDetailInfo = dirFiles.map((file) => {
// 略..
}
}

優化程式碼

動態 Content-type

之前有一篇文章提到關於 Content-type ,若不清楚它是做什麼的可以先閱讀

伺服器傳送資源到客戶端時,檔案型態千百種,我們需要告訴客戶端瀏覽器當前的 response 檔案需要用什麼檔案類型來解析。
首先可以透過 Node.js 提供的 path 核心模組中的 path.extname API 取得檔案副檔名

1
2
3
// 引入核心模組
var path = require('path')
path.extname('index.js') // output: '.js'

了解使用方式後,就可以製作一個函式,將常見的檔案類型和對應的 Content-type 寫起來

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var returnContentType = function (fileName) {
var deputy = path.extname(fileName)
switch(deputy) {
case '.html':
return 'text/html; charset=utf-8'
break
case '.js':
return 'application/x-javascript; charset=utf-8'
break
case '.css':
return 'text/css; charset=utf-8'
break
case '.txt':
return 'text/plain; charset=utf-8'
break
case '.jpg':
return 'image/jpg'
break
case '.jpeg':
return 'image/jpeg'
break
case '.png':
return 'image/png'
break
default:
return 'charset=utf-8'
break
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
// 如果有index,則回傳該檔案
if (indexItem.hasIndexFile) {
var indexPathLocation = fullPath + '/' + indexItem.fileName
fs.readFile(indexPathLocation, function(err, indexFile) {
if (err) {
return res.end('404 Not Found.')
}
res.setHeader('Content-type', returnContentType(indexItem.fileName)) // **
res.end(indexFile)
})
}

將 error page 導向包裝成函式

1
2
3
4
var toErrorPage = function () {
res.setHeader('Content-type', 'text/html')
return res.end('<h1>404 Not Found.</h1>')
}

接著將程式碼中所有的 return res.end('404 Not Found.') 替換成 toErrorPage()

Demo

資源