How Vite Loads Modules and Parses Vue SFCs: Core Mechanics Explained
This article dissects Vite’s fast development workflow by detailing how it rewrites bare module imports, resolves dependencies from node_modules, parses Vue single‑file components into template, script, and style sections, compiles them, and dynamically injects the resulting code for seamless browser execution.
When we talk about Vite’s “speed”, many think of its native ESM‑based dev mode; this article shows the concrete implementation steps Vite uses to load modules and parse single‑file Vue components.
1. JS Loading and Bare Module Path Rewrite: Making the Browser Understand Imports
Browsers support ESM but only recognize relative or absolute URLs; bare imports like import Vue from 'vue' are not directly loadable. Vite rewrites such imports to a virtual path /@modules/… using a rewriteImport helper.
function rewriteImport(content) {
return content.replace(/ from ['"](.*)['"]/g, (s1, s2) => {
// relative/absolute paths are left unchanged
if (s2.startsWith('./') || s2.startsWith('/') || s2.startsWith('../')) {
return s1;
} else {
// bare module rewritten to /@modules/xxx
return ` from '/@modules/${s2}'`;
}
});
}When the dev server receives a request for a JS file (e.g., /src/main.js), it reads the file, runs rewriteImport, and returns the transformed source. Example:
Original: import Vue from 'vue' Rewritten: import Vue from '/@modules/vue' The browser then requests /@modules/vue instead of failing.
2. Bare Module Loading: Finding the Real Dependency Files
The server intercepts paths that start with /@modules/, extracts the module name, locates it in node_modules, reads its package.json to get the ESM entry ( module field), and returns that file after applying rewriteImport recursively.
else if (url.startsWith("/@modules/")) {
const moduleName = url.replace("/@modules/", "");
const prefix = path.join(__dirname, "node_modules", moduleName);
const modulePath = require(path.join(prefix, "package.json")).module;
const result = fs.readFileSync(path.join(prefix, modulePath), "utf-8");
ctx.body = rewriteImport(result);
}This mirrors Vite’s simplified pre‑bundle logic: converting bare imports into real files that the browser can load as ESM.
3. Parsing SFC (.vue Files): Splitting the Three Blocks
(1) Handling Main Requests (no type query)
The server reads the .vue file, uses @vue/compiler-sfc to parse it into an AST, extracts the <script> content, rewrites imports, and generates import statements for scoped styles and the template.
const res = compilerSFC.parse(fs.readFileSync(p, 'utf-8'));
const scriptContent = res.descriptor.script.content;
const script = scriptContent.replace("export default", "const __script = ");
const hasScoped = res.descriptor.styles && res.descriptor.styles.some(s => s.scoped);
const scopeId = hasScoped ? `data-v-${res.descriptor.id}` : '';
const styleImports = (res.descriptor.styles || [])
.map((s, i) => `import '${url}?type=style&index=${i}&lang=${s.lang || 'css'}'`)
.join('
');
ctx.body = `
${rewriteImport(script)}
import { render as __render } from '${url}?type=template'
${styleImports}
${hasScoped ? `__script.__scopeId = '${scopeId}'` : ''}
__script.render = __render
export default __script
`;Key points added for style handling:
Detect whether the component contains <style> and if it uses scoped.
Generate a scopeId (e.g., data-v-xxx) to isolate scoped styles.
Automatically create import statements for each style block (e.g., import 'Component.vue?type=style&index=0').
(2) Handling Template Requests ( type=template )
else if (query.type === 'template') {
const tpl = res.descriptor.template.content;
const hasScoped = res.descriptor.styles && res.descriptor.styles.some(s => s.scoped);
const scopeId = hasScoped ? `data-v-${res.descriptor.id}` : undefined;
const render = compilerDOM.compile(tpl, { mode: 'module', scopeId }).code;
ctx.body = rewriteImport(render);
}If the component uses scoped styles, the compiled render function receives the scopeId so that generated DOM nodes carry the appropriate attribute.
(3) Handling Style Requests ( type=style )
else if (query.type === "style") {
const index = parseInt(query.index);
const style = res.descriptor.styles[index];
const id = `data-v-${res.descriptor.id}`;
const result = await compilerSFC.compileStyleAsync({
source: style.content,
filename: p,
id,
scoped: style.scoped,
preprocessLang: style.lang,
});
const cssContent = result.code;
ctx.body = `
const style = document.createElement('style');
style.setAttribute('type','text/css');
style.textContent = \`${cssContent.replace(/`/g, '\\`')}\`;
document.head.appendChild(style);
`;
}This block performs three essential tasks:
Style compilation: Uses compilerSFC.compileStyleAsync to process scoped styles and preprocessors (e.g., SCSS).
Dynamic injection: Creates a <style> element and appends it to the document head.
Compatibility handling: Escapes backticks in the generated CSS to keep the surrounding template string valid.
4. Code Generation and Template Compilation: From Source to Executable Code
The overall goal is to emit browser‑runnable ESM code. The pipeline consists of:
JS code generation: For plain JS files, rewriteImport rewrites paths and returns the source; for .vue files, the script, template, and style parts are combined.
Template compilation: compilerDOM.compile turns the <template> into a render function, optionally with a scopeId for scoped styles.
Style handling: Compiled CSS is injected via a dynamically created <style> tag, supporting both scoped isolation and preprocessors.
Example Vue component:
<template>
<div class="box">{{ msg }}</div>
</template>
<style scoped>
.box { color: red; }
</style>After processing:
The compiled render function adds data-v-xxx to the <div>.
The compiled CSS becomes .box[data-v-xxx] { color: red; }.
The CSS is injected into the page via a <style> element, affecting only the component’s elements.
Summary of the simplified Vite workflow :
Server serves index.html; the browser requests main.js.
JS processing rewrites bare module paths so the browser can resolve dependencies.
Requests to /@modules/… locate the corresponding package in node_modules and return its ESM entry.
SFC parsing splits .vue files into template, script, and style, handling scoped styles and generating import statements.
Template compilation produces a render function with optional scopeId.
Style compilation yields CSS that is dynamically injected.
The assembled module is executed by the browser, completing the fast dev experience.
Although this hand‑written version lacks Vite’s advanced features such as hot‑module replacement and dependency pre‑bundling, it clearly demonstrates the core principle: leveraging native ESM and a dev server that rewrites imports and parses files on‑the‑fly to avoid a bundling step.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
