This article is an original article, please quote the source, welcome everyone to collect and share 💐💐

background

Some time ago with Vite2. X made a Vue3 personal project, under the support of Vite, whether the project cold start, hot update and construction, compared with webpack speed has increased N00% (n≥10) above, with a strong curiosity, the Vite source code to get down to learn next, By the way, the water article is convenient to review later 😂😂😂.

Learn about the build tool development service “Dev Server”

A development service is an additional local service that the build tool launches when a developer develops a front-end project locally. For example, after executing the NPM run dev command, the framework performs the service startup operation. After the startup, you can enter http://localhost:xxxx (” XXXX “as the port number) through the browser to see the page you have developed.

OK, let’s talk about Vite’s local service… Its design is very unique, Vite is also through this mechanism to achieve more efficient processing speed and better development experience!

For comparison, let’s take a look at the traditional bundle package service startup method, using WebPack as an example.

Development services for Webpack

At the cold start of the project,webpackthroughentryImport file checks file dependencies layer by layer, for example, there are 3 TS files:

// a.ts
export const a = 1;

// b.ts
export const b = 2;

// sum.ts
import { a } from './a.ts';
import { b } from './b.ts';

export default sum() => a + b;


// The bundle looks something like this
// bundle.js
const a = 1;
const b = 2;

const sum = function() {
    return a + b;
}

export default sum;
Copy the code

For ease of understanding, the above code is abbreviated, but as you can see, webPack collects all dependencies and packs them into a bundle.js file before successfully starting the development service. This packaging method can effectively integrate the dependencies between modules, unify the output, and reduce the amount of resource loading. However, it also has disadvantages: First, the startup of the service needs to be completed by packaging the pre-dependent components. When the components become more and more complex, the startup time of the project will be longer and longer (tens of seconds or even minutes). Second, in the hot update project, even if the HRM method is used to diff file differences, the modified effect will take several seconds to be reflected in the browser. Over and over again, slow feedback can have a huge impact on a developer’s productivity and happiness.

Vite development services

The following is a reference to the official Vite development service parsing.

Vite improves development server startup times by initially separating application modules into dependent and source modules.

  • The dependencies are mostly pure JavaScript that does not change at development time. Some large dependencies, such as component libraries with hundreds of modules, are also expensive to handle. Dependencies also often exist in multiple modular formats (such as ESM or CommonJS). Vite will pre-build dependencies using esBuild. Esbuild is written using Go and is 10-100 times faster than a prepacker-built dependency written in JavaScript.
  • The source code usually contains files that are not directly JavaScript that need to be transformed (such as JSX, CSS, or Vue/Svelte components) and often edited. Also, not all source code needs to be loaded at the same time (such as code modules based on route splitting). Vite provides the source code in native ESM mode. This essentially lets the browser take over part of the packaging: Vite simply transforms the source code when the browser requests it and makes it available on demand. Code is dynamically imported according to the situation, which is only processed if it is actually used on the current screen.

The NPM run dev command enables Vite to start the local server and collect dependencies through the project portal. The process is called pre-bundling dependencies that are not available in the ESM format and large numbers of internal dependencies. After pre-optimization, when the page needs to load dependencies, resources are requested back through HTTP, so as to achieve true on-demand loading.

How to implement pre-optimization is detailed below.

Vite1.0 and 2.0 pre-optimization tool differences

Vite has released two major versions so far. There is a big difference between Vite1.0 and 2.0 pre-optimization. Vite2.0 uses the HTTP + Connect module at the bottom to replace some of the capabilities of the KOA framework in 1.0, according to the developers. In addition, the pre-optimized tool was replaced by esBuild plug-in of CommonJS rollup. The optimization of these two key points greatly increased the execution efficiency.

You can get a feel for the speed bonus of Esbuild

Esbuild performs better than rollup because of its underlying principles:

  1. Js is single-threaded serial, esbuild is a new process, and then multi-threaded parallel execution;
  2. Esbuild is written in go syntax, which is pure machine code and executes faster than JIT.
  3. The construction process is optimized by eliminating AST abstract syntax tree generation.

Key words: Connect, ESbuild

Vite preliminary optimization

Before, the clone Vite source code directly to the address of “making”, in the packages/Vite/SRC/node/server/index found ts server startup functions: CreateServer, where you can find the pre-optimized entry optimizeDeps method:

export async function createServer(
  inlineConfig: InlineConfig = {}
) :Promise<ViteDevServer> {
  // Omit a lot of code here...
  
  const runOptimize = async() = > {if (config.cacheDir) {
      server._isRunningOptimizer = true
      try {
        server._optimizeDepsMetadata = await optimizeDeps(
          config,
          config.server.force || server._forceOptimizeOnRestart
        )
      } finally {
        server._isRunningOptimizer = false
      }
      server._registerMissingImport = createMissingImporterRegisterFn(server)
    }
  }

  // A long code is omitted here...
  
  return server
}
Copy the code

Next we to packages/vite/SRC/node/optimizer index. The definition of ts find optimizeDeps method:

export async function optimizeDeps(
  config: ResolvedConfig,
  force = config.server.force,
  asCommand = false, newDeps? : Record<string.string>, // missing imports encountered after server has startedssr? :boolean
) :Promise<DepOptimizationMetadata | null> {
  // omit long code...

  const result = await build({
    absWorkingDir: process.cwd(),
    entryPoints: Object.keys(flatIdDeps),
    bundle: true.format: 'esm'.target: config.build.target || undefined.external: config.optimizeDeps? .exclude,logLevel: 'error'.splitting: true.sourcemap: true.outdir: cacheDir,
    ignoreAnnotations: true.metafile: true,
    define,
    plugins: [ ...plugins, esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr) ], ... esbuildOptions })// omit long code...
}
Copy the code

Build () comes from the build method of esbuild. If you are interested in the parameters, you can check the build-API website.

Pulgins includes the esbuildDepPlugin, which is the core logic of Vite’s esBuild package. The plugin works as follows:

Specific format file external

First, the plug-in externals files in a particular format, because they are not processed in the ESBuild phase, so they need to be found and parsed ahead of time. Array of externalTypes < fileType > type, can be in the packages/vite/SRC/node/optimizer/esbuildDepPlugin. The definition of ts to find it.

// externalize assets and commonly known non-js file types
build.onResolve(
  {
    filter: new RegExp(` \. (` + externalTypes.join('|') + `) (\? . *)? $`)},async ({ path: id, importer, kind }) => {
    const resolved = await resolve(id, importer, kind)
    if (resolved) {
      return {
        path: resolved,
        external: true}}})Copy the code

Parsing different modules

The developer divides packaging modules into two types: entry modules and dependency modules.

Import module: a module that is directly imported or specified by include, e.g., import Vue from ‘Vue ‘; Dependencies: The dependencies of the entry module itself, also called dependencies

function resolveEntry(id: string) {
  const flatId = flattenId(id)
  if (flatId in qualified) {
    return {
      path: flatId,
      namespace: 'dep'
    }
  }
}

build.onResolve(
  { filter: /^[\w@][^:]/ },
  async ({ path: id, importer, kind }) => {
    if(moduleListContains(config.optimizeDeps? .exclude, id)) {return {
        path: id,
        external: true}}// ensure esbuild uses our resolved entries
    let entry: { path: string; namespace: string } | undefined
    // if this is an entry, return entry namespaceresolve result if (! importer) {if ((entry = resolveEntry(id))) return entry
      // check if this is aliased to an entry - also return entry namespace
      const aliased = await _resolve(id, undefined.true)
      if (aliased && (entry = resolveEntry(aliased))) {
        return entry
      }
    }

    // use vite's own resolver
    const resolved = await resolve(id, importer, kind)
    if (resolved) {
      if (resolved.startsWith(browserExternalId)) {
        return {
          path: id,
          namespace: 'browser-external'}}if (isExternalUrl(resolved)) {
        return {
          path: resolved,
          external: true}}return {
        path: path.resolve(resolved)
      }
    }
  }
)
Copy the code

To make it easier to understand, here is a piece of pseudocode that executes the following logic before each dependency is packaged:

ifThe entry module resolves the module to namespace='dep'Processing flow ofelse
    ifResolve the module to namespace= for browser-External modules'browser-external'Processing flow ofifModules imported with HTTP (s) resolve modules to external reference moduleselseDirect resolution pathCopy the code

rightnamespacefordepDependency packaging

After module classification, the next step is to parse and package the DEP module.

build.onLoad({ filter: /. * /.namespace: 'dep' }, ({ path: id }) => {
  const entryFile = qualified[id]

  let relativePath = normalizePath(path.relative(root, entryFile))
  if (
    !relativePath.startsWith('/') &&
    !relativePath.startsWith('.. / ') && relativePath ! = ='. '
  ) {
    relativePath = `. /${relativePath}`
  }

  let contents = ' '
  const data = exportsData[id]
  const [imports, exports] = data
  if(! imports.length && !exports.length) {
    // cjs
    contents += `export default require("${relativePath}"); `
  } else {
    if (exports.includes('default')) {
      contents += `import d from "${relativePath}"; export default d; `
    }
    if (
      data.hasReExports ||
      exports.length > 1 ||
      exports[0]! = ='default'
    ) {
      contents += `\nexport * from "${relativePath}"`}}let ext = path.extname(entryFile).slice(1)
  if (ext === 'mjs') ext = 'js'
  return {
    loader: ext as Loader,
    contents,
    resolveDir: root
  }
})
Copy the code
  1. First of all toentryFileThe relative path of therelativePathStored in a variable;
  2. Analyze the module type. HerecontentsThe assignment. Analysis of the entry module by esbuild lexicalimportexportInformation, when both keywords are absent, is judged to be onecjsModule, which generates the following format contents;
    contents += `export default require("${relativePath}"); `
    Copy the code
  3. If the condition in step 2 is not met, the system considers it to be aesmModule, generate corresponding contents:
    contents += `import d from "${relativePath}"; export default d; `
    // or 
    contents += `\nexport * from "${relativePath}"`
    Copy the code
  4. Parse the file extension to get the correspondingloader;
  5. returnloaderType, module content, import path to esbuild package, syntax referenceesbuild onLoad result.

In steps 2 and 3, contents are stored as relative paths to modules (i.e., relativePath in step 1), which allows the program to generate the correct cache file directory structure.

rightnamespaceforbrowser-externalDependency packaging

      build.onLoad(
        { filter: /. * /.namespace: 'browser-external' },
        ({ path: id }) => {
          return {
            contents:
              `export default new Proxy({}, {
  get() {
    throw new Error('Module "${id}" has been externalized for ` +
              `browser compatibility and cannot be accessed in client code.') } })`}})Copy the code

Compatible withyarn pnpThe environment

if (isRunningWithYarnPnp) {
  build.onResolve(
    { filter: /. * / },
    async ({ path, importer, kind, resolveDir }) => ({
      // pass along resolveDir for entries
      path: await resolve(path, importer, kind, resolveDir)
    })
  )
  build.onLoad({ filter: /. * / }, async (args) => ({
    contents: await require('fs').promises.readFile(args.path),
    loader: 'default'}}))Copy the code

conclusion

In general, in the pre-optimization area, Vite will first classify dependent modules and reference paths for different types of resolved modules, then start ESbuild package and output ES Module, and finally pull resources through HTTP network to assemble resources into the application to complete on-demand loading.

Write in the last

At this point, Vite2.0 pre-optimization part is basically finished, due to the short time, in some details may be a little rough, but the main process is roughly the same, details will be gradually filled up 🤝🤝 when free.

In addition, hereafter have time to make a Vite rendering mechanism, when a resource request return, how to apply colours to a drawing from the original form into the final, CSS, js | ts vue template.