Vite入门从手写一个乞丐版的Vite开始(下)

上一篇Vite入门从手写一个乞丐版的Vite开始(上)我们已经成功的将页面渲染出来了,这一篇我们来简单的实现一下热更新的功能。

所谓热更新就是修改了文件,不用刷新页面,页面的某个部分就自动更新了,听着似乎挺简单的,但是要实现一个很完善的热更新还是很复杂的,要考虑的情况很多,所以本文只会实现一个最基础的热更新效果。

创建WebSocket连接

浏览器显然是不知道文件有没有修改的,所以需要后端进行推送,我们先来建立一个WebSocket连接。

// app.js
const server = http.createServer(app);
const WebSocket = require("ws");

// 创建WebSocket服务
const createWebSocket = () => {
    // 创建一个服务实例
    const wss = new WebSocket.Server({ noServer: true });// 不用额外创建http服务,直接使用我们自己创建的http服务

    // 接收到http的协议升级请求
    server.on("upgrade", (req, socket, head) => {
        // 当子协议为vite-hmr时就处理http的升级请求
        if (req.headers["sec-websocket-protocol"] === "vite-hmr") {
            wss.handleUpgrade(req, socket, head, (ws) => {
                wss.emit("connection", ws, req);
            });
        }
    });

    // 连接成功
    wss.on("connection", (socket) => {
        socket.send(JSON.stringify({ type: "connected" }));
    });

    // 发送消息方法
    const sendMsg = (payload) => {
        const stringified = JSON.stringify(payload, null, 2);

        wss.clients.forEach((client) => {
            if (client.readyState === WebSocket.OPEN) {
                client.send(stringified);
            }
        });
    };

    return {
        wss,
        sendMsg,
    };
};
const { wss, sendMsg } = createWebSocket();

server.listen(3000);

WebSocket和我们的服务共用一个http请求,当接收到http协议的升级请求后,判断子协议是否是vite-hmr,是的话我们就把创建的WebSocket实例连接上去,这个子协议是自己定义的,通过设置子协议,单个服务器可以实现多个WebSocket 连接,就可以根据不同的协议处理不同类型的事情,服务端的WebSocket创建完成以后,客户端也需要创建,但是客户端是不会有这些代码的,所以需要我们手动注入,创建一个文件client.js:

// client.js

// vite-hmr代表自定义的协议字符串
const socket = new WebSocket("ws://localhost:3000/", "vite-hmr");

socket.addEventListener("message", async ({ data }) => {
  const payload = JSON.parse(data);
});

接下来我们把这个client.js注入到html文件,修改之前html文件拦截的逻辑:

// app.js
const clientPublicPath = "/client.js";

app.use(async function (req, res, next) {
    // 提供html页面
    if (req.url === "/index.html") {
        let html = readFile("index.html");
        const devInjectionCode = `

`;
        html = html.replace(//, `${devInjectionCode}`);
        send(res, html, "html");
    }
})

通过import的方式引入,所以我们需要拦截一下这个请求:

// app.js
app.use(async function (req, res, next) {
    if (req.url === clientPublicPath) {
        // 提供client.js
        let js = fs.readFileSync(path.join(__dirname, "./client.js"), "utf-8");
        send(res, js, "js");
    }
})

可以看到已经连接成功。

监听文件改变

接下来我们要初始化一下对文件修改的监听,监听文件的改变使用chokidar[1]:

// app.js
const chokidar = require(chokidar);

// 创建文件监听服务
const createFileWatcher = () => {
  const watcher = chokidar.watch(basePath, {
    ignored: [/node_modules/, /.git/],
    awaitWriteFinish: {
      stabilityThreshold: 100,
      pollInterval: 10,
    },
  });
  return watcher;
};
const watcher = createFileWatcher();

watcher.on("change", (file) => {
    // file文件修改了
})

构建导入依赖图

为什么要构建依赖图呢,很简单,比如一个模块改变了,仅仅更新它自己肯定还不够,依赖它的模块都需要修改才对,要做到这一点自然要能知道哪些模块依赖它才行。

// app.js
const importerMap = new Map();
const importeeMap = new Map();

// map : key -> set
// map : 模块 -> 依赖该模块的模块集合
const ensureMapEntry = (map, key) => {
  let entry = map.get(key);
  if (!entry) {
    entry = new Set();
    map.set(key, entry);
  }
  return entry;
};

需要用到的变量和函数就是上面几个,importerMap用来存放模块到依赖它的模块之间的映射;importeeMap用来存放模块到该模块所依赖的模块的映射,主要作用是用来删除不再依赖的模块,比如a一开始依赖b和c,此时importerMap里面存在b -> a和c -> a的映射关系,然后我修改了一下a,删除了对c的依赖,那么就需要从importerMap里面也同时删除c -> a的映射关系,这时就可以通过importeeMap来获取到之前的a -> [b, c]的依赖关系,跟此次的依赖关系a -> [b]进行比对,就可以找出不再依赖的c模块,然后在importerMap里删除c -> a的依赖关系。

接下来我们从index.html页面开始构建依赖图,index.html内容如下:

可以看到它依赖了main.js,修改拦截html的方法:

// app.js
app.use(async function (req, res, next) {
    // 提供html页面
    if (req.url === "/index.html") {
        let html = readFile("index.html");
        // 查找模块依赖图
        const scriptRE = /(

test.js又引入了test2.js:

// test.js
import test2 from "./test2.js";

export default function () {
  let a = test2();
  let b = "我是测试1";
  return a + " --- " + b;
}

// test2.js
export default function () {
    return '我是测试2'
}

接下来修改test2.js测试效果:

可以看到重新发送了请求,但是页面并没有更新,这是为什么呢,其实还是缓存问题:

App.vue导入的两个文件之前已经请求过了,所以浏览器会直接使用之前请求的结果,并不会重新发送请求,这要怎么解决呢,很简单,可以看到请求的App.vue的url是带了时间戳的,所以我们可以检查请求模块的url是否存在时间戳,存在则把它依赖的所有模块路径也都带上时间戳,这样就会触发重新请求了,修改一下模块路径转换方法parseBareImport:

// app.js
// 处理裸导入
const parseBareImport = async (js, importer) => {
    // ...
    // 检查模块url是否存在时间戳
    let hast = checkQueryExist(importer, "t");// ++
    // ...
    parseResult[0].forEach((item) => {
        let url = "";
        if (item.n[0] !== "." && item.n[0] !== "/") {
            url = `/@module/${item.n}?import${hast ? "&t=" + Date.now() : ""}`;// ++
        } else {
            url = `${item.n}?import${hast ? "&t=" + Date.now() : ""}`;// ++
        }
        // ...
    })
    // ...
}

再来测试一下:

可以看到成功更新了。最后我们再来测试运行刷新整个页面的情况,修改一下main.js文件即可:

总结

本文参考Vite-1.0.0-rc.5版本写了一个非常简单的Vite,简化了非常多的细节,旨在对Vite及热更新有一个基础的认识,其中肯定有不合理或错误之处,欢迎指出~

示例代码在:https://github.com/wanglin2/vite-demo[3]。


[1]

chokidar: https://github.com/paulmillr/chokidar

[2]

lru-cache: https://github.com/isaacs/node-lru-cache

[3]

https://github.com/wanglin2/vite-demo: https://github.com/wanglin2/vite-demo

展开阅读全文

页面更新:2024-03-28

标签:路径   乞丐   模块   入门   协议   页面   关系   简单   文件   测试   时间   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top