前端生产包发布实现热更新

具体流程

需求: 每次前端包更新后,无需用户刷新页面后生效, 实现热更新。

这边前端用vue,后端用gin,作为例子。

1、客户端 - vue

  • 开启websocket, 建立message监听

WebSocket.vue

js 复制代码
<template>
  <div>
    <h1>WebSocket Component version-9</h1>
  </div>
</template>

<script setup>
  import { ref, onMounted, onBeforeUnmount } from 'vue'
  import { debounce } from 'lodash'

  let socket = null

  const connectWebSocket = () => {
    // Connect to WebSocket  ->  这里跟后端的ws地址相对应
    socket = new WebSocket('ws://localhost:8888/hero-ranking/ws')

    // Listen for WebSocket connection success event
    socket.addEventListener('open', (event) => {
      console.log('WebSocket connected:', event)
    })

    // Listen for WebSocket received message event
    socket.addEventListener(
      'message',
      debounce((event) => {
        const message = event.data
        console.log('Received message-----:', message)
        location.reload()
      }, 2000),
    )

    // Listen for WebSocket close event
    socket.addEventListener('close', (event) => {
      console.log('WebSocket closed:', event)
    })

    // Listen for WebSocket error event
    socket.addEventListener('error', (event) => {
      console.error('WebSocket error:', event)
    })
  }

  const closeWebSocket = () => {
    // Close WebSocket connection before component is unmounted
    if (socket) {
      socket.close()
    }
  }

  onMounted(() => {
    connectWebSocket()
  })

  onBeforeUnmount(() => {
    closeWebSocket()
  })
</script>

2、客户端 - gin

github源码

2-1 创建Websocket

  • 创建包 Websocket.go , 封装下webpack
go 复制代码
package Websocket

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "github.com/gorilla/websocket"
    "net/http"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}

// ?连接对象全局定义
var Conn *websocket.Conn

func Connect(c *gin.Context) {
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        fmt.Println(err)
        return
    }
    // defer conn.Close()
    Conn = conn
}
// 发送消息函数
func SendClientMsg(msg string) {
    Conn.WriteMessage(websocket.TextMessage, []byte(msg))
}

2-2 创建监听文件库FSNotify

  • 创建包 FSNotify.go , 利用fsnotify监听前端打包文件dist, 文件变化后通过websoket发送消息。
  • 这段代码参考了 这里
go 复制代码
package FSNotify

import (
    "fmt"
    "fuck-go/src/main/Websocket"
    "github.com/fsnotify/fsnotify"
    "os"
    "path/filepath"
)

type NotifyFile struct {
    watch *fsnotify.Watcher
}

func NewNotifyFile() *NotifyFile {
    w := new(NotifyFile)
    w.watch, _ = fsnotify.NewWatcher()
    return w
}

// 监控目录
func (this *NotifyFile) WatchDir(dir string) {
    //通过Walk来遍历目录下的所有子目录
    filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
        //判断是否为目录,监控目录,目录下文件也在监控范围内,不需要加
        if info.IsDir() {
            path, err := filepath.Abs(path)
            if err != nil {
                return err
            }
            err = this.watch.Add(path)
            if err != nil {
                return err
            }
            fmt.Println("监控 : ", path)
        }
        return nil
    })

    go this.WatchEvent() //协程
}

func (this *NotifyFile) WatchEvent() {
    for {
        select {
        case ev := <-this.watch.Events:
            fmt.Println("file update")
            // 监听到文件变化,通过Websocket发送消息
            Websocket.SendClientMsg("file update")
            // 如果是目录的创建操作,则添加监控
            if ev.Op&fsnotify.Create == fsnotify.Create {
                file, err := os.Stat(ev.Name)
                if err == nil && file.IsDir() {
                    this.watch.Add(ev.Name)
                    fmt.Println("添加监控 : ", ev.Name)
                }
            }
        case err := <-this.watch.Errors:
            fmt.Println("error:", err)
        }
    }
    return
    for {
        select {
        case ev := <-this.watch.Events:
            {
                /*fmt.Println("文件进行变化了~")
                os.Stat(ev.Name)
                return*/
                if ev.Op&fsnotify.Create == fsnotify.Create {
                    fmt.Println("创建文件 : ", ev.Name)
                    //获取新创建文件的信息,如果是目录,则加入监控中
                    file, err := os.Stat(ev.Name)
                    if err == nil && file.IsDir() {
                        this.watch.Add(ev.Name)
                        fmt.Println("添加监控 : ", ev.Name)
                    }
                }

                if ev.Op&fsnotify.Write == fsnotify.Write {
                    //fmt.Println("写入文件 : ", ev.Name)
                }

                if ev.Op&fsnotify.Remove == fsnotify.Remove {
                    fmt.Println("删除文件 : ", ev.Name)
                    //如果删除文件是目录,则移除监控
                    fi, err := os.Stat(ev.Name)
                    if err == nil && fi.IsDir() {
                        this.watch.Remove(ev.Name)
                        fmt.Println("删除监控 : ", ev.Name)
                    }
                }

                if ev.Op&fsnotify.Rename == fsnotify.Rename {
                    //如果重命名文件是目录,则移除监控 ,注意这里无法使用os.Stat来判断是否是目录了
                    //因为重命名后,go已经无法找到原文件来获取信息了,所以简单粗爆直接remove
                    fmt.Println("重命名文件 : ", ev.Name)
                    this.watch.Remove(ev.Name)
                }
                if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
                    fmt.Println("修改权限 : ", ev.Name)
                }
                fmt.Println("文件进行变化了~")
            }
        case err := <-this.watch.Errors:
            {
                fmt.Println("error : ", err)
                return
            }
        }
    }
}

2-3 创建路由包Routers

  • Routers.go
go 复制代码
package Routers

import (
    "fmt"
    "fuck-go/src/main/FSNotify"
    "fuck-go/src/main/Websocket"
    "github.com/gin-gonic/gin"
    "net/http"
    "strings"
)

func CreateRouter() {
    // 1.创建路由
    var r = gin.Default()
  
    // 设置路由组(统一前缀地址)
    apiGroup := r.Group("/hero-ranking")
    
    // 加载路由
    loadRoute(apiGroup)
    
    // 触发监听文件
    go watchFile()

    r.Run(":8888")
}

func loadRoute(apiGroup *gin.RouterGroup) {
    // ?websocket
    apiGroup.GET("/ws", func(c *gin.Context) {
        Websocket.Connect(c)
    })
}

func watchFile() {
    watch := FSNotify.NewNotifyFile()
    // 监听你服务端前端包地址
    watch.WatchDir("/Users/tog/Documents/work-space/personal/hero-ranking/dist")
    select {}
}

2-4 执行 main.go

  • 然后启动go服务就好了
go 复制代码
package main

import (
    "fuck-go/src/main/Routers"
)

func main() {
    Routers.CreateRouter()
}

3、看看实际效果

我们这里本地测启动下生产包

看下效果

题外话

  • websocket 是双向的,按照热更新的逻辑,其实只需要服务端 推送到客户端 .
  • 经过同事科普,单向的话 Server-sent events 应该也可以实现需求。
相关推荐
我只会写Bug啊1 小时前
Vue文件预览终极方案:PNG/EXCEL/PDF/DOCX/OFD等10+格式一键渲染,开源即用!
前端·vue.js·pdf·excel·预览
橙子家3 小时前
Serilog 日志库简单实践(二):控制台与调试 Sinks(.net8)
后端
扯蛋4383 小时前
LangChain的学习之路( 一 )
前端·langchain·mcp
Mr.Jessy3 小时前
Web APIs学习第一天:获取 DOM 对象
开发语言·前端·javascript·学习·html
想不明白的过度思考者3 小时前
Rust——异步递归深度指南:从问题到解决方案
开发语言·后端·rust
ConardLi4 小时前
Easy Dataset 已经突破 11.5K Star,这次又带来多项功能更新!
前端·javascript·后端
芒克芒克4 小时前
ssm框架之Spring(上)
java·后端·spring
冴羽4 小时前
10 个被严重低估的 JS 特性,直接少写 500 行代码
前端·javascript·性能优化
rising start4 小时前
四、CSS选择器(续)和三大特性
前端·css
冒泡的肥皂4 小时前
MVCC初学demo(二
数据库·后端·mysql