This article refers to the source code of 0.x version of Vite. Since 0.x version only supports VUE and ESM dependencies, react does not have AN ESM package, so there are some changes in support of React. After reading this article, readers should be able to understand:

  • Knowledge about es Module
  • Basic use of KOA
  • The core principle of Vite
  • This section describes how the dev-server is automatically updated
  • Basic use of esbuild

Preliminary knowledge

es modules

Vite loads dependencies via es Modules supported by newer browsers. You need to declare that the script is a module by putting type=”module” in the script tag

<script type="module"> </script>Copy the code

When import is encountered, an HTTP request is automatically sent to obtain the content of the corresponding module, with the corresponding type content-type=text/javascript

Basic architecture

Vite principle

First let’s create the Vite project and run it

yarn create vite my-react-app --template react
yarn dev
Copy the code

You can see:



The browser makes a request, requesting main.jsx





Looking at the contents of main. JSX, we can see that the server started by Vite has processed the path of the imported module, as well as the JSX writing, into code that can be run by the browser

Continue to look at





In the client, we see the Websocket code, so we can understand the Vite server to inject the websocket code of the client, to get the server code changes notification, thus achieving the hot update effect

In summary, we know a few things about the Vite server:

  • Read the local code file
  • Parse the path of the imported module and override it
  • Websocket code is injected into the client

Code implementation

The full code for this article is at: github.com/yklydxtt/vi… Here we have five steps:

  1. Create a service
  2. Read the local static resource
  3. Overwrite the module path
  4. Resolving module paths
  5. Processing CSS files
  6. Websocket code is injected into the client

1. Create a service

Create index. Js

// index.js const Koa = require('koa'); const serveStaticPlugin = require('./plugins/server/serveStaticPlugin'); const rewriteModulePlugin=require('./plugins/server/rewriteModulePlugin'); const moduleResolvePlugin=require('./plugins/server/moduleResolvePlugin'); function createServer() { const app = new Koa(); const root = process.cwd(); Const context = {app, root} const resolvePlugins = [// moduleResolvePlugin, Resolveplugins.foreach (f => f(context)); // Configure the static resource service serveStaticPlugin,] resolveplugins.foreach (f => f(context)); return app; } module.exports = createServer; createServer().listen(3001);Copy the code

Here we create a service using KOA and register three plug-ins to configure static resources, resolve module contents, and import other module paths in the module to implement the functions of these three plug-ins

2. Configure static resources and read local codes

const  KoaStatic = require('koa-static');
const path = require('path');

module.exports = function(context) {
    const { app, root } = context;
    app.use(KoaStatic(root));
    app.use(KoaStatic(path.join(root,'static')));
}
Copy the code

We create a static directory



We use koa-static to proxy static resources in the static directory

The contents of index.html are as follows:



perform

node index.js
Copy the code

Visit loaclhost: 3001

You can go to the contents of the index. HTML that we just wrote

3. Rewrite the module path

Import React,{ReactDOM} from ‘es-react’ to import React,{ReactDOM} from ‘/__module/es-react’

// plugins/server/rewriteModulePlugin.js const {readBody,rewriteImports}=require('./utils'); module.exports=function({app,root}){ app.use(async (ctx,next)=>{ await next(); If (cxx.url === '/index.html') {// Modify the path in the script tag const HTML = await readBody(cxx.body) cxx.body = html.replace( /(<script\b[^>]*>)([\s\S]*?) <\/script>/gm, (_, openTag, script) => { return `${openTag}${rewriteImports(script)}</script>` } ) } if(ctx.body&&ctx.response.is('js')){ // Const content=await readBody(cx. Body); ctx.body=rewriteImports(content,ctx.path); }}); }Copy the code

Implement the rewriteImports function and readBody

const path = require('path'); const { parse } = require('es-module-lexer'); const {Readable} =require('stream'); const resolve=require('resolve-from'); const MagicString = require('magic-string'); async function readBody(stream){ if(stream instanceof Readable){ return new Promise((resolve,reject)=>{ let res=''; stream.on('data',(data)=>res+=data); stream.on('end',()=>resolve(res)); stream.on('error',(e)=>reject(e)); }) }else{ return stream.toString(); } } function rewriteImports(source,modulePath){ const imports=parse(source)[0]; const magicString=new MagicString(source); imports.forEach(item=>{ const {s,e}=item; let id = source.substring(s,e); const reg = /^[^\/\.]/; const moduleReg=/^\/__module\//; If (modulereg.test (modulePath)){// If /__module/ prefix, If (modulepath.endswith ('.js')){id= '${path.dirname(modulePath)}/${id}'}else{ id=`${modulePath}/${id}`; } magicString.overwrite(s,e,id); return; } if(reg.test(id)){// add id= '/__module/${id}'; magicString.overwrite(s,e,id); }}); return magicString.toString(); }Copy the code



4. Read the node_modules module

ModuleResolvePlugin we implement moduleResolvePlugin because we only proxy static files, so we need to read the node_modules file. The main function is to parse the /__module prefix, and then go to node_modules to read the module contents

// ./plugins/server/moduleResolvePlugin.js const { createReadStream } = require('fs'); const { Readable } = require('stream'); const { rewriteImports, resolveModule } = require('./utils'); Module. exports = function ({app, root}) {app.use(async (CTX, next) => {// koa 's' await next(); // Read the contents of the file in node_modules const moduleReg = /^\/__module\//; if (moduleReg.test(ctx.path)) { const id = ctx.path.replace(moduleReg, ''); ctx.type = 'js'; const modulePath = resolveModule(root, id); if (id.endsWith('.js')) { ctx.body = createReadStream(modulePath); return; } else { ctx.body = createReadStream(modulePath); return; }}}); }Copy the code

To obtain the node module:

// ./plugins/server/utils.js const path = require('path'); const { parse } = require('es-module-lexer'); const {Readable} =require('stream'); const resolve=require('resolve-from'); Const MagicString = require(' MagicString '); // The return value is the path of require const MagicString = require(' MagicString '); // Return the absolute path of node_modules dependencies function resolveModule(root,moduleName){let modulePath; if(moduleName.endsWith('.js')){ modulePath=path.join(path.dirname(resolve(root,moduleName)),path.basename(moduleName)); return modulePath; } const userModulePkg=resolve(root,`${moduleName}/package.json`); modulePath=path.join(path.dirname(userModulePkg),'index.js'); return modulePath; }Copy the code

At this point, the basic function is complete. Add code to static:

// React does not have esM packages {ReactDOM} from 'e-react' import LikeButton from './like_button.js'; const e = React.createElement; const domContainer=document.getElementById("like_button_container"); ReactDOM.render(e(LikeButton), domContainer); export default function add(a, b) { return a + b; }Copy the code
// static/like_button.js import React from 'es-react' const e = React.createElement; export default class LikeButton extends React.Component { constructor(props) { super(props); this.state = { liked: false }; } render() { if (this.state.liked) { return 'You liked this.'; Return e('button', {onClick: () => this.setstate ({liked: true }) }, 'Like' ); }}Copy the code

Try to perform

node index.js
Copy the code

See the following page

5. Process the CSS file

Add one like like_button.css

// ./static.like_button.css

h1{
  color: #ff0
}
Copy the code

Introduced in like_button.js

// like_button.js

import './like_button.css';
Copy the code

When you refresh the page, you see an error like this:



Es Modules does not support CSS, so you need to convert CSS files to JS. Or import it in the link tag

Add the judgment of handling CSS to rewritemoduleplugin.js

const {readBody,rewriteImports}=require('./utils'); module.exports=function({app,root}){ app.use(async (ctx,next)=>{ await next(); if (ctx.url === '/index.html') { const html = await readBody(ctx.body) ctx.body = html.replace( /(<script\b[^>]*>)([\s\S]*?) <\/script>/gm, (_, openTag, script) => { return `${openTag}${rewriteImports(script)}</script>` } ) } if(ctx.body&&ctx.response.is('js')){ const content=await readBody(ctx.body); ctx.body=rewriteImports(content,ctx.path); } / / CSS if (CTX) type = = = "text/CSS") {CTX. Type = 'javascript'; const code=await readBody(ctx.body); ctx.body=` const style=document.createElement('style'); style.type='text/css'; style.innerHTML=${JSON.stringify(code)}; document.head.appendChild(style) ` } }); }Copy the code

Restarting services



We have the pattern

The request body of like_button.css changed to the following

6. Implement hot updates

Hot updates use WebSocket to implement the client code

// ./plugins/client/hrmClient.js const socket = new WebSocket(`ws://${location.host}`) socket.addEventListener('message',({data})=>{ const {type}=JSON.parse(data); switch(type){ case 'update': location.reload(); break; }})Copy the code

The server adds a middleware hMRWatcherPlugin.js that sends hRMClient.js content to the client, listens for code changes, and sends messages to the client via WS if there are changes

// ./plugins/server/hmrWatcherPlugin.js const fs = require('fs'); const path = require('path'); const chokidar =require('chokidar'); module.exports = function ({ app,root }) { const hmrClientCode = fs.readFileSync(path.resolve(__dirname, '.. /client/hmrClient.js')) app.use(async (ctx, next) => { await next(); If (cxx.url === '/__hmrClient') {cxx.type = 'js'; ctx.body = hmrClientCode; } if(ctx.ws){// listen for changes in native code const ws=await ctx.ws(); const watcher = chokidar.watch(root, { ignored: [/node_modules/] }); watcher.on('change',async ()=>{ ws.send(JSON.stringify({ type: 'update' })); })}})}Copy the code

Modify the handling of index.html in rewritemoduleplugin.js

// plugins/server/rewriteModulePlugin.js ... app.use(async (ctx,next)=>{ await next(); if (ctx.url === '/') { const html = await readBody(ctx.body); ctx.body = html.replace( /(<script\b[^>]*>)([\s\S]*?) <\/script>/gm, (_, openTag, Script) => {// Add a request to websock code return '${openTag}import "/__hmrClient"\n${rewriteImports(script)}</script>'})}...Copy the code

After the addition, restart the service to modify like_button.js, add an exclamation mark to the button, and save it

. return e( 'button', { onClick: () => this.setState({ liked: true }) }, 'Like! '); .Copy the code

You can see the page is updated, the exclamation point is there

7. Handle JSX code

Vite handles rewriteImports via ESbuild. Convert JSX to React. CreateElement using ESBuild

// plugins/server/utils.js

function rewriteImports(source,modulePath){
 		// ...
        const code=esbuild.transformSync(source, {
        loader: 'jsx',
      }).code;
    const imports=parse(code)[0];
    const magicString=new MagicString(code);
    imports.forEach(item=>{
        const {s,e}=item;
        let id = code.substring(s,e);
        const reg = /^[^\/\.]/;
        const moduleReg=/^\/__module\//;
        if(moduleReg.test(modulePath)){
            if(modulePath.endsWith('.js')){
                id=`${path.dirname(modulePath)}/${id}`
            }else{
                id=`${modulePath}/${id}`;
            }
            magicString.overwrite(s,e,id);
            return;
        }
    // ...
}
Copy the code



The JSX code is also rendered

The end of the

This article is written by reading the vite source code and add a bit of their own understanding, in order to facilitate everyone to understand, only the core function, convenient details did not do too much, if there are errors hope to be corrected. If you have something, please give me a like Thanks to the Blue lane: Vite principle analysis