Hello, my name is Xiaoyu Xiaoyu and I’m dedicated to sharing interesting and useful articles.

Content is divided into original and translation, if you have questions, please feel free to comment or private message, I am happy to discuss with you, progress together.

Sharing is not easy, hope to get everyone’s support and attention.


Vite out for a long time, also out of a lot of related articles, I also wanted to, and then I wrote. 🙂

The vITE version of this document is 2.0.0-beta.4
Vite document

The whole process

In my opinion, Vite is an innovative dev building tool that stands on the shoulders of giants, inherited from:

  • Onion Model service
  • rollup
  • es module

The Onion model is similar to rollup’s plug-in driver if next() is placed at the bottom of the function.

That said, pluggable architecture is the whole idea of Vite, not only can you write internal plug-ins and atomize internal plug-ins, but you can also use various existing plug-ins on NPM. Very flexible.

Why the ES Module?

Vite uses the ES Module for module import, which is supported natively by modern browsers. When importing a module, a request is made, which is why the ES Module can only be used in a service. In addition, the request will be resend if the path of the imported module changes, including query.


Now let’s go into the whole

Vite uses the Monorepo architecture, and there are two main parts of the code we need to care about:

  • vite cli
  • @vitejs/plugin-vue

Let’s start with vite CLI.

Here is the entrance to Vite:

const { createServer } = await import('./server')
try {
  const server = await createServer(
    {
      root,
      mode: options.mode,
      logLevel: options.logLevel,
      server: cleanOptions(options) as ServerOptions
    },
    options.config
  )
  await server.listen()
Copy the code

Simply create a service on the createServer and start listening. Let’s go ahead and open the createServer.

export async function createServer(inlineConfig: UserConfig & { mode? :string} = {}, configPath? :string | false
) :Promise<ViteDevServer> {
  // There is too much code to put, put something convenient to read, if you are interested, you can open the code and read the comments here and look at the code
  
  // Configuration related, such as recording local configuration files, integration plug-ins, environment variables, etc

  // Initialize the service with Connect. Connect is an HTTP framework that provides scalable services for nodes using middleware

  // Create webSocket service

  // Use chokidar to listen to files
  
  // Vite inheritance Rollup implements a mini version of the construct resolution build tool
  
  // Create a diagram to maintain relationships between modules
  
  // Perform HMR operations when files are changed, as described later
  
  // Access a variety of middleware, such as interface proxy, static service, parsing request resources, redirection, processing HTML, etc., the most important one is parsing request resources
  
  // This step exposes everything in vite to the user, such as the node service app, configuration, file listener, socket, etc. This step is bold and bad, but I love it
  
  // Return the node service for listening
}
Copy the code

After running this pile, we started a service, and we found that Vite, so far, doesn’t have any code for packaging, so where is it?

In fact, one of the reasons why Vite is fast is that it does not pack, and its packaging is really on demand.

After the service is started, we visit the page and send requests, which are processed by the middleware, which then packages, injects, and so on.

The core of the middleware is the request resource described in the comment above, which is called transformMiddleware in Vite

export function transformMiddleware(
  server: ViteDevServer
) :Connect.NextHandleFunction {
  const {
    config: { root, logger },
    moduleGraph
  } = server

  return async (req, res, next) => {
      // Other code
      
      // Only apply the transform pipeline to:
      // - requests that initiate from ESM imports (any extension)
      // - CSS (even not from ESM)
      // - Source maps (only for resolving)
      if (
        isJSRequest(url) || / / specified sx (j | t)? | MJS | vue such documents, or no suffix
        isImportRequest(url) || / / the import
        isCSSRequest(url) || // css
        isHTMLProxy(url) || // html-proxy
        server.config.transformInclude(withoutQuery) // Hit the one that needs parsing
      ) {
        // Remove the import query, for example: (\? |$)import=xxxx
        url = removeImportQuery(url)

        // Delete idPrefix. Invalid browser specifiers generated by importAnalysis are preresolved
        if (url.startsWith(VALID_ID_PREFIX)) {
          url = url.slice(VALID_ID_PREFIX.length)
        }

        // for CSS, we need to differentiate between normal CSS requests and
        // imports
        // Handle CSS links
        if(isCSSRequest(url) && req.headers.accept? .includes('text/css')) {
          url = injectQuery(url, 'direct')}// check if we can return 304 early
        const ifNoneMatch = req.headers['if-none-match']
        // Hit the browser cache to take advantage of the browser features
        if (
          ifNoneMatch &&
          (awaitmoduleGraph.getModuleByUrl(url))? .transformResult? .etag === ifNoneMatch ) { res.statusCode =304
          return res.end()
        }

        // Parse vue JS CSS files such as key
        const result = await transformRequest(url, server)
        if (result) {
          const type = isDirectCSSRequest(url) ? 'css' : 'js'
          const isDep =
            DEP_VERSION_RE.test(url) ||
            url.includes(`node_modules/${DEP_CACHE_DIR}`)
          return send(
            req,
            res,
            result.code,
            type,
            result.etag,
            // allow browser to cache npm deps!
            isDep ? 'max-age=31536000,immutable' : 'no-cache',
            result.map
          )
        }
      }
    } catch (e) {
      return next(e)
    }

    next()
  }
}
Copy the code

One of the most important is transformRequest, which performs caching, request resource resolution, load, and transformation operations.

export async function transformRequest(
  url: string,
  { config: { root }, pluginContainer, moduleGraph, watcher }: ViteDevServer
) :Promise<TransformResult | null> {
  url = removeTimestampQuery(url)
  const prettyUrl = isDebug ? prettifyUrl(url, root) : ' '

  // Check the last transformResult. This thing will be removed in the HMR
  const cached = (awaitmoduleGraph.getModuleByUrl(url))? .transformResultif (cached) {
    isDebug && debugCache(`[memory] ${prettyUrl}`)
    return cached
  }

  // resolve
  const id = (awaitpluginContainer.resolveId(url))? .id || urlconst file = cleanUrl(id)

  let code = null
  let map: SourceDescription['map'] = null

  // load
  const loadStart = Date.now()
  const loadResult = await pluginContainer.load(id)
  // Load failed, read the file directly
  if (loadResult == null) {
    // try fallback loading it from fs as string
    // if the file is a binary, there should be a plugin that already loaded it
    // as string
    try {
      code = await fs.readFile(file, 'utf-8')
      isDebug && debugLoad(`${timeFrom(loadStart)} [fs] ${prettyUrl}`)}catch (e) {
      if(e.code ! = ='ENOENT') {
        throw e
      }
    }
    if (code) {
      map = (
        convertSourceMap.fromSource(code) ||
        convertSourceMap.fromMapFileSource(code, path.dirname(file))
      )?.toObject()
    }
  } else {
    isDebug && debugLoad(`${timeFrom(loadStart)} [plugin] ${prettyUrl}`)
    if (typeof loadResult === 'object') {
      code = loadResult.code
      map = loadResult.map
    } else {
      code = loadResult
    }
  }
  if (code == null) {
    throw new Error(`Failed to load url ${url}. Does the file exist? `)}// Add the address of the currently processed request to the maintained diagram
  const mod = await moduleGraph.ensureEntryFromUrl(url)
  / / to monitor
  if(mod.file && ! mod.file.startsWith(root +'/')) {
    watcher.add(mod.file)
  }

  // transform
  const transformStart = Date.now()
  // All plugins are saved by the closure and then call a hook function on the pluginContainer, which loops the plugins to do the operation
  const transformResult = await pluginContainer.transform(code, id, map)
  if (
    transformResult == null| | -typeof transformResult === 'object' && transformResult.code == null)) {// no transform applied, keep code as-is
    isDebug &&
      debugTransform(
        timeFrom(transformStart) + chalk.dim(` [skipped] ${prettyUrl}`))}else {
    isDebug && debugTransform(`${timeFrom(transformStart)} ${prettyUrl}`)
    if (typeof transformResult === 'object') {
      code = transformResult.code!
      map = transformResult.map
    } else {
      code = transformResult
    }
  }

  // Return and cache the current conversion result
  return (mod.transformResult = {
    code,
    map,
    etag: getEtag(code, { weak: true})}as TransformResult)
}
Copy the code

There are three hook functions provided by the plug-in:

  • pluginContainer.resolveId
  • pluginContainer.load
  • pluginContainer.transform

The resolveId and load parse the requested URL into the content in the corresponding file for the transform to use

For example, vite provides plugin-vue, which translates vUE, and plugin-vue-jsx, which supports JSX writing. If you want to support other framework languages, you can add them yourself.

At this point, the general flow of Vite ends.

The code may not be intuitive, but here’s a simple example:

<! DOCTYPEhtml>
<html lang="en">

<head>
    <title>Vite App</title>
</head>

<body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
</body>

</html>
Copy the code
// main.js
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
Copy the code
// app.vue
<template>
    <div>hello world</div>
</template>
Copy the code

This is what the browser sees in app.vue:

In addition to the render functions, there are createHotContext, import.meta-.hot.Accept, which are related to HMR, as described below.

hmr

HMR also played an important role in our development project. What about Vite?

Part involved:

  • The client provides an HMR context that contains the update method corresponding to the current file, which is invoked during WS notification

  • For example, the HMR module is injected into the HMR API provided in the client

  • Plugin-vue injects the Vue context and concatenates methods from the client into the current module

When we import a module, we send a request. While the current request is processed by the transformMiddleware, the current request URL is added to the diagram and then processed by the transform of various plug-ins, including the importsAnalysis plug-in. ImportsAnalysis resolves the import export via es-modole-Lexer, inserts the current module into the module diagram, and establishes dependencies between the current importe and the imported importedModules.

// importsAnalysis.ts
if(! isCSSRequest(importer)) {const prunedImports = await moduleGraph.updateModuleInfo(
      importerModule, // The body of the current parse
      importedUrls, // The file to be imported
      normalizedAcceptedUrls,
      isSelfAccepting
    )
    if (hasHMR && prunedImports) {
      handlePrunedModules(prunedImports, server)
    }
}
Copy the code

And the HMR API is added to the currently requested file.

// importsAnalysis.ts
if (hasHMR) {
    // inject hot context
    str().prepend(
      `import { createHotContext } from "${CLIENT_PUBLIC_PATH}"; ` +
        `import.meta.hot = createHotContext(The ${JSON.stringify(
          importerModule.url
        )}); `)}Copy the code

In addition to the importsAnalysis plug-in, there is a plugin-Vue plug-in transform that inserts the re-render method.

// /plugin-vue/src/main.ts
if(devServer && ! isProduction) { output.push(`_sfc_main.__hmrId = The ${JSON.stringify(descriptor.id)}`)
    output.push(
      `__VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main)`
    )
    // check if the template is the only thing that changed
    if (prevDescriptor && isOnlyTemplateChanged(prevDescriptor, descriptor)) {
      output.push(`export const _rerender_only = true`)
    }
    output.push(
      `import.meta.hot.accept(({ default: updated, _rerender_only }) => {`.` if (_rerender_only) {`.` __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)`.` } else {`.` __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated)`.`} `.` `}))}Copy the code

__VUE_HMR_RUNTIME__ is exposed by vue Runtime and has been introduced in main.js. The following import.meta-.hot. Accept is exposed by the client. Import. meta specifies the metadata of the current es module.

The client is the browser side of the HMR related logic, which is dependent on the above plug-in injection method.

// client.ts
function acceptDeps(deps: string[], callback: HotCallback['fn'] = () => {}) {
    // hotModulesMap is saved by the closure
    // ownerPath is the id address of the current module passed in when importsAnalysis instantiates the HMR context
    const mod: HotModule = hotModulesMap.get(ownerPath) || {
      id: ownerPath,
      callbacks: []
    }
    mod.callbacks.push({
      deps,
      fn: callback
    })
    hotModulesMap.set(ownerPath, mod)
  }
  // Add to the file by importsAnalysis
  // Plugin-vue plugin will use this method to add a module (mod), and will add some vue related content, such as:
  // Add the vue render method for HMR to call
const hot = {
    // Add a refresh method to the callback when called
    accept(deps: any, callback? :any) {
      if (typeof deps === 'function'| |! deps) {// self-accept: hot.accept(() => {})
        acceptDeps([ownerPath], ([mod]) = > deps && deps(mod))
      } else if (typeof deps === 'string') {
        // explicit deps
        acceptDeps([deps], ([mod]) = > callback && callback(mod))
      } else if (Array.isArray(deps)) {
        acceptDeps(deps, callback)
      } else {
        throw new Error(`invalid hot.accept() usage.`)}},// ...
}
Copy the code

When we call import.meta.hot.accept, such as an incoming method, the update method will be added to a hotModulesMap with the importer module as the key. Record the current module to be updated.

Next, WS sends a message to the browser after the file changes. This step involves determining whether the HMR type is self-updated (mainly based on the body content of the Accept method, the specific logic can be viewed by yourself) and whether there is a logic such as an importer to determine the HMR type.

Let’s continue with the example of HMR type js-update.

The main two methods are fetchUpdate, which gets the module to be updated, import the module, and returns a method called re-render, and queueUpdate, which executes the method returned by the fetchUpdate.

After entering fetchUpdate, it will determine whether the current module is updated. If yes, add the current module to modulesToUpdate; if not, add the dependent submodule to the record to be updated, modulesToUpdate, and then filter out the previously collected modules to be updated. Loop through the import operation, but with the current timestamp on the path of the import module to force the HTTP request, replace the previous module with the new imported module, and finally return the re-render method provided by plugin-vue.

async function fetchUpdate({ path, acceptedPath, timestamp }: Update) {
  // The currently updated module
  const mod = hotModulesMap.get(path)
  if(! mod) {return
  }

  const moduleMap = new Map(a)/ / since the update
  const isSelfUpdate = path === acceptedPath

  // make sure we only import each dep once
  const modulesToUpdate = new Set<string> ()if (isSelfUpdate) {
    // self update - only update self
    modulesToUpdate.add(path)
  } else {
    // dep update
    for (const { deps } of mod.callbacks) {
      deps.forEach((dep) = > {
        if (acceptedPath === dep) {
          modulesToUpdate.add(dep)
        }
      })
    }
  }

  // determine the qualified callbacks before we re-import the modules
  // Only update functions that conform to the standard are retained
  const qualifiedCallbacks = mod.callbacks.filter(({ deps }) = > {
    return deps.some((dep) = > modulesToUpdate.has(dep))
  })

  // Change modulesToUpdate to the update function of the corresponding module
  await Promise.all(
    Array.from(modulesToUpdate).map(async (dep) => {
      const disposer = disposeMap.get(dep)
      if (disposer) await disposer(dataMap.get(dep))
      const [path, query] = dep.split(`? `)
      try {
        // Another request will be made, and the new module will come down, but the DOM tree will not change, and the downloaded file will have the ID of the module that is currently being updated
        const newMod = await import(
          /* @vite-ignore */
          path + `? t=${timestamp}${query ? ` &${query}` : ' '}`
        )
        moduleMap.set(dep, newMod)
      } catch (e) {
        warnFailedFetch(e, dep)
      }
    })
  )

  // Return a function whose contents were injected by accept in plugin-vue, i.e. the vue file is the render update method of vue
  // This will call the render method in the new file to update the module on the browser side
  return () = > {
    for (const { deps, fn } of qualifiedCallbacks) {
      fn(deps.map((dep) = > moduleMap.get(dep)))
    }
    const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
    console.log(`[vite] hot updated: ${loggedPath}`)}}Copy the code

The results of the fetchUpdate flow to queueUpdate, which puts the update task into a microtask that automatically collects renderings over a certain period of time.

async function queueUpdate(p: Promise"The (() = >void) | undefined>) {
  queued.push(p)
  if(! pending) { pending =true
    await Promise.resolve()
    pending = false
    constloading = [...queued] queued = [] ; (await Promise.all(loading)).forEach((fn) = > fn && fn())
  }
}
Copy the code

Vite simplified flow chart

conclusion

Vite’s use of the ES Module is amazing. It solves all the pain points of big project builds in one go, and with the perfect rollup set, any rollup plugin can be used in Vite.

Of course, Vite’s idea isn’t new. Snowpack’s use of es Modules has been around for a while.

Vite currently mainly solves the problem of dev environment, production environment or need to build to use, Vite use esBuild for production environment packaging, ESbuild use go development, native to native, interested friends can go to have a look, here is not to teach fish to fish.

Finally, thank you for your inner reading, if you feel good, you can follow, like, retweet a lot of support ~

I wish you all success in your work and promotion