Recently in the study of vite related, simple implementation

The project file

The original files are in the target folder,.cache is generated later, and the code implemented will be placed in the SRC path. Here is what the React project initializes

Interception handling of requests

Enable an HTTP service to block requests from the browser. When the type is module, the browser will send a request to obtain the corresponding file. The request address is file path. At the same time, we’ll stuff index.html with a file that will later be used as the webSocket client for the hot update implementation

const app = express()
const server = createServer(app)
const ws = createWebSocketServer(server);

app.get('/'.(req, res) = > {
  res.set('Content-Type'.'text/html')
  const htmlPath = join(__dirname, '.. /target'.'index.html')
  let html = readFileSync(htmlPath, 'utf-8')
  // Insert a client here
  html = html.replace('<head>'.`<head>\n <script type="module" src="/@vite/client"> </script>`).trim()
  res.send(html)
})
Copy the code

Next, we will block incoming requests for browser files in dev.js, mainly.jsx and.css files

import express from 'express'
import { createServer } from 'http'
import { read, readFileSync } from 'fs'
import { transformCode, transformJSX, transformCss } from './transform'

app.get('/target/*'.(req, res) = > {
  const filePath = join(__dirname, '.. ', req.path.slice(1))
  // This is a static resource, which will be added to the end of the.svg file request. import
  if('import' in req.query){
    res.set('Content-Type'.'application/javascript')
    res.send(`export default "${req.path}"`)
    return
  }
  switch(extname(req.path)){
    case '.svg':
      res.set('Content-Type'.'image/svg+xml')
      res.send(
        readFileSync(filePath, 'utf-8'))break
    case '.css':
      res.set('Content-Type'.'application/javascript')
      res.send(
        transformCss({
          path: req.path,
          code: readFileSync(filePath, 'utf-8')}))break
    default:
      res.set('Content-Type'.'application/javascript')
      res.send(
        transformJSX({
          appRoot: join(__dirname, '.. /target'),
          path: req.path,
          code: readFileSync(filePath, 'utf-8')
        }).code
      )
  }
})
Copy the code

Implement the transform family of methods in transform.js

import { transformSync } from 'esbuild'
import path, { join,extname, dirname } from 'path'

// Encapsulates an esbuild-based code handler
export function transformCode(opts){
  return transformSync(opts.code, {
    loader: opts.loader || 'js'.sourcemap: true.format: 'esm'})}// If it is CSS, insert a style tag in the page browser
export function transformCss(opts){
 return`
  import { updateStyle } from '/@vite/client'
  const id = "${opts.path}"
  const css = "${opts.code.replace(/\n/g.' ')}"
  updateStyle(id, css)
  export default css
 `.trim()
}
// The JSX file is returned directly after esbuild, but you need to deal with the code references in it, if it is node_modules file, from the processed cache, if it is local, it is directly retrieved
export function transformJSX(opts){
  const ext = extname(opts.path).slice(1)
  const ret = transformCode({
    loader: ext,
    code: opts.code
  })
  let { code } = ret
  code = code.replace(
    /\bimport(? ! \s+type)(? :[\w*{}\n\r\t, ]+from\s*)? \s*("([^"]+)"|'([^']+)')/gm.(a,b,c) = > {
      let from
      if(c.charAt(0) = = ='. ') {from = join(dirname(opts.path), c)
        from = JSON.parse(JSON.stringify(from).replace(/\\\\/g."/"))
        if(['svg'].includes(extname(from).slice(1))) {from = `The ${from}? import`}}else{
        from = `/target/.cache/${c}/cjs/${c}.development.js`
      }
      return a.replace(b, `"The ${from}"`)})return {
    ...ret,
    code
  }
}
Copy the code

Improve the relevant functions in client.js

const sheetMap = new Map(a)// This is an implementation of inserting the style tag into the page
export function updateStyle(id,content){
  let style = sheetMap.get(id)
  if(! style){ style =document.createElement('style')
    style.setAttribute('type'.'text/css')
    style.innerHTML = content
    document.head.appendChild(style)
  }else{
     style.innerHTML = content
  }
  sheetMap.set(id, style)
}
Copy the code

Vite’s dependency cache

Vite caches some of the dependencies in the project when it starts

/ / optmize. Js

import { build } from 'esbuild'
import { join } from 'path'

const appRoot = join(__dirname, '.. ')
// The cache path is in the target/. Cache folder
const cache = join(appRoot, 'target'.'.cache');

export async function optmize(pkgs = ['react'.'react-dom']){
  const ep = pkgs.reduce((c, n) = > {
    c.push(join(appRoot, 'node_modules', n, `cjs/${n}.development.js`))
    return c
  }, [])

  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")}}}Copy the code

Hot update

Hot update is implemented through webSocket. Dev.js starts the server, and inserts the client into the browser’s client.js, so as to achieve file changes and notify the browser

// dev.js
import WebSocket from 'ws'
const targetRootPath = join(__dirname, '.. /target');

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()
    }
  }
}

function watch () {
  return chokidar.watch(targetRootPath, {
    ignored: ['**/node_modeules/**'.'**/.cache/**'].ignoreInitial: true.ignorePermissionErrors: true.disableGlobbing: true})}function getShortName(file, root){
  return file.startsWith(root + '/')? posix.relative(root, file) : file }function handleHMRUpdate(opts){
  const {file, ws} = opts
  const timestamp = Date.now()
  // const shortFile = getShortName(file, targetRootPath)
  const shortFile = '/App.jsx'
  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
/ / clent. Js
const host = location.host

const socket = new WebSocket(`ws://${host}`.`vite-hmr`)
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')},3000)
      break
    case 'update':
      console.log(payload)
      payload.updates.forEach(async (update) => {
        if(update.type === 'js-update') {console.log('[vite] is update')
          await import(`/target/${update.path}? t=${update.timestamp}`)
          location.reload()
        }  
      })
      break; }}Copy the code

To start the project, execute the optmize file first and then execute dev.js to successfully start the project.