前言: 本篇文章主要是想讲解 .html 文件和 .CSS 文件在实际开发中和后端服务器交互最后上线的基础原理。
面向的人群🆕:是刚入行不久,且目前只会写前端业务代码而不清楚整个工作流的前端新人。我会从 0 开始一步一步带你理解整个流程的底层逻辑是什么,希望你能跟着我一起做完今天的所有步骤。
一. 前期准备
-
为了能让更多的人明白这其中的原理,今天我们回归前端最原始的本质,抛开 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
对创建服务器的流程做了最大化的精简,来确保读者能够适应服务这个概念。在实际开发中,公司真正的后端服务开发绝不是这么简单。