Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.
Suppose we have a target folder with the following file structure
Target ├─ app.CSS ├─ index.css ├─ index.html ├─ index.jsx ├─ logo.svgCopy the code
// index.html key code
<script type="module" src="/target/index.jsx"></script>
// In index.jsx, this is an entry file for the standard 'react' syntax
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App.jsx';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>.document.getElementById('root'));Copy the code
Next, we’ll get this file to work in the browser, using the core ideas of Vite without the help of familiar WebPack or other CLI tools.
Step by step, implement a simple Vite development version
There are several node modules used here, which can be pre-written in package.json and then installed by NPM install
{
// ...
"devDependencies": {
"esbuild": "^ 0.9.7"."esno": "^ 0.5.0"."express": "^ 4.17.1"."react": "^ 17.0.0"."react-dom": "^ 17.0.0"."ws": "^ 7.4.5"
},
"dependencies": {
"chokidar": "^ 3.5.2." "}}Copy the code
1. Set up an HTTP service and return our entry fileindex.html
At the target level, create a new SRC directory and create dev.js under SRC
// dev.js
import express from "express";
import { createServer } from "http";
import { join } from 'path'; // File path related operation API
import { readFileSync } from "fs"; // File read related operation API
// target Specifies the absolute path to the folder
const targetRootPath = join(__dirname, '.. /target');
export async function dev() {
const app = express();
// Intercepts the request root path and returns the contents of the index.html file
app.get('/'.(req, res) = > {
// Read the index.html file
const htmlPath = join(targetRootPath, 'index.html');
let html = readFileSync(htmlPath, 'utf-8');
// Set the returned content type to text/ HTML
res.set('Content-Type'.'text/html');
// Returns the string of the index.html file
res.send(html);
});
// Create a server
const server = createServer(app);
const port = 9001;
// Listen on the port
server.listen(port, () = > {
console.log('App is running at http://127.0.0.1:' + port)
});
}
Copy the code
Create a new dev.mand.js that imports dev.js and executes the dev() method
// dev.commamd.js
import { dev } from './dev';
dev().catch(console.error);
Copy the code
Create a new dev script in package.json and use the esno module to execute the ES syntax file
// package.json
{
// ...
"scripts": {
"dev": "esno src/dev.command.js"}},Copy the code
In the terminal, run the NPM run dev command, open http://127.0.0.1:9001/ in the browser, and check the Network panel. You can find the contents of the index. HTML file. It has returned normally, but the page is still blank.
2. Handle static resources
In the previous step, the index.html was parsed normally and returned, but found that it introduced the index.jsx resource 404
The convention here is that static resource paths all start with /target, so they can be treated uniformly in the route
2.1 Processing Script Files (JS and JSX)
// Esbuild is used to convert various types of JS files into esM formats that browsers can recognize
// dev.js
// ...
import { transformSync } from 'esbuild'; // Build code
const transformCode = opts= > {
return transformSync(opts.code, {
loader: opts.loader || 'js'.sourcemap: true.format: 'esm'})}const transformJSX = opts= > {
const ext = extname(opts.path).slice(1); // 'jsx'
const ret = transformCode({ // jsx -> js
loader: ext,
code: opts.code
});
let { code } = ret;
return {
code
}
}
// target Specifies the absolute path to the folder
const targetRootPath = join(__dirname, '.. /target');
export async function dev() {
// ...
// Intercepts static resource paths and returns resources recognized by the appropriate browser
app.get('/target/*'.(req, res) = > {
// req.path -----> /target/index.jsx
// Full file path
const filePath = join(__dirname, '.. ', req.path.slice(1));
// JSX files are processed one by one
switch (extname(req.path)) {
case '.jsx': {
res.set('Content-Type'.'application/javascript');
// This encapsulates a method for converting JSX files to JS files, since browsers cannot parse JSX files
res.send(
transformJSX({
appRoot: targetRootPath,
path: req.path,
code: readFileSync(filePath, 'utf-8')
}).code
)
break;
}
default:
break; }})// ...
}
Copy the code
JSX: import node_modules: import node_modules: import node_modules: import node_modules: import node_modules: import node_modules
Local resource to be imported, concatenate /target prefix, unify its static resource path
/target/. Cache /${moduleName}/index.js, and convert the files in node_modules into ESM modules. /target/. Cache /${moduleName}; Compilation module using ESbuild, faster performance
A transform.js file is extracted to handle file compilation and conversion operations
// transform.js
import { transformSync, build } from 'esbuild';
import { extname, dirname, join } from 'path'
import { existsSync } from 'fs'
// Cache the compiled node_modules module to prevent multiple compilations
let nodeModulesMap = new Map(a);const appRoot = join(__dirname, '.. ')
const cache = join(appRoot, 'target'.`.cache/`);
/ * * * *@param {Object} Opts compiles configuration objects *@returns Code is the compiled code */
export const transformCode = opts= > {
return transformSync(opts.code, {
loader: opts.loader || 'js'.sourcemap: true.format: 'esm'})}/ * * * *@param {Array}} PKGS the set of node_modules modules to compile */
const buildNodeModule = async pkgs => {
const ep = pkgs.reduce((c, n) = > {
c.push(join(appRoot, "node_modules", n, `index.js`));
returnc; } []);// console.log(111, ep);
await build({
entryPoints: ep,
bundle: true.format: 'esm'.logLevel: 'error'.splitting: true.sourcemap: true.outdir: cache,
treeShaking: 'ignore-annotations'.metafile: true.define: {
"process.env.NODE_ENV": JSON.stringify("development") // Default development mode}})}// Convert js, JSX code to ESM module
export const transformJSX = async opts => {
const ext = extname(opts.path).slice(1); // 'jsx'
const ret = transformCode({ // jsx -> js
loader: ext,
code: opts.code
});
let { code } = ret;
// Used to save the node_module that needs to be compiled
let needbuildModule = [];
/** * import from local files, node_modules * import React from 'React '; * The react re checks whether it is a local file or a tripartite library */
code = code.replace(
/\bimport(? ! \s+type)(? :[\w*{}\n\r\t, ]+from\s*)? \s*("([^"]+)"|'([^']+)')/gm.(a, b, c) = > {
let from;
if (c.charAt(0) = = ='. ') { // Local file
from = join(dirname(opts.path), c);
const filePath = join(opts.appRoot, from);
if(! existsSync(filePath)) {if (existsSync(`${filePath}.js`)) {
from = `The ${from}.js`}}if (['svg'].includes(extname(from).slice(1))) {
from = `The ${from}? import`}}else { // from node_modules
from = `/target/.cache/${c}/index.js`;
if(! nodeModulesMap.get(c)) { needbuildModule.push(c); nodeModulesMap.set(c,true)}}return a.replace(b, `"The ${from}"`)})// If there are third-party modules that need to be compiled
if(needbuildModule.length) {
await buildNodeModule(needbuildModule);
}
return {
...ret,
code
}
}
Copy the code
2.2 Processing Other Static resource files
As of the previous step, after restarting the NPM run dev command, you should see that the browser has parsed most of the files properly, and the page should display normally. Check the Network panel and find that CSS and SVG files are not processed properly. In the static resource handler, continue adding conditions
// dev.js
import { transformCss, transformJSX } from './transform';
// ...
// Intercepts static resource paths and returns resources recognized by the appropriate browser
app.get('/target/*'.async (req, res) => {
// ...
// There is a difference between different file processing
switch (extname(req.path)) {
// ...
case '.svg':
// SVG files are actually recognized by browsers
res.set('Content-Type'.'image/svg+xml');
res.send(
readFileSync(filePath, 'utf-8'))break;
case ".css":
// a CSS file that encapsulates a transformCss method that returns type script
res.set('Content-Type'.'application/javascript');
res.send(
transformCss({
path: req.path,
code: readFileSync(filePath, 'utf-8')
})
)
res.send()
break;
default:
break; }})// transform.js
// ...
// concatenate a code block that automatically adds style to head
export const transformCss = opts= > {
return `
var insertStyle = function(content) {
let style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.innerHTML = content;
document.head.appendChild(style);
}
const css = "${opts.code.replace(/\n/g.' ')}";
insertStyle(css);
insertStyle = null;
export default css;
`
}
Copy the code
Here may also involve other types of files, such as PNG, JSON, less and other files, can be processed according to this method, return the corresponding resources can be, no example;
At this point, after restarting the NPM run dev command, the page should be displayed and running normally
3. Hot update
Hot updates can be an essential feature for development engineers during development, so let’s implement a simple hot update
Question to consider: What is the hot update process like?
Modify the code -> the page automatically refreshes
To be more specific:
Modify the code -> Node finds that the XXX file has changed -> notify the browser that the XXX file has changed -> the browser receives the message -> the browser requests the XXX file again -> the page automatically refreshes
Based on the above flow, it’s easy to think of:
1. The Node application needs to be able to listen for file changes
2. The Node application needs to be able to send messages to the target browser
3. The browser needs to be able to accept messages
4. The browser needs to be able to pull the changed file and refresh it accordingly
So, chokidar is used to listen for file changes, the browser receives messages using WebSocket, and the Node process communicates with the browser using the WS module
Here’s another question: How do I embed WebSocket code in the browser?
In the previous process, we will read the index.html through the Node module, then returned to the browser, so you can read the file content, insert a script tag, in which the corresponding embedded code, not solved ~
For the sake of elegance, we can stuff an ESM module and then intercept the request and return the corresponding code
So first of all, let’s create a new oneclient.js
WebSockte code for the browser
Reload (). There are many other ways to use vue and react. Such as the react – hot – loader, etc
// client.js
console.log('[vite] is connecting.... ');
const host = location.host;
// The client-server establishes a communication
const socket = new WebSocket(`ws://${host}`.'vite-hmr');
// Listen for the traffic, take the data, and do the processing
socket.addEventListener('message'.async ({ data }) => {
handleMessage(JSON.parse(data)).catch(console.error);
})
async function handleMessage(payload) {
switch (payload.type) {
case 'connected':
console.log('[vite] connected.');
setInterval(() = > socket.send('ping'), 30000);
break;
case 'update':
payload.updates.forEach(async (update) => {
if (update.type === 'js-update') {
console.log('[vite] js update.... ');
await import(`/target/${update.path}? t=${update.timestamp}`);
// mocklocation.reload(); }})break; }}Copy the code
Create a newwebSocket.js
, listen for file changes and notify the browser
import chokidar from 'chokidar'
import WebSocket from 'ws';
import { posix } from 'path'
// Expose the webSocket creation method
// Create a WebSocket service that encapsulates the send method
export function createWebSocketServer(server) {
const wss = new WebSocket.Server({ noServer: true })
server.on('upgrade'.(req, socket, head) = > {
if (req.headers['sec-websocket-protocol'= = ='vite-hmr') {
wss.handleUpgrade(req, socket, head, (ws) = > {
wss.emit('connection', ws, req); }); }}); wss.on('connection'.(socket) = > {
socket.send(JSON.stringify({ type: 'connected' }));
});
wss.on('error'.(e) = > {
if(e.code ! = ='EADDRINUSE') {
console.error(
chalk.red(`WebSocket server error:\n${e.stack || e.message}`)); }});return {
send(payload) {
const stringified = JSON.stringify(payload);
wss.clients.forEach((client) = > {
if(client.readyState === WebSocket.OPEN) { client.send(stringified); }}); },close(){ wss.close(); }}},// Expose the method of listening for file changes
export function watch(targetRootPath) {
return chokidar.watch(targetRootPath, {
ignored: ['**/node_modules/**'.'**/.cache/**'].ignoreInitial: true.ignorePermissionErrors: true.disableGlobbing: true})},function getShortName(file, root) {
return file.startsWith(root + '/')? posix.relative(root, file) : file; }// Expose the file change function
// The file changes the callback that executes, which actually pushes the change data with websocket
export function handleHMRUpdate(opts) {
const { file, ws } = opts;
const shortFile = getShortName(file, opts.targetRootPath);
const timestamp = Date.now();
let updates
if (shortFile.endsWith('.css') || shortFile.endsWith('.jsx')) {
updates = [
{
type: 'js-update',
timestamp,
path: ` /${shortFile}`.acceptedPath: ` /${shortFile}`
}
]
}
ws.send({
type: 'update',
updates
})
}
Copy the code
dev.js
The corresponding modification
// dev.js
import { createWebSocketServer, watch, handleHMRUpdate} from './webSocket'
// ...
// Intercepts the request root path and returns the contents of the index.html file
app.get('/'.(req, res) = > {
// ...
// Returns the string of the index.html file
html = html.replace('<head>'.`\n `).trim()
res.send(html);
});
// Return the client code to the browser in ESM format
app.get('/@vite/client'.(req, res) = > {
res.set('Content-Type'.'application/javascript');
res.send(
// What is returned here is the actual built-in client code
transformCode({
code: readFileSync(join(__dirname, 'client.js'), 'utf-8')
}).code
)
});
// ...
const ws = createWebSocketServer(server);
// Listen for file changes
watch(targetRootPath).on('change'.async (file) => {
handleHMRUpdate({ file, ws, targetRootPath });
})
const port = 9001;
// Listen on the port
server.listen(port, () = > {
console.log('App is running at http://127.0.0.1:' + port)
});
Copy the code
conclusion
At this point, a simple Vite development can be said to be complete.
A brief overview of the process:
1.vite
Based on esM mechanism
Import content will pull resources through requests, and we can build a service to intercept the return of these requests and return the content we have processed
2. No build process compiled
The entire application is based entirely on Node services, with static resource loading and no compilation and build process, which is sure to be fast.
3. Hot update
The basic principle is: modify the code -> Node finds that the XXX file has changed -> notify the browser that the XXX file has changed -> the browser receives the message -> the browser requests the XXX file again -> the page automatically refreshes
Full Project Path
Github