手写搭建一个vite服务器,啃透vite的底层原理

Vite的底层工作原理,关键点

  1. 原生ES模块(ESM)支持 :利用现代浏览器原生支持ES模块系统无需打包
  2. 按需编译 :只编译当前页面需要的文件,而非整个应用
  3. 预构建依赖 :对node_modules中的依赖进行预构建
  4. 即时服务 :基于ESM的即时服务器启动毫秒级热更新

手写简易Vite实现

做个简化版 的 Vite服务器:

node服务器,处理浏览器加载各种资源请求

  1. index.html
  2. js
  3. vue

目录结构如下:

1. 基础服务器搭建

  1. 基础服务器搭建
javascript 复制代码
const Koa = require("koa");
const fs = require("fs");
const path = require("path");

const app = new Koa(); // 创建Koa应用实例

// 裸模块路径重写函数(稍后定义)
function rewriteImport(content) { /*...*/ }

app.listen(3001, () => {
  console.log("KVite Server running at 3001");
});

这部分初始化了一个Koa服务器,设置了3001端口监听,并预留了核心的rewriteImport函数位置。

  1. 主请求处理中间件
javascript 复制代码
app.use(async (ctx) => {
  const { url } = ctx.request; // 获取请求路径
  
  // 路由分发处理(下面分四个部分详细说明)
  if (url === "/") { /* 处理首页 */ }
  else if (url.endsWith(".js")) { /* 处理JS文件 */ }
  else if (url.startsWith("/@modules/")) { /* 处理npm模块 */ }
  else if (url.indexOf(".vue") > -1) { /* 处理Vue单文件组件 */ }
});

这是核心中间件,根据不同的URL路径分发到对应的处理逻辑。

  1. 首页请求处理
javascript 复制代码
if (url === "/") {
  ctx.type = "text/html";
  let content = fs.readFileSync(path.join(__dirname, "..", "public", "index.html"), "utf-8");
  
  // 重写HTML内联模块脚本中的导入路径
  content = content.replace(
    /<script type="module">([\s\S]*?)<\/script>/g,
    (match, scriptContent) => {
      const rewrittenScript = rewriteImport(scriptContent);
      return `<script type="module">${rewrittenScript}</script>`;
    }
  );
  ctx.body = content;
}

处理根路径请求,读取index.html并重写其中的模块导入路径。

  1. 普通JS文件处理
javascript 复制代码
else if (url.endsWith(".js")) {
  const p = path.join(__dirname, "..", "public", url);
  ctx.type = "application/javascript";
  const content = fs.readFileSync(p, "utf-8");
  ctx.body = rewriteImport(content); // 重写JS文件中的导入路径
}

处理.js文件请求,读取文件内容并重写其中的模块导入路径。

  1. NPM模块请求处理
javascript 复制代码
else if (url.startsWith("/@modules/")) {
  const moduleName = ctx.request.url.replace("/@modules/", "");
  
  // 特殊处理vue模块
  if (moduleName === "vue") {
    const vuePath = path.join(__dirname, "..", "node_modules", "vue", "dist", "vue.esm-browser.js");
    ctx.type = "application/javascript";
    ctx.body = rewriteImport(fs.readFileSync(vuePath, "utf-8"));
  }
  // 处理@vue/开头的作用域包
  else if (moduleName.startsWith("@vue/")) {
    const [scope, packageName] = moduleName.split("/");
    const modulePath = path.join(__dirname, "..", "node_modules", scope, packageName);
    // 通过package.json找到入口文件
    const modulePkg = require(path.join(modulePath, "package.json"));
    const entryPath = path.join(modulePath, modulePkg.module || modulePkg.main || "index.js");
    ctx.type = "application/javascript";
    ctx.body = rewriteImport(fs.readFileSync(entryPath, "utf-8"));
  }
  // 处理普通npm包
  else {
    const modulePath = path.join(__dirname, "..", "node_modules", moduleName);
    // 同上,通过package.json找到入口文件
    const modulePkg = require(path.join(modulePath, "package.json"));
    const entryPath = path.join(modulePath, modulePkg.module || modulePkg.main || "index.js");
    ctx.type = "application/javascript";
    ctx.body = rewriteImport(fs.readFileSync(entryPath, "utf-8"));
  }
}

处理被重写为/@modules/开头的npm模块请求,根据package.json找到正确的入口文件。

  1. Vue单文件组件处理
javascript 复制代码
else if (url.indexOf(".vue") > -1) {
  const p = path.join(__dirname, "..", "public", url.split("?")[0]);
  const file = fs.readFileSync(p, "utf-8");
  
  // 使用Vue官方编译器解析SFC
  const { parse, compileTemplate } = require("@vue/compiler-sfc");
  const { descriptor } = parse(file);

  // 首次请求(不带type参数)
  if (!ctx.request.query.type) {
    let scriptContent = descriptor.script.content.replace("export default", "const __script =");
    ctx.type = "application/javascript";
    ctx.body = `
      ${rewriteImport(scriptContent)}
      import { render as __render } from "${url}?type=template"
      __script.render = __render
      export default __script
    `;
  }
  // 模板部分请求
  else if (ctx.request.query.type === "template") {
    const { code } = compileTemplate({ source: descriptor.template.content });
    ctx.type = "application/javascript";
    ctx.body = rewriteImport(code);
  }
}

处理.vue文件请求,将单文件组件拆分为脚本和模板两部分分别处理。

  1. 路径重写函数
javascript 复制代码
function rewriteImport(content) {
  return content.replace(/from ['"](.*)['"]/g, (s1, s2) => {
    // 保留相对路径和绝对路径
    if (s2.startsWith("./") || s2.startsWith("../") || s2.startsWith("/")) {
      return s1;
    }
    // 重写裸模块为/@modules/路径
    else {
      return `from '/@modules/${s2}'`;
    }
  });
}

将代码中的裸模块导入(如import vue from 'vue')重写为浏览器可识别的路径格式(import vue from '/@modules/vue')。

index.html

js 复制代码
  // 1. 返回宿主页
  if (url === "/") {
    ctx.type = "text/html";
    let content = fs.readFileSync(path.join(__dirname, "..", "public", "index.html"), "utf-8");
    // 处理HTML中的内联模块脚本
    content = content.replace(
      /<script type="module">([\s\S]*?)<\/script>/g,
      (match, scriptContent) => {
        const rewrittenScript = rewriteImport(scriptContent);
        return `<script type="module">${rewrittenScript}</script>`;
      }
    );
    ctx.body = content;
  }

这段代码在服务器返回HTML页面时,会自动查找并重写其中的<script type="module">内容,将裸模块导入(如import vue from 'vue')转换为浏览器可识别的路径格式(如/@modules/vue),使得浏览器能够正确加载Node模块。

js

js 复制代码
  // 2. 处理JS文件请求
  else if (url.endsWith(".js")) {
    const p = path.join(__dirname, "..", "public", url);
    ctx.type = "application/javascript";
    const content = fs.readFileSync(p, "utf-8");
    ctx.body = rewriteImport(content);
  }

这段代码处理.js文件请求,读取文件内容后通过rewriteImport函数转换模块导入路径(如将'vue'转成'/@modules/vue'),确保浏览器能正确解析依赖。

vue

专门处理/@modules/开头的请求,根据模块名(如vue@vue/runtime-dom或其他npm包)从node_modules中读取对应的ES模块文件,并通过rewriteImport转换内部依赖路径后返回给浏览器。

2. 添加Vue SFC文件支持

处理.vue单文件组件请求:首次请求返回脚本逻辑并注入模板导入,带?type=template的二次请求则编译模板为渲染函数,最终组合成完整的Vue组件返回给浏览器。

javascript 复制代码
// 在中间件中添加SFC文件处理逻辑
else if (url.indexOf('.vue') > -1) {
  const p = path.join(__dirname, url.split('?')[0])
  const file = fs.readFileSync(p, 'utf-8')
  
  // 解析SFC
  const { parse } = require('@vue/compiler-sfc')
  const { descriptor } = parse(file)
  
  // 处理不带查询参数的请求
  if (!ctx.request.query.type) {
    // 处理script部分
    let scriptContent = descriptor.script.content
    scriptContent = scriptContent.replace('export default', 'const __script =')
    
    ctx.type = 'application/javascript'
    ctx.body = `
${rewriteImport(scriptContent)}
import { render as __render } from "${url}?type=template"
__script.render = __render
export default __script
`
  } 
  // 处理template部分请求
  else if (ctx.request.query.type === 'template') {
    // 编译模板为渲染函数
    const { compileTemplate } = require('@vue/compiler-dom')
    const template = descriptor.template.content
    const { code } = compileTemplate({ source: template })
    
    ctx.type = 'application/javascript'
    ctx.body = rewriteImport(code)
  }
}

3. 客户端HTML示例

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <script type="module">
    // 浏览器环境变量hack
    window.process = { env: { NODE_ENV: 'development' } }
    
    import { createApp } from 'vue'
    import App from './App.vue'
    
    createApp(App).mount('#app')
  </script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

完整工作流程

  1. 浏览器请求index.html
  2. HTML中加载main.jstype=module
  3. 处理main.js中的裸模块导入 (如vue
    • 重写为/@modules/vue
    • 服务器返回预构建的vue模块
  4. 处理.vue文件请求
    • 解析SFC为描述对象
    • 分别处理script和template部分
    • 动态生成组件代码
  5. 浏览器执行生成的代码,渲染应用

总结

当然,有的小伙伴说还欠其他的文件兼容,比如css,比如图片,比如json,最后还有热更,和预构建

  1. CSS处理 :支持<style>块解析
  2. 静态资源处理图片JSON等资源导入
  3. 热更新(HMR) :通过WebSocket实现
  4. 预构建优化 :使用esbuild预构建依赖

当面试官问:为什么 Vite 速度比 Webpack 快?,这时候我们知道怎么回答了吧。

1. 组成结构

  • 开发服务器:基于原生ES模块,支持极速HMR
  • 生产构建:使用Rollup预配置优化打包

2. 核心特性

  • ⚡ 毫秒级冷启动(无打包)
  • 🔥 即时模块热更新
  • 🎯 真正的按需编译

3. 核心优势

对比维度 Webpack Vite
启动 需构建依赖图 直接启动
HMR 全依赖链重编译 单模块更新
编译 全量预编译 动态按需编译

4. 工作原理

复制代码
浏览器 → 请求ES模块 → 服务端动态编译 → 返回编译结果

这个简易实现展示了Vite的核心思想:利用浏览器原生ES模块系统,按需编译,实现快速开发体验。

相关推荐
奕辰杰3 小时前
关于npm前端项目编译时栈溢出 Maximum call stack size exceeded的处理方案
前端·npm·node.js
JiaLin_Denny4 小时前
如何在NPM上发布自己的React组件(包)
前端·react.js·npm·npm包·npm发布组件·npm发布包
路光.5 小时前
触发事件,按钮loading状态,封装hooks
前端·typescript·vue3hooks
我爱996!5 小时前
SpringMVC——响应
java·服务器·前端
咔咔一顿操作6 小时前
Vue 3 入门教程7 - 状态管理工具 Pinia
前端·javascript·vue.js·vue3
kk爱闹6 小时前
用el-table实现的可编辑的动态表格组件
前端·vue.js
漂流瓶jz7 小时前
JavaScript语法树简介:AST/CST/词法/语法分析/ESTree/生成工具
前端·javascript·编译原理
换日线°7 小时前
css 不错的按钮动画
前端·css·微信小程序
风象南7 小时前
前端渲染三国杀:SSR、SPA、SSG
前端
90后的晨仔7 小时前
表单输入绑定详解:Vue 中的 v-model 实践指南
前端·vue.js