vite流程分析及代码实现

1,412 阅读5分钟

一、概述

    文本通过对比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=timestamp{timestamp}{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基本流程进行了代码实现。本文只是笔者浅显的理解,肯定会不足以及错误,本文的一些结论是笔者结合实际的独立的思考,并非官方解答,所以仅做参考,抛砖引玉。谢谢各位抽空阅读。