前言: 本篇文章主要是想講解 .html 文件和 .CSS 文件在實際開發中和後端服務器交互最後上線的基礎原理。
面向的人羣🆕:是剛入行不久,且目前只會寫前端業務代碼而不清楚整個工作流的前端新人。我會從 0 開始一步一步帶你理解整個流程的底層邏輯是什麼,希望你能跟着我一起做完今天的所有步驟。
<hr/>
一. 前期準備
- 為了能讓更多的人明白這其中的原理,今天我們迴歸前端最原始的本質,拋開 Vue 或 React 這些前端框架,只用最原始的 .js 、.css 文件開始今天的講解。
- 你的電腦需要安裝 node,因為會用到一些文件讀寫的操作。
- 創建一個文件夾,然後創建出下面兩個文件,一個
index.html文件和server.js文件。
-
index.html文件如下,你也可以自己寫喜歡的內容,但是如果懶得寫,請 copy 我的代碼<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <h1>JavaScript is the best language in the world!</h1> </body> </html>- 我相信每個前端開發的讀者都,或多或少安裝過下面這個插件
Live Server。
實現的效果是將你編寫的.html文件直接在瀏覽器打開,可以幫我們極大提升開發體驗,實現的效果如下圖:
- 我相信每個前端開發的讀者都,或多或少安裝過下面這個插件
- 看起來很神奇對吧?其實
Live Server實現的功能和我們今天要講解的知識非常接近,讓我們暫時先關閉它,然後準備實現一個自己的簡易版Live Server。
二. 什麼是服務器
- 在此之前,我覺得很有必要先解釋一下服務器這個概念,因為這個概念之前困擾我很久。
- 對於前端來講,因為不像後端開發者一樣需要日常跟服務器打交道,所以可能會覺得服務器是一個很高大上的東西,是一個我們前端開發人員觸不可及的東西。包括我剛入行的時候,我也是這樣想的,但隨着工作閲歷的不斷增加,發現服務器是一個再普通不過的東西罷了。
-
我們從現實生活類比,服務器就好比一家超市。
- 你去超市的目的是什麼?
- 答:購買日用品 ,那麼此時你就是消費者,那麼超市就是給你提供服務的地方對吧?在這種場景下,你充當的其實就是 客户端(client) 的角色,而超市就充當着服務器的角色。服務器並不一定只能提供單一的服務,就像超市提供了很多類型的櫃枱,有日用品,有肉類,有水果,這些不同的櫃枱充當着給你提供服務的角色,而超市是容納這些服務運行的地方,這就是服務器和服務之間的關係。
- 你還可以自己再類比所有日常生活中需要你花錢消費的地方。如飯店、賓館、花店、網吧等等。它們都是為了滿足你的需求,來為你提供這些需求的場所,它們都充當着 “服務器” 的角色。
- 讓我們回到網絡上面來,你完全可以把 “服務器” 和上面你類比現實世界所理解到的概念畫等號。比如你今天想看視頻,我們拿 B站舉例, 那麼B站 此時就是一個服務器,當你在地址欄輸入
bilibili後,你就相當於走進了 "視頻分類" 櫃枱 ,它上面存放着各種各樣的視頻,B站 它現在提供了觀看視頻的一項服務給你。
- 然後你看視頻看累了,想看一看漫畫,此時就變成了它提供 “漫畫” 服務給你。
- 還是有點抽象?讓我們回到
Live Server。剛剛我們關閉了Live Server,所以導致我們在瀏覽器裏輸入localhost:5500這個地址後,頁面出現了訪問錯誤。
- 但是當我們打開
Live Server的時候,我們發現頁面又恢復了正常工作。
-
那麼上面的步驟,我們就可以這樣理解:
- 當
Live Server運行的時候,我們的電腦給了我們一個可以使用瀏覽器訪問本地.html文件的服務。這個服務的提供者就是Live Server這個程序。那麼誰是服務器呢?沒錯,就是幫你運行Live Server的主機-----你的電腦。 - 當你關閉
Live Server的時候,相當於你的電腦關閉了提供服務的程序,導致你失去了訪問.html的能力。
- 當
- 通過上面的例子,不難發現,其實服務本質上是一段代碼,而運行這段代碼的容器被我們叫做了服務器。
- 這裏有一個十分重要的概念----端口號。也就是
Live Server啓動的 5500 這個數字。這個數字你可以暫時把它理解為,你的電腦(也就是服務器)給它找了一個唯一的服務窗口,這個窗口號是 5500。(想象一個政務大廳,裏面不同的業務都開設在不同的窗口位置,即使此時沒人訪問,它也需要有工作人員坐在那裏等待。)
-
那麼上面整個過程可以這樣理解:
- 😣 Live Server: “好煩,週一又要上班了。喂,服務器(你的電腦),5500 這個窗口還沒人值班吧?沒人的話我就先坐這了。”
- 🧑🏫 你的電腦:“我看一下啊,哦,暫時沒人用。那你坐這裏吧,即使沒人來,你也不能跑出去啊!”
- 聰明的你也許會想到,那如果 5500 窗口如果有人先佔了怎麼辦?注意,這裏服務器(你的電腦)會自動安排你的程序到另外的端口號上提供服務(執行代碼)。(如果你的程序中也支持這麼做的話,不妨打開兩個
vscode自己嘗試一下同時開啓live Server看看會是什麼效果。)
三. 編寫 server.js 文件
- 既然我們已經知道了,服務其實就是一段代碼。那麼我們就可以根據需求,去編寫出這樣的代碼。其實我們的需求很簡單,就是想讓我的瀏覽器可以正常渲染我的
.html文件。 - 這裏我們需要從
node中引入http模塊,這個模塊封裝了一些可以讓我們快速編寫http服務器的方法。
-
注意:這裏我説了編寫 “http 服務” 這個概念,結合我們上面對服務器的理解。這句話的完整含義應該是:
node 提供的 http 模塊,讓我們可以快速在電腦上,編寫一段代碼程序。當我們的電腦運行這段程序的時候,我們的電腦可以提供 http 這樣一項服務,此時瀏覽器可以通過使用 http 協議來和這個 http 服務程序進行通信。
- 然後我們調用
http模塊提供的createServer方法,具體用法在註釋中寫的很清楚了,不過多贅述。
- 現在的你已經創建出了一個服務實例,它雖然還沒有任何功能,但你已經可以告訴你的電腦,它現在可以被當作一個服務程序啓動了。那麼此時你還需要告訴電腦你想在那個窗口(端口) 去提供服務,這裏我隨便寫了一個 7777,你可以選擇一個任意你喜歡的數字。(注意,有些端口號是操作系統獨享的,你不能佔用,最好使用 5000-65535 範圍內的數字) 然後使用
http_server.listen()方法去向電腦申請這個端口號。
- 讓我們在
http_server的回調函數中,打印一些數據,來看看我們的服務是正常啓動了。
- 讓我們在終端用
node運行這個文件,你可以在控制枱看到你的這段代碼已經被你的電腦成功啓動了。
- 可以看到,隨着我用瀏覽器去訪問這個在窗口 7777 提供的服務,我們回調函數監聽到請求後成功打印了相對應的輸出。
- 但是此時我們的瀏覽器好像呆呆的,沒有展示任何信息。這是因為你這項服務現在還不夠到位,你沒有返回給瀏覽器任何信息。此時我們需要調去
response身上的end函數。response.end()。這個函數第一個參數是你要告訴瀏覽器的數據,第二個參數也是一個回調函數,會在你返回給瀏覽器消息後被調用。那麼我們就可以這樣寫:
(這裏別忘了需要ctrl c,然後重新執行這個文件)
- 你會看到雖然我們的服務成功打印了相應的輸出,但是我們瀏覽器顯示的卻是亂碼。
- 這是因為你沒告訴瀏覽器應該用什麼格式去渲染這段數據,你可能會有疑問,瀏覽器這麼笨嗎?默認為
utf-8不就行了?如果你能聯想到這裏,不得不給你點個贊👍,但是假如這段數據是圖片、視頻呢,那不就亂套了嗎?這裏不賣關子,解決方法很簡單,就是我們的服務在返回數據之前,告訴瀏覽器該如何展示我們的內容,怎麼告訴?調用response.writeHeader()設置相對應的Content-type即可。
現在的顯示效果就符合我們的預期啦!
四. 讀取 html 文件
- 這裏涉及到 node 的一些知識,不過不是本篇文章的重點,故不會做過多解釋。
- 這裏有兩個重點,第一個就是引入
fs文件系統模塊,它提供了一個方法叫做readFileSync,這個函數是同步讀取指定路徑下的文件,默認返回值為buffer類型。
- 當拿到這個
data後,我們就可以返回給瀏覽器這個數據。此時你的瀏覽器應該已經正確渲染出這些內容了。
五. CSS 文件生效的原理
- 讓我們在跟文件夾下生成一個
global.css的文件。
- 別忘了我們最初是如何引入
css在index.html的。
- 這裏有一個關鍵的知識點需要了解,我們打開
localhost:7777,其實是會向我們的服務發起三個請求的。其中,發送index.html是我們的主動行為,favicon.ico這個請求是瀏覽器的默認行為,global.css是由於我們的index.html攜帶了\<link/>標籤,從而引起瀏覽器附帶請求導致的。
- 讓我們打印一下
request的url參數信息,這裏包含了瀏覽器請求資源的地址。
它對應了瀏覽器request字段的信息。
- 刷新一下瀏覽器,你會看到控制枱有以下三個輸出,和我們上面的推測是符合的。注意,這裏的根路徑
/路徑之後會被我們替換為index.html。
- 聰明的你可能已經發現了,我們瀏覽器其實已經請求了
global.css但是樣式好像沒有正確的生效。那是因為.css文件沒有設置正確的mime格式。被瀏覽器當成普通的文件格式處理了。
-
這裏我們就需要為
index.html和.css分別設置不同的content-type來讓css文件生效。此時你的http_server的代碼應該如下。const http_server = http.createServer((request, response) => { let file_path = ""; //1. 這裏存放文件的真實路徑 let data = ""; //2. 這裏準備存放文件的 buffer 數據 let ext = ""; //3. 這裏準存放文件的後綴名稱 if (request.url === "/") { //4. 如果請求路徑是跟路徑,那麼替換為 index.html file_path = "index.html"; } else { file_path = request.url.replace("/", ""); //5. 否則的話,去掉路徑前面的斜槓 '\' } data = fs.readFileSync(file_path); }- 這裏最關鍵的後綴名如何獲取呢?我們需要引入另一個模塊
path。我們利用path.extname方法,將切割好的file_path傳遞為參數即可獲取到正確的文件後綴名。
- 這裏最關鍵的後綴名如何獲取呢?我們需要引入另一個模塊
- 之後為每次請求設置正確的類型即可。具體文件類型
mime和content-type的映射關係請參照:MDN提供的 MIME 對照表。
- 此時我們可以看到,樣式已經正確生效。
六. 80 端口的含義
- 想必大家都知道
http服務是跑在80端口這一前端常識的吧?其實它沒什麼特別的,它只不過是把端口申請在服務器的80窗口上而已,然後我們配合瀏覽器的默認行為---當沒有指定明確端口號時,幫你自動填寫為 80 端口。 - 我們來試驗一下。
注意,此時我沒有像之前一樣輸入7777,但是瀏覽器卻依然正確找到了我http-服務的位置,和我們對瀏覽器默認行為的猜想一致。
- 所以不要再死記硬背
80和443這兩個數字了,它們只不過是你的後端搭檔在代碼程序里根據業務不同而寫下的一個普通數字罷了。 - 為什麼要這樣做?如果每個
http-server開發者,大家都用不同的端口號。那麼你就需要不僅僅需要把它們的域名記下來,還要記住相對應的端口號。就像上面一樣,你不覺得每次手動輸入7777很麻煩嗎?那麼幹脆大家和瀏覽器商量好,就用80這個端口,瀏覽器默認幫你填寫就好了。
七. 源碼
這裏故意屏蔽了 favicon.ico 的請求,和文章整體內容關係不大。
const http = require("node:http"); //從 node 中引入 http 模塊
const fs = require("node:fs"); //引入 fs 模塊
const path = require("node:path");
// 這個函數接收一個回調函數
// 1.第一個函數接收的是前端傳遞過來的 request 參數
// 2.第二個函數是要返回給瀏覽器的信息
const http_server = http.createServer((request, response) => {
let file_path = ""; //1. 這裏存放文件的真實路徑
let data = ""; //2. 這裏準備存放文件的 buffer 數據
let ext = ""; //3. 這裏準存放文件的後綴名稱
if (request.url === "/") {
//4. 如果請求路徑是跟路徑,那麼替換為 index.html
file_path = "index.html";
} else {
file_path = request.url.replace("/", ""); //5. 否則的話,去掉路徑前面的斜槓 '\'
}
if (file_path !== "favicon.ico") {
response.writeHeader = `Content-type:text/${ext}`;
data = fs.readFileSync(file_path);
ext = path.extname(file_path);
}
response.end(data);
});
// 告訴你的電腦,你想用 7777 這個端口
http_server.listen("7777", () => {
console.log("我提供的服務在 7777 窗口");
});
八. 總結
- 首先我們要對服務器有清晰的認知,任何一個設備都可以當作一個服務器,你的手機,你的筆記本,你的台式機,一個大型的存儲計算機,都可以被叫做服務器。
- 所謂的服務就是跑在服務器上的一段普通代碼程序而已。當代碼運行起來後,服務器需要為這個服務分配一個唯一端口號,其它應用可以訪問這個端口來接受你提供的服務。
- 有些服務並不是要公開為別人使用的,查看你的任務管理器或活動監視器。你的電腦開啓了這麼多服務,它們佔用着不同的端口,而這些服務有的是隻為操作系統提供的,並不對普通用户提供任何服務。
- 我們的前端代碼,不管是
.vue、.tsx、.ts等等文件,最後都會被打包為原始的html和css和js文件,因為瀏覽器只認識這些內容。(不信你去看看有content-type: vue這種mime類型嗎?)然後後端會部署一個 http-server 程序,讓它跑在一個專屬服務器上執行這段程序,通過我們上面講解的內容來傳遞給80或者其他任何端口上,等待別人訪問。 - 你在實際開發中使用的
npm run dev後,你的前端代碼呈現在瀏覽器上,其底層的原理和上面無異,不過是開發工具幫你將上面步驟封裝的功能更加完善和便捷而已。
- 本文中對於
server.js對創建服務器的流程做了最大化的精簡,來確保讀者能夠適應服務這個概念。在實際開發中,公司真正的後端服務開發絕不是這麼簡單。