Optimization of 2.0

This chapter will cover some of the work that Vite2.0 is doing in pre-bundling. For a better reading experience and a more complete chapter reading. We recommend that you read it at the original address, which contains all updates to vite1.0-Vite2.0 articles. Vite – design. The firm. Sh/guide / 2.0 / o…

Functions overview

Vite2.0, while the underlying code is a big change from 1.0, doesn’t look very different in terms of overall philosophy and usage at the moment.

One major change in Vite2.0’s underlying code is probably the use of the HTTP + Connect module instead of some of the ability to use the KOA framework directly in 1.0. And the pre-optimized tool was replaced by esBuild with the CommonJS plug-in of Rollup. During the use of 1.0, I found some bugs of commonJS plug-in of rollup and raised some issues. However, SINCE I was busy developing my own SSR framework, I did not follow up the subsequent progress. Now that you see esbuild 2.0, not only is the build speed much faster, but there are fewer bugs. Before officially reading the source code, I thought Vite just did the simple operation of the module format: ESM, but after careful reading, I found that Vite did a lot of work. Vite2.0 code is very good to learn whether it is the warehouse specification or the specific code, and the size is not easy to debug, than Webpack these big tools estimated that even the author can not master all the code is much better.

Local debugging

The debugging mode is basically the same as 1.0, except that the 2.0 architecture is changed to the form of Monorepo. Of course, we do not need to take care of other packages, just need to debug Vite.

$ git clone [email protected]:vitejs/vite.git
$ cd vite && yarn
$ cd packages/vite && yarn build && yarn link
$ yarn dev
Copy the code

Then link Vite by creating a simple example using Vite scaffolding

$ npm init @vitejs/app my-vue-app --template vue
$ cd my-vue-app && yarn && yarn link vite
$ npx vite optimize --force
Copy the code

Then you can have fun debugging the source code

Vite esbuild plug-in parsing

We’ll skip the logic of the Vite Resolve module, which isn’t important in this chapter, and just look at what Vite does with esBuild

// vite/src/node/optimizer/index.ts

const esbuildService = await ensureService()
await esbuildService.build({
  entryPoints: Object.keys(flatIdDeps),
  bundle: true.format: 'esm'.external: config.optimizeDeps? .exclude,logLevel: 'error'.splitting: true.sourcemap: true.outdir: cacheDir,
  treeShaking: 'ignore-annotations'.metafile: esbuildMetaPath,
  define,
  plugins: [esbuildDepPlugin(flatIdDeps, flatIdToExports, config)]
})
Copy the code

Here’s where the final Vite calls the ESbuild API, so let’s take a look at what it does.

entryPoints

EntryPoints are the entryPoints for packaging, so let’s not worry about how Vite collects module dependencies. We’ll look at that in a later chapter. All you need to know is that the default example flatIdDeps looks something like this.

{
  vue: '/Users/yuuang/Desktop/my-vue-app/node_modules/vue/dist/vue.runtime.esm-bundler.js'
}
Copy the code

Vite will find the absolute path of the final parsed file based on the order of module, jsNext :main, and jsNext fields. For example, the Module field of Vue points to dist/ vue.Runtime.esm-bundler.js, so the resulting object is shown above. Keys (entryPoints: [‘vue’]) is an array of module dependencies when your application relies on other modules such as entryPoints: [‘vue’, ‘vue-router’].

bundle

Bundle: true encapsulates the module’s dependencies and the module itself into a single file. The reference here is to the Bundle function of Webpack rather than TSC, Babel, a tool that maps the original module to the module files after convert.

external

Rely on external modules that do not need to be processed. This option is often used when doing server-side rendering or application volume optimization. For example, when this option is turned on and some configuration is done.

import * as React from 'react'
Copy the code

Instead of packing the React code, the packaged code remains the same.

format

The output module format is ESM, which is nothing to say

outdir

Preoptimized cache folder, default is node_modules/.vite

plugins

The esbuildDepPlugin is the core logic of Vite in the ESBuild package. Let’s see what he did. Before analyzing the source code of this plug-in, let’s take a look at a simple plug-in example from the esBuild official to see how to write an ESBuild plug-in and understand a basic workflow.

let envPlugin = {
  name: 'env'.setup(build) {
    build.onResolve({ filter: /^env$/ }, args= > ({
      path: args.path,
      namespace: 'env-ns',
    }))
    build.onLoad({ filter: /. * /, namespace: 'env-ns' }, () = > ({
      contents: JSON.stringify(process.env),
      loader: 'json',}}}))require('esbuild').build({
  entryPoints: ['app.js'].bundle: true.outfile: 'out.js'.plugins: [envPlugin],
}).catch(() = > process.exit(1))
Copy the code

Here we have written a plug-in called env. What does it do, like we have this piece of source code here

import { PATH } from 'env'
console.log(`PATH is ${PATH}`)
Copy the code

Esbuild matches env, the module we want to import, with the re in the onResolve phase and hands it off to a process called env-ns for final processing. In enV-ns, we return the current process.env environment variable stringify as a JSON string to contents. Env, which ultimately returns the value of process.env

Ok, now that we have a basic specification of the EsBuild plugin, let’s look at the esbuildDepPlugin. Also, let’s get rid of the resolve module logic, which doesn’t need relationships for the time being, and just look at the core logic

Specific file external

The first is external processing for files of a particular format that esBuild either can’t handle or shouldn’t be handled by it, and Vite itself has additional processing logic for those types of files.


const externalTypes = [
  'css'.// supported pre-processor types
  'less'.'sass'.'scss'.'styl'.'stylus'.'postcss'.// known SFC types
  'vue'.'svelte'.// JSX/TSX may be configured to be compiled differently from how esbuild
  // handles it by default, so exclude them as well
  'jsx'.'tsx'. KNOWN_ASSET_TYPES ]export const KNOWN_ASSET_TYPES = [
  // images
  'png'.'jpe? g'.'gif'.'svg'.'ico'.'webp'.'avif'.// media
  'mp4'.'webm'.'ogg'.'mp3'.'wav'.'flac'.'aac'.// fonts
  'woff2? '.'eot'.'ttf'.'otf'.// other
  'wasm'
]
 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

Distinguish between entry modules and dependency modules

Vite uses different rules for entry modules, which we import directly or include, and for dependent modules. Dependencies are dependencies of the entry module itself. As you can see here, if it is an entry module, we give it to the process whose name is dep to continue processing, and we return only a bare name. For example, vue, the original name of vue-router. Instead of an entry module, we return the absolute path to a module entry file.

function resolveEntry(id: string, isEntry: boolean) {
  const flatId = flattenId(id)
  if (flatId in qualified) {
    return isEntry
      ? {
          path: flatId,
          namespace: 'dep'}, {path: path.resolve(qualified[flatId])
        }
  }
}

build.onResolve(
  { filter: /^[\w@][^:]/ },
  async ({ path: id, importer, kind }) => {
    constisEntry = ! importer// ensure esbuild uses our resolved entires
    let entry
    // if this is an entry, return entry namespace resolve result
    if ((entry = resolveEntry(id, isEntry))){
      return entry
    }
  }
)
Copy the code

Dep to deal with

This work is basically the core of pre-optimization. The only thing Vite does here is generate a proxy module to export the original id of the original module. For example, we mentioned above that Vite hands the entry module to a process with a namespace deP for further processing. And only a primitive Bare ID was passed. Vite uses esbuild’s parse logic to analyze the import and export information of the import module. When an import module has neither import keyword nor export keyword, we consider it a CJS module. The generated proxy module has the following format

contents += `export default require("${relativePath}"); `
Copy the code

When the import module is exported using Export Default, the format of the generated proxy module is as follows

contents += `import d from "${relativePath}"; export default d; `
Copy the code

Export * from ‘./xxx.js’ or the export keyword appears more than 1 when an entry module has ReExports. Or when there is no export default, the format of the generated proxy module is as follows, which is also the final processing format of most conforming modules.

contents += `\nexport * from "${relativePath}"`
Copy the code

Vue, for example, when we’re done. Perform the import Vue from ‘Vue’, ‘Vue’ actual return contents is export * from “. / node_modules/Vue/dist/Vue. Runtime. Esm – bundler. Js. “”

From the comments, we can see that the purpose of this is twofold

  • Make the final esbuild output conform to the desired structure
  • If you do not separate the proxy module from the real module, esBuild may repackage the same module

For the first reason, I tested my own debugging to see what the resulting format would look like if I didn’t hand it to DEP.

  • When we use deP, the resulting file is in the cache directorynode_modules/vite/vue.js
  • When deP is not used, the resulting file is in the cache directorynode_modules/vite/vue.runtime.esm-bundler.jsThe application will tell you that the Vue file cannot be found.

And when we have more than one entry module, such as [‘vue’, ‘vue-router’], a structure with folders will be generated without using DEP.

│ ├── │ ├─ │ ├─ │ ├─ │ ├─ │ ├─ │ ├─ │ ├─ │ ├─ │ ├─ │ ├─ │ ├─ │ ├─ │ ├─ ├ ─ 088, ├ ─ 088, ├ ─ 088, ├ ─ 088, ├ ─ 088, ├ ─ 088Copy the code

So I guess Vite does this for the convenience of the upper layer unified processing, otherwise generate file name file structure is not certain will increase the difficulty of processing. As for the second reason, according to the notes, it is because real modules may be imported by relative reference, resulting in repeated packaging. I tried several test cases and failed to reproduce them, which may be due to inaccurate understanding of this specific meaning. Anyone interested in reading this article can analyze it and submit PR to update this document.

 // For entry files, we'll read it ourselves and construct a proxy module
// to retain the entry's raw id instead of file path so that esbuild
// outputs desired output file structure.
// It is necessary to do the re-exporting to separate the virtual proxy
// module from the actual module since the actual module may get
// referenced via relative imports - if we don't separate the proxy and
// the actual module, esbuild will create duplicated copies of the same
// module!
const root = path.resolve(config.root)
build.onLoad({ filter: /. * /, namespace: 'dep' }, ({ path: id }) = > {
  const entryFile = qualified[id]
  let relativePath = normalizePath(path.relative(root, entryFile))
  if(! relativePath.startsWith('. ')) {
    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

Esm tools

Our team is going to build an ESM-based application similar to CodesandBox. We want to use the mature capabilities of Vite for module processing. So I’m going to break Vite’s optimize functionality down to an ESM-Optimize module. Make it project-independent and do not need to create a viet.config configuration file. Find specific modules to update based on the input, allowing upper-level businesses to determine how to use the ability optimize.

use

We provide cli form or module form direct import use

$ npm i -g esm-optimize
$ esm react vue Optimize the React Vue module
$ esm react vue --force # force resoptimize by deleting the cache directory
$ esm react vue --config Display the resulting config
Copy the code

Use it as a module

import { optimize } from 'esm-optimize'

await optimize({
  root: string, // The default is CWD
  optimizeCacheDir: string, // The default vite cache folder is' node_modules/. Vite '
  optimizeDeps: {
    include: [] // The module to be processed
  },
  force: true // Force optimization
})
Copy the code

SSR framework

Finally, I recommend the SSR framework I wrote in the latest V5.0 version, which supports both React and Vue server rendering framework, and provides one-click publishing to the cloud in the form of Serverless. We can say with high confidence that it is the most advanced SSR framework on the planet. Best practices for Vue3 + Vite + SSR will be integrated in the near future. Welcome to use it.