• vite源码分析之dev


    最近研究socket, 所以就顺便看了一下vite源码, vite的热更新就是根据socket实现的, 所以正好记录一下.

    前端任何脚手架的入口,肯定是在package.json文件中,当我们输入script命令时, 会经历什么样的步骤呢? 接下来我们一起来探索一下~~~

    入口-package.json

    看下面就是一个普通前端vite脚手架启动的服务

    当我们在终端输入npm run dev时, 会如何调用vite进行项目的启动呢???

    当我们输入npm run dev时, 当然我们就相当于执行vite --mode dev

    命令

    然后就会区node-module文件夹中的.bin文件夹中找, 我们能够找到两个关于vite命令的文件

    这两个不同的命令,表示在不同的操作系统,调用不同的命令执行

    .cmd为后缀名是在window操作系统下执行的命令, 而没有后缀名的,则是在linux系统里面执行的

    .cmd

        @IF EXIST "%~dp0\node.exe" (
          "%~dp0\node.exe"  "%~dp0..\vite\bin\vite.js" %*
        ) ELSE (
          @SETLOCAL
          @SET PATHEXT=%PATHEXT:;.JS;=;%
          node  "%~dp0..\vite\bin\vite.js" %*
        )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    .cmd文件是Windows系统下的批处理文件,用于执行命令行命令的集合。.cmd文件的主要特点是:1. 只能运行在Windows系统下。
    2. 以文本格式保存,可以用记事本编辑。

    1. 扩展名为.cmd。
    2. 支持Windows命令行命令,如dir、copy、del等,也支持if语法、for循环等逻辑控制语句。
    3. 需要管理员权限才能执行某些命令。
    4. 在文件开头指定编码格式,如@echo off和chcp 65001等,否则会出现中文乱码。

    .sh

    这个看不到后缀名的是sh文件

    以#!/bin/sh开头

            #!/bin/sh
            basedir=$(dirname "$(echo "$0" | sed -e 's,\,/,g')")
    
            case `uname` in
                *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
            esac
    
            if [ -x "$basedir/node" ]; then
              "$basedir/node"  "$basedir/../vite/bin/vite.js" "$@"
              ret=$?
            else 
              node  "$basedir/../vite/bin/vite.js" "$@"
              ret=$?
            fi
            exit $ret
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    是Linux/Unix Shell脚本的标记,用于声明脚本的shell类型和执行路径。当系统遇到以#!/开头的脚本时,会提取脚本第一行的信息来确定执行该脚本的shell环境与路径。例如,以#!/bin/sh开头的脚本会使用/bin/sh路径下的shell来执行脚本。
    常见的shell类型有:

    1. /bin/sh:指向系统默认的shell,通常是bash。

    2. /bin/bash:直接指定bash shell来执行脚本。

    3. /usr/bin/perl:使用perl来执行脚本。

    4. /usr/bin/python:使用python来执行脚本。

    通过#!/bin/sh指定要使用系统默认shell(通常是bash)来执行该脚本。

    vite源码目录

    可以从上面的命令文件中,找到, 其实最后就是执行了/…/vite/bin/vite.js文件

    这还是相对路径指定,就是指定在node-modules文件夹下的vite文件

    展开之后就是这样

    /bin/vite.js

    vite.js其实最重要的一句就是start函数

    function start() {
      require('../dist/node/cli')
    }
    
    • 1
    • 2
    • 3

    inspector

    这里可以讲一下inspector.Session

    inspector.Session是Chrome DevTools中的API,用于与Chrome浏览器建立调试会话,以调试和分析页面。
    使用inspector.Session API可以:

    1. 与目标页面建立调试连接,随时启动或停止调试。
    2. 收集页面的事件、网络请求、控制台输出等信息。
    3. 设置断点和黑箱断点,调试运行中的JavaScript代码。
    4. 执行运行时命令,如:清理缓存、重新加载页面等。
    5. 分析DOM、CSS、内存等,检查页面的布局、样式和性能问题。

    不过可以看到这是node环境, 所以没有直接使用inspector对象, 而是用require引入

    process.argv.splice(profileIndex, 1)
      const next = process.argv[profileIndex]
      if (next && !next.startsWith('-')) {
        process.argv.splice(profileIndex, 1)
      }
      const inspector = require('inspector')
      const session = (global.__vite_profile_session = new inspector.Session())
      session.connect()
      session.post('Profiler.enable', () => {
        session.post('Profiler.start', start)
      })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这部分是什么功能呢?

    在启动项目时添加–profile,可以打开Chrome的"性能"面板,用于分析项目在运行时的CPU/内存使用情况,找出潜在的性能瓶颈。例如,在一个Vue项目中,可以这样启动:

    npm run serve --profile
    
    • 1

    这会在Chrome中打开"性能"面板,并开始记录项目的性能数据。
    在面板中,你可以看到项目在运行时:

    1. CPU的使用占比。可以找出消耗CPU最多的文件/函数。
    2. 内存的增长曲线。查看内存泄露或非常耗内存的组件。
    3. 事件的触发情况。如鼠标点击、页面滚动等事件的频率。
    4. 帧率的变化。帧率过低会造成卡顿,需要优化。
    5. 页面加载与渲染的时间轴。分析页面加载流程与瓶颈。
    6. 网络请求的数量和耗时。
    7. 网络请求的数量和耗时。这些可以缩短加载时间以获得更快的体验。

    /node/cli- vite命令

    这个文件记录了vite有哪些命令

        const cli = cac('vite') // Command And Conquer 是一个用于构建 CLI 应用程序的 JavaScript 库。
    
    • 1

    比如执行了vite dev 或者vite serve, 就会去执行action里面的逻辑

    执行了vite build, 就会执行下面的action逻辑

    vite中就只有dev和build这两个命令最重要

    接下来介绍一下各个命令

    build: 打包

    dev: 运行

    preview: 预览生产环境构建结果

    optimize: 用于对Vite项目进行生产环境构建与优化

    version: 查看当前项目中使用的Vite版本

    help: 查看Vite CLI提供的所有命令与选项的帮助信息

    parse: 解析Vite项目中的import语句与别名,获得其最终解析结果

    dev

    action里面就是创建一个serve,然后开始监听

    server中处理vite.config.ts配置, 处理httpsConfig, 处理ChokidarOptions

    这里介绍一下Chokidar

    chokidar是一个用于Node.js的文件系统监视器,可以监听文件和文件夹的变化,并执行相应的回调函数。它支持跨平台运行,并可以监视新增、修改、删除、移动等文件系统操作。chokidar还支持通过正则表达式或glob模式对指定的文件进行过滤,并且支持批量处理多个文件。由于其功能强大和易于使用,chokidar已经成为Node.js生态系统中最受欢迎的文件系统监视器之一。

    后面就是这个监听文件是否变化, 然后执行HMR(hot modules replacement)更新的

    当然,这个只能监听本地文件, 所以它监听的参数只能是相对或绝对路径, 不能监控远程文件的变化

    启动一个httpServer, 这个服务是node端处理文件是否发生变化, 以及发生变化之后去处理

    resolveHttpServer

    创建一个http服务直接使用node自带的http模块建立

    函数具体源代码如下:

        export async function resolveHttpServer(
          { proxy }: CommonServerOptions,
          app: Connect.Server,
          httpsOptions?: HttpsServerOptions,
        ): Promise<HttpServer> {
          if (!httpsOptions) {
            const { createServer } = await import('node:http')
            return createServer(app)
          }
    
          // #484 fallback to http1 when proxy is needed.
          if (proxy) {
            const { createServer } = await import('node:https')
            return createServer(httpsOptions, app)
          } else {
            const { createSecureServer } = await import('node:http2')
            return createSecureServer(
              {
                // Manually increase the session memory to prevent 502 ENHANCE_YOUR_CALM
                // errors on large numbers of requests
                maxSessionMemory: 1000,
                ...httpsOptions,
                allowHTTP1: true,
              },
              // @ts-expect-error TODO: is this correct?
              app,
            ) as unknown as HttpServer
          }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    启动一个http服务之后就是在node端建立websocket

    server-createSocket

    可以看到socekt经过封装之后, 返回了listen, on, off, send , close方法和clients属性

    后面node端发送socket信息, 就是使用send方法

    函数具体源代码如下:

    
     export function createWebSocketServer(
        server: Server | null,
        config: ResolvedConfig,
        httpsOptions?: HttpsServerOptions,
      ): WebSocketServer {
        let wss: WebSocketServerRaw
        let wsHttpServer: Server | undefined = undefined
    
        const hmr = isObject(config.server.hmr) && config.server.hmr
        const hmrServer = hmr && hmr.server
        const hmrPort = hmr && hmr.port
        // TODO: the main server port may not have been chosen yet as it may use the next available
        const portsAreCompatible = !hmrPort || hmrPort === config.server.port
        const wsServer = hmrServer || (portsAreCompatible && server)
        const customListeners = new Map<string, Set<WebSocketCustomListener<any>>>()
        const clientsMap = new WeakMap<WebSocketRaw, WebSocketClient>()
        const port = hmrPort || 24678
        const host = (hmr && hmr.host) || undefined
    
        if (wsServer) {
          wss = new WebSocketServerRaw({ noServer: true })
          wsServer.on('upgrade', (req, socket, head) => {
            if (req.headers['sec-websocket-protocol'] === HMR_HEADER) {
              wss.handleUpgrade(req, socket as Socket, head, (ws) => {
                wss.emit('connection', ws, req)
              })
            }
          })
        } else {
          // http server request handler keeps the same with
          // https://github.com/websockets/ws/blob/45e17acea791d865df6b255a55182e9c42e5877a/lib/websocket-server.js#L88-L96
          const route = ((_, res) => {
            const statusCode = 426
            const body = STATUS_CODES[statusCode]
            if (!body)
              throw new Error(`No body text found for the ${statusCode} status code`)
    
            res.writeHead(statusCode, {
              'Content-Length': body.length,
              'Content-Type': 'text/plain',
            })
            res.end(body)
          }) as Parameters<typeof createHttpServer>[1]
          if (httpsOptions) {
            wsHttpServer = createHttpsServer(httpsOptions, route)
          } else {
            wsHttpServer = createHttpServer(route)
          }
          // vite dev server in middleware mode
          // need to call ws listen manually
          wss = new WebSocketServerRaw({ server: wsHttpServer })
        }
    
        wss.on('connection', (socket) => {
          socket.on('message', (raw) => {
            if (!customListeners.size) return
            let parsed: any
            try {
              parsed = JSON.parse(String(raw))
            } catch {}
            if (!parsed || parsed.type !== 'custom' || !parsed.event) return
            const listeners = customListeners.get(parsed.event)
            if (!listeners?.size) return
            const client = getSocketClient(socket)
            listeners.forEach((listener) => listener(parsed.data, client))
          })
          socket.on('error', (err) => {
            // config.logger.error(`${colors.red(`ws error:`)}\n${err.stack}`, {
            //   timestamp: true,
            //   error: err,
            // })
            console.error(`ws error:`)
          })
          socket.send(JSON.stringify({ type: 'connected' }))
          if (bufferedError) {
            socket.send(JSON.stringify(bufferedError))
            bufferedError = null
          }
        })
    
        wss.on('error', (e: Error & { code: string }) => {
          if (e.code === 'EADDRINUSE') {
            config.logger.error(
              colors.red(`WebSocket server error: Port is already in use`),
              { error: e },
            )
          } else {
            config.logger.error(
              colors.red(`WebSocket server error:\n${e.stack || e.message}`),
              { error: e },
            )
          }
        })
    
        // Provide a wrapper to the ws client so we can send messages in JSON format
        // To be consistent with server.ws.send
        function getSocketClient(socket: WebSocketRaw) {
          if (!clientsMap.has(socket)) {
            clientsMap.set(socket, {
              send: (...args) => {
                let payload: HMRPayload
                if (typeof args[0] === 'string') {
                  payload = {
                    type: 'custom',
                    event: args[0],
                    data: args[1],
                  }
                } else {
                  payload = args[0]
                }
                socket.send(JSON.stringify(payload))
              },
              socket,
            })
          }
          return clientsMap.get(socket)!
        }
    
        // On page reloads, if a file fails to compile and returns 500, the server
        // sends the error payload before the client connection is established.
        // If we have no open clients, buffer the error and send it to the next
        // connected client.
        let bufferedError: ErrorPayload | null = null
    
        return {
          listen: () => {
            wsHttpServer?.listen(port, host)
          },
          on: ((event: string, fn: () => void) => {
            if (wsServerEvents.includes(event)) wss.on(event, fn)
            else {
              if (!customListeners.has(event)) {
                customListeners.set(event, new Set())
              }
              customListeners.get(event)!.add(fn)
            }
          }) as WebSocketServer['on'],
          off: ((event: string, fn: () => void) => {
            if (wsServerEvents.includes(event)) {
              wss.off(event, fn)
            } else {
              customListeners.get(event)?.delete(fn)
            }
          }) as WebSocketServer['off'],
    
          get clients() {
            return new Set(Array.from(wss.clients).map(getSocketClient))
          },
    
          send(...args: any[]) {
            let payload: HMRPayload
            if (typeof args[0] === 'string') {
              payload = {
                type: 'custom',
                event: args[0],
                data: args[1],
              }
            } else {
              payload = args[0]
            }
    
            if (payload.type === 'error' && !wss.clients.size) {
              bufferedError = payload
              return
            }
    
            const stringified = JSON.stringify(payload)
            wss.clients.forEach((client) => {
              // readyState 1 means the connection is open
              if (client.readyState === 1) {
                client.send(stringified)
              }
            })
          },
    
          close() {
            return new Promise((resolve, reject) => {
              wss.clients.forEach((client) => {
                client.terminate()
              })
              wss.close((err) => {
                if (err) {
                  reject(err)
                } else {
                  if (wsHttpServer) {
                    wsHttpServer.close((err) => {
                      if (err) {
                        reject(err)
                      } else {
                        resolve()
                      }
                    })
                  } else {
                    resolve()
                  }
                }
              })
            })
          },
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202

    moduleGraph: 将所有文件的保存在这里

    然后就是使用chokidar监听文件的change, add, unlink

    当监听到文件change, 就触发onHMRUpdate

    接下来就到了处理热更新handleHMRUpdate

    handleHMRUpdate

    handleHMRUpdate方法中, 首先处理了如果是配置文件发生改变

    如果是.env环境变量改变, 如果是依赖发生改变

    就需要重启服务server.restart()

    如果仅仅只有客户端, 没有node端, 也就是说不在开发环境, 是不需要热更新的

    (仅限开发)客户端本身不能热更新。

    这个时候socket只会发送消息,让客户端重新加载

    接着处理html文件, html文件是不支持热更新的

    所以如果是html文件发生改变, 也需要重新加载页面

    如果以上情况都不是就进行更新模块updateModules

    函数具体源代码如下:

        export async function handleHMRUpdate(
          file: string,
          server: ViteDevServer,
          configOnly: boolean,
        ): Promise<void> {
          const { ws, config, moduleGraph } = server
          const shortFile = getShortName(file, config.root)
          const fileName = path.basename(file)
    
          const isConfig = file === config.configFile
          const isConfigDependency = config.configFileDependencies.some(
            (name) => file === name,
          )
          const isEnv =
            config.inlineConfig.envFile !== false &&
            (fileName === '.env' || fileName.startsWith('.env.'))
          if (isConfig || isConfigDependency || isEnv) {
            // auto restart server
            debugHmr?.(`[config change] ${colors.dim(shortFile)}`)
            config.logger.info(
              colors.green(
                `${path.relative(process.cwd(), file)} changed, restarting server...`,
              ),
              { clear: true, timestamp: true },
            )
            try {
              await server.restart()
            } catch (e) {
              config.logger.error(colors.red(e))
            }
            return
          }
    
          if (configOnly) {
            return
          }
    
          debugHmr?.(`[file change] ${colors.dim(shortFile)}`)
    
          // (dev only) the client itself cannot be hot updated.
          if (file.startsWith(normalizedClientDir)) {
            ws.send({
              type: 'full-reload',
              path: '*',
            })
            return
          }
    
          const mods = moduleGraph.getModulesByFile(file)
    
          // check if any plugin wants to perform custom HMR handling
          const timestamp = Date.now()
          const hmrContext: HmrContext = {
            file,
            timestamp,
            modules: mods ? [...mods] : [],
            read: () => readModifiedFile(file),
            server,
          }
    
          for (const hook of config.getSortedPluginHooks('handleHotUpdate')) {
            const filteredModules = await hook(hmrContext)
            if (filteredModules) {
              hmrContext.modules = filteredModules
            }
          }
    
          if (!hmrContext.modules.length) {
            // html file cannot be hot updated
            if (file.endsWith('.html')) {
              config.logger.info(colors.green(`page reload `) + colors.dim(shortFile), {
                clear: true,
                timestamp: true,
              })
              ws.send({
                type: 'full-reload',
                path: config.server.middlewareMode
                  ? '*'
                  : '/' + normalizePath(path.relative(config.root, file)),
              })
            } else {
              // loaded but not in the module graph, probably not js
              debugHmr?.(`[no modules matched] ${colors.dim(shortFile)}`)
            }
            return
          }
    
          updateModules(shortFile, hmrContext.modules, timestamp, server)
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    我简单画了一下流程图

    updateModules

    模块更新就需要上面讲到的moduleGraph

    这里存储了所有的文件模块图

    当有监听到有模块更新, moduleGraph就有发生改变

    去除无效的模块, 找到需要更新的模块

    最后当发出send类型为update类型, 就是一个文件发生变化啦

        export function updateModules(
          file: string,
          modules: ModuleNode[],
          timestamp: number,
          { config, ws, moduleGraph }: ViteDevServer,
          afterInvalidation?: boolean,
        ): void {
          const updates: Update[] = []
          const invalidatedModules = new Set<ModuleNode>()
          const traversedModules = new Set<ModuleNode>()
          let needFullReload = false
    
          for (const mod of modules) {
            moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true)
            if (needFullReload) {
              continue
            }
    
            const boundaries: { boundary: ModuleNode; acceptedVia: ModuleNode }[] = []
            const hasDeadEnd = propagateUpdate(mod, traversedModules, boundaries)
            if (hasDeadEnd) {
              needFullReload = true
              continue
            }
    
            updates.push(
              ...boundaries.map(({ boundary, acceptedVia }) => ({
                type: `${boundary.type}-update` as const,
                timestamp,
                path: normalizeHmrUrl(boundary.url),
                explicitImportRequired:
                  boundary.type === 'js'
                    ? isExplicitImportRequired(acceptedVia.url)
                    : undefined,
                acceptedPath: normalizeHmrUrl(acceptedVia.url),
              })),
            )
          }
    
          if (needFullReload) {
            config.logger.info(colors.green(`page reload `) + colors.dim(file), {
              clear: !afterInvalidation,
              timestamp: true,
            })
            ws.send({
              type: 'full-reload',
            })
            return
          }
    
          if (updates.length === 0) {
            debugHmr?.(colors.yellow(`no update happened `) + colors.dim(file))
            return
          }
    
          config.logger.info(
            colors.green(`hmr update `) +
              colors.dim([...new Set(updates.map((u) => u.path))].join(', ')),
            { clear: !afterInvalidation, timestamp: true },
          )
          ws.send({
            type: 'update',
            updates,
          })
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    就比如下图, 文件路径和名称就来自于moduleGraph

    流程图附上

    client-createSocket

    讲完了node端如何发送socket

    接下来肯定要有客户端监听到socket

    查看目录时, 我们能够看到有一个client目录

    这里其实就是客户端处理socket的文件

    不知道有没有人好奇, 怎么把文件引入到客户端去, 没有看源码之前, 反正我是很好奇的

    不知道大家有没有记得,在action里面有一个createServer

    在createServer里面有一个_createServer

    在_createServer里面有一个resolveConfig

    在resolveConfig里面有resolvePlugins

    在resolvePlugins里面有importAnalysisPlugin

    就是在importAnalysisPlugin里面

    通过字符串导入方式把createHotContext导入到了客户端

    这里str是什么

    这是来自一个外部的npm包

    magic-string

    假设您有一些源代码。您想要对其进行一些轻微的修改 - 在这里和那里替换一些字符,用页眉和页脚包装它等等 - 理想情况下您希望在它的末尾生成一个源映射。您考虑过使用 recast 之类的东西(它允许您从一些 JavaScript 生成 AST,对其进行操作,并使用 sourcemap 重新打印它而不会丢失您的注释和格式),但它似乎对您的需求(或者可能是源代码不是 JavaScript)。

    能够在客户端代码里面注入,这样就将createHotContext注入到客户端中,并使用

    在createHotContent中, 就存在setupWebSocket啦

    在浏览器端建立socket核心代码如下:

    
        function setupWebSocket(
          protocol: string,
          hostAndPath: string,
          onCloseWithoutOpen?: () => void,
        ) {
          const socket = new WebSocket(`${protocol}://${hostAndPath}`, 'socket-hmr')
          let isOpened = false
    
          socket.addEventListener(
            'open',
            () => {
              isOpened = true
            },
            { once: true },
          )
    
          // Listen for messages
          socket.addEventListener('message', async ({ data }) => {
            // handleMessage(JSON.parse(data))
            console.log(data)
          })
    
          // ping server
          socket.addEventListener('close', async ({ wasClean }) => {
            if (wasClean) return
    
            if (!isOpened && onCloseWithoutOpen) {
              onCloseWithoutOpen()
              return
            }
    
            console.log(`[socket] server connection lost. polling for restart...`)
            await waitForSuccessfulPing(protocol, hostAndPath)
            location.reload()
          })
    
          return socket
        }
    
    
        function warnFailedFetch(err: Error, path: string | string[]) {
          if (!err.message.match('fetch')) {
            console.error(err)
          }
          console.error(
            `[hmr] Failed to reload ${path}. ` +
              `This could be due to syntax errors or importing non-existent ` +
              `modules. (see errors above)`,
          )
        }
    
    
        async function waitForSuccessfulPing(
          socketProtocol: string,
          hostAndPath: string,
          ms = 1000,
        ) {
          const pingHostProtocol = socketProtocol === 'wss' ? 'https' : 'http'
    
          const ping = async () => {
            // A fetch on a websocket URL will return a successful promise with status 400,
            // but will reject a networking error.
            // When running on middleware mode, it returns status 426, and an cors error happens if mode is not no-cors
            try {
              await fetch(`${pingHostProtocol}://${hostAndPath}`, {
                mode: 'no-cors',
                headers: {
                  // Custom headers won't be included in a request with no-cors so (ab)use one of the
                  // safelisted headers to identify the ping request
                  Accept: 'text/x-socket-ping',
                },
              })
              return true
            } catch {}
            return false
          }
    
          if (await ping()) {
            return
          }
          await wait(ms)
    
          // eslint-disable-next-line no-constant-condition
          while (true) {
            if (document.visibilityState === 'visible') {
              if (await ping()) {
                break
              }
              await wait(ms)
            } else {
              await waitForWindowShow()
            }
          }
        }
    
        function waitForWindowShow() {
          return new Promise<void>((resolve) => {
            const onChange = async () => {
              if (document.visibilityState === 'visible') {
                resolve()
                document.removeEventListener('visibilitychange', onChange)
              }
            }
            document.addEventListener('visibilitychange', onChange)
          })
        }
    
    
        function wait(ms: number) {
          return new Promise((resolve) => setTimeout(resolve, ms))
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112

    这样就建立了浏览器端与node端的通信啦

    好了, 今天分享到这里就结束了, 源码分析解释还是篇幅太长, 所以我这里就只提到了dev命令.

    vite的其他命令其实也是这样分析, 如果有什么疑问或者不好之处, 欢迎大家在评论区指出, 祝大家有个愉快的周末!

  • 相关阅读:
    ISP 基础知识储备
    为什么 eBPF 如此受欢迎?
    Visual Studio 删除行尾空格
    django学习记录07——订单案例(复选框+ajax请求)
    2022暑假 动态规划总结
    Java自学(三)面向对象编程
    <C++入门基础>【下】
    java毕业设计——基于java+Spring+JSP的宠物网站设计与实现(毕业论文+程序源码)——宠物网站
    贪心算法之活动安排问题
    Spring的注解@Bean
  • 原文地址:https://blog.csdn.net/qq_44859233/article/details/130904277