用 Node.js 寫一個簡單的伺服器

拉爾夫的技術隨筆
52 min readJan 19, 2023

--

(此為 Wilsen Ren 在 Udemy 開設之線上課程「2023 網頁全端開發」學習筆記,另有部分內容為自行補充,將持續隨著學習進度更新,若內容有誤,歡迎留言指正!)

第一步:在本機以 http.createServer() 建立伺服器並隨時監聽
第二步:處理 http.createServer() 中的 req 、res 兩物件
第三步:設定路由
第四步:讀取檔案

要打開後端世界的大門,首先要做的事就是建立「伺服器(server)」,讓使用者/客戶端(client)向這個伺服器發送請求(request),伺服器端再回傳回應(response)。

雖然說現在多半會使用 Express 這類應用程式架構寫伺服器,但若要了解更底層的伺服器運作方式,還是可以先看如何用原生的 Node.js 實現。

第一步:在本機以 http.createServer() 建立伺服器並隨時監聽

下載 Node.js 之後,執行環境中即包含 http 原生模組,可以先用 require 取得這個模組,再套用模組的 createServer 方法在本機建立伺服器。

建立的伺服器可透過 listen 方法,隨時監聽指定通訊埠(port)上回傳的內容。

//先取得Node.js的http模組
const http = require("http");

//取用http的createServer方法
//方法中包含一回呼函式,會製造出req跟res兩物件
const server = http.createServer((req, res) => {});

//伺服器在3000埠上隨時監聽
server.listen(3000, () => {
console.log("listening on port 3000");
});

使用終端機執行時,即可看見伺服器監聽拿到的內容如下:

若打開瀏覽器,則可發現目前伺服器沒有給任何回應,因此不會顯示任何東西:

第二步:處理 http.createServer() 中的 reqres 兩物件

剛剛在 http.createServer()的回呼函式中什麼都沒寫,現在我們透過 console.log() 分別來看 reqres 兩物件長什麼模樣。

首先觀察 req 結果:

const http = require(“http”);

//把req物件顯示出來
const server = http.createServer((req, res) => {
console.log(req);
});

server.listen(3000, () => {
console.log("listening on port 3000");
});

在終端機的執行結果如下,可發現 req 是非常大的一包物件:

<ref *2> IncomingMessage {
_readableState: ReadableState {
objectMode: false,
highWaterMark: 16384,
buffer: BufferList { head: null, tail: null, length: 0 },
length: 0,
pipes: [],
flowing: null,
ended: false,
endEmitted: false,
reading: false,
constructed: true,
sync: true,
needReadable: false,
emittedReadable: false,
readableListening: false,
resumeScheduled: false,
errorEmitted: false,
emitClose: true,
autoDestroy: true,
destroyed: false,
errored: null,
closed: false,
closeEmitted: false,
defaultEncoding: ‘utf8’,
awaitDrainWriters: null,
multiAwaitDrain: false,
readingMore: true,
dataEmitted: false,
decoder: null,
encoding: null,
[Symbol(kPaused)]: null
},
_events: [Object: null prototype] { end: [Function: clearRequestTimeout] },
_eventsCount: 1,
_maxListeners: undefined,
socket: <ref *1> Socket {
connecting: false,
_hadError: false,
_parent: null,
_host: null,
_readableState: ReadableState {
objectMode: false,
highWaterMark: 16384,
buffer: BufferList { head: null, tail: null, length: 0 },
length: 0,
pipes: [],
flowing: true,
ended: false,
endEmitted: false,
reading: true,
constructed: true,
sync: false,
needReadable: true,
emittedReadable: false,
readableListening: false,
resumeScheduled: false,
errorEmitted: false,
emitClose: false,
autoDestroy: true,
destroyed: false,
errored: null,
closed: false,
closeEmitted: false,
defaultEncoding: ‘utf8’,
awaitDrainWriters: null,
multiAwaitDrain: false,
readingMore: false,
dataEmitted: false,
decoder: null,
encoding: null,
[Symbol(kPaused)]: false
},
_events: [Object: null prototype] {
end: [Array],
timeout: [Function: socketOnTimeout],
data: [Function: bound socketOnData],
error: [Function: socketOnError],
close: [Array],
drain: [Function: bound socketOnDrain],
resume: [Function: onSocketResume],
pause: [Function: onSocketPause]
},
_eventsCount: 8,
_maxListeners: undefined,
_writableState: WritableState {
objectMode: false,
highWaterMark: 16384,
finalCalled: false,
needDrain: false,
ending: false,
ended: false,
finished: false,
destroyed: false,
decodeStrings: false,
defaultEncoding: ‘utf8’,
length: 0,
writing: false,
corked: 0,
sync: true,
bufferProcessing: false,
onwrite: [Function: bound onwrite],
writecb: null,
writelen: 0,
afterWriteTickInfo: null,
buffered: [],
bufferedIndex: 0,
allBuffers: true,
allNoop: true,
pendingcb: 0,
constructed: true,
prefinished: false,
errorEmitted: false,
emitClose: false,
autoDestroy: true,
errored: null,
closed: false,
closeEmitted: false,
[Symbol(kOnFinished)]: []
},
allowHalfOpen: true,
_sockname: null,
_pendingData: null,
_pendingEncoding: ‘’,
server: Server {
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_connections: 1,
_handle: [TCP],
_usingWorkers: false,
_workers: [],
_unref: false,
allowHalfOpen: true,
pauseOnConnect: false,
httpAllowHalfOpen: false,
timeout: 0,
keepAliveTimeout: 5000,
maxHeadersCount: null,
maxRequestsPerSocket: 0,
headersTimeout: 60000,
requestTimeout: 0,
_connectionKey: ‘6::::3000’,
[Symbol(IncomingMessage)]: [Function: IncomingMessage],
[Symbol(ServerResponse)]: [Function: ServerResponse],
[Symbol(kCapture)]: false,
[Symbol(async_id_symbol)]: 2
},
_server: Server {
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_connections: 1,
_handle: [TCP],
_usingWorkers: false,
_workers: [],
_unref: false,
allowHalfOpen: true,
pauseOnConnect: false,
httpAllowHalfOpen: false,
timeout: 0,
keepAliveTimeout: 5000,
maxHeadersCount: null,
maxRequestsPerSocket: 0,
headersTimeout: 60000,
requestTimeout: 0,
_connectionKey: ‘6::::3000’,
[Symbol(IncomingMessage)]: [Function: IncomingMessage],
[Symbol(ServerResponse)]: [Function: ServerResponse],
[Symbol(kCapture)]: false,
[Symbol(async_id_symbol)]: 2
},
parser: HTTPParser {
‘0’: [Function: bound setRequestTimeout],
‘1’: [Function: parserOnHeaders],
‘2’: [Function: parserOnHeadersComplete],
‘3’: [Function: parserOnBody],
‘4’: [Function: parserOnMessageComplete],
‘5’: [Function: bound onParserExecute],
‘6’: [Function: bound onParserTimeout],
_headers: [],
_url: ‘’,
socket: [Circular *1],
incoming: [Circular *2],
outgoing: null,
maxHeaderPairs: 2000,
_consumed: true,
onIncoming: [Function: bound parserOnIncoming],
[Symbol(resource_symbol)]: [HTTPServerAsyncResource]
},
on: [Function: socketListenerWrap],
addListener: [Function: socketListenerWrap],
prependListener: [Function: socketListenerWrap],
setEncoding: [Function: socketSetEncoding],
_paused: false,
_httpMessage: ServerResponse {
_events: [Object: null prototype],
_eventsCount: 1,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
destroyed: false,
_last: false,
chunkedEncoding: false,
shouldKeepAlive: true,
maxRequestsOnConnectionReached: false,
_defaultKeepAlive: true,
useChunkedEncodingByDefault: true,
sendDate: true,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: null,
_hasBody: true,
_trailer: ‘’,
finished: false,
_headerSent: false,
_closed: false,
socket: [Circular *1],
_header: null,
_keepAliveTimeout: 5000,
_onPendingData: [Function: bound updateOutgoingData],
req: [Circular *2],
_sent100: false,
_expect_continue: false,
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: null
},
[Symbol(async_id_symbol)]: 7,
[Symbol(kHandle)]: TCP {
reading: true,
onconnection: null,
_consumed: true,
[Symbol(owner_symbol)]: [Circular *1]
},
[Symbol(kSetNoDelay)]: false,
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: null,
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kCapture)]: false,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0,
[Symbol(RequestTimeout)]: undefined
},
httpVersionMajor: 1,
httpVersionMinor: 1,
httpVersion: ‘1.1’,
complete: false,
rawHeaders: [
‘Host’,
‘localhost:3000’,
‘User-Agent’,
‘Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0’,
‘Accept’,
‘text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8’,
‘Accept-Language’,
‘zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3’,
‘Accept-Encoding’,
‘gzip, deflate, br’,
‘Connection’,
‘keep-alive’,
‘Cookie’,
‘fbm_495277332386929=base_domain=.localhost; fblo_700590737403665=y; fblo_1155346648576268=y’,
‘Upgrade-Insecure-Requests’,
‘1’,
‘Sec-Fetch-Dest’,
‘document’,
‘Sec-Fetch-Mode’,
‘navigate’,
‘Sec-Fetch-Site’,
‘none’,
‘Sec-Fetch-User’,
‘?1’
],
rawTrailers: [],
aborted: false,
upgrade: false,
url: ‘/’,
method: ‘GET’,
statusCode: null,
statusMessage: null,
client: <ref *1> Socket {
connecting: false,
_hadError: false,
_parent: null,
_host: null,
_readableState: ReadableState {
objectMode: false,
highWaterMark: 16384,
buffer: BufferList { head: null, tail: null, length: 0 },
length: 0,
pipes: [],
flowing: true,
ended: false,
endEmitted: false,
reading: true,
constructed: true,
sync: false,
needReadable: true,
emittedReadable: false,
readableListening: false,
resumeScheduled: false,
errorEmitted: false,
emitClose: false,
autoDestroy: true,
destroyed: false,
errored: null,
closed: false,
closeEmitted: false,
defaultEncoding: ‘utf8’,
awaitDrainWriters: null,
multiAwaitDrain: false,
readingMore: false,
dataEmitted: false,
decoder: null,
encoding: null,
[Symbol(kPaused)]: false
},
_events: [Object: null prototype] {
end: [Array],
timeout: [Function: socketOnTimeout],
data: [Function: bound socketOnData],
error: [Function: socketOnError],
close: [Array],
drain: [Function: bound socketOnDrain],
resume: [Function: onSocketResume],
pause: [Function: onSocketPause]
},
_eventsCount: 8,
_maxListeners: undefined,
_writableState: WritableState {
objectMode: false,
highWaterMark: 16384,
finalCalled: false,
needDrain: false,
ending: false,
ended: false,
finished: false,
destroyed: false,
decodeStrings: false,
defaultEncoding: ‘utf8’,
length: 0,
writing: false,
corked: 0,
sync: true,
bufferProcessing: false,
onwrite: [Function: bound onwrite],
writecb: null,
writelen: 0,
afterWriteTickInfo: null,
buffered: [],
bufferedIndex: 0,
allBuffers: true,
allNoop: true,
pendingcb: 0,
constructed: true,
prefinished: false,
errorEmitted: false,
emitClose: false,
autoDestroy: true,
errored: null,
closed: false,
closeEmitted: false,
[Symbol(kOnFinished)]: []
},
allowHalfOpen: true,
_sockname: null,
_pendingData: null,
_pendingEncoding: ‘’,
server: Server {
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_connections: 1,
_handle: [TCP],
_usingWorkers: false,
_workers: [],
_unref: false,
allowHalfOpen: true,
pauseOnConnect: false,
httpAllowHalfOpen: false,
timeout: 0,
keepAliveTimeout: 5000,
maxHeadersCount: null,
maxRequestsPerSocket: 0,
headersTimeout: 60000,
requestTimeout: 0,
_connectionKey: ‘6::::3000’,
[Symbol(IncomingMessage)]: [Function: IncomingMessage],
[Symbol(ServerResponse)]: [Function: ServerResponse],
[Symbol(kCapture)]: false,
[Symbol(async_id_symbol)]: 2
},
_server: Server {
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_connections: 1,
_handle: [TCP],
_usingWorkers: false,
_workers: [],
_unref: false,
allowHalfOpen: true,
pauseOnConnect: false,
httpAllowHalfOpen: false,
timeout: 0,
keepAliveTimeout: 5000,
maxHeadersCount: null,
maxRequestsPerSocket: 0,
headersTimeout: 60000,
requestTimeout: 0,
_connectionKey: ‘6::::3000’,
[Symbol(IncomingMessage)]: [Function: IncomingMessage],
[Symbol(ServerResponse)]: [Function: ServerResponse],
[Symbol(kCapture)]: false,
[Symbol(async_id_symbol)]: 2
},
parser: HTTPParser {
‘0’: [Function: bound setRequestTimeout],
‘1’: [Function: parserOnHeaders],
‘2’: [Function: parserOnHeadersComplete],
‘3’: [Function: parserOnBody],
‘4’: [Function: parserOnMessageComplete],
‘5’: [Function: bound onParserExecute],
‘6’: [Function: bound onParserTimeout],
_headers: [],
_url: ‘’,
socket: [Circular *1],
incoming: [Circular *2],
outgoing: null,
maxHeaderPairs: 2000,
_consumed: true,
onIncoming: [Function: bound parserOnIncoming],
[Symbol(resource_symbol)]: [HTTPServerAsyncResource]
},
on: [Function: socketListenerWrap],
addListener: [Function: socketListenerWrap],
prependListener: [Function: socketListenerWrap],
setEncoding: [Function: socketSetEncoding],
_paused: false,
_httpMessage: ServerResponse {
_events: [Object: null prototype],
_eventsCount: 1,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
destroyed: false,
_last: false,
chunkedEncoding: false,
shouldKeepAlive: true,
maxRequestsOnConnectionReached: false,
_defaultKeepAlive: true,
useChunkedEncodingByDefault: true,
sendDate: true,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: null,
_hasBody: true,
_trailer: ‘’,
finished: false,
_headerSent: false,
_closed: false,
socket: [Circular *1],
_header: null,
_keepAliveTimeout: 5000,
_onPendingData: [Function: bound updateOutgoingData],
req: [Circular *2],
_sent100: false,
_expect_continue: false,
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: null
},
[Symbol(async_id_symbol)]: 7,
[Symbol(kHandle)]: TCP {
reading: true,
onconnection: null,
_consumed: true,
[Symbol(owner_symbol)]: [Circular *1]
},
[Symbol(kSetNoDelay)]: false,
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: null,
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kCapture)]: false,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0,
[Symbol(RequestTimeout)]: undefined
},
_consuming: false,
_dumped: false,
[Symbol(kCapture)]: false,
[Symbol(kHeaders)]: {
host: ‘localhost:3000’,
‘user-agent’: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0’,
accept: ‘text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8’,
‘accept-language’: ‘zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3’,
‘accept-encoding’: ‘gzip, deflate, br’,
connection: ‘keep-alive’,
cookie: ‘fbm_495277332386929=base_domain=.localhost; fblo_700590737403665=y; fblo_1155346648576268=y’,
‘upgrade-insecure-requests’: ‘1’,
‘sec-fetch-dest’: ‘document’,
‘sec-fetch-mode’: ‘navigate’,
‘sec-fetch-site’: ‘none’,
‘sec-fetch-user’: ‘?1’
},
[Symbol(kHeadersCount)]: 24,
[Symbol(kTrailers)]: null,
[Symbol(kTrailersCount)]: 0,
[Symbol(RequestTimeout)]: undefined
}

但事實上,上面那包大多數的資訊我們都用不到,只需要關心 req 中的 headers ,一樣可以透過 console.log() 觀察:

const http = require(“http”);

//把req物件headers的樣子顯示出來
const server = http.createServer((req, res) => {
console.log(req.headers);
});

server.listen(3000, () => {
console.log("listening on port 3000");
});

顯示結果包含伺服器域名(host)、可接受回應內容的類型(accept)、瀏覽器想要優先使用的回應類型(connection),以及伺服器傳送的 cookie:

{
host: ‘localhost:3000’,
‘user-agent’: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0’,
accept: ‘text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8’,
‘accept-language’: ‘zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3’,
‘accept-encoding’: ‘gzip, deflate, br’,
connection: ‘keep-alive’,
cookie: ‘fbm_495277332386929=base_domain=.localhost; fblo_700590737403665=y; fblo_1155346648576268=y’,
‘upgrade-insecure-requests’: ‘1’,
‘sec-fetch-dest’: ‘document’,
‘sec-fetch-mode’: ‘navigate’,
‘sec-fetch-site’: ‘none’,
‘sec-fetch-user’: ‘?1’
}

現在改觀察 res 物件:

const http = require(“http”);

//把res物件顯示出來
const server = http.createServer((req, res) => {
console.log(res);
});

server.listen(3000, () => {
console.log("listening on port 3000");
});

res 物件一樣也是好大一包:

<ref *2> ServerResponse {
_events: [Object: null prototype] { finish: [Function: bound resOnFinish] },
_eventsCount: 1,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
destroyed: false,
_last: false,
chunkedEncoding: false,
shouldKeepAlive: true,
maxRequestsOnConnectionReached: false,
_defaultKeepAlive: true,
useChunkedEncodingByDefault: true,
sendDate: true,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: null,
_hasBody: true,
_trailer: ‘’,
finished: false,
_headerSent: false,
_closed: false,
socket: <ref *1> Socket {
connecting: false,
_hadError: false,
_parent: null,
_host: null,
_readableState: ReadableState {
objectMode: false,
highWaterMark: 16384,
buffer: BufferList { head: null, tail: null, length: 0 },
length: 0,
pipes: [],
flowing: true,
ended: false,
endEmitted: false,
reading: true,
constructed: true,
sync: false,
needReadable: true,
emittedReadable: false,
readableListening: false,
resumeScheduled: false,
errorEmitted: false,
emitClose: false,
autoDestroy: true,
destroyed: false,
errored: null,
closed: false,
closeEmitted: false,
defaultEncoding: ‘utf8’,
awaitDrainWriters: null,
multiAwaitDrain: false,
readingMore: false,
dataEmitted: false,
decoder: null,
encoding: null,
[Symbol(kPaused)]: false
},
_events: [Object: null prototype] {
end: [Array],
timeout: [Function: socketOnTimeout],
data: [Function: bound socketOnData],
error: [Function: socketOnError],
close: [Array],
drain: [Function: bound socketOnDrain],
resume: [Function: onSocketResume],
pause: [Function: onSocketPause]
},
_eventsCount: 8,
_maxListeners: undefined,
_writableState: WritableState {
objectMode: false,
highWaterMark: 16384,
finalCalled: false,
needDrain: false,
ending: false,
ended: false,
finished: false,
destroyed: false,
decodeStrings: false,
defaultEncoding: ‘utf8’,
length: 0,
writing: false,
corked: 0,
sync: true,
bufferProcessing: false,
onwrite: [Function: bound onwrite],
writecb: null,
writelen: 0,
afterWriteTickInfo: null,
buffered: [],
bufferedIndex: 0,
allBuffers: true,
allNoop: true,
pendingcb: 0,
constructed: true,
prefinished: false,
errorEmitted: false,
emitClose: false,
autoDestroy: true,
errored: null,
closed: false,
closeEmitted: false,
[Symbol(kOnFinished)]: []
},
allowHalfOpen: true,
_sockname: null,
_pendingData: null,
_pendingEncoding: ‘’,
server: Server {
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_connections: 1,
_handle: [TCP],
_usingWorkers: false,
_workers: [],
_unref: false,
allowHalfOpen: true,
pauseOnConnect: false,
httpAllowHalfOpen: false,
timeout: 0,
keepAliveTimeout: 5000,
maxHeadersCount: null,
maxRequestsPerSocket: 0,
headersTimeout: 60000,
requestTimeout: 0,
_connectionKey: ‘6::::3000’,
[Symbol(IncomingMessage)]: [Function: IncomingMessage],
[Symbol(ServerResponse)]: [Function: ServerResponse],
[Symbol(kCapture)]: false,
[Symbol(async_id_symbol)]: 2
},
_server: Server {
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_connections: 1,
_handle: [TCP],
_usingWorkers: false,
_workers: [],
_unref: false,
allowHalfOpen: true,
pauseOnConnect: false,
httpAllowHalfOpen: false,
timeout: 0,
keepAliveTimeout: 5000,
maxHeadersCount: null,
maxRequestsPerSocket: 0,
headersTimeout: 60000,
requestTimeout: 0,
_connectionKey: ‘6::::3000’,
[Symbol(IncomingMessage)]: [Function: IncomingMessage],
[Symbol(ServerResponse)]: [Function: ServerResponse],
[Symbol(kCapture)]: false,
[Symbol(async_id_symbol)]: 2
},
parser: HTTPParser {
‘0’: [Function: bound setRequestTimeout],
‘1’: [Function: parserOnHeaders],
‘2’: [Function: parserOnHeadersComplete],
‘3’: [Function: parserOnBody],
‘4’: [Function: parserOnMessageComplete],
‘5’: [Function: bound onParserExecute],
‘6’: [Function: bound onParserTimeout],
_headers: [],
_url: ‘’,
socket: [Circular *1],
incoming: [IncomingMessage],
outgoing: null,
maxHeaderPairs: 2000,
_consumed: true,
onIncoming: [Function: bound parserOnIncoming],
[Symbol(resource_symbol)]: [HTTPServerAsyncResource]
},
on: [Function: socketListenerWrap],
addListener: [Function: socketListenerWrap],
prependListener: [Function: socketListenerWrap],
setEncoding: [Function: socketSetEncoding],
_paused: false,
_httpMessage: [Circular *2],
[Symbol(async_id_symbol)]: 7,
[Symbol(kHandle)]: TCP {
reading: true,
onconnection: null,
_consumed: true,
[Symbol(owner_symbol)]: [Circular *1]
},
[Symbol(kSetNoDelay)]: false,
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: null,
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kCapture)]: false,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0,
[Symbol(RequestTimeout)]: undefined
},
_header: null,
_keepAliveTimeout: 5000,
_onPendingData: [Function: bound updateOutgoingData],
req: IncomingMessage {
_readableState: ReadableState {
objectMode: false,
highWaterMark: 16384,
buffer: BufferList { head: null, tail: null, length: 0 },
length: 0,
pipes: [],
flowing: null,
ended: false,
endEmitted: false,
reading: false,
constructed: true,
sync: true,
needReadable: false,
emittedReadable: false,
readableListening: false,
resumeScheduled: false,
errorEmitted: false,
emitClose: true,
autoDestroy: true,
destroyed: false,
errored: null,
closed: false,
closeEmitted: false,
defaultEncoding: ‘utf8’,
awaitDrainWriters: null,
multiAwaitDrain: false,
readingMore: true,
dataEmitted: false,
decoder: null,
encoding: null,
[Symbol(kPaused)]: null
},
_events: [Object: null prototype] { end: [Function: clearRequestTimeout] },
_eventsCount: 1,
_maxListeners: undefined,
socket: <ref *1> Socket {
connecting: false,
_hadError: false,
_parent: null,
_host: null,
_readableState: [ReadableState],
_events: [Object: null prototype],
_eventsCount: 8,
_maxListeners: undefined,
_writableState: [WritableState],
allowHalfOpen: true,
_sockname: null,
_pendingData: null,
_pendingEncoding: ‘’,
server: [Server],
_server: [Server],
parser: [HTTPParser],
on: [Function: socketListenerWrap],
addListener: [Function: socketListenerWrap],
prependListener: [Function: socketListenerWrap],
setEncoding: [Function: socketSetEncoding],
_paused: false,
_httpMessage: [Circular *2],
[Symbol(async_id_symbol)]: 7,
[Symbol(kHandle)]: [TCP],
[Symbol(kSetNoDelay)]: false,
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: null,
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kCapture)]: false,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0,
[Symbol(RequestTimeout)]: undefined
},
httpVersionMajor: 1,
httpVersionMinor: 1,
httpVersion: ‘1.1’,
complete: false,
rawHeaders: [
‘Host’,
‘localhost:3000’,
‘User-Agent’,
‘Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0’,
‘Accept’,
‘text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8’,
‘Accept-Language’,
‘zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3’,
‘Accept-Encoding’,
‘gzip, deflate, br’,
‘Connection’,
‘keep-alive’,
‘Cookie’,
‘fbm_495277332386929=base_domain=.localhost; fblo_700590737403665=y; fblo_1155346648576268=y’,
‘Upgrade-Insecure-Requests’,
‘1’,
‘Sec-Fetch-Dest’,
‘document’,
‘Sec-Fetch-Mode’,
‘navigate’,
‘Sec-Fetch-Site’,
‘none’,
‘Sec-Fetch-User’,
‘?1’
],
rawTrailers: [],
aborted: false,
upgrade: false,
url: ‘/’,
method: ‘GET’,
statusCode: null,
statusMessage: null,
client: <ref *1> Socket {
connecting: false,
_hadError: false,
_parent: null,
_host: null,
_readableState: [ReadableState],
_events: [Object: null prototype],
_eventsCount: 8,
_maxListeners: undefined,
_writableState: [WritableState],
allowHalfOpen: true,
_sockname: null,
_pendingData: null,
_pendingEncoding: ‘’,
server: [Server],
_server: [Server],
parser: [HTTPParser],
on: [Function: socketListenerWrap],
addListener: [Function: socketListenerWrap],
prependListener: [Function: socketListenerWrap],
setEncoding: [Function: socketSetEncoding],
_paused: false,
_httpMessage: [Circular *2],
[Symbol(async_id_symbol)]: 7,
[Symbol(kHandle)]: [TCP],
[Symbol(kSetNoDelay)]: false,
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: null,
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kCapture)]: false,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0,
[Symbol(RequestTimeout)]: undefined
},
_consuming: false,
_dumped: false,
[Symbol(kCapture)]: false,
[Symbol(kHeaders)]: {
host: ‘localhost:3000’,
‘user-agent’: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0’,
accept: ‘text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8’,
‘accept-language’: ‘zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3’,
‘accept-encoding’: ‘gzip, deflate, br’,
connection: ‘keep-alive’,
cookie: ‘fbm_495277332386929=base_domain=.localhost; fblo_700590737403665=y; fblo_1155346648576268=y’,
‘upgrade-insecure-requests’: ‘1’,
‘sec-fetch-dest’: ‘document’,
‘sec-fetch-mode’: ‘navigate’,
‘sec-fetch-site’: ‘none’,
‘sec-fetch-user’: ‘?1’
},
[Symbol(kHeadersCount)]: 24,
[Symbol(kTrailers)]: null,
[Symbol(kTrailersCount)]: 0,
[Symbol(RequestTimeout)]: undefined
},
_sent100: false,
_expect_continue: false,
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: null
}

一如 req 物件,res 物件絕大多數的內容我們都用不到。

res 物件的功能是要伺服器回傳資料給客戶端,可以用 res.write() 方法寫下回傳內容、再用 res.end() 結束此一回應:

const http = require(“http”);

//把res物件套上write()方法寫下回傳內容,再用res.end()結束回應
const server = http.createServer((req, res) => {
res.write("Welcome to the page");
res.end();
});

server.listen(3000, () => {
console.log("listening on port 3000");
});

在瀏覽器上,就可以看到指定通訊埠 localhost:3000/ 畫面中,出現回傳內容:

如果把 res.write() 的回傳內容改寫成中文,也會顯示打好的內容嗎?

const http = require(“http”);

//把res.write()回傳內容改成中文
const server = http.createServer((req, res) => {
res.write("歡迎來到我的網頁");
res.end();
});

server.listen(3000, () => {
console.log("listening on port 3000");
});

打開瀏覽器會發現,顯示內容變成亂碼:

這是因為編碼有誤,可以透過 writeHead() 方法寫 header,並在當中指定編碼為 utf-8,就可以處理中文內容了:

const http = require(“http”);

const server = http.createServer((req, res) => {
//把res物件的header寫出來
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.write("歡迎來到我的網頁");
res.end();
});

server.listen(3000, () => {
console.log("listening on port 3000");
});

瀏覽器顯示結果如下:

第三步:設定路由

目前我們只在首頁(網址為 localhost:3000/)設定顯示結果,但在同一個通訊埠下,還可以透過路由(route)設定,讓不同頁面各自顯示自己的內容:

舉例來說,現在我們設定三種路由,如果網址是「/」就顯示「首頁」、如果網址是「/anotherPage」就顯示「另一頁」、如果網址不是上述兩者,就顯示「你要找的網頁並不存在」:

const http = require(“http”);

const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
//設定路由
if (req.url == “/”) {
res.write(“首頁”);
} else if (req.url == “/anotherPage”) {
res.write(“另一頁”);
} else {
res.write(“你要找的網頁並不存在”);
}
});

server.listen(3000, () => {
console.log(“listening on port 3000”);
});

在瀏覽器上,網址為「localhost:3000/」時,畫面如下:

網址切換為「localhost:3000/anotherPage」時,畫面如下:

若網址為其他路由,則畫面如下:

第四步:讀取檔案

上述範例都只有讓伺服器回傳一段文字,但客戶端真正希望拿到的是網頁,是否可以回傳一個完整的 html 檔呢?

可以的,但要讀取 html 檔,得先引入 Node.js 的 fs 模組。現假設要讀取的檔名為 index.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML Page</title>
</head>
<body>
<h1>This is HTML page</h1>
</body>
</html>

JavaScript 程式碼範例如下:

const http = require(“http”);
//讀取file system模組
const fs = require(“fs”);

const server = http.createServer((req, res) => {
res.writeHead(200, { “Content-Type”: “text/html; charset=utf-8” });
//改成使用readFile方法,讀取並回傳index.html,用data代表index.html
fs.readFile(“index.html”, (e, data) => {
//處理有錯誤的狀況,處理完執行res.end()
if (e) {
res.write(“error occurred when reading index.html”);
res.end();
} else {
//處理有成功拿到資料的狀況,寫入data資料,就可以顯示index.html內容
res.write(data);
res.end();
}
});
});

server.listen(3000, () => {
console.log(“listening on port 3000”);
});

如果網址正確,瀏覽器就可以顯示 index.html:

如果讀取檔案時發生問題,即會顯示 if 條件判斷式設定的錯誤訊息:

--

--

拉爾夫的技術隨筆

我是北國老虎 | Ralph 拉爾夫🐯​,這個部落個不聊芬蘭、也不聊生活,而是滿滿的技術分享,軟性主題文章請參考 @ralphhong5465