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:
- Create a service
- Read the local static resource
- Overwrite the module path
- Resolving module paths
- Processing CSS files
- 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
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 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