“This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!”

Vite is a Web development tool developed by the author of Vue Yu Yuxi, Yu Yuxi made a brief introduction to Vite when he promoted it on weibo:

Vite, a browser-based native ES Imports development server. Using the browser to parse imports, compiling them back as needed on the server side, bypassing packaging altogether and making them available on the server as needed. Not only does Vue file support exist, but hot updates are fixed, and the speed of hot updates does not slow down as more modules are added. The same code can be packaged in Rollup for a production environment. Although still relatively rough, but this direction I think there is potential, well done can completely solve the problem of changing a line of code and so on half a day hot update.

We can extract some key information from this passage

  • Vite is based on the ESM, thus enabling fast start and real-time module hot update capabilities;
  • Vite implements on-demand compilation on the server side.

So to put it bluntly: Vite has no packaging and build process in the development environment.

The ESM import syntax written by the developer in the code is sent directly to the server, and the server directly processes the ESM module content and sends it to the browser. Modern browsers then parse script modules and make HTTP requests for each imported module, and the server continues to process and respond to these HTTP requests.

Vite implementation principle interpretation

Environment set up

The idea of Vite is easy to understand and easy to implement. Next, let’s analyze the Vite source code

First, we create a learning environment, create a Vite based application, and launch:

$ yarn global add vite
$ npm init vite-app vite-app

$ cd vite-app

$ yarn

$ yarn dev

Copy the code

You get a directory structure like the following:

Start-up project:

$ yarn dev
Copy the code

The browser requests: **http://localhost:3000/**, and the obtained content is the index. HTML content of our application project.

Entrance to the source code

Pull the source code, open the command line implementation section,

cli
  .command('[root]') // default command
  .alias('serve')
  .option('--host [host]'.`[string] specify hostname`)
  .option('--port <port>'.`[number] specify port`)
  .option('--https'.`[boolean] use TLS + HTTP/2`)
  .option('--open [path]'.`[boolean | string] open browser on startup`)
  .option('--cors'.`[boolean] enable CORS`)
  .option('--strictPort'.`[boolean] exit if specified port is already in use`)
  .option('-m, --mode <mode>'.`[string] set env mode`)
  .option(
    '--force'.`[boolean] force the optimizer to ignore the cache and re-bundle`
  )
  .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 {
      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

The createServer starts an HTTP service that responds to browser requests.

const { createServer } = await import('./server')

Copy the code

The createServer method is implemented with the following code

export async function createServer(inlineConfig) {
    // Configuration file processing
    const config = await resolveConfig(inlineConfig, 'serve'.'development')
    const root = config.root
    const serverConfig = config.server
    const httpsOptions = await resolveHttpsConfig(config)
    let { middlewareMode } = serverConfig
    // Create vite server in middleware mode, not using Vite server
    if (middlewareMode === true) {
      middlewareMode = 'ssr'
    }
  
    const middlewares = connect()
    // Create an HTTP instance. Note that middlewareMode = 'SSR' is used to create the server using middleware
    const httpServer = middlewareMode
      ? null
        : await resolveHttpServer(serverConfig, middlewares, httpsOptions)
    / / HMR connections
    const ws = createWebSocketServer(httpServer, config, httpsOptions)
  
    const{ ignored = [], ... watchOptions } = serverConfig.watch || {}// File listening
    const watcher = chokidar.watch(path.resolve(root), {
      ignored: ['**/node_modules/**'.'**/.git/**'. ignored],ignoreInitial: true.ignorePermissionErrors: true.disableGlobbing: true. watchOptions })const plugins = config.plugins
    const container = await createPluginContainer(config, watcher)
    const moduleGraph = new ModuleGraph(container)
    const closeHttpServer = createServerCloseFn(httpServer)
  
    // eslint-disable-next-line prefer-const
    let exitProcess
  
    const server = {
      config: config,
      middlewares,
      get app() {
        config.logger.warn(
          `ViteDevServer.app is deprecated. Use ViteDevServer.middlewares instead.`
        )
        return middlewares
      },
      httpServer,
      watcher,
      pluginContainer: container,
      ws,
      moduleGraph,
      transformWithEsbuild,
      transformRequest(url, options) {
        return transformRequest(url, server, options)
      },
      transformIndexHtml: null.ssrLoadModule(url) {
        if(! server._ssrExternals) { server._ssrExternals = resolveSSRExternal( config, server._optimizeDepsMetadata ?Object.keys(server._optimizeDepsMetadata.optimized)
              : []
          )
        }
        return ssrLoadModule(url, server)
      },
      ssrFixStacktrace(e) {
        if (e.stack) {
          e.stack = ssrRewriteStacktrace(e.stack, moduleGraph)
        }
      },
      listen(port, isRestart) {
        return startServer(server, port, isRestart)
      },
      async close() {
        process.off('SIGTERM', exitProcess)
  
        if(! middlewareMode && process.env.CI ! = ='true') {
          process.stdin.off('end', exitProcess)
        }
  
        await Promise.all([
          watcher.close(),
          ws.close(),
          container.close(),
          closeHttpServer()
        ])
      },
      _optimizeDepsMetadata: null._ssrExternals: null._globImporters: {},
      _isRunningOptimizer: false._registerMissingImport: null._pendingReload: null
    }
  
    server.transformIndexHtml = createDevHtmlTransformFn(server)
  
    exitProcess = async() = > {try {
        await server.close()
      } finally {
        process.exit(0)}}// Stop the service if the terminating signal handle is received
    process.once('SIGTERM', exitProcess)
  
    if(! middlewareMode && process.env.CI ! = ='true') {
      process.stdin.on('end', exitProcess)
    }
  
    watcher.on('change'.async (file) => {
      file = normalizePath(file)
      // invalidate module graph cache on file change
      moduleGraph.onFileChange(file)
      if(serverConfig.hmr ! = =false) {
        try {
          await handleHMRUpdate(file, server)
        } catch (err) {
          ws.send({
            type: 'error'.err: prepareError(err)
          })
        }
      }
    })
  
    watcher.on('add'.(file) = > {
      handleFileAddUnlink(normalizePath(file), server)
    })
  
    watcher.on('unlink'.(file) = > {
      handleFileAddUnlink(normalizePath(file), server, true)})// Plug-in processing
    // apply server configuration hooks from plugins
    const postHooks = []
    for (const plugin of plugins) {
      if (plugin.configureServer) {
        postHooks.push(await plugin.configureServer(server))
      }
    }
  
    // The following is some middleware processing
    // Internal middlewares ------------------------------------------------------
  
    // request timer
    // Request time debugging
    if (process.env.DEBUG) {
      middlewares.use(timeMiddleware(root))
    }
  
    // cors (enabled by default)
    const { cors } = serverConfig
    if(cors ! = =false) {
      middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
    }
  
    // proxy
    const { proxy } = serverConfig
    if (proxy) {
      middlewares.use(proxyMiddleware(httpServer, config))
    }
  
    // base
    if(config.base ! = ='/') {
      middlewares.use(baseMiddleware(server))
    }
  
    // open in editor support
    middlewares.use('/__open-in-editor', launchEditorMiddleware())
  
    // hmr reconnect ping
    // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ... `
    middlewares.use('/__vite_ping'.function viteHMRPingMiddleware(_, res) {
      res.end('pong')})//decode request url
    middlewares.use(decodeURIMiddleware())
  
    // serve static files under /public
    // this applies before the transform middleware so that these files are served
    // as-is without transforms.
    if (config.publicDir) {
      middlewares.use(servePublicMiddleware(config.publicDir))
    }
  
    // main transform middleware
    middlewares.use(transformMiddleware(server))
  
    // serve static files
    middlewares.use(serveRawFsMiddleware(server))
    middlewares.use(serveStaticMiddleware(root, config))
  
    // spa fallback
    if(! middlewareMode || middlewareMode ==='html') {
      middlewares.use(
        history({
          logger: createDebugger('vite:spa-fallback'),
          // support /dir/ without explicit index.html
          rewrites: [{from: / / / $/.to({ parsedUrl }) {
                const rewritten = parsedUrl.pathname + 'index.html'
                if (fs.existsSync(path.join(root, rewritten))) {
                  return rewritten
                } else {
                  return `/index.html`}}}]})}// run post config hooks
    // This is applied before the html middleware so that user middleware can
    // serve custom content instead of index.html.
    postHooks.forEach((fn) = > fn && fn())
  
    if(! middlewareMode || middlewareMode ==='html') {
      // transform index.html
      middlewares.use(indexHtmlMiddleware(server))
      // handle 404s
      // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ... `
      middlewares.use(function vite404Middleware(_, res) {
        res.statusCode = 404
        res.end()
      })
    }
  
    // error handlermiddlewares.use(errorMiddleware(server, !! middlewareMode))const runOptimize = async() = > {if (config.cacheDir) {
        server._isRunningOptimizer = true
        try {
          server._optimizeDepsMetadata = await optimizeDeps(config)
        } finally {
          server._isRunningOptimizer = false
        }
        server._registerMissingImport = createMissingImporterRegisterFn(server)
      }
    }
  
    if(! middlewareMode && httpServer) {// overwrite listen to run optimizer before server start
      const listen = httpServer.listen.bind(httpServer)
      httpServer.listen = (async(port, ... args) => {try {
          await container.buildStart({})
          await runOptimize()
        } catch (e) {
          httpServer.emit('error', e)
          return
        }
        returnlisten(port, ... args) }) httpServer.once('listening'.() = > {
        // update actual port since this may be different from initial value
        serverConfig.port = (httpServer.address()).port
      })
    } else {
      await container.buildStart({})
      await runOptimize()
    }
  
    return server
  }
Copy the code

The code is very long, but it simply does these things:

  • Create a server that acts as a static server to respond to requests from the application
  • Create a webSocket that provides HMR
  • Use Chokidar to enable file listening and process file modifications
  • Plug-in processing
  • Listener handle to stop service if a stop signal is encountered

Enable the function of the server

After the browser visits http://localhost:3000/, it gets the following:

<body>

  <di v id="app"></div>

  <script type="module" src="/src/main.js"></script>

</body>

Copy the code

When the type attribute of a script tag is Module, the browser sends an HTTP request to the module. Processed by Vite Server.

We can see that after Vite Server processing http://localhost:3000/src/main.js request, finally returned to the content of the picture above. However, this content is different from the./ SRC /main.js in our project

Here’s the source code

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')

Copy the code

This is what happens after Vite

import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css? import'

createApp(App).mount('#app')

Copy the code

Here we break it down into two parts.

Import {createApp} from ‘/@modules/vue.js’ import {createApp} from ‘/@modules/vue.js’ import {createApp} from ‘/@modules/vue.js’ import {createApp} from ‘/@modules/vue.js’ If you use the module name “import”, an error will be reported immediately.

Add /@module/ to import from ‘A’ with the resolve plugin when Vite Server processes A request.

The whole process and call link is long, I do a simple summary of Vite handling import method:

  • Get the body content of the requested path in the createServer;

  • Use es-module-lexer to parse the resource AST and get the import content.

  • If it is determined that the import resource is an absolute path, it can be considered as the NPM module, and the processed resource path is returned. For example, vue → /@modules/vue.

For examples such as: import App from ‘./ app.vue ‘and import ‘./index.css’, the same is true:

  • Get the body content of the requested path in the createSercer;

  • Use es-module-lexer to parse the resource AST and get the import content.

  • If it is determined that the import resource is a relative path, it can be considered as the resource in the project application, and the processed resource path is returned. Vue → / SRC/app.vue

Next, the browser requests the following items based on the contents of main.js:

/@modules/vue.js
/src/App.vue
/src/index.css?import
Copy the code

For the /@module/ class request it is easy, we just need to complete the following three steps:

  • Get the body content of the requested path in the createServer middleware.

  • Check whether the path starts with /@module/. If so, pull out the package name (in this case vue.js).

  • Go to node_modules and find the corresponding NPM library and return the contents.

The above steps are implemented in Vite using the Resolve middleware.

Next, it is time to process the/SRC/app.vue class request, which involves the compilation capability of the Vite server.

Let’s take a look at the results first. Compared to app.vue in the project, the results obtained by the browser request are obviously very different:

In fact, a single file component such as app.vue corresponds to script, style, and template. When processed by the Vite Server, the Server processes script, style, and template separately. The corresponding middleware is @vitejs/plugin-vue. The implementation of this plug-in is simple. It takes a.vue file request, parses the single file component using parseSFC, and splits the single file component into content like above using compileSFCMain. The key middleware content can be found in the source vuePlugin. In the source code, what parseSFC does specifically is to call @vue/compiler-sfc for single-file component parsing. Boiled down to my own logic to help you understand:

In general, each.vue single file component is split into multiple requests. For example, in the above scenario, the browser receives the actual content corresponding to app.vue and issues helloWorld.vue and app.vue? Type =template request (the type query indicates template or style). The createServer processes the requests separately and returns them, and these requests are still processed separately by the @vitejs/plugin-vue plugin mentioned above: For a template request, the service uses @vue/compiler-dom to compile the template and return the content.

For the above mentioned at http://localhost:3000/src/index.css? The import request is a little more special. In the CSS plug-in, it is resolved using the transform of the cssPostPlugin object:

    transform(css, id, ssr) {
      if(! cssLangRE.test(id) || commonjsProxyRE.test(id)) {return
      }

      constmodules = cssModulesCache.get(config)! .get(id)const modulesCode =
        modules && dataToEsm(modules, { namedExports: true.preferConst: true })

      if (config.command === 'serve') {
        if (isDirectCSSRequest(id)) {
          return css
        } else {
          // server only
          if (ssr) {
            return modulesCode || `export default The ${JSON.stringify(css)}`
          }
          return [
            `import { updateStyle, removeStyle } from The ${JSON.stringify(
              path.posix.join(config.base, CLIENT_PUBLIC_PATH)
            )}`.`const id = The ${JSON.stringify(id)}`.`const css = The ${JSON.stringify(css)}`.`updateStyle(id, css)`.// css modules exports change on edit so it can't self accept
            `${modulesCode || `import.meta.hot.accept()\nexport default css`}`.`import.meta.hot.prune(() => removeStyle(id))`
          ].join('\n')}}// build CSS handling ----------------------------------------------------

      // record css
      styles.set(id, css)

      return {
        code: modulesCode || `export default The ${JSON.stringify(css)}`.map: { mappings: ' ' },
        // avoid the css module from being tree-shaken so that we can retrieve
        // it in renderChunk()
        moduleSideEffects: 'no-treeshake'}},Copy the code

Call the transform method in cssPostPlugin:

This method can be carried in the browser updateStyle method, like http://localhost:3000/src/index.css? The source code for import is as follows:

import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/src/components/HelloWorld.vue? vue&type=style&index=0&scoped=true&lang.css");import { updateStyle, removeStyle } from "/@vite/client"
const id = "/Users/study/vite-app/src/components/HelloWorld.vue? vue&type=style&index=0&scoped=true&lang.css"
const css = "\nh1[data-v-469af010] {\n font-size:18px; \n}\n"
updateStyle(id, css)
import.meta.hot.accept()
export default css
import.meta.hot.prune(() = > removeStyle(id))
Copy the code

Finally, the style is inserted into the browser.

So far, we have parsed and enumerated a lot of source content. The above content needs to follow the train of thought, step by step comb, also strongly suggest you open Vite source code to do their own analysis. If you are still a little confused, don’t worry. Read it again with the following illustration. I believe it will be more fruitful.

And webpack contrast

Webpack Bundleless

Vite Bundleless

conclusion

  • Vite takes advantage of the browser’s native support for ESM, omits the packaging of modules, eliminates the need to generate bundles, and is therefore faster at first boot and HMR friendly.

  • In the Vite development mode, the Node server is started and the module rewriting (such as parsing and compiling of single files) and request processing are completed on the server side, so as to realize the real on-demand compilation.

  • Vite Server relies on the middleware for most of its logic. The middleware, after intercepting the request, completes the following:

    • Handle ESM syntax, such as converting import third-party dependency paths in business code to browser-recognized dependency paths;

    • Compile files such as.ts and.vue in real time.

    • Compile Sass/Less modules that need to be precompiled.

    • Establish a socket connection with the browser to implement HMR.