最近研究了新技术 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 只是一个打包工具。
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>
#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 长连接的支持。