我们来创建一个简单的 vite 开发服务器,来了解服务器的原理。
启动服务,开启监听
通过下面的命令创建一个项目:vite-dev-server
。
bash
mkdir vite-dev-server
cd vite-dev-server
yarn init -y # -y 的作用是:初始化 yarn 时,全部使用默认配置
添加 koa,koa 是 Express 的下一代基于 Node.js 的 web 框架。
bash
yarn add koa
创建 index.js,使用 koa 监听 5173 端口。
javascripty
const Koa = require("koa");
const app = new Koa();
app.listen(5173, () => {
console.log("vite dev server listen on 5173");
});
注意: index.js 是通过 node 执行的,所以导入模块时,必须使用 common js 规范,而不能使用 es modules 的规范。
使用 node 运行 index.js
bash
node index.js
此时使用浏览器访问:http://localhost:5173
,将会返回 Not Found。
响应 html 文件
对于任何请求,app 将调用 app.use
注入的异步函数处理请求:
javascript
const Koa = require("koa");
const app = new Koa();
// 当请求来临的时候,会执行 use 注入的回调中
app.use(async (ctx) => {
console.log(ctx.request);
console.log(ctx.response);
});
app.listen(5173, () => {
console.log("vite dev server listen on 5173");
});
此时,我们就可以获取到请求和回复内容:
ruby
// request
{
method: 'GET',
url: '/',
header: {
host: 'localhost:5173',
connection: 'keep-alive',
'cache-control': 'max-age=0',
'sec-ch-ua': '"Google Chrome";v="119", "Chromium";v="119", "Not?A_Brand";v="24"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'sec-fetch-site': 'none',
'sec-fetch-mode': 'navigate',
'sec-fetch-user': '?1',
'sec-fetch-dest': 'document',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
cookie: 'Webstorm-32b69a0d=a5b6a987-8d4c-4c91-bfc5-9d9f4e2f897f'
}
}
// response
{
status: 404,
message: 'Not Found',
header: [Object: null prototype] {},
body: undefined
}
node 服务最频繁做的事情就是在处理请求和操作文件。在处理文件和路径时,node 会使用到 fs
和 path
模块。
注意: 在 node 环境下,不需要通过
yarn add
或者npm install
安装fs
和path
模块。这两个模块是 node 来提供的。不同的 js 宿主环境,会赋予 js 不同的能力,比如:
- 在浏览器环境中,浏览器将提供的特殊能力注入到 window 下面的。然后,我们就可以使用这些特殊的能力,例如:通过
document.getElementById(id 名)
来获取指定 ID 的元素。- 在 node 的执行环境中,在遇到导入的模块是 node 提供的模块时,就会在 node 模块中查找。node 模块中没有的时候,才会从 node_modules 中查找。
在处理请求获取根路径的信息时,一般返回一个 html 页面作为响应。创建一个 index.html 文件,并设置下面的内容。
html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Vite dev server</title>
</head>
<body>
hello vite dev server
</body>
</html>
在响应的回调函数中,通过 ctx.request.url
来获取请求的路径,通过设置 ctx.response.body 来设置响应体,通过"Content-Type"
来设置响应的格式,从而影响到接收方以怎样的方式来解析接收到的内容:
ini
const Koa = require("koa");
const fs = require("fs");
const path = require("path");
const app = new Koa();
app.use(async (ctx) => {
console.log("ctx:", ctx.request, ctx.response);
if (ctx.request.url === "/") {
const indexContent = await fs.promises.readFile(path.resolve(__dirname, "./index.html")); // 在服务端一般不这么写,而是使用文件流的方式
// 设置响应体发给请求资源的对象
ctx.response.body = indexContent;
// 设置响应格式
ctx.response.set("Content-Type", "text/html");
}
});
app.listen(5173, () => {
console.log("vite dev server listen on 5173");
});
此时,在浏览器访问:http://localhost:5173
,将会接收到我们返回的 html 的内容,并正确的解析。
响应 vue 文件
假如浏览器请求了一个 vue 文件,vite 会对 vue 文件做 AST 语法分析,通过 createElement() 来构建原生的 dom 等一系列操作,最终生成原生的 javascripty 内容,然后返回给浏览器。简单的说,vite 服务器会对 vue 文件中的字符串内容做一个字符串替换,例如会对 <template>
标签进行字符串替换,生成原生的 javascript 内容。
所以在请求 vue 文件时,并不是直接把读取到的内容返回给浏览器,而是将生成的原生 JavaScript 作为返回内容。此时浏览器在遇到 .vue
后缀的文件后,依然无法解析。这时,就可以通过设置 "Content-Type"
来告诉浏览器,以 JavaScript 文件来解析。
这就是为什么浏览器会处理 .vue 格式的文件了,因为:
- 浏览器接收到的是处理之后的原始 JavaScript 内容,并不是将原始的 vue 文件中的内容直接返回给了浏览器;
- 服务器通过设置
"Content-Type"
告诉浏览器以 JavaScript 文件来解析文件内容。
例如,在 index.html 中,添加一个引用 ./main.js 文件:
xml
<!DOCTYPE html>
<html lang="en">
<head>
<title>Vite dev server</title>
</head>
<body>
hello vite dev server
<script type="module" src="./main.js"></script>
</body>
</html>
main.js 内容如下
arduino
import "./App.vue"
console.log("main 123");
App.vue 文件的内容如下:
arduino
console.log("app vue 123");
index.js 中,添加处理获取 main.js 和 App.vue 的请求:
javascript
const Koa = require("koa");
const fs = require("fs");
const path = require("path");
const app = new Koa();
app.use(async (ctx) => {
if (ctx.request.url === "/") {
const indexContent = await fs.promises.readFile(path.resolve(__dirname, "./index.html")); // 在服务端一般不这么写,而是使用文件流的方式
ctx.response.body = indexContent;
ctx.response.set("Content-Type", "text/html");
}
if (ctx.request.url === "/main.js") {
const mainContent = await fs.promises.readFile(path.resolve(__dirname, "./main.js"));
ctx.response.body = mainContent;
ctx.response.set("Content-Type", "text/javascript");
}
if (ctx.request.url === "/App.vue") {
const appContent = await fs.promises.readFile(path.resolve(__dirname, "./App.vue"));
// 省略将 appContent 解析为原生 javascripty 的过程
ctx.response.body = appContent;
ctx.response.set("Content-Type", "text/javascript");
}
});
app.listen(5173, () => {
console.log("vite dev server listen on 5173");
});
此时,浏览器中就可以正确的运行 main.js 和 App.vue 中的内容了。