Node.js 系列 - 搭建靜態資源服務器

做爲還在漫漫前端學習路上的一位自學者。我以學習分享的方式來整理本身對於知識的理解,同時也但願可以給你們做爲一份參考。但願可以和你們共同進步,若有任何紕漏的話,但願你們多多指正。感謝萬分!css


在上一章, 咱們搭建了一個很是簡單的 "Hello World" 服務器. 在這一章裏, 咱們要繼續上一章所學的知識, 進一步嘗試搭建, 提供靜態資源的服務器.html

什麼是靜態資源服務器?

那先說什麼是 靜態資源, 它指的是不會被服務器的動態運行所改變或者生成的文件. 它最初在服務器運行以前是什麼樣子, 到服務器結束運行時, 它仍是那個樣子. 好比平時寫的 js, css, html 文件, 均可以算是靜態資源. 那麼很容易理解, 靜態資源服務器的功能就是向客戶端提供靜態資源.前端

話很少說, 開始寫代碼:node

首先咱們知道, 它先是一個 "服務器". 那根據上一章的所學, 咱們要先用 http 模塊建立一個 HTTP 服務器.npm

var http = require('http');

var server = http.createServer(function(req, res) {
    // 業務邏輯, 等會兒再寫.
});

server.listen(3000, function() {
    console.log("靜態資源服務器運行中.");
    console.log("正在監聽 3000 端口:")
})
複製代碼

url 模塊

url 模塊 - 文檔json

有了 HTTP 服務器以後, 咱們就能夠獲取從客戶端發過來的 HTTP 請求了.api

請求報文中包含着請求 URL. 前文說過, URL 用於定位網絡上的資源. 客戶端經過 URL 來指明想要的服務器上資源. 那麼服務器爲了搞清楚客戶端到底想要什麼, 咱們須要處理和解析 URL. 在 Node.js 中, 咱們使用 url 模塊來完成這類操做.瀏覽器

咱們知道 URL 字符串是具備結構的字符串,包含多個意義不一樣的組成部分。 經過 url.parse() 函數, URL 字符串能夠被解析爲一個 URL 對象,其屬性對應於字符串的各組成部分。以下圖所示.bash

Screen Shot 2018-10-05 at 2.18.56 AM


那麼回到咱們的靜態文件服務器代碼.:服務器

先在 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 模塊

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 模塊

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. 個人效果以下:

Screen Shot 2018-10-06 at 2.22.08 PM

設置 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 試試吧!

Screen_Shot_2018-10-07_at_11_12_19_PM

能夠看到如今 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 目錄, 用於存放模塊. 如下是我目前的項目結構.

Screen Shot 2018-10-08 at 3.55.58 PM

封裝好了模塊以後, 咱們就能夠刪去服務器代碼裏那段讀取文件的代碼了, 直接引用模塊就好了. 如下是我修改後的 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 謝謝各位的支持❗️

相關文章
相關標籤/搜索