Skip to main content

如何与 Vue 3 一起使用

¥How to use with Vue 3

本指南展示了如何在 Vue 3 应用中使用 Socket.IO。

¥This guide shows how to use Socket.IO within a Vue 3 application.

示例

¥Example

结构:

¥Structure:

src
├── App.vue
├── components
│ ├── ConnectionManager.vue
│ ├── ConnectionState.vue
│ └── MyForm.vue
├── main.js
└── socket.js

Socket.IO 客户端在 src/socket.js 文件中初始化:

¥The Socket.IO client is initialized in the src/socket.js file:

src/socket.js

import { reactive } from "vue";
import { io } from "socket.io-client";

export const state = reactive({
connected: false,
fooEvents: [],
barEvents: []
});

// "undefined" means the URL will be computed from the `window.location` object
const URL = process.env.NODE_ENV === "production" ? undefined : "http://localhost:3000";

export const socket = io(URL);

socket.on("connect", () => {
state.connected = true;
});

socket.on("disconnect", () => {
state.connected = false;
});

socket.on("foo", (...args) => {
state.fooEvents.push(args);
});

socket.on("bar", (...args) => {
state.barEvents.push(args);
});
info

在开发过程中,你需要在服务器上启用 CORS:

¥During development, you will need to enable CORS on your server:

const io = new Server({
cors: {
origin: "http://localhost:8080"
}
});

参考:处理 CORS

¥Reference: Handling CORS

tip

事件监听器在 src/socket.js 文件中注册,因为我们强烈建议不要在组件中注册监听器。如下 有更多信息。

¥The event listeners are registered in the src/socket.js file, as we strongly advise against registering listeners in your components. More on that below.

然后你可以在你的组件中使用它:

¥You can then use it in your components:

  • src/components/ConnectionState.vue
<template>
<p>State: {{ connected }}</p>
</template>

<script>
import { state } from "@/socket";

export default {
name: "ConnectionState",

computed: {
connected() {
return state.connected;
}
}
}
</script>
  • src/components/ConnectionManager.vue
<template>
<button @click="connect()">Connect</button>
<button @click="disconnect()">Disconnect</button>
</template>

<script>
import { socket } from "@/socket";

export default {
name: "ConnectionManager",

methods: {
connect() {
socket.connect();
},
disconnect() {
socket.disconnect();
}
}
}
</script>
tip

socket 对象也可以在不立即使用 autoConnect 选项连接的情况下进行初始化:

¥The socket object can also be initialized without connecting right away with the autoConnect option:

export const socket = io(URL, {
autoConnect: false
});

例如,当用户在连接之前必须提供一些凭据时,这可能很有用。

¥This can be useful for example when the user must provide some credentials before connecting.

  • src/components/MyForm.vue
<template>
<form @submit.prevent="onSubmit">
<input v-model="value" />

<button type="submit" :disabled="isLoading">Submit</button>
</form>
</template>

<script>
import { socket } from "@/socket";

export default {
name: "MyForm",

data() {
return {
isLoading: false,
value: ""
}
},

methods: {
onSubmit() {
this.isLoading = true;

socket.timeout(5000).emit("create-something", this.value, () => {
this.isLoading = false;
});
},
}
}
</script>

参考:https://vuejs.org/guide/scaling-up/state-management.html

¥Reference: https://vuejs.org/guide/scaling-up/state-management.html

重要注意

¥Important notes

info

这些言论对于任何前端框架都有效。

¥These remarks are valid for any front-end framework.

热模块重载

¥Hot module reloading

包含 Socket.IO 客户端初始化的文件(即上例中的 src/socket.js 文件)的热重载可能会使先前的 Socket.IO 连接保持活动状态,这意味着:

¥The hot reloading of a file that contains the initialization of a Socket.IO client (i.e. the src/socket.js file in the example above) might leave the previous Socket.IO connection alive, which means that:

  • 你的 Socket.IO 服务器上可能有多个连接

    ¥you might have multiple connections on your Socket.IO server

  • 你可能会收到来自先前连接的事件

    ¥you might receive events from the previous connection

唯一已知的解决方法是在更新此特定文件时进行全页重新加载(或完全禁用热重新加载,但这可能有点极端)。

¥The only known workaround is to do a full-page reload when this specific file is updated (or disable hot reloading altogether, but that might be a bit extreme).

参考:https://vue-loader.vuejs.org/guide/hot-reload.html

¥Reference: https://vue-loader.vuejs.org/guide/hot-reload.html

子组件中的监听器

¥Listeners in a child component

我们强烈建议不要在子组件中注册事件监听器,因为它将 UI 的状态与事件接收时间联系起来:如果未安装该组件,则可能会丢失一些消息。

¥We strongly advise against registering event listeners in your child components, because it ties the state of the UI with the time of reception of the events: if the component is not mounted, then some messages might be missed.

src/components/MyComponent.vue

<script>
import { socket } from "@/socket";

export default {
name: "MyComponent",

data() {
return {
fooEvents: []
}
},

mounted() {
// BAD
socket.on("foo", (...args) => {
this.fooEvents.push(args);
});
}
}
</script>
note

不过,这在你的根组件中很好(因为它总是被安装的)。

¥This is fine in your root component though (since it is always mounted).

暂时断线

¥Temporary disconnections

WebSocket 连接虽然非常强大,但并不总是启动并运行:

¥While very powerful, WebSocket connections are not always up and running:

  • 用户和 Socket.IO 服务器之间的任何事情都可能会遇到临时故障或重新启动

    ¥anything between the user and the Socket.IO server may encounter a temporary failure or be restarted

  • 作为自动缩放策略的一部分,服务器本身可能会被终止

    ¥the server itself may be killed as part of an autoscaling policy

  • 如果使用移动浏览器,用户可能会失去连接或从 Wi-Fi 切换到 4G

    ¥the user may lose connection or switch from Wi-Fi to 4G, in case of a mobile browser

这意味着你需要正确处理临时断开连接,以便为用户提供良好的体验。

¥Which means you will need to properly handle the temporary disconnections, in order to provide a great experience to your users.

好消息是 Socket.IO 包含一些可以帮助你的功能。请检查:

¥The good news is that Socket.IO includes some features that can help you. Please check:

使用 Pinia

¥With Pinia

info

Pinia 是 Vue 的存储库,它允许你跨组件/页面共享状态。

¥Pinia is a store library for Vue, it allows you to share a state across components/pages.

更多信息可参见 此处

¥More information can be found here.

Pinia 的存储和 Socket.IO 连接可以通过以下模式同步:

¥Pinia's stores and Socket.IO connection can be synced with the following pattern:

stores/item.js
import { defineStore } from "pinia";
import { socket } from "@/socket";

export const useItemStore = defineStore("item", {
state: () => ({
items: [],
}),

actions: {
bindEvents() {
// sync the list of items upon connection
socket.on("connect", () => {
socket.emit("item:list", (res) => {
this.items = res.data;
});
});

// update the store when an item was created
socket.on("item:created", (item) => {
this.items.push(item);
});
},

createItem(label) {
const item = {
id: Date.now(), // temporary ID for v-for key
label
};
this.items.push(item);

socket.emit("item:create", { label }, (res) => {
item.id = res.data;
});
},
},
});
stores/connection.js
import { defineStore } from "pinia";
import { socket } from "@/socket";

export const useConnectionStore = defineStore("connection", {
state: () => ({
isConnected: false,
}),

actions: {
bindEvents() {
socket.on("connect", () => {
this.isConnected = true;
});

socket.on("disconnect", () => {
this.isConnected = false;
});
},

connect() {
socket.connect();
}
},
});

然后在你的根组件中:

¥And then in your root component:

App.vue
<script setup>
import { useItemStore } from "@/stores/item";
import { useConnectionStore } from "@/stores/connection";
import { socket } from "@/socket";

const itemStore = useItemStore();
const connectionStore = useConnectionStore();

// remove any existing listeners (after a hot module replacement)
socket.off();

itemStore.bindEvents();
connectionStore.bindEvents();
</script>

示例项目

¥Sample projects

返回示例列表

¥Back to the list of examples