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

具体流程

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

这边前端用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 应该也可以实现需求。
相关推荐
木子七1 分钟前
vue3-setup中使用响应式
前端·vue
廖子默11 分钟前
提供html2canvas+jsPDF将HTML页面以A4纸方式导出为PDF后,内容分页时存在截断的解决思路
前端·pdf·html
Moment14 分钟前
毕业半年,终于拥有了两个近 500 star 的开源项目了 🤭🤭🤭
前端·后端·开源
计算机毕设指导616 分钟前
基于SpringBoot共享汽车管理系统【附源码】
java·spring boot·后端·mysql·spring·汽车·intellij idea
光影少年43 分钟前
react和vue图片懒加载及实现原理
前端·vue.js·react.js
蜗牛丨43 分钟前
Go Vue3 CMS管理后台(前后端分离模式)
mysql·docker·go·vue3·axios·gin·jwt·分页·跨域·ant design vue·log·gorm·otp动态码登录·validator·模型绑定·权限判断
AndyGoWei1 小时前
react react-router-dom history 实现原理,看这篇就够了
前端·javascript·react.js
小仓桑1 小时前
深入理解 JavaScript 中的 AbortController
前端·javascript
摸鱼也很难1 小时前
解决 node.js 执行 npm下载 报无法执行脚本的错
前端·npm·node.js