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:
- Record the module dependency chain when rewriting the module address
importMaps
. This allows you to know which files need to be hot updated during subsequent updates.
- Can be used in code
import.meta.hot
Interface to mark “HMR Boundary”.
- Then, when the file is updated, it follows the previous record
imoprtMaps
The chain structure finds the corresponding “HMR Boundary” from where the corresponding updated module is reloaded.
- 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.
-
Pre-packaged to ensure that each dependency corresponds to only one request/file. Such as lodash. Please refer to github.com/vitejs/vite…
-
Code split. This can be done with the manualChunks built into RollUp.
-
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…