Vite claims to be the next generation of front-end development and build tools, and is now gaining popularity in the front-end community. It uses the new idea of unbundle to improve the overall front-end development experience. This is a qualitative improvement in performance and speed over traditional WebPack builds. So the following article, will mainly introduce how to use and how to work.

What is the

Vite comes from French meaning rapid, quickly. Reflects its core selling point — “fast”. Overall functionality similar to pre-configured WebPack plus Dev Server is implemented to improve the overall build speed of front-end projects. According to the tests, the server startup speed and HMR can reach the millisecond level basically.

Method of use

Vite is simple to use, and currently officially provides scaffolding to jumpstart a new project:

npm init @vitejs/app

// yarn
yarn create @vitejs/app
Copy the code

It then goes into interactive mode, allowing you to select the corresponding template, enter the project name, and so on. To manually specify the template and project name, run the following command:

npm init @vitejs/app my-vite-demo --template react
Copy the code

All relevant project templates specified here are available at github.com/vitejs/awes… Found in the warehouse. Once the project is started, you can start and preview directly using the following commands

# Install dependencies
yarn install

Use in the development environment
yarn dev

# packaged
yarn run build
# to preview the effect of the package
yarn run serve
Copy the code

Plug-in mechanism

Vite mainly uses plug-ins to extend functions. You can see that the simplest initialization project mentioned above is started, under its configuration file vite. Config. ts, there is the following code:

import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'

// [https:](https://vitejs.dev/config/) [//vitejs.dev/config/](https://vitejs.dev/config/)
export default defineConfig({
  plugins: [reactRefresh()]
})
Copy the code

As you can see, there is a reference to a plug-in called reactRefresh that can modify react components without losing their state. Similarly, if additional functionality is needed, it can be extended using vite’s plug-in mechanism. These third-party plug-in modules are available at github.com/vitejs/awes… This warehouse was found. Also, since the Vite plugin extends rollup’s interface, implementing your own vite plugin is similar to writing the Rollup plugin. Here, you can refer to the plugin API | Vite official Chinese documents.

The working principle of

With all that said, how does Vite achieve an ultra-fast development experience? Github.com/vitejs/vite… As we all know, traditional packaged build tools need to parse and build the entire application from the entry file before the server is started. So, a lot of time is spent on dependency build, build compile.

Vite mainly follows the ESM(Es Modules) specification to execute the code. Since modern browsers generally support the ESM specification, there is no need to package and compile the code into ES5 modules to run in the browser during the development phase. We just need to start from the entry file and load the corresponding module into the browser when we encounter the corresponding import statement. Therefore, this non-packaging feature is what makes Vite so fast.

At the same time, the translation of ts/ JSX files will be accelerated with the help of ESbuild. Internally, Vite starts a Dev Server and accepts HTTP requests from individual modules, leaving the browser to parse and handle module loading itself. The following uses the official demo as an example. You can see that when you visit the corresponding page after running, the bundle.js file is not loaded as a whole, but loaded by module.

In terms of code implementation, after the YARN dev command is allowed, Vite starts a dev server, loads various middleware, and listens for front-end access requests. Github.com/vitejs/vite…

const { createServer } = await import('./server')
try {
  const server = await createServer({
    root,
    base: options.base,
    mode: options.mode,
    configFile: options.config,
    logLevel: options.logLevel,
    clearScreen: options.clearScreen,
    server: cleanOptions(options) as ServerOptions
  })
  await server.listen()
} catch (e) {
  createLogger(options.logLevel).error(
    chalk.red(`error when starting dev server:\n${e.stack}`)
  )
  process.exit(1)}Copy the code

At the same time, Vite’s own client code is injected into the development environment to listen to HMR and other processing. Github.com/vitejs/vite…

Bare module rewrite

Because the ESM currently does not support raw module loading such as import vue from “vue” (import Maps proposal github.com/WICG/import… Can solve this problem, but not yet), so you need to override the module loading address. Convert this to something like import vue from “/ @modules/vue”. The implementation principle is mainly through the es-module-Lexer and magic-String package replacement, compared with AST semantic parsing and transformation, in performance advantages. Here are the two packages:

Es-module-lexer

Github.com/guybedford/… Although the lexical analysis of JS code usually uses Babel, Acorn and other tools, but for ESM files, using the es-Modole-Lexer library can greatly improve the performance, its compressed volume is only 4KB. Moreover, according to the official example given, the 720KB Angular1 library takes more than 100ms to parse through Acorn, while using the ES-Modole-Lexer library takes only 5ms, which is nearly a 20-fold improvement in performance.

Magic-string

Github.com/rich-harris… Vite uses a lot of this library for string substituting to avoid manipulating the AST. The specific code can be referred to github.com/vitejs/vite… The overall idea is something like this:

import { init, parse as parseImports, ImportSpecifier } from 'es-module-lexer'

// Parse the import statement with the es-module-lexer
imports = parseImports(source)[0]

// Then use magic-String to replace the source code during dependency analysis and path rewriting.
let s: MagicString | undefined
const str = () = > s || (s = new MagicString(source))

// Omit part of the code
for (let index = 0; index < imports.length; index++) {
        const {
          s: start,
          e: end,
          ss: expStart,
          se: expEnd,
          d: dynamicIndex,
          n: specifier
        } = imports[index]

// Omit part of the code

// Parse the code
 const { imports, importsString, exp, endIndex, base, pattern } =
              await transformImportGlob(
                source,
                start,
                importer,
                index,
                root,
                normalizeUrl
              )
            str().prepend(importsString)
            str().overwrite(expStart, endIndex, exp)
            imports.forEach((url) = > importedUrls.add(url.replace(base, '/')))
            if(! (importerModule.file!in server._globImporters)) {
              server._globImporters[importerModule.file!] = {
                module: importerModule,
                importGlobs: [] } } server._globImporters[importerModule.file!] .importGlobs.push({ base, pattern }) }// Finally return the processed code
if (s) {
  return s.toString()
} else {
  return source
}       
Copy the code

Custom block processing

Is this functionality via the link at the back of the module? Type = parameter to distinguish between different blocks. Each block is then processed individually.

Depending on the block type, different plug-ins are used to compile the transform. In the following example, when processing a file ending in xxx.json, the JSON plug-in will first match whether the module ID is JSON or not. Then the translation works.


// Custom json filter for vite
const jsonExtRE = /\.json($|\?) (? ! commonjs-proxy)/

export function jsonPlugin(
  options: JsonOptions = {},
  isBuild: boolean
) :Plugin {
  return {
    name: 'vite:json'.transform(json, id) {
      if(! jsonExtRE.test(id))return null
      if (SPECIAL_QUERY_RE.test(id)) return null

      try {
        if (options.stringify) {
          if (isBuild) {
            return {
              code: `export default JSON.parse(The ${JSON.stringify(
                JSON.stringify(JSON.parse(json))
              )}) `.map: { mappings: ' '}}}else {
            return `export default JSON.parse(The ${JSON.stringify(json)}) `}}const parsed = JSON.parse(json)
        return {
          code: dataToEsm(parsed, {
            preferConst: true.namedExports: options.namedExports
          }),
          map: { mappings: ' '}}}catch (e) {
        const errorMessageList = /[\d]+/.exec(e.message)
        const position = errorMessageList && parseInt(errorMessageList[0].10)
        const msg = position
          ? `, invalid JSON syntax found at line ${position}`
          : `. `
        this.error(`Failed to parse JSON file` + msg, e.idx)
      }
    }
  }
}
Copy the code

HMR

Hot updates are an important part of the front-end development experience, so Vite relies on the following steps to implement HMR functionality:

  1. Record the module dependency chain when rewriting the module addressimportMaps. This allows you to know which files need to be hot updated during subsequent updates.

  1. Can be used in codeimport.meta.hotInterface to mark “HMR Boundary”.

  1. Then, when the file is updated, it follows the previous recordimoprtMapsThe chain structure finds the corresponding “HMR Boundary” from where the corresponding updated module is reloaded.

  1. If the corresponding boundary is not encountered, the entire application is reflushed.

The use method is as follows:

import foo from './foo.js'

foo()

if (import.meta.hot) {
    import.meta.hot.accept('./foo.js'.(newFoo) = > {
        newFoo.foo()
    })
}
Copy the code

The following will be specific code to introduce its principle. Client logic: github.com/vitejs/vite…

// record for HMR import chain analysis
// make sure to normalize away base
importedUrls.add(url.replace(base, '/'))

Copy the code
if(hasHMR && ! ssr) { debugHmr(`${
      isSelfAccepting
        ? `[self-accepts]`
        : acceptedUrls.size
        ? `[accepts-deps]`
        : `[detected api usage]`
    } ${prettyImporter}`
  )
  // Inject Vite client code into user business code
  str().prepend(
    `import { createHotContext as __vite__createHotContext } from "${clientPublicPath}"; ` +
      `import.meta.hot = __vite__createHotContext(The ${JSON.stringify(
        importerModule.url
      )}); `)}Copy the code

Github.com/vitejs/vite…

case 'update':
     notifyListeners('vite:beforeUpdate', payload)
      // When an error occurs, reload the entire page
      if (isFirstUpdate && hasErrorOverlay()) {
        window.location.reload()
        return
      } else {
        clearErrorOverlay()
        isFirstUpdate = false
      }
      
      payload.updates.forEach((update) = > {
        if (update.type === 'js-update') {
          // js update logic, will enter a cache queue, batch update, so as to ensure the order of update
          queueUpdate(fetchUpdate(update))
        } else {
          // CSS update logic. When the CSS detects an update, it directly replaces the link of the corresponding module and initiates the request again
          let { path, timestamp } = update
          path = path.replace(/ \? . * /.' ')

          const el = (
            [].slice.call(
              document.querySelectorAll(`link`))as HTMLLinkElement[]
          ).find((e) = > e.href.includes(path))
          if (el) {
            const newPath = `${path}${
              path.includes('? ')?'&' : '? '
            }t=${timestamp}`
            el.href = new URL(newPath, el.href).href
          }
          console.log(`[vite] css hot updated: ${path}`)}})break
break
Copy the code

Server processing HMR module update logic: github.com/vitejs/vite…

export async function handleHMRUpdate(
  file: string,
  server: ViteDevServer
) :Promise<any> {
  const { ws, config, moduleGraph } = server
  const shortFile = getShortName(file, config.root)

  const isConfig = file === config.configFile
  const isConfigDependency = config.configFileDependencies.some(
    (name) = > file === path.resolve(name)
  )
  constisEnv = config.inlineConfig.envFile ! = =false && file.endsWith('.env')
  if (isConfig || isConfigDependency || isEnv) {
    / / restart the server
    await restartServer(server)
    return
  }

  // (dev only) the client itself cannot be hot updated.
  if (file.startsWith(normalizedClientDir)) {
    ws.send({
      type: 'full-reload'.path: The '*'
    })
    return
  }

  const mods = moduleGraph.getModulesByFile(file)

  // check if any plugin wants to perform custom HMR handling
  const timestamp = Date.now()
  const hmrContext: HmrContext = {
    file,
    timestamp,
    modules: mods ? [...mods] : [],
    read: () = > readModifiedFile(file),
    server
  }

  for (const plugin of config.plugins) {
    if (plugin.handleHotUpdate) {
      const filteredModules = await plugin.handleHotUpdate(hmrContext)
      if (filteredModules) {
        hmrContext.modules = filteredModules
      }
    }
  }

  if(! hmrContext.modules.length) {// html file cannot be hot updated
    if (file.endsWith('.html')) {
      [config.logger.info](http://config.logger.info/)(chalk.green(`page reload `) + chalk.dim(shortFile), {
        clear: true.timestamp: true
      })
      ws.send({
        type: 'full-reload'.path: config.server.middlewareMode
          ? The '*'
          : '/' + normalizePath(path.relative(config.root, file))
      })
    } else {
      // loaded but not in the module graph, probably not js
      debugHmr(`[no modules matched] ${chalk.dim(shortFile)}`)}return
  }

  updateModules(shortFile, hmrContext.modules, timestamp, server)
}

function updateModules(
  file: string,
  modules: ModuleNode[],
  timestamp: number,
  { config, ws }: ViteDevServer
) {
  const updates: Update[] = []
  const invalidatedModules = new Set<ModuleNode>()
  let needFullReload = false

  for (const mod of modules) {
    invalidate(mod, timestamp, invalidatedModules)
    if (needFullReload) {
      continue
    }

    const boundaries = new SetThe < {boundary: ModuleNode
      acceptedVia: ModuleNode
    }>()
    
    // Pass updates up until a boundary is reached
    const hasDeadEnd = propagateUpdate(mod, timestamp, boundaries)
    if (hasDeadEnd) {
      needFullReload = true
      continue} updates.push( ... [...boundaries].map(({ boundary, acceptedVia }) = > ({
        type: `${boundary.type}-update` as Update['type'],
        timestamp,
        path: boundary.url,
        acceptedPath: acceptedVia.url
      }))
    )
  }

  if (needFullReload) {
    // Reload the page
  } else {
   // The Websocket listens to the update module and does the corresponding processing.
    ws.send({
      type: 'update',
      updates
    })
  }
}
Copy the code

Optimization strategy

Because the vite package is loaded by the browser module by module, it is easy to have a cascade of HTTP requests (the browser can concurrently request up to six requests at a time). In order to solve this problem, Vite mainly adopted three solutions.

  1. Pre-packaged to ensure that each dependency corresponds to only one request/file. Such as lodash. Please refer to github.com/vitejs/vite…

  2. Code split. This can be done with the manualChunks built into RollUp.

  3. Etag 304 status code that allows the browser to directly use the browser cache during repeated loading.

Github.com/vitejs/vite…

// check if we can return 304 early
const ifNoneMatch = req.headers['if-none-match']
if (
  ifNoneMatch &&
  (awaitmoduleGraph.getModuleByUrl(url))? .transformResult? .etag === ifNoneMatch ) { isDebug && debugCache(` [304]${prettifyUrl(url, root)}`)
  res.statusCode = 304
  return res.end()
}
Copy the code

The use of esbuild

Github.com/vitejs/vite… Use ESbuild to convert TS/JSX files for faster compilation.

export async function transformWithEsbuild(
  code: string,
  filename: string, options? : TransformOptions, inMap? :object
) :Promise<ESBuildTransformResult> {
  // if the id ends with a valid ext, use it (e.g. vue blocks)
  // otherwise, cleanup the query before checking the ext
  const ext = path.extname(
    /\.\w+$/.test(filename) ? filename : cleanUrl(filename)
  )

  let loader = ext.slice(1)
  if (loader === 'cjs' || loader === 'mjs') {
    loader = 'js'
  }

  const resolvedOptions = {
    loader: loader as Loader,
    sourcemap: true.// ensure source file name contains full query
    sourcefile: filename, ... options }as ESBuildOptions

  delete resolvedOptions.include
  delete resolvedOptions.exclude
  delete resolvedOptions.jsxInject

  try {
    const result = await transform(code, resolvedOptions)
    if (inMap) {
      const nextMap = JSON.parse(result.map)
      nextMap.sourcesContent = []
      return {
        ...result,
        map: combineSourcemaps(filename, [
          nextMap as RawSourceMap,
          inMap as RawSourceMap
        ]) as SourceMap
      }
    } else {
      return {
        ...result,
        map: JSON.parse(result.map)
      }
    }
  } catch (e) {
    debug(`esbuild error with options used: `, resolvedOptions)
    // patch error information
    if (e.errors) {
      e.frame = ' '
      e.errors.forEach((m: Message) = > {
        e.frame += `\n` + prettifyMessage(m, code)
      })
      e.loc = e.errors[0].location
    }
    throw e
  }
}
Copy the code

conclusion

In general, Vite is a very different approach to front-end build tools than WebPack, and solves the problem of slow builds during the front-end development phase. It is expected to take the front-end development experience to the next level. At the same time, the source code of vite.js is also in the process of continuous iteration, if you want to know more about its specific implementation details, or hope to read its source code in person. This paper mainly hopes to play a role in attracting jade.

Reference documentation

Cn. Vitejs. Dev/guide / # over…

www.youtube.com/watch?v=xXr…

www.youtube.com/watch?v=fgw…