作為還在漫漫前端學習路上的一位自學者。我以學習分享的方式來整理自己對於知識的理解,同時也希望能夠給大家作為一份參考。希望能夠和大家共同進步,如有任何紕漏的話,希望大家多多指正。感謝萬分!
在上一章, 我們搭建了一個非常簡單的 "Hello World" 伺服器. 在這一章裡, 我們要繼續上一章所學的知識, 進一步嘗試搭建, 提供靜態資源的伺服器.
什麼是靜態資源伺服器?
那先說什麼是 靜態資源, 它指的是不會被伺服器的動態執行所改變或者生成的檔案. 它最初在伺服器執行之前是什麼樣子, 到伺服器結束執行時, 它還是那個樣子. 比如平時寫的 js
, css
, html
檔案, 都可以算是靜態資源. 那麼很容易理解, 靜態資源伺服器的功能就是向客戶端提供靜態資源.
話不多說, 開始寫程式碼:
首先我們知道, 它先是一個 "伺服器". 那根據上一章的所學, 我們要先用 http
模組建立一個 HTTP 伺服器.
var http = require('http');
var server = http.createServer(function(req, res) {
// 業務邏輯, 等會兒再寫.
});
server.listen(3000, function() {
console.log("靜態資源伺服器執行中.");
console.log("正在監聽 3000 埠:")
})
複製程式碼
url 模組
有了 HTTP 伺服器之後, 我們就可以獲取從客戶端發過來的 HTTP 請求了.
請求報文中包含著請求 URL. 前文說過, URL 用於定位網路上的資源. 客戶端通過 URL 來指明想要的伺服器上資源. 那麼伺服器為了搞清楚客戶端到底想要什麼, 我們需要處理和解析 URL. 在 Node.js 中, 我們使用 url
模組來完成這類操作.
我們知道 URL 字串是具有結構的字串,包含多個意義不同的組成部分。 通過 url.parse()
函式, URL 字串可以被解析為一個 URL 物件,其屬性對應於字串的各組成部分。如下圖所示.
那麼回到我們的靜態檔案伺服器程式碼.:
先在 http.createServer
函式被呼叫之前, 引入 url
模組:
var url = require('url');
複製程式碼
然後在 HTTP 伺服器裡解析請求 URL. 客戶端發來的請求 URL 作為屬性存放在 http.createServer
的回撥函式引數所接收的請求物件裡, 屬性名為 url
.
var server = http.createServer(function(req, res) {
var urlObj = url.parse(req.url);
});
複製程式碼
path 模組
接下來從解析後的 URL 物件 urlObj
裡取得請求 URL 中的路徑名(pathname). 路徑名儲存在 pathname
屬性裡.
var server = http.createServer(function(req, res) {
var urlObj = url.parse(req.url);
var urlPathname = urlObj.pathname;
});
複製程式碼
但是光有 URL 物件裡面的路徑名是不夠的. 我們還需要獲得目標檔案在伺服器中所在目錄的目錄名(dirname).
假如說我們的專案結構是下面這樣的:
.
├── public
│ ├── index.css
│ └── index.html
└── server.js
複製程式碼
我們的伺服器程式碼寫在 server.js
檔案裡. 客戶端想要請求儲存在 public
目錄裡的 index.html
檔案. 使用者在瀏覽器中輸入 URL 的時候, 他只知道他想要的檔案叫 index.html
, 但這個檔案在 HTTP 伺服器所在的裝置中的 『 絕對位置 』是不被知道的. 所以我們需要讓 HTTP 伺服器自己去處理這部分操作.
在這裡就需要使用 Node.js 自帶的 path
模組. 其提供了一些工具函式,用於處理檔案與目錄的路徑.
使用起來很簡單, 首先還是在 http.createServer
函式被呼叫之前, 引入 path
模組:
var path = require('path');
複製程式碼
之後我們用 path.join
這個方法來把 目標檔案所在目錄的目錄名和請求 URL 中的路徑名合併起來. 在這個例子中, 客戶端可以訪問的靜態檔案全部在 public
這個目錄中, 而 public
目錄又在 server.js
檔案所在的目錄中. server.js
中儲存的是我們的伺服器程式碼.
想要獲得 server.js
所在目錄的在整個裝置中的絕對路徑, 我們可以在伺服器程式碼中呼叫變數 __dirname
, 它是當前檔案在被模組包裝器包裝時傳入的變數, 儲存了當前模組的目錄名。
var server = http.createServer(function(req, res) {
var urlObj = url.parse(req.url);
var urlPathname = urlObj.pathname;
var filePathname = path.join(__dirname, "/public", urlPathname);
});
複製程式碼
如果你想的話, 你可以用 console.log(filePathname)
來看看伺服器執行後, 從客戶端收到的請求 URL 會被轉換成什麼樣.
fs 模組
現在來到了最重要的一步, 讀取目標檔案, 並且返回檔案給客戶端.
我們需要用 Node.js 自帶的 fs
模組中的 fs.write
方法來實現這一步. 該方法第一個引數為目標檔案的路徑, 最後一個引數為一個回撥函式, 回撥有兩個引數 (err, data),其中 data
是檔案的內容, 如果發生錯誤的話 err
儲存錯誤資訊. fs.write
方法可以在第二個引數中指定字元編碼, 如果未指定則返回原始的 buffer. 在這個例子中, 我們不考慮這一項.
那麼具體程式碼如下:
首先引入 fs
模組, 我就不贅述了, 參照前面就可以了. 下面是讀取檔案的程式碼.
var server = http.createServer(function(req, res) {
var urlObj = url.parse(req.url);
var urlPathname = urlObj.pathname;
var filePathname = path.join(__dirname, "/public", urlPathname);
fs.readFile(filePathname, (err, data) => {
// 如果有問題返回 404
if (err) {
res.writeHead(404);
res.write("404 - File is not found!");
res.end();
// 沒問題返回檔案內容
} else {
res.writeHead(200);
res.write(data);
res.end();
}
})
});
複製程式碼
現在我們就實現了一個基本的『 靜態檔案伺服器 』可以在允許客戶端請求儲存在伺服器中公開的靜態檔案了. 你可以嘗試啟動伺服器, 然後讓瀏覽器中訪問 http://localhost:3000/index.html
. 我的效果如下:
設定 MIME 型別
MIME 文件 - MDN Content-Type 文件 - MDN
多用途 Internet 郵件擴充套件(MIME)型別是用一種標準化的方式來表示文件的 "性質" 和 "格式"。 簡單說, 瀏覽器通過 MIME 型別來確定如何處理文件. 因此在響應物件的頭部設定正確 MIME 型別是非常重要的.
MIME 的組成結構非常簡單: 由型別與子型別兩個字串中間用 '/'
分隔而組成, 其中沒有空格. MIME 型別對大小寫不敏感,但是傳統寫法都是小寫.
例如:
text/plain
: 是文字檔案預設值。意思是 未知的文字檔案 ,瀏覽器認為是可以直接展示的.text/html
: 是所有的HTML內容都應該使用這種型別.image/png
: 是 PNG 格式圖片的 MIME 型別.
在伺服器中, 我們通過設定 Content-Type
這個響應頭部的值, 來指示響應回去的資源的 MIME 型別. 在 Node.js 中, 可以很方便的用響應物件的 writeHead
方法來設定響應狀態碼和響應頭部.
假如我們要響應給客戶端一個 HTML 檔案, 那麼我們應該使用下面這條程式碼:
res.writeHead(200, {"Content-Type":"text/html"});
複製程式碼
你會發現我在上面的靜態資源伺服器的程式碼中, 沒有設定響應資源的 MIME 型別. 但如果你試著執行伺服器的話, 你會發現靜態資源也以正確方式被展示到了瀏覽器.
之所以會這樣的原因是在缺失 MIME 型別或客戶端認為檔案設定了錯誤的 MIME 型別時,瀏覽器可能會通過檢視資源來進行猜測 MIME 型別, 叫做 『 MIME 嗅探 』. 不同的瀏覽器在不同的情況下可能會執行不同的操作。所以為了保證資源在每一個瀏覽器下的行為一致性, 我們需要手動設定 MIME 型別.
那麼首先我們需要獲取到準備響應給客戶端的檔案的 字尾名.
要做到這一步我們需要使用 path
模組的 parse
方法. 這個方法可以將一段路徑解析成一個物件, 其中的屬性對應路徑的各個部位.
繼續再剛才靜態檔案伺服器案例的程式碼上新增:
var server = http.createServer(function(req, res) {
var urlObj = url.parse(req.url);
var urlPathname = urlObj.pathname;
var filePathname = path.join(__dirname, "/public", urlPathname);
// 解析後物件的 ext 屬性中儲存著目標檔案的字尾名
var ext = path.parse(urlPathname).ext;
// 讀取檔案的程式碼...
});
複製程式碼
獲取了檔案字尾之後, 我們需要查詢其對應的 MIME 型別了. 這一步可以很輕鬆的使用第三方模組 MIME 來實現. 你可以自行去 NPM 上去查閱它的使用文件.
對於我們目前的需求來說, 只需要用到 MIME 模組的 getType()
方法. 這個方法接收一個字串引數 (字尾名), 返回其對應的 MIME 型別, 如果沒有就返回 null
.
使用的話, 首先要用 npm 安裝 MIME 模組 ( 如果你還沒建立 package.json 檔案的話, 別忘了先執行 npm init
)
npm install mime --save
複製程式碼
安裝完畢. 引入模組到伺服器程式碼中, 然後我們就直接用剛剛獲得的字尾去找到其對應的 MIME 型別
var mime = require('mime');
var server = http.createServer(function(req, res) {
var urlObj = url.parse(req.url);
var urlPathname = urlObj.pathname;
var filePathname = path.join(__dirname, "/public", urlPathname);
// 解析後物件的 ext 屬性中儲存著目標檔案的字尾名
var ext = path.parse(urlPathname).ext;
// 獲取字尾對應的 MIME 型別
var mimeType = mime.getType(ext);
// 讀取檔案的程式碼...
});
複製程式碼
好了, 現在最重要的東西 MIME 型別我們已經得到了. 接下來只要在響應物件的 writeHead
方法裡設定好 Content-Type
就行了.
var server = http.createServer(function(req, res) {
// 程式碼省略...
var mimeType = mime.getType(ext);
fs.readFile(filePathname, (err, data) => {
// 如果有問題返回 404
if (err) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.write("404 - File is not found!");
res.end();
// 沒問題返回檔案內容
} else {
// 設定好響應頭
res.writeHead(200, { "Content-Type": mimeType });
res.write(data);
res.end();
}
})
});
複製程式碼
階段性勝利 ✌️ 現在執行伺服器, 在瀏覽器裡訪問一下 localhost:3000/index.html
試試吧!
可以看到現在 Content-Type
已經被正確設定了!
重構程式碼
現在來看看你的程式碼, 是不是開始感覺有點亂糟糟的. 我想聰明的你已經發現, 整個靜態檔案伺服器的程式碼就是在做一件事: 響應回客戶端想要的靜態檔案. 這段程式碼職責單一, 且複用頻率很高. 那麼我們有理由將其封裝成一個模組.
具體的過程我就不贅述了. 以下是我的模組程式碼:
// readStaticFile.js
// 引入依賴的模組
var path = require('path');
var fs = require('fs');
var mime = require('mime');
function readStaticFile(res, filePathname) {
var ext = path.parse(filePathname).ext;
var mimeType = mime.getType(ext);
// 判斷路徑是否有字尾, 有的話則說明客戶端要請求的是一個檔案
if (ext) {
// 根據傳入的目標檔案路徑來讀取對應檔案
fs.readFile(filePathname, (err, data) => {
// 錯誤處理
if (err) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.write("404 - NOT FOUND");
res.end();
} else {
res.writeHead(200, { "Content-Type": mimeType });
res.write(data);
res.end();
}
});
// 返回 false 表示, 客戶端想要的 是 靜態檔案
return true;
} else {
// 返回 false 表示, 客戶端想要的 不是 靜態檔案
return false;
}
}
// 匯出函式
module.exports = readStaticFile;
複製程式碼
用於讀取靜態檔案的模組 readStaticFile
封裝好了之後. 我們可以在專案目錄裡新建一個 modules 目錄, 用於存放模組. 以下是我目前的專案結構.
封裝好了模組之後, 我們就可以刪去伺服器程式碼裡那段讀取檔案的程式碼了, 直接引用模組就行了. 以下是我修改後的 server.js 程式碼:
// server.js
// 引入相關模組
var http = require('http');
var url = require('url');
var path = require('path');
var readStaticFile = require('./modules/readStaticFile');
// 搭建 HTTP 伺服器
var server = http.createServer(function(req, res) {
var urlObj = url.parse(req.url);
var urlPathname = urlObj.pathname;
var filePathname = path.join(__dirname, "/public", urlPathname);
// 讀取靜態檔案
readStaticFile(res, filePathname);
});
// 在 3000 埠監聽請求
server.listen(3000, function() {
console.log("伺服器執行中.");
console.log("正在監聽 3000 埠:")
})
複製程式碼
? 好啦,今天的分享就告一段落啦。下一篇中,我會介紹 "如何搭建伺服器路由" 和 "處理瀏覽器表單提交"
傳送門 - Node.js 系列 - 搭建路由 & 處理表單提交
如果喜歡的話就點個關注吧!O(∩_∩)O 謝謝各位的支援❗️