一、概述
文本通过对比webpack-dev-server,旨在分析为什么需要vite,以及vite通过什么方案解决了该问题。并通过对vite的分析,实现了一个简易的vite代码。
二、webpack-dev-server
1、webpack-dev-server简介、优势
“webpack-dev-server将webpack与提供实时重载的开发服务器一起使用。这应该仅用于开发。它在后台使用了webpack-dev-middleware,它提供对webpack资产的快速内存访问。”webpack-dev-server启动了一个开发环境的服务器,通过以下几点优势,大大提高了前端的开发效率。
(1)前后端分离。之前的前端如果想要开发,需要启动完整的前后端生成环境,这对于纯前端来说往往是比较复杂且没有必要的。通过webpack-dev-server,可以使得前端同学直接使用其他后台同同学的接口,或者启动生成环境的后台(一般是一个脚本命令)就可以完成日常工作。
(2)前端服务器是热加载的前提,热加载是需要对文件有控制监听才能完成的功能,只有前端自己启动了服务器,才能使得热加载变成可能。
(3)多种功能配置,如代理等。
2、webpack-dev-server流程、问题
webpack-dev-server启动服务器需要经过以下流程:
(1)启动服务,监听端口,将所有文件进行打包处理。
(2)请求主页,返回完整的打包文件app.js等。
(3)建立websocket,用以热加载。
(4)更新文件,对所有文件进行分析处理,websocket返回告知前端该文件更新。
(5)前端请求更新文件,返回文件,执行render函数。
从上述流程可以看到,(1)(3)对所有文件进行了分析处理,这是没有必要的,如果项目十分的庞大,这部分工作就会变得非常耗时,这也就是为什么我们启动项目(npm run dev)需要消耗较长时间的原因。而开发人员往往只需要关注当前页面的情况即可,所以这部分耗时就变成没有意义。
三、vite简介、核心流程
vite启动是一个面向现代浏览器的开发环境的服务器,因为现代浏览器基本都支持了原生的ES module的模块加载的方式,所以vite使用该方式让浏览器帮我们处理文件依赖的问题。使得在启动服务器时,无需处理所有的文件依赖,仅仅对当前文件处理即可。以下是vite启动服务器的大致流程:
(1)启动服务器,监听端口(resolveHttpServer)。启动websocket服务器(createWebSocketServer)。
(2)请求主页,通过vite.config.js中的配置项plugins,使用pre-transoform对主页进行预处理,插入client.js,websocket请求文件。执行主页中的main.js。
(3)根据main.js的逻辑,依次请求相应文件。
(4)文件更新,后台通过websocket返回message给前端,前端发送"await import xxx?import&t={query ? &${query} : ''}"重新请求该文件,执行render函数。
四、vite核心逻辑实现
1、代码目录结构
项目目录结构。lib可以理解为node_modules,其中存放第三方库,这里的vite的核心逻辑就放在其中。其他如src、index.html等,都和常见项目的文件结构一致。
---vite-test-code
---lib
---vite
---client
client.js
---node
server.js
wsServer.js
---src
App.vue
main.ts
index.html
package.json
2、项目文件代码
(1)/index.html,直接从vite项目中复制而来,需要注意的是script标签中需要添加type="module"。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="src/main.ts"></script>
</body>
</html>
(2)/src/main.ts,项目主要的逻辑入口。
import App from './App.vue';
document.getElementById("app").innerHTML = App;
(3)/src/App.vue,这里直接返回一个div,渲染在#app中。测试hmr时可以直接更新该文件。
export default `<div >渲染<div>`;
3、vite核心流程实现
(1)/lib/vite/node/server.js,启动服务器的核心逻辑代码文件,具体流程及详细代码如下:
①启动服务器,监听4000端口。建立websocket服务器。
const http = require("http");
const port = 4000;
// 建立websocket服务器
const ws = createWebSocketServer();
let server = http.createServer((req, res) => {
// 解析请求的url
let pathname = getPathname(req.url);
// 获取文件路径
let filePath = getFilePath(pathname);
// 返回文件
readFile(filePath, res, pathname);
});
server.listen(port); //监听端口
console.log('服务成功开启:localhost:4000');
②如果是主页index.html,注入client.js文件。
// 将ws注入
function injectWs(data) {
return `<script src=${clientFile}></script>` + data;
}
③监听文件
function watchFile(url, pathname) {
if (!watchList[url]) {
fs.watchFile(url, {interval: 1000}, () => {
if (ws) {
ws.clients.forEach((client) => {
client.send(JSON.stringify({
type: "update",
path: pathname
}));
})
}
});
watchList[url] = true;
}
}
④返回文件请求文件,这里对.ts和.vue文件进行了content-type处理,让浏览器当做其是一个.js文件。
function readFile(filePath, res, pathname) {
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404, {
'content-type': 'text/plain'
});
res.write('404,not found');
res.end(0);
} else {
const type = path.extname(filePath);
if (type === ".ts" || type === ".vue") {
res.writeHead(200, {
'content-type': 'application/javascript'
});
}
if (filePath.endsWith(indexHtml)) {
// 注入websocket
data = injectWs(data);
}
// 监听文件hmr
watchFile(filePath, pathname);
res.write(data);
res.end(0);
}
});
}
⑤完整代码
// 三方
const http = require("http");
const url = require("url");
const path = require("path");
const fs = require("fs");
// wsServer
const createWebSocketServer = require("./wsServer.js");
// 常量
const port = 4000;
const indexHtml = `index.html`;
const clientFile = "./lib/vite/client/client.js";
let watchList = {};
// 建立websocket服务器
const ws = createWebSocketServer();
let server = http.createServer((req, res) => {
// 解析请求的url
let pathname = getPathname(req.url);
// 获取文件路径
let filePath = getFilePath(pathname);
// 返回文件
readFile(filePath, res, pathname);
});
// 解析url
function getPathname(reqUrl) {
let pathname = url.parse(reqUrl).pathname;
return pathname === `/` ? `index.html` : pathname;
}
function getFilePath(pathname) {
// index.html根目录
return path.join(__dirname, "../../../", pathname)
}
function readFile(filePath, res, pathname) {
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404, {
'content-type': 'text/plain'
});
res.write('404,not found');
res.end(0);
} else {
const type = path.extname(filePath);
if (type === ".ts" || type === ".vue") {
res.writeHead(200, {
'content-type': 'application/javascript'
});
}
if (filePath.endsWith(indexHtml)) {
// 注入websocket
data = injectWs(data);
}
// 监听文件hmr
watchFile(filePath, pathname);
res.write(data);
res.end(0);
}
});
}
function watchFile(url, pathname) {
if (!watchList[url]) {
fs.watchFile(url, {interval: 1000}, () => {
if (ws) {
ws.clients.forEach((client) => {
client.send(JSON.stringify({
type: "update",
path: pathname
}));
})
}
});
watchList[url] = true;
}
}
// 将ws注入
function injectWs(data) {
return `<script src=${clientFile}></script>` + data;
}
server.listen(port); //监听端口
console.log('服务成功开启:localhost:4000');
(2)/lib/vite/node/wsServer.js,建立websocket服务器,监听4002端口
const ws = require("ws");
function createWebSocketServer() {
let WebScoketServer = ws.Server;
let wsServer = new WebScoketServer({port: 4002}) || null;
wsServer.on(`connection`, (ws) => {
ws.send(JSON.stringify({
type: 'connect'
}));
});
return wsServer;
}
module.exports = createWebSocketServer;
(3)/lib/vite/client/client.js,前端请求websocket代码。这里在请求到新文件时,触发了页面刷新。实际情况是执行了render函数,这里做了简化处理
const wsPort = 4002;
const socket = new WebSocket(`ws://${location.hostname}:${wsPort}/`);
socket.addEventListener("message", async({data}) => {
//获取到了文件
data = JSON.parse(data);
if(data.type === 'update') {
await import(`${data.path}?import&t=${Math.random() * 1000}`);
// 这里应该触发组件render函数,重新渲染,暂用reload代替,直接全局刷新
location.reload();
}
});
socket.addEventListener("close", () => {
console.log("close");
});
五、vite使用感受
笔者在预研过vite之后,新的项目还选择了webpack来进行工程能力的搭建,具体是考虑到了以下几点问题:
(1)脚手架集成度不够,考虑项目管理,jest、elint等组件基本是必不可少的,而当前vite搭建的项目并没有这些组件的选择,而一个一个配置又比较麻烦,自己配置时又经常出现一些版本上的问题。
(2)更新较快,不够稳定。笔者新的项目同时还引入了vue3,这两者都处在急速更新的阶段,在加上vue3的视图生态不完善(element+, ant),这几者经常出现版本配套等问题。
(3)基于当前项目以及未来的规划,该项目并没有且也不会很大规模,使用webpack并不会存在困扰。
六、总结
本文只是简单的聊了聊vite出现的原因,并在阅读源码的基础上,对vite基本流程进行了代码实现。本文只是笔者浅显的理解,肯定会不足以及错误,本文的一些结论是笔者结合实际的独立的思考,并非官方解答,所以仅做参考,抛砖引玉。谢谢各位抽空阅读。