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

用 Node.js 實作一個 Apache HTTP Server (一)中,已經初步用 Node.js 做出 Apache server 的功能了,我們可以透過 url 訪問伺服器 www 資料夾內的對應資源。接著我們要來實現另一項功能:

圖片來源: 網路
在 Apache 中,如果請求地址對應的資源是一個資料夾,Apache 會回應一個網頁,並將該資料夾內部的檔案顯示在網頁中。

目前程式碼

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
// server.js
var http = require('http')
var fs = require('fs')
var server = http.createServer()

var wwwDir = './www'

server.on('request', function (req, res) {
var url = req.url
var filePath
// 在 Apache 中,當請求路徑為 / 時,預設回傳 index.html
url === '/' ? filePath = '/index.html' : filePath = url
var fullPath = wwwDir + filePath

fs.readFile(fullPath, function (error, data) {
if (error) {
return res.end('404 Not Found.')
}
res.end(data)
})
})

server.listen('9000', function () {
console.log('伺服器正在運行...')
})

判斷請求路徑是否為資料夾

當請求路徑為資料夾時,顯示目錄頁面

目前我們可以造訪 ./www 資料夾內的檔案,但如果是請求路徑對應到的資源是資料夾的話,伺服器目前會回傳 404。現在我們需要做兩件事情:

  1. 當收到客戶端請求時,先確認該請求路徑在 ./www 資料夾中,是否有對應資源
  2. 若確認有對應資源,判斷該請求路徑對應的目標是資料夾或是檔案,若是資料夾就渲染目錄頁面,若是檔案就直接回應該檔案

fs.access() 判斷路徑是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server.on('request', function (req, res) {
var url = req.url
var filePath
url === '/' ? filePath = '/index.html' : filePath = url
var fullPath = wwwDir + filePath
// *************Start***************
fs.access(fullPath, function (err) {
if (err) {
console.log('資源不存在')
return res.end('404 Not Found.')
} else {
console.log('資源存在')
// Do 2.
}
})
// **************End****************
})

fs.statSync() 取得資源的實例(Stats)

將檔案路徑帶入參數

1
fs.statSync(fullPath)

返回一個物件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Stats {
dev: 447961964,
mode: 16822,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: undefined,
ino: 10977524092275140,
size: 0,
blocks: undefined,
atimeMs: 1568251013478.8442,
mtimeMs: 1568251013478.8442,
ctimeMs: 1568251013478.8442,
birthtimeMs: 1568251006016.5771,
atime: 2019-09-12T01:16:53.479Z,
mtime: 2019-09-12T01:16:53.479Z,
ctime: 2019-09-12T01:16:53.479Z,
birthtime: 2019-09-12T01:16:46.017Z }

isDirectory() 判斷是否為資料夾

我們可以透過 fs.statSync(fullPath).isDirectory() 來判斷該請求路徑是否為資料夾,該 API 會回傳一個布林值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fs.access(fullPath, function (err) {
if (err) {
console.log('資源不存在')
return res.end('404 Not Found.')
} else {
console.log('資源存在')
// *************Start***************
if (fs.statSync(fullPath).isDirectory()) {
console.log('路徑為資料夾')
// 路徑為資料夾時,抓資料夾成員內容,透過模板引擎寫入資料夾成員到目錄頁面的HTML模板,並回傳到客戶端
// ... 下方待續
} else {
// 路徑為檔案時,回傳該檔案
console.log('路徑為一般檔案')
fs.readFile(fullPath, function (error, data) {
if (error) {
return res.end('404 Not Found.')
}
res.end(data)
})
}
// **************End****************
}
})

接著就可以專心處理請求路徑是資料夾時要做的後續邏輯了

抓取資料夾內檔案/資料夾的名稱

現在我們已經可以判斷請求路徑是不是資料夾了,接著我們需要:

  • 透過 Node.js 抓取目前請求路徑資料夾內的所有成員(包含資料夾及檔案)的名稱,丟到畫面上讓使用者看

fs.readdir() 取資料夾內的成員

fs.readdir() 接收兩個參數

  1. 資料夾路徑字串
  2. callback function,此函式有兩個參數
    • 第一個參數是 error
      • 讀取成功時為 null
      • 讀取失敗時為一個物件
    • 第二個參數是 data
      • 讀取成功時為一個陣列,包含該資料夾內所有成員檔名字串 ex. [‘index.html’, ‘main.css’, ‘a-dir’]
      • 讀取失敗時為 undefined
1
2
3
4
5
6
7
8
9
if (fs.statSync(fullPath).isDirectory()) {
fs.readdir(fullPath, function (err, dirFiles) {
if (err) {
return res.end('404 Not Found.')
}
// 將 dirFiles 陣列資料套入模板引擎,產出出目錄頁面 HTML,接著回傳給客戶端
// ... 下方待續
})
}

模板引擎 art-template

基本的 art-template 用法請參考在 Node.js 中使用 art-template 模板引擎

延續上面的程式碼,接著我們要使用模板引擎將取得的資料夾成員處理成目錄頁面回傳給客戶端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (fs.statSync(fullPath).isDirectory()) {
fs.readdir(fullPath, function (err, dirFiles) {
if (err) {
return res.end('404 Not Found.')
}
// **************Start****************
// 將 dirFiles 陣列資料套入模板引擎,產出出目錄頁面 HTML 字串,接著回傳給客戶端
fs.readFile('./template.html', function (err, templateFile) {
if (err) {
return res.end('404 Not Found.')
}
var htmlStr = template.render(templateFile.toString(), {
files: dirFiles
})
res.end(htmlStr)
})
// ***************End******************
})
}

目錄頁面模板 (template.html)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<body>
<h1 id="header">Index Of C:/nodejs/www</h1>
<table>
<thead>
<tr class="header" id="theader">
<th>名稱</th>
<th class="detailsColumn">大小</th>
<th class="detailsColumn">創建時間</th>
<th class="detailsColumn">修改時間</th>
</tr>
</thead>
<tbody id="tbody">
{{ each files }}
<tr>
<td><a class="icon file" href="#">{{ $value }}</a></td>
<td class="detailsColumn">XXX Byte</td>
<td class="detailsColumn">2019-9-11 11:3</td>
<td class="detailsColumn">2019-9-11 11:3</td>
</tr>
{{/each}}
</tbody>
</table>
</body>

這邊的 HTML 省略了 CSS 樣式,需要完整版請參考目錄頁面模板

Demo

現在我在專案目錄下的 www 開放資源資料夾新增些檔案,接著來訪問看看這些資源,看看效果吧。

還有一些可以優化的部分,下一篇再讓我們繼續吧:

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

資源