Author: Obsidian
Article author authorizes this account to publish, do not reprint without permission
What is the Vite
Vite is a browser native ES Modules development server. Using the browser to parse the module, compiling and returning it on demand on the server side, bypassing the concept of packaging completely and allowing the server to be used on demand. Not only does it have Vue file support, it also handles hot updates, and the speed of hot updates does not slow down with the increase of modules.
Vite(pronounced like weɪt, fast) is a Web development build tool powered by the native ES Module. Developed based on browser native ES Module in development environment and packaged based on Rollup in production environment.
The characteristics of Vite
- Lightning Fast Cold Server Start – Lightning cold start speed
- Instant Hot Module replacement (HMR)
- True on-demand compilation – True on-demand compilation
In order to achieve the above features, Vite requires that the project consists entirely of ES Module modules. The common.js Module cannot be used directly on Vite. Therefore, how to compatible with some SDKS that do not provide es Module format code becomes a key problem in production environment. Until this issue is resolved, packaging will still require traditional packaging tools such as rollup. So for now Vite is more like a development tool like Webpack-dev-server.
Webpack vs Vite
Let’s take the template project created by Vite and VUe-CLI as an example
Cold start speed comparison
From left to right: VUe-CLI3 + VUe3 demo, Vite 1.0.0-RC + Vue 3 Demo, vue-cli3 + VUe2 Demo.
The advantages of Vite are already evident in this GIF. Vue2 takes about 5 seconds to start with VUUE-Cli3, Vue3 takes about 4 seconds with VuE-CLI3, and vite only takes about 1 second.
Vite is theoretically implemented by ES Module. The startup time does not increase as the project grows. The webpack startup time increases significantly as the code size increases.
Hot update speed comparison
Vite hot update speed is difficult to compare directly with the graph (hot update speed is very fast in the small project), only in theory, because Vite changes the code only request to modify the part of the code is not affected by the size of the code, and the use of esbuild such a theoretical fast Webpack package tens of times. So compared to webPack, where you have to rebundle every change, it’s much faster to update hot.
Principle of contrast
When using a packaging tool such as WebPack, it is common to change a small line of code, and webPack often takes seconds or even tens of seconds to repackage. This is because WebPack needs to package all modules into one or more modules. This is the Bundle solution represented by WebPack.
Take the following code as an example when we use the packaging tool of the WebPack class. You end up packing all your code into a bundle.js file.
// a.js
export const a = 10
// b.js
export const b = 20;
// main.js
import { a } from 'a.js'
import { b } from 'b.js'
export const getNumber = () = > {
return a + b;
}
// bundle.js
const a = 10;
const b = 20;
const getNumber = () = > {
return a + b;
}
export { getNumber };
Copy the code
Bundle-schema-based tools (Webpack rollup) always have a core problem. When we modify a submodule in the Bundle module, the entire bundle file is repackaged and exported. As the project grows, more resources need to be packaged and the packaging takes longer and longer.
We commonly use thread-loader, cache-loader and other methods for optimization. However, with the further expansion of the project scale, the hot update speed will slow down. Another round of optimization followed. As the scale of the project continues to expand, the optimization of the project based on the Bundle solution will reach a certain limit.
Webpack is slow because Webpack packs many resources into one or more bundles (which can take a lot of packaging time). If we skipped the packaging process, wouldn’t this be perfectly solved by asking for a module when it was needed?
Hence, Vite. A Web development and construction tool driven by native ES Module, fully load on demand, once and for all to solve the problem of slow hot update!
Vite principle implementation
Front knowledge
ES Modules
ES Modules is a browser-supported modularity scheme that allows modularity in the browser. Using the template created by Vite as an example, the code is as follows
<template> <img Alt ="Vue logo" SRC ="./assets/logo.png" /> <HelloWorld MSG ="Hello Vue 3.0 + Vite" /> import HelloWorld from './components/HelloWorld.vue' export default { name: 'App', components: { HelloWorld } } </script>Copy the code
When the browser parses the import HelloWorld from ‘. / components/HelloWorld. Vue ‘, will turn out for the current domain name to send a request to obtain the corresponding resources
It’s worth noting that the esM code we wrote in Webpack ended up in a giant bundle.js bundle. A giant bundle.js file is a better solution) so naturally some ESM features cannot be used either.
Vite uses ES Module to realize Module loading. The ES Module, based on web standards, now covers more than 90% of browsers. As an ECMA standard, more browsers will support the ECMA specification in the future
ES build
Vite handles JS/TS using esBuild instead of traditional packaging tools like GLup and rollup. Esbuild is a new js packaging tool that supports functions such as Babel, compression, etc. It is fast (dozens of times faster than rollup etc.). You can learn more about Esbuild here.
The main reason for the speed is the use of go as the underlying language (static languages like GO are much faster than dynamic ones), native code, and some optimizations specifically for compilation speed.
Principle of Request interception
The basic implementation of Vite is to start a KOA server to intercept requests from the browser to the ES Module. Through the requested path to find the corresponding file in the directory for certain processing and finally returned to the client in ES Modules format
Node_modules module processing
First of all, we’ll talk about the limitations of ES Module based modules when we write code. When referencing a node_modules module, we write it as follows.
import vue from 'vue'
Copy the code
Packaging tools such as Webpack & Rollup (rollup has plug-ins) will help us find the module’s path. But the browser can only find it through a relative path. To solve this problem, Vite does some special treatment to it. Vite official demo, for example, when we request localhost: 3000, Vite default return localhost: 3000 / index. The HTML code. Then send the request SRC /main.js. The main.js code is as follows.
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
Copy the code
You can see that when the browser requests vue.js, the request path is @modules/vue.js. In Vite, path is considered a node_modules module if the requested path meets the format /^\/@modules\//. In Vite, requests from Node modules are handled accordingly.
- /:id converts to /@modules/:id
Vite uses ES Module Lexer processing for js file modules in ES Module form. The Lexer finds the modules in the code imported with import syntax and returns them as arrays. Vite obtains the value of this array to determine whether it is a node_modules module. If so, use @modules/:id.
// Plugin for rewriting served js.
// - Rewrites named module imports to `/@modules/:id` requests, e.g.
// "vue" => "/@modules/vue"
export const moduleRewritePlugin: ServerPlugin = ({ root, app, watcher, resolver }) = > {
app.use(async (ctx, next) => {
await initLexer // Initialize the ES Module Lexer to get the modules imported from the code
// Check whether it is a node_modules
const importer = removeUnRelatedHmrQuery(
resolver.normalizePublicPath(ctx.url)
)
/ / importsctx.body = rewriteImports( root, content! , importer, resolver, ctx.query.t ) }) }Copy the code
There is also processing for ESM modules imported with script tags. Use regular expressions to find code in the format
const scriptRE = /(
\b[^>
const srcRE = /\bsrc=(? :"([^"]+)"|'([^']+)'|([^'"\s]+)\b)/
async function rewriteHtml(importer: string, html: string) {
awaitinitLexer html = html! .replace(scriptRE,(matched, openTag, script) = > {
if (script) {
/ /...
} else {
const srcAttr = openTag.match(srcRE)
if (srcAttr) {
// register script as a import dep for hmr
const importee = resolver.normalizePublicPath(
cleanUrl(slash(path.resolve('/', srcAttr[1] || srcAttr[2])))
)
ensureMapEntry(importerMap, importee).add(importer)
}
return matched
}
})
return injectScriptToHtml(html, devInjectionCode)
}
Copy the code
- Run /@modules/:id to find the module in node_modules
After the browser sends the request with path /@modules/:id. Will be Vite client to do a layer of interception. /@modules/:id => id, then reslove module to get the corresponding module code returned.
export const moduleRE = /^\/@modules\//
// plugin for resolving /@modules/:id requests.
app.use(async (ctx, next) => {
if(! moduleRE.test(ctx.path)) {return next()
}
// path maybe contain encode chars
const id = decodeURIComponent(ctx.path.replace(moduleRE, ' '))
ctx.type = 'js'
const serve = async (id: string, file: string, type: string) => {
// Make a cache in the code, and the next time you visit the same path, get 304 directly from the map
moduleIdToFileMap.set(id, file)
moduleFileToIdMap.set(file, ctx.path)
debug(` (${type}) ${id} -> ${getDebugPath(root, file)}`)
await ctx.read(file)
return next()
}
}
// Alias is compatible
const importerFilePath = importer ? resolver.requestToFile(importer) : root
const nodeModulePath = resolveNodeModuleFile(importerFilePath, id)
// If it is a node_modules module, read the file.
if (nodeModulePath) {
return serve(id, nodeModulePath, 'node_modules')}})Copy the code
Processing of vue module
When Vite encounters a file with a.vue suffix. Due to the particularity of.vue template file, it is divided into template, CSS, script module three modules for processing respectively. Finally, multiple requests are sent to script, template, and CSS.
Request app.vue to obtain the script code as shown in the figure above, app.vue? Type =template Get template, app.vue? Type = style. This code is inserted into the code returned by app.vue.
if (descriptor.customBlocks) {
descriptor.customBlocks.forEach((c, i) = > {
const attrsQuery = attrsToQuery(c.attrs, c.lang)
const blockTypeQuery = &blockType=${qs.escape(c.type)}
let customRequest = publicPath + ?type=custom&index=${i}${blockTypeQuery}${attrsQuery}
const customVar = block${i}
code += `\nimport ${customVar} from The ${JSON.stringify(customRequest)}\n`
code += `if (typeof ${customVar} === 'function') ${customVar}(__script)\n`})}if (descriptor.template) {
const templateRequest = publicPath + `? type=template`
code += `\nimport { render as __render } from The ${JSON.stringify(templateRequest)}`
code += `\n__script.render = __render`` } code += `\n__script.__hmrId = ${JSON.stringify(publicPath)}`
code += `\n__script.__file = ${JSON.stringify(filePath)}`
code += `\nexport default __script`
Copy the code
Loading of static resources (Statics & Assets & JSON
When the requested path conforms to imageRE, mediaRE, fontsRE, or JSON format, it is considered a static resource. Static resources are returned as ES Module modules.
// src/node/utils/pathUtils.ts
const imageRE = /\.(png|jpe? g|gif|svg|ico|webp)(\? . *)? $/
const mediaRE = /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\? . *)? $/
const fontsRE = /\.(woff2? |eot|ttf|otf)(\? . *)? $/i
export const isStaticAsset = (file: string) = > {
return imageRE.test(file) || mediaRE.test(file) || fontsRE.test(file)
}
// src/node/server/serverPluginAssets.ts
app.use(async (ctx, next) => {
if (isStaticAsset(ctx.path) && isImportRequest(ctx)) {
ctx.type = 'js'
ctx.body = export defaultThe ${JSON.stringify(ctx.path)}
return
}
return next()
})
export const jsonPlugin: ServerPlugin = ({ app }) = > {
app.use(async (ctx, next) => {
await next()
// handle .json imports
// note ctx.body could be null if upstream set status to 304
if (ctx.path.endsWith('.json') && isImportRequest(ctx) && ctx.body) {
ctx.type = 'js'
ctx.body = dataToEsm(JSON.parse((awaitreadBody(ctx.body))!) , {namedExports: true.preferConst: true})}})}Copy the code
Hot Module Replacement principle
The hot loading principle of Vite is that a Websocket connection is established between the client and the server. When the code is modified, the server sends a message to inform the client to request the modification of the module code to complete the hot update.
Server Principles
What the server does is listen for changes to the code file and, when appropriate, send websocket messages to inform the client to request new module code.
Client Principles
Client-side Websocket-related code in Vite is written into the code as HTML is processed. You can see that the vite/client code has been inserted while the HTML is being processed.
export const clientPublicPath = `/vite/client`
const devInjectionCode = `\n<script type="module">import "${clientPublicPath}"</script>\n`
async function rewriteHtml(importer: string, html: string) {
return injectScriptToHtml(html, devInjectionCode)
}
Copy the code
When request.path is /vite/client, request prewritten webSocket code. So in the client we create a Websocket service and establish a connection with the server.
Vite receives the message from the client. Events are triggered by different messages. Achieve instant hot module replacement (hot update) on the browser side. Take the following as an example, divided into connect, vue-Reload, vue-rerender and other times. Trigger component vue reload, render, etc. (implementation is not the focus).
// Listen for messages
socket.addEventListener('message'.async ({ data }) => {
const payload = JSON.parse(data) as HMRPayload | MultiUpdatePayload
if (payload.type === 'multi') {
payload.updates.forEach(handleMessage)
} else {
handleMessage(payload)
}
})
async function handleMessage(payload: HMRPayload) {
const { path, changeSrcPath, timestamp } = payload as UpdatePayload
console.log(path)
switch (payload.type) {
case 'connected':
console.log(`[vite] connected.`)
break
case 'vue-reload':
queueUpdate(
import(`${path}? t=${timestamp}`)
.catch((err) = > warnFailedFetch(err, path))
.then((m) = > () = > {
__VUE_HMR_RUNTIME__.reload(path, m.default)
console.log(`[vite] ${path} reloaded.`)}))break
case 'vue-rerender':
const templatePath = `${path}? type=template`
import(`${templatePath}&t=${timestamp}`).then((m) = > {
__VUE_HMR_RUNTIME__.rerender(path, m.render)
console.log(`[vite] ${path} template updated.`)})break
case 'style-update':
// check if this is referenced in html via <link>
const el = document.querySelector(`link[href*='${path}'] `)
if (el) {
el.setAttribute(
'href'.`${path}${path.includes('? ')?'&' : '? '}t=${timestamp}`
)
break
}
const importQuery = path.includes('? ')?'&import' : '? import'
await import(`${path}${importQuery}&t=${timestamp}`)
console.log(`[vite] ${path} updated.`)
break
case 'js-update':
queueUpdate(updateModule(path, changeSrcPath, timestamp))
break
case 'custom':
const cbs = customUpdateMap.get(payload.id)
if (cbs) {
cbs.forEach((cb) = > cb(payload.customData))
}
break
case 'full-reload':
if (path.endsWith('.html')) {
// if html file is edited, only reload the page if the browser is
// currently on that page.
const pagePath = location.pathname
if (
pagePath === path ||
(pagePath.endsWith('/') && pagePath + 'index.html' === path)
) {
location.reload()
}
return
} else {
location.reload()
}
}
}
Copy the code
Some optimizations based on Vite
Vite is based on ES Modules when using certain modules. Because modules depend on other modules, dependent modules are based on other modules. Hundreds of module requests can be sent at a time during page initialization.
In the case of Lodash-ES, 651 requests were sent. The total cost is 1.53s.
To optimize this situation, Vite provides an optimize directive. We can use it directly with Vite Optimize
Optimize is similar to webPack’s DL-plugin loader in that it packs modules in Dependencies in package.json into esModule modules. Taking LoDash-ES as an example, he packaged the whole LoDash-ES into an ES-Module. This reduces the number of requests when retrieving modules.
As you can see, only 14 requests were sent after optimization.
By the way, one problem that is easy to see is what happens if my components are deeply nested and one component imports ten components, and ten components import ten more.
- First, we can see that the 651 requests for LoDash took only 1.53s. This time is perfectly acceptable.
- Vite is fully loaded on demand and only a few components of the page are requested to be initialized during page initialization. (Use optimizations such as Dynamic import)
- ES Module is optimized so that the browser caches the requested module. When the request path is exactly the same, the browser uses the browser-cached code. For more information about the ES module can see segmentfault.com/a/119000001…
- Vite is just a tool for the development environment, and the production environment will still be called as a commonJS bundle.
- Es Module uses bundleless to take full advantage of the multiplexing capabilities provided by HTTP2. Splitting resources into multiple parallel requests is definitely faster than requesting a bundle.
For these reasons, a project launched by Vite sends a lot of requests when it first hits the page. But it is perfectly acceptable when it comes to cost (and will be faster than WebPack). And because of caching, when the code is modified, only part of the code is requested (sending the request is accompanied by a parameter t=timestamp).
Feasibility under React
With all that said, is it tempting to try Vite in React? Vite already supports React development due to community contributions. You can try using NPM init viet-app –template React.
Q&A
Finally, I will summarize the questions you raised
Are there any downsides to Vite (the way esM is built)?
- Currently Vite is still using es Module module can not directly use the production environment (compatibility issues).
- Production environments using rollup packaging can cause inconsistencies between development and production environments.
- Many third party SDKS do not produce ems format code, this needs to do some compatibility.
Can YOU use ESM directly in a production environment?
The main issue is probably compatibility. If your project doesn’t need a browser that is compatible with IE11 or a lower version, you can use it. However, a more common solution might be to implement the principle similar to Ployfill. IO, build the bundle.js and ES Module versions of the code in advance, and dynamically choose which module to import according to the actual compatibility of the browser.
For some modules that do not produce CommonJS, how to compatibility.
First of all, there are some methods such as LEBAB that can quickly convert CommJS code to ESM, but for some code with non-standard format, it may need to be handled separately.
Will there be community help like @Types to provide some ES packages specifically for some SDKS?
It may need more help from the community.
Refer to the article
- Koa: koa.bootcss.com/
- Esbuild: github.com/evanw/esbui…