Recently, when BUILDING a project with Vite, the biggest feeling is how fast and comfortable the development process is. Why do you feel this way? I think it’s mainly these two things that make people feel refreshed

  • The project starts fast
  • Project hot update fast

Always want to find out, also read some articles, and then summarize their own, to see the vite source code, after all, has been very curious about hot update.

Start the

vite

Because modern browsers support ES module, but do not support the import of bare modules

<script type="module"> import { createApp } from "/node_modules/.vite/vue.js? v=c260ab7b"; Import {createApp} from 'vue'; </script>Copy the code

Vite uses esbuild to convert CommonJS/UMD to ESM format and cache it in node_modules/. Vite:

Then rewrite the link, for example /node_modules/.vite/vue.js? V = c260AB7b, when the browser requests a resource, hijacks the browser’s HTTP request, converts non-javascript files (such as JSX, CSS or Vue/Svelte components), and returns them to the browser.

Contrast webpack

Compared to Webpack is the resolution of module dependencies, package generated Buddle, start the server, and Vite is through the ES way to load the module, in the browser to send a request is on demand to provide source code, let the browser took over part of the package, so it will feel fast.

Hot update

In Vite, hot updates are performed on the native ESM. It also feels faster to have the browser rerequest a module when its contents change, rather than recompiling all of its dependencies like WebPack.

Vite also uses HTTP headers to speed up entire page reloads. Source module requests are cached according to 304 Not Modified, and dependent module requests are cached through cache-control: Max-age =31536000,immutable Strong cache, so once cached they do not need to be requested again.

Learn about hot updates from the source code:

Create browser and server communication through WebSocket, use Chokidar to listen for file changes, send a message to notify the client when the module content changes, and only reload the module that has changed.

Server (Node)

In the Node folder, start the service by createServer in cli.ts. First, through CAC (a JavaScript library used to build an application’s CLI), create a command-line interaction that executes the cli.action callback function when NPM run dev is executed in the project:

// dev cli .command('[root]', 'start dev server') // default command .alias('serve') // the command is called 'serve' in Vite's API .alias('dev') // alias to align with the script name .option('--host [host]', `[string] specify hostname`) ... .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => { // output structure is preserved even after bundling so require() // is ok here Const {createServer} = await import('./server') try {// create service const server = await createServer({root, base: options.base, mode: options.mode, configFile: options.config, logLevel: options.logLevel, clearScreen: options.clearScreen, server: cleanOptions(options) }) ... await server.listen() ... Server.printurls ()} catch (e) {... }Copy the code

Vite does some work on createServer, including starting the service, listening for file changes, generating module dependencies, intercepting browser requests, and processing returned files.

export async function createServer( inlineConfig: InlineConfig = {} ): Promise<ViteDevServer> {// Generate all configuration items, Const config = await resolveConfig(inlineConfig, 'serve', Const middlewares = connect() as connect.server const httpServer = middlewareMode? null : await resolveHttpServer(serverConfig, middlewares, httpsOptions) const ws = createWebSocketServer(httpServer, config, // initialize file listening const watcher = chokidar.watch(path.resolve(root), {ignored: [ '**/node_modules/**', '**/.git/**', ...(Array.isArray(ignored) ? ignored : [ignored]) ], ignoreInitial: true, ignorePermissionErrors: true, disableGlobbing: true, ... WatchOptions}) as FSWatcher // Generate module dependencies, quickly position modules, hot update const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) => container.resolveId(url, undefined, {SSR})) const container = await createPluginContainer(config, moduleGraph, Watcher. On ('change', async (file) => { file = normalizePath(file) if (file.endsWith('/package.json')) { return invalidatePackageDjianata(packageCache, file) } // invalidate module graph cache on file change moduleGraph.onFileChange(file) if (serverConfig.hmr ! == false) {try {// perform hot update await handleHMRUpdate(file, server)} catch (err) {ws.send({type: 'error', err: Watcher. On ('add', (file) => {handleFileAddUnlink(normalizePath(file), Watcher. On ('unlink', (file) => {handleFileAddUnlink(normalizePath(file), server, True)}) // The main middleware, requesting file conversion, returns a browser-recognized JS file middlewares. Use (transformMiddleware(server))... return server }Copy the code

Modulegraph.onfilechange (file) when listening for file content change

onFileChange(file: string): void { const mods = this.getModulesByFile(file) if (mods) { const seen = new Set<ModuleNode>() mods.forEach((mod) => { this.invalidateModule(mod, seen) }) } } invalidateModule( mod: ModuleNode, seen: Set<ModuleNode> = new Set(), timestamp: number = Date.now() ): Void {/ / modified timestamp mod. LastInvalidationTimestamp = timestamp / / make the conversion result is invalid, Ensure that the module is reprocessed on the next request mod.transformResult = null mod.ssrTransformResult = null invalidateSSRModule(mod, seen)}Copy the code

Then perform handleHMRUpdate function, through moduleGraph getModulesByFile (file), need to update the module, call updateModules function, at this time will be special treatment for some file, In the case of.env configuration files, HTML files, etc., WS sends full-reload and the page is refreshed.

  • handleHMRUpdate
const isEnv = config.inlineConfig.envFile ! == false && (file === '.env' || file.startsWith('.env.')) ... / / by file contains the information and module const mods. = moduleGraph getModulesByFile (file)... if (! hmrContext.modules.length) { if (file.endsWith('.html'){ ... ws.send({ type: 'full-reload', path: config.server.middlewareMode ? '*' : '/' + normalizePath(path.relative(config.root, file)) }) } }Copy the code
  • updateModules
if (needFullReload) { config.logger.info(colors.green(`page reload `) + colors.dim(file), { clear: true, timestamp: true }) ws.send({ type: 'full-reload' }) } else { config.logger.info( updates .map(({ path }) => colors.green(`hmr update `) + colors.dim(path))  .join('\n'), { clear: true, timestamp: true } ) ws.send({ type: 'update', updates }) }Copy the code

Client

In the Client folder, ws receives the update type and performs the corresponding operation

Async function handleMessage(payload: HMRPayload) {switch (payload-type) {// Case 'Connected ':... SetInterval (() => socket.send('ping'), __HMR_TIMEOUT__) break Case 'custom': {... Break} // Full update case 'full-reload':... location.reload() ... Break // Clear case 'prune':... Break // error case 'error': {... break } default: { const check: never = payload return check } } }Copy the code

The fetchUpdate function is called during partial updates

async function fetchUpdate({ path, acceptedPath, timestamp }: Update) { ... await Promise.all( Array.from(modulesToUpdate).map(async (dep) => { const disposer = disposeMap.get(dep) if (disposer) Disposer (datamap.get (dep)) const [path, query] = dep.split('? ') try {// import to update the file, Const newMod = await import(/* @vite-ignore */ base + path.slice(1) + '? Import&t =${timestamp}${query? `&${query}` : ''}` ) moduleMap.set(dep, newMod) } catch (e) { warnFailedFetch(e, dep) } }) ) ... }Copy the code

The problem

How does Vite handle this component when the browser requests’ app.vue ‘during service startup, for example

  • How to optimize the browser cache?
  • How do I request and process style files in the component?

Let’s take a look at some of the actions mentioned in transformMiddleware at startup:

export function transformMiddleware( server: ViteDevServer ): Connect.NextHandleFunction { const { config: { root, logger }, moduleGraph } = server ... Return async function viteTransformMiddleware(req, res, next) {// Instead of getting the request, skip if (req.method! == 'GET' || knownIgnoreList.has(req.url!) ) { return next() } ... / / url, regular matching, including isJSRequest regular for: / / const knownJsSrcRE = / \. (sx (j | t)? |mjs|vue|marko|svelte|astro)($|\?) / if ( isJSRequest(url) || isImportRequest(url) || isCSSRequest(url) || isHTMLProxy(url) ) { ... // HTTP negotiation cache: If the change returns a complete response, add a new etag value to the response header. Otherwise return 304, use browser cache const ifNoneMatch = req.headers['if-none-match'] if (ifNoneMatch && (await) moduleGraph.getModuleByUrl(url, false))? .transformResult ? .etag === ifNoneMatch ) { isDebug && debugCache(`[304] ${prettifyUrl(url, Root)} ') res.statusCode = 304 return res.end()} Max-age =31536000,immutable to cache strongly, so once cached they do not need to request const result = await transformRequest(URL, server, {HTML: req.headers.accept? .includes('text/html') }) if (result) { const type = isDirectCSSRequest(url) ? 'css' : 'js' const isDep = DEP_VERSION_RE.test(url) || isOptimizedDepUrl(url) return send(req, res, result.code, type, { etag: result.etag, // allow browser to cache npm deps! cacheControl: isDep ? 'max-age=31536000,immutable' : 'no-cache', headers: server.config.server.headers, map: result.map }) } ... next() }Copy the code

In the previous step, the transformRequest function, such as the vue file, is called. Template and style are processed using the @vite/plugin-vue plug-in

export function transformRequest( url: string, server: ViteDevServer, options: TransformOptions = {} ): Promise<TransformResult | null> { ... const request = doTransform(url, server, options, timestamp) ... return request } async function doTransform( url: string, server: ViteDevServer, options: TransformOptions, timestamp: number ) { ... / / the plugin hit const id = (await pluginContainer. ResolveId (url, undefined, {SSR}))? .id || url ... // load const loadResult = await pluginContainer. Load (id, {SSR})... / / conversion const transformResult = await pluginContainer. Transform (code, id, {inMap: map, SSR})... Etag const result = SSR? await ssrTransform(code, map as SourceMap, url) : ({ code, map, etag: getEtag(code, { weak: true }) } as TransformResult) return result }Copy the code

The conversion is performed by hitting the Vue file in the @vite/plugin-vue plug-in

export async function transformMain( code: string, filename: string, options: ResolvedOptions, pluginContext: TransformPluginContext, ssr: boolean, asCustomElement: boolean ) { ... // script const { code: scriptCode, map } = await genScriptCode( descriptor, options, pluginContext, ssr ) // template const hasTemplateImport = descriptor.template && ! isUseInlineTemplate(descriptor, ! devServer) let templateCode = '' let templateMap: RawSourceMap | undefined if (hasTemplateImport) { ; ({ code: templateCode, map: templateMap } = await genTemplateCode( descriptor, options, pluginContext, ssr )) } // styles const stylesCode = await genStyleCode( descriptor, pluginContext, asCustomElement, attachedProps ) ... }Copy the code

Converting them into ES modules is introduced, and the browser sends the request, which is processed back to the browser through middleware.

conclusion

There are a lot of details in the source code. If you are interested, you can go to in-depth study. In this process, you understand some NPM packages and their functions, such as CAC, Chokidar and so on, as well as HTTP cache applications. I also found that there would be a lot of use of map and set data structures in the code, including data addition, deletion, modification, and existence, etc., so I continued to explore with questions.

Refer to the article

  • Vite official document
  • In-depth understanding of Vite core principles