SpringBoot 2 +Vue3+Vite + WebSocket + Nginx 二级目录部署发布

最近研究了新技术 Vue3 + Vite ,需要引用 WebSocket 长连接 及 Nginx 发布,有一些心得体会,在此记录一下方便以后查阅,同时跟小伙伴分析一些新技术。

后端配置:

后端采用SpringBoot2 的技术,需要配置

WebSocketConfig.java

package com.dechnic.waystation.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * @description:
 * @author:houqd
 * @time: 2022/5/28 16:22
 */
@Slf4j
@Configuration
//注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/webSocket")//注册为STOMP的端点
                .setAllowedOriginPatterns("*")//可以跨域
                .withSockJS();//支持sockJs
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 设置广播节点
        registry.enableSimpleBroker("/topic");
//        // 客户端向服务端发送消息需有/app 前缀
//        registry.setApplicationDestinationPrefixes("/app");
//        // 指定用户发送(一对一)的前缀 /user/
//        registry.setUserDestinationPrefix("/user");
    }
}

备注:此配置类配置后台webSocket 接入点,及发布主题

前端:

前端采用VUE3+VITE 构建工具,比Vue-cli3 快 10-100 倍,尤其对开发环境特别有利,也是尤大大 针对VUE3 极力推荐的工具,类似于后端里的SpringBoot 当作一个集成框架,而WebPack 只是一个打包工具。

项目结构目录:
SpringBoot 2 +Vue3+Vite + WebSocket + Nginx 二级目录部署发布_第1张图片

vite.config.js

import { defineConfig, loadEnv } from 'vite'
import path from 'path'
import createVitePlugins from './vite/plugins'
import globalConfig from './public/config'
// https://vitejs.dev/config/
export default defineConfig(({ mode, command }) => {
  const env = loadEnv(mode, process.cwd())
  const { VITE_APP_ENV } = env
  return {
    // 部署生产环境和开发环境下的URL。
    // 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
    // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
    base: VITE_APP_ENV === 'production' ? globalConfig.publicPath : '/',
    plugins: createVitePlugins(env, command === 'build'),
    build: {
      minify: false,
      sourcemap: true
    },
    resolve: {
      // https://cn.vitejs.dev/config/#resolve-alias
      alias: {
        // 设置路径
        '~': path.resolve(__dirname, './'),
        // 设置别名
        '@': path.resolve(__dirname, './src')
      },
      // https://cn.vitejs.dev/config/#resolve-extensions
      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
    },
    // vite 相关配置
    server: {
      port: 800,
      host: true,
      open: true,
      proxy: {
        '/dev-api/': {
          target: 'http://localhost:8081/waystation',
          changeOrigin: true,
          rewrite: (p) => p.replace(/^\/dev-api/, '')
        },
        /*'/dev-api/webSocket/': {
          target: 'http://localhost:8081/waystation/webSocket',
          ws: true,
          changeOrigin: true,
          rewrite: (p) => p.replace(/^\/dev-api/, '')
        }*/
      }
    },
    //fix:error:stdin>:7356:1: warning: "@charset" must be the first rule in the file
    css: {
      postcss: {
        plugins: [
          {
            postcssPlugin: 'internal:charset-removal',
            AtRule: {
              charset: (atRule) => {
                if (atRule.name === 'charset') {
                  atRule.remove();
                }
              }
            }
          }
        ]
      }
    }
  }
})

备注:VITE 里获取当前环境的方式和Vue-cli3 有所不同,vue-cli3 可以通过过process.env.xx 来获取,而 VITE 则要功过 loadEnv(); 还有一个更便利的获取方式
import.meta.env.xxx 来获取,比如:import.meta.env.PROD , import.meta.env.VITE_APP_BASE_API

public 文件夹下自定义 config.js

let globalConfig = {
    httpUrl: '/waystation',
    publicPath:'/waystation-app/',
    title:'管理系统'
}
export default globalConfig

request.js

import axios from 'axios'
import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus'
import store from '@/store'
import { getToken,getEasyGbsToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams, blobValidate } from '@/utils/ruoyi'
import cache from '@/plugins/cache'
import { saveAs } from 'file-saver'
import globalConfig from '../../public/config'

let downloadLoadingInstance;
// 是否显示重新登录
export let isRelogin = { show: false };

axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
let baseURL = import.meta.env.PROD ? globalConfig.httpUrl: import.meta.env.VITE_APP_BASE_API

// 创建axios实例
const service = axios.create({
  // axios中请求配置有baseURL选项,表示请求URL公共部分
  baseURL: baseURL,
  // 超时
  timeout: 10000
})

// request拦截器
service.interceptors.request.use(config => {
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  // 是否需要防止数据重复提交
  const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
  if (getToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  }
  // 添加EasyGbsToken
  if (getEasyGbsToken()){
    config.headers['token'] = getEasyGbsToken()
  }

  // get请求映射params参数
  if (config.method === 'get' && config.params) {
    let url = config.url + '?' + tansParams(config.params);
    url = url.slice(0, -1);
    config.params = {};
    config.url = url;
  }
  if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
    const requestObj = {
      url: config.url,
      data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
      time: new Date().getTime()
    }
    const sessionObj = cache.session.getJSON('sessionObj')
    if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
      cache.session.setJSON('sessionObj', requestObj)
    } else {
      const s_url = sessionObj.url;                // 请求地址
      const s_data = sessionObj.data;              // 请求数据
      const s_time = sessionObj.time;              // 请求时间
      const interval = 1000;                       // 间隔时间(ms),小于此时间视为重复提交
      if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
        const message = '数据正在处理,请勿重复提交';
        console.warn(`[${s_url}]: ` + message)
        return Promise.reject(new Error(message))
      } else {
        cache.session.setJSON('sessionObj', requestObj)
      }
    }
  }
  return config
}, error => {
    console.log(error)
    Promise.reject(error)
})

// 响应拦截器
service.interceptors.response.use(res => {
    // 未设置状态码则默认成功状态
    const code = res.data.code || 200;
    // 获取错误信息
    const msg = errorCode[code] || res.data.msg || errorCode['default']
    // 二进制数据则直接返回
    if(res.request.responseType ===  'blob' || res.request.responseType ===  'arraybuffer'){
      return res.data
    }
    if (code === 401) {
      if (!isRelogin.show) {
        isRelogin.show = true;
        ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }
      ).then(() => {
        isRelogin.show = false;
        store.dispatch('LogOut').then(() => {
          location.href = '/index';
        })
      }).catch(() => {
        isRelogin.show = false;
      });
    }
      return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
    } else if (code === 500) {
      ElMessage({
        message: msg,
        type: 'error'
      })
      return Promise.reject(new Error(msg))
    } else if (code !== 200) {
      ElNotification.error({
        title: msg
      })
      return Promise.reject('error')
    } else {
      return  Promise.resolve(res.data)
    }
  },
  error => {
    console.log('err' + error)
    let { message } = error;
    if (message == "Network Error") {
      message = "后端接口连接异常";
    }
    else if (message.includes("timeout")) {
      message = "系统接口请求超时";
    }
    else if (message.includes("Request failed with status code")) {
      message = "系统接口" + message.substr(message.length - 3) + "异常";
    }
    ElMessage({
      message: message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

// 通用下载方法
export function download(url, params, filename) {
  downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
  return service.post(url, params, {
    transformRequest: [(params) => { return tansParams(params) }],
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    responseType: 'blob'
  }).then(async (data) => {
    const isLogin = await blobValidate(data);
    if (isLogin) {
      const blob = new Blob([data])
      saveAs(blob, filename)
    } else {
      const resText = await data.text();
      const rspObj = JSON.parse(resText);
      const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
      ElMessage.error(errMsg);
    }
    downloadLoadingInstance.close();
  }).catch((r) => {
    console.error(r)
    ElMessage.error('下载文件出现错误,请联系管理员!')
    downloadLoadingInstance.close();
  })
}

export default service

router/index.js

import { createWebHistory, createRouter } from 'vue-router'
import Layout from '@/layout';
import globalConfig from '../../public/config';

/**
 * Note: 路由配置项
 *
 * hidden: true                     // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
 * alwaysShow: true                 // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
 *                                  // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
 *                                  // 若你想不管路由下面的 children 声明的个数都显示你的根路由
 *                                  // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
 * redirect: noRedirect             // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
 * name:'router-name'               // 设定路由的名字,一定要填写不然使用时会出现各种问题
 * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
 * meta : {
    noCache: true                   // 如果设置为true,则不会被  缓存(默认 false)
    title: 'title'                  // 设置该路由在侧边栏和面包屑中展示的名字
    icon: 'svg-name'                // 设置该路由的图标,对应路径src/assets/icons/svg
    breadcrumb: false               // 如果设置为false,则不会在breadcrumb面包屑中显示
    activeMenu: '/system/user'      // 当路由设置了该属性,则会高亮相对应的侧边栏。
  }
 */

// 公共路由
export const constantRoutes = [
  {
    path: '/redirect',
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect/index.vue')
      }
    ]
  },
  {
    path: '/login',
    component: () => import('@/views/login'),
    hidden: true
  },
  {
    path: '/register',
    component: () => import('@/views/register'),
    hidden: true
  },
  {
    path: "/:pathMatch(.*)*",
    component: () => import('@/views/error/404'),
    hidden: true
  },
  {
    path: '/401',
    component: () => import('@/views/error/401'),
    hidden: true
  },
  {
    path: '',
    component: Layout,
    redirect: '/index',
    children: [
      {
        path: '/index',
        component: () => import('@/views/index'),
        name: 'Index',
        meta: { title: '首页', icon: 'dashboard', affix: true }
      }
    ]
  },
  {
    path: '/user',
    component: Layout,
    hidden: true,
    redirect: 'noredirect',
    children: [
      {
        path: 'profile',
        component: () => import('@/views/system/user/profile/index'),
        name: 'Profile',
        meta: { title: '个人中心', icon: 'user' }
      }
    ]
  },
  {
    path: '/system/user-auth',
    component: Layout,
    hidden: true,
    children: [
      {
        path: 'role/:userId(\\d+)',
        component: () => import('@/views/system/user/authRole'),
        name: 'AuthRole',
        meta: { title: '分配角色', activeMenu: '/system/user' }
      }
    ]
  },
  {
    path: '/system/role-auth',
    component: Layout,
    hidden: true,
    children: [
      {
        path: 'user/:roleId(\\d+)',
        component: () => import('@/views/system/role/authUser'),
        name: 'AuthUser',
        meta: { title: '分配用户', activeMenu: '/system/role' }
      }
    ]
  },
  {
    path: '/system/dict-data',
    component: Layout,
    hidden: true,
    children: [
      {
        path: 'index/:dictId(\\d+)',
        component: () => import('@/views/system/dict/data'),
        name: 'Data',
        meta: { title: '字典数据', activeMenu: '/system/dict' }
      }
    ]
  },
  {
    path: '/monitor/job-log',
    component: Layout,
    hidden: true,
    children: [
      {
        path: 'index',
        component: () => import('@/views/monitor/job/log'),
        name: 'JobLog',
        meta: { title: '调度日志', activeMenu: '/monitor/job' }
      }
    ]
  },
  {
    path: '/tool/gen-edit',
    component: Layout,
    hidden: true,
    children: [
      {
        path: 'index/:tableId(\\d+)',
        component: () => import('@/views/tool/gen/editTable'),
        name: 'GenEdit',
        meta: { title: '修改生成配置', activeMenu: '/tool/gen' }
      }
    ]
  }
];

const router = createRouter({
  history: createWebHistory(import.meta.env.PROD?globalConfig.publicPath:'/'),
  routes: constantRoutes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { top: 0 }
    }
  },
});

export default router;

index.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="renderer" content="webkit">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <link rel="icon" href="/favicon.ico">
  <title>驿站管理系统</title>
  <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
  <style>
    html,
    body,
    #app {
      height: 100%;
      margin: 0px;
      padding: 0px;
    }

    .chromeframe {
      margin: 0.2em 0;
      background: #ccc;
      color: #000;
      padding: 0.2em 0;
    }

    #loader-wrapper {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 999999;
    }

    #loader {
      display: block;
      position: relative;
      left: 50%;
      top: 50%;
      width: 150px;
      height: 150px;
      margin: -75px 0 0 -75px;
      border-radius: 50%;
      border: 3px solid transparent;
      border-top-color: #FFF;
      -webkit-animation: spin 2s linear infinite;
      -ms-animation: spin 2s linear infinite;
      -moz-animation: spin 2s linear infinite;
      -o-animation: spin 2s linear infinite;
      animation: spin 2s linear infinite;
      z-index: 1001;
    }

    #loader:before {
      content: "";
      position: absolute;
      top: 5px;
      left: 5px;
      right: 5px;
      bottom: 5px;
      border-radius: 50%;
      border: 3px solid transparent;
      border-top-color: #FFF;
      -webkit-animation: spin 3s linear infinite;
      -moz-animation: spin 3s linear infinite;
      -o-animation: spin 3s linear infinite;
      -ms-animation: spin 3s linear infinite;
      animation: spin 3s linear infinite;
    }

    #loader:after {
      content: "";
      position: absolute;
      top: 15px;
      left: 15px;
      right: 15px;
      bottom: 15px;
      border-radius: 50%;
      border: 3px solid transparent;
      border-top-color: #FFF;
      -moz-animation: spin 1.5s linear infinite;
      -o-animation: spin 1.5s linear infinite;
      -ms-animation: spin 1.5s linear infinite;
      -webkit-animation: spin 1.5s linear infinite;
      animation: spin 1.5s linear infinite;
    }


    @-webkit-keyframes spin {
      0% {
        -webkit-transform: rotate(0deg);
        -ms-transform: rotate(0deg);
        transform: rotate(0deg);
      }

      100% {
        -webkit-transform: rotate(360deg);
        -ms-transform: rotate(360deg);
        transform: rotate(360deg);
      }
    }

    @keyframes spin {
      0% {
        -webkit-transform: rotate(0deg);
        -ms-transform: rotate(0deg);
        transform: rotate(0deg);
      }

      100% {
        -webkit-transform: rotate(360deg);
        -ms-transform: rotate(360deg);
        transform: rotate(360deg);
      }
    }


    #loader-wrapper .loader-section {
      position: fixed;
      top: 0;
      width: 51%;
      height: 100%;
      background: #7171C6;
      z-index: 1000;
      -webkit-transform: translateX(0);
      -ms-transform: translateX(0);
      transform: translateX(0);
    }

    #loader-wrapper .loader-section.section-left {
      left: 0;
    }

    #loader-wrapper .loader-section.section-right {
      right: 0;
    }


    .loaded #loader-wrapper .loader-section.section-left {
      -webkit-transform: translateX(-100%);
      -ms-transform: translateX(-100%);
      transform: translateX(-100%);
      -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
      transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
    }

    .loaded #loader-wrapper .loader-section.section-right {
      -webkit-transform: translateX(100%);
      -ms-transform: translateX(100%);
      transform: translateX(100%);
      -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
      transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
    }

    .loaded #loader {
      opacity: 0;
      -webkit-transition: all 0.3s ease-out;
      transition: all 0.3s ease-out;
    }

    .loaded #loader-wrapper {
      visibility: hidden;
      -webkit-transform: translateY(-100%);
      -ms-transform: translateY(-100%);
      transform: translateY(-100%);
      -webkit-transition: all 0.3s 1s ease-out;
      transition: all 0.3s 1s ease-out;
    }

    .no-js #loader-wrapper {
      display: none;
    }

    .no-js h1 {
      color: #222222;
    }

    #loader-wrapper .load_title {
      font-family: 'Open Sans';
      color: #FFF;
      font-size: 19px;
      width: 100%;
      text-align: center;
      z-index: 9999999999999;
      position: absolute;
      top: 60%;
      opacity: 1;
      line-height: 30px;
    }

    #loader-wrapper .load_title span {
      font-weight: normal;
      font-style: italic;
      font-size: 13px;
      color: #FFF;
      opacity: 0.5;
    }
  </style>
</head>

<body>
  <div id="app">
    <div id="loader-wrapper">
      <div id="loader"></div>
      <div class="loader-section section-left"></div>
      <div class="loader-section section-right"></div>
      <div class="load_title">正在加载系统资源,请耐心等待</div>
    </div>
  </div>
  <script type="module" src="/src/main.js"></script>
  <script type="module" src="/public/config.js"></script>
  <script src="/js/liveplayer-lib.min.js"></script>
</body>

</html>

备注: index.html 里 通过

<script type="module" src="/public/config.js"></script> 

ES6模块加载的方式,供vite 加载

index.vue 页面使用webSocket

<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="区县:">
        <CountySelect ref="countyselect" @select="handleCountySelect"></CountySelect>
      </el-form-item>

      <el-form-item label="驿站:">
        <WayStationSelect ref="waystationselect" @select="handleWayStaionSelect"></WayStationSelect>
      </el-form-item>

      <el-form-item label="状态" prop="status">
        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
          <el-option v-for="dict in dm_is_open" :key="dict.value" :label="dict.label" :value="dict.value" />
        </el-select>
      </el-form-item>

      <el-form-item>
        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <el-table v-loading="loading" :data="deviceList" @selection-change="handleSelectionChange">
      <el-table-column label="序号" type="index" width="50" />
      <el-table-column label="区县" align="center" prop="parentName" />
      <el-table-column label="驿站" align="center" prop="deptName" />
      <el-table-column label="门禁名称" align="center" prop="deviceName" />

      <el-table-column label="在线状态" align="center" prop="isOnline">
        <template #default="scope">
          <dict-tag :options="dm_is_online" :value="scope.row.isOnline"/>
        </template>
      </el-table-column>
      <el-table-column label="工作状态" align="center" prop="status">
        <template #default="scope">
          <dict-tag :options="dm_is_open" :value="scope.row.status"/>
        </template>
      </el-table-column>
      <el-table-column label="远程控制" align="center">
        <template #default="scope">
          <el-button type="text" style="margin-left: 0px" @click="handleUpdateDeviceStatus(scope.row)">开门</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script setup>
  import {getDeviceList ,controllAcs} from '@/api/dm/device';
  import CountySelect from "@/components/CountySelect";
  import WayStationSelect from "@/components/WayStationSelect";
  import SockJS from 'sockjs-client/dist/sockjs.min';
  import Stomp from 'stompjs';

  const { proxy } = getCurrentInstance();
  const showSearch = ref(true);
  const loading = ref(true);
  const total = ref(0);
  const { dm_is_online, dm_is_open} = proxy.useDict("dm_is_online", "dm_is_open");

  const baserUrl = proxy.baseURL;
  console.log("baserUrl:"+baserUrl)

  const data = reactive({
    form: {},
    queryParams: {
      pageNum: 1,
      pageSize: 10,
      deptCode: null,
      parentCode: null,
      deviceType: 'ACS'
    },
  });



  const { queryParams, form, rules } = toRefs(data);
  const deviceList = ref([]);

  function handleCountySelect(countyCode) {
    // console.log("countyselect:"+countyCode)
    queryParams.value.parentCode = countyCode;
    proxy.$refs.waystationselect.getOptions(countyCode);
  }
  function handleWayStaionSelect(waystationCode) {
    // console.log("waystationCode:"+waystationCode);
    queryParams.value.deptCode = waystationCode;
  }

  /** 搜索按钮操作 */
  function handleQuery() {
    queryParams.value.pageNum = 1;
    getList();
  }

  /**重置区县,驿站选择*/
  function reset() {
    proxy.$refs.countyselect.reset();
    queryParams.value.parentCode = null;
    proxy.$refs.waystationselect.reset();
    queryParams.value.deptCode = null;
    queryParams.value.deviceType = 'ACS';
  }
  /** 重置按钮操作 */
  function resetQuery() {
    proxy.resetForm("queryRef");
    reset();
    handleQuery();
  }

  function getList() {
    loading.value = true;
    getDeviceList(queryParams.value).then(result => {
      deviceList.value = result.rows;
      total.value =result.total;
      loading.value = false;
    })
  }

  getList();


  // websocket 相关
  const  ws = {
    url: baserUrl+ "/webSocket",
    socket: null,
    stompClient: null,
    reconnecting: false,
    headers: {Authorization: ''}
  }
  function initWebSocket(url){
    ws.socket = new SockJS(url);
    ws.stompClient = Stomp.over(ws.socket);

    ws.stompClient.connect(
            ws.headers,// 可添加客户端的认证信息
            frame => {// 连接成功回调
              console.log("webSocket 连接成功")
              connectSucceed()
            },
            error=>{
              console.log("webSocket 连接失败:"+error);
              reconnect(ws.url,connectSucceed)
            }
    )
  }
  function connectSucceed(){
    // ws.stompClient.heartbeat.outgoing = 10000;
    // ws.stompClient.heartbeat.incoming = 0;
    ws.stompClient.subscribe('/topic/controll/acs',function(response){
      const result = JSON.parse(response.body);
      console.log(result)
      if (response && result.result){
        proxy.$modal.msgSuccess("开门成功!");
      }else{
        proxy.$modal.msgSuccess("开门失败!");
      }
    })

  }
  let count = 1;
  function reconnect(socketUrl,callback){
    ws.reconnecting = true;
    let connected = false;
    const timer = setInterval(()=>{
      ws.socket = new SockJS(socketUrl);
      ws.stompClient = Stomp.over(ws.socket);
      ws.stompClient.connect(ws.headers,frame =>{
        ws.reconnecting = false;
        connected = true;
        clearInterval(timer);
        callback();
      },error=>{
        console.log("WebSocket Reconnected failed!")
        count += count;
        if (!connected){
          console.log(error)
        }
        if (count >10){
          clearInterval(timer);
        }
      })
    },1000)

  }
  function closeSocket(){
    if (ws.stompClient !=null){
      ws.stompClient.disconnect()
    }
  }

  function handleUpdateDeviceStatus(row) {
    console.log("当前门禁:"+row.deviceId + " "+row.deviceCode +" "+row.deviceName+ " "+row.status)
    row.cmd = 'open';
    controllAcs(row).then(result => {
      console.log(result.msg)
      proxy.$modal.msgSuccess(result.msg)
    })

  }


  onMounted(()=>{
    initWebSocket(ws.url);
  })

  onUnmounted(()=>{
    closeSocket();
  })

</script>

<style scoped lang="scss">
</style>

Nginx 发布配置


#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

	
	
	server {
		listen      30090;
		server_name   localhost;
		proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host:$server_port; #这里是重点,这样配置才不会丢失端口
		
		location /waystation-app {
			alias   D:/waystation/dist/;
			index  index.html index.htm;
			try_files $uri $uri/ /waystation-app/index.html;
        }
		
		location /waystation/{
			proxy_pass http://localhost:8081/waystation/;
			proxy_set_header Host $host;
			proxy_set_header X-Real_IP $remote_addr;
			proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
		}
	
		location /waystation/webSocket/ {
			proxy_pass http://localhost:8081/waystation/webSocket/;
			proxy_set_header Host $host;
			proxy_set_header X-Real_IP $remote_addr;
			proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
			proxy_http_version 1.1;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection "upgrade";
		}
		
	
		
		
		error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }	
	}

    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

备注:nginx 配置里面添加了对 webSocket 长连接的支持。

小结:

  1. Vite 获取环境变量的方式 import.meta.env.xxx
  2. Vite 下 配置Nginx 发布二级目录时,自定义 config.js 里采用Es6 的import, export 方式配置,同时在index.html 页面里 引入js 时指定type = module 模式。
  3. Nginx 下 配置 webSocket 时 要单独配置支持长连接的代理。

你可能感兴趣的:(SpringBoot,+VUE,系列,websocket,spring,boot,nginx)