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

具体流程

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

这边前端用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 应该也可以实现需求。
相关推荐
YBN娜5 分钟前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=5 分钟前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
一只爱打拳的程序猿6 分钟前
【Spring】更加简单的将对象存入Spring中并使用
java·后端·spring
minDuck9 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
小政爱学习!30 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。35 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼42 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k09331 小时前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13581 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning1 小时前
React.lazy() 懒加载
前端·react.js·前端框架