Deprecated: 自 6.9.0 版本起,使用参数调用函数 WP_Dependencies->add_data() 已弃用!IE conditional comments are ignored by all supported browsers. in D:\wwwroot\zyxy\wordpress\wp-includes\functions.php on line 6131

Deprecated: 自 6.9.0 版本起,使用参数调用函数 WP_Dependencies->add_data() 已弃用!IE conditional comments are ignored by all supported browsers. in D:\wwwroot\zyxy\wordpress\wp-includes\functions.php on line 6131

Vue集成数据库变更通知(e.g., PostgreSQL LISTEN/NOTIFY):实现端到端的数据库级响应性

Vue集成数据库变更通知(e.g., PostgreSQL LISTEN/NOTIFY):实现端到端的数据库级响应性

大家好,今天我们来探讨一个非常有趣且实用的主题:如何将Vue前端与数据库变更通知(以PostgreSQL的LISTEN/NOTIFY为例)集成,从而构建一个端到端的数据库级响应式应用。

为什么需要数据库变更通知?

在传统的Web应用开发中,前端通常通过定时轮询或长轮询的方式来获取数据库数据的更新。这种方式存在一些明显的缺点:

  • 资源浪费: 即使数据没有发生变化,前端仍然会不断地发起请求,浪费服务器和客户端的资源。
  • 延迟高: 由于轮询的间隔时间有限制,前端无法实时地反映数据的变化,导致用户体验较差。
  • 扩展性差: 当用户数量增加时,大量的轮询请求会给服务器带来很大的压力,难以扩展。

数据库变更通知则是一种更高效、更实时的解决方案。它允许数据库在数据发生变化时主动通知应用程序,从而避免了轮询的开销,降低了延迟,提高了扩展性。

PostgreSQL LISTEN/NOTIFY机制

PostgreSQL提供了一套内置的发布/订阅机制,称为LISTEN/NOTIFY。其工作原理如下:

  1. LISTEN: 客户端使用LISTEN channel_name命令监听一个特定的频道。
  2. NOTIFY: 当数据库中的数据发生变化时,可以使用NOTIFY channel_name, payload命令向指定的频道发送通知。
  3. 客户端收到通知: 监听该频道的客户端会收到通知,并可以根据通知中的payload来采取相应的操作。

集成方案:后端Node.js(NestJS) + 前端Vue

我们将使用Node.js(NestJS框架)作为后端,负责与PostgreSQL数据库交互,并使用Vue作为前端,负责展示数据和处理通知。

1. 后端(NestJS)搭建

首先,我们需要创建一个NestJS项目:

npm i -g @nestjs/cli
nest new postgres-vue-integration
cd postgres-vue-integration

安装必要的依赖:

npm install pg nestjs-config @nestjs/platform-socket.io @nestjs/websockets

2. 数据库配置 (使用nestjs-config)

创建一个配置文件 config/default.ts:

// config/default.ts
export default () => ({
  database: {
    host: process.env.DATABASE_HOST || 'localhost',
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
    username: process.env.DATABASE_USER || 'postgres',
    password: process.env.DATABASE_PASSWORD || 'password',
    database: process.env.DATABASE_NAME || 'mydb',
  },
});

app.module.ts中引入配置模块:

// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from 'nestjs-config';
import * as path from 'path';

@Module({
  imports: [
    ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

3. 创建PostgreSQL连接和监听器

创建一个database.service.ts文件,负责管理数据库连接和监听通知:

// database.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy, Inject } from '@nestjs/common';
import { ConfigService } from 'nestjs-config';
import { Client } from 'pg';

@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
  private client: Client;

  constructor(@Inject(ConfigService) private readonly configService: ConfigService) {}

  async onModuleInit() {
    const dbConfig = this.configService.get('database');
    this.client = new Client(dbConfig);

    try {
      await this.client.connect();
      console.log('Connected to PostgreSQL database');

      // 监听 'products_channel' 频道
      await this.listenForNotifications('products_channel');

    } catch (error) {
      console.error('Error connecting to PostgreSQL:', error);
    }
  }

  async onModuleDestroy() {
    if (this.client) {
      await this.client.end();
      console.log('Disconnected from PostgreSQL database');
    }
  }

  async listenForNotifications(channel: string) {
    this.client.query(`LISTEN ${channel}`);

    this.client.on('notification', (msg) => {
      console.log(`Received notification on channel ${msg.channel}:`, msg.payload);
      // 在这里处理收到的通知,例如,发送给WebSocket客户端
      this.handleNotification(msg);
    });
  }

  private handleNotification(msg: any) {
    // TODO: 将通知通过 WebSocket 发送给前端
    // 这将在下一步 WebSocket 集成中实现
  }

  getClient(): Client {
      return this.client;
  }
}

4. 集成WebSocket

创建一个websocket.gateway.ts文件,用于处理WebSocket连接和发送通知:

// websocket.gateway.ts
import { WebSocketGateway, SubscribeMessage, MessageBody, WebSocketServer, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';
import { Logger } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
import { DatabaseService } from './database.service';
import { Inject } from '@nestjs/common';

@WebSocketGateway({ cors: true })
export class WebsocketGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {

  @WebSocketServer()
  server: Server;

  private logger: Logger = new Logger('WebsocketGateway');

  constructor(@Inject(DatabaseService) private readonly databaseService: DatabaseService) {}

  afterInit(server: Server) {
    this.logger.log('Initialized!');
    // 将 handleNotification 方法绑定到正确的 this 上下文
    this.databaseService['handleNotification'] = this.handleNotification.bind(this);
  }

  handleConnection(client: Socket, ...args: any[]) {
    this.logger.log(`Client connected: ${client.id}`);
  }

  handleDisconnect(client: Socket) {
    this.logger.log(`Client disconnected: ${client.id}`);
  }

  handleNotification(msg: any) {
    this.server.emit('product_updates', msg.payload); // 发送给所有连接的客户端
  }

  @SubscribeMessage('message')
  handleMessage(@MessageBody() message: string): void {
    this.server.emit('message', message);
  }

}

app.module.ts中注册 DatabaseServiceWebsocketGateway:

// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from 'nestjs-config';
import * as path from 'path';
import { DatabaseService } from './database.service';
import { WebsocketGateway } from './websocket.gateway';

@Module({
  imports: [
    ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
  ],
  controllers: [AppController],
  providers: [AppService, DatabaseService, WebsocketGateway],
})
export class AppModule {}

5. 创建一个触发 NOTIFY 的 API (可选)

创建一个简单的 controller 和 service 来模拟数据库更新并发送通知。

  • products.controller.ts:
// products.controller.ts
import { Controller, Post } from '@nestjs/common';
import { ProductsService } from './products.service';

@Controller('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @Post('update')
  async updateProduct(): Promise<string> {
    return this.productsService.updateProductAndNotify();
  }
}
  • products.service.ts:
// products.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { DatabaseService } from './database.service';
import { Client } from 'pg';

@Injectable()
export class ProductsService {
  constructor(@Inject(DatabaseService) private readonly databaseService: DatabaseService) {}

  async updateProductAndNotify(): Promise<string> {
    const client: Client = this.databaseService.getClient();

    try {
      // 模拟数据库更新
      await client.query("UPDATE products SET price = price + 1 WHERE id = 1");

      // 发送通知
      await client.query("NOTIFY products_channel, 'Product price updated'");

      return 'Product updated and notification sent!';
    } catch (error) {
      console.error('Error updating product and sending notification:', error);
      return 'Error updating product';
    }
  }
}
  • products.module.ts:
// products.module.ts
import { Module } from '@nestjs/common';
import { ProductsController } from './products.controller';
import { ProductsService } from './products.service';
import { DatabaseService } from './database.service';

@Module({
  controllers: [ProductsController],
  providers: [ProductsService, DatabaseService],
})
export class ProductsModule {}
  • app.module.ts 中导入 ProductsModule:
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from 'nestjs-config';
import * as path from 'path';
import { DatabaseService } from './database.service';
import { WebsocketGateway } from './websocket.gateway';
import { ProductsModule } from './products.module';

@Module({
  imports: [
    ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
    ProductsModule,
  ],
  controllers: [AppController],
  providers: [AppService, DatabaseService, WebsocketGateway],
})
export class AppModule {}

6. 前端(Vue)实现

创建一个Vue项目:

vue create vue-postgres-integration
cd vue-postgres-integration

安装 socket.io-client:

npm install socket.io-client

修改 src/App.vue

<template>
  <div id="app">
    <h1>Product Updates</h1>
    <p>Last Update: {{ lastUpdate }}</p>
    <ul>
      <li v-for="product in products" :key="product.id">
        {{ product.name }}: ${{ product.price }}
      </li>
    </ul>
  </div>
</template>

<script>
import { io } from 'socket.io-client';

export default {
  name: 'App',
  data() {
    return {
      lastUpdate: 'No updates yet',
      products: [
        { id: 1, name: 'Product A', price: 10 },
        { id: 2, name: 'Product B', price: 20 },
      ], // 初始产品数据
      socket: null,
    };
  },
  mounted() {
    this.socket = io('http://localhost:3000'); // 连接到 NestJS WebSocket 服务器

    this.socket.on('connect', () => {
      console.log('Connected to WebSocket server');
    });

    this.socket.on('product_updates', (data) => {
      console.log('Received product update:', data);
      this.lastUpdate = new Date().toLocaleTimeString(); // 更新时间戳
      //TODO: 重新从数据库获取产品数据, 这里为了演示直接更新价格
      this.products.forEach(product => {
          product.price = product.price + 1;
      });
    });
  },
  beforeDestroy() {
    if (this.socket) {
      this.socket.disconnect();
    }
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

7. 数据库配置和测试

  • 创建数据库和表:
CREATE DATABASE mydb;
c mydb

CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  price DECIMAL NOT NULL
);

INSERT INTO products (name, price) VALUES ('Product A', 10.00);
INSERT INTO products (name, price) VALUES ('Product B', 20.00);
  • 运行NestJS后端: npm run start:dev
  • 运行Vue前端: npm run serve

在浏览器中打开Vue应用,然后调用NestJS后端的 POST /products/update 接口,你应该能够看到前端数据实时更新。

代码示例总结

文件名 描述
config/default.ts 数据库配置信息。
database.service.ts 负责建立PostgreSQL连接,监听指定的频道,并在收到通知时调用 handleNotification 函数。
websocket.gateway.ts 使用 @nestjs/websockets 库创建WebSocket网关,处理客户端连接和断开,并将从数据库接收到的通知通过WebSocket发送给前端。
products.controller.ts 定义了一个 updateProduct 路由,用于触发 ProductsService 中的数据库更新和通知发送逻辑。
products.service.ts 包含了更新数据库和发送 NOTIFY 命令的逻辑。
App.vue Vue组件,用于连接WebSocket服务器,接收数据库变更通知,并更新页面上的数据。

进一步优化和扩展

  • Payload处理:NOTIFY命令中,payload可以包含更多信息,例如,更新的ID、字段等。前端可以根据payload的内容来更精确地更新数据,避免全量刷新。
  • 错误处理: 完善错误处理机制,例如,在连接数据库失败时进行重试,在WebSocket连接断开时自动重连。
  • 权限控制: 根据用户的角色和权限,控制可以监听的频道和可以修改的数据。
  • 数据同步策略: 根据业务需求,选择合适的数据同步策略。例如,可以全量刷新、增量更新或差异化同步。
  • 使用 ORM (TypeORM, Sequelize): 可以使用 ORM 来简化数据库操作,提高开发效率。
  • 使用 Redis Pub/Sub: 如果需要更高的性能和可扩展性,可以考虑使用 Redis Pub/Sub 代替 PostgreSQL 的 LISTEN/NOTIFY。

集成带来的好处

通过将Vue前端与PostgreSQL LISTEN/NOTIFY集成,我们可以获得以下好处:

  • 实时性: 前端可以实时地反映数据库数据的变化,提供更好的用户体验。
  • 高效性: 避免了轮询的开销,节省了服务器和客户端的资源。
  • 可扩展性: 可以轻松地扩展到大量的用户,而不会给服务器带来很大的压力。
  • 解耦性: 前端和后端之间解耦,可以独立地进行开发和部署。

关键技术的总结

我们通过NestJS构建后端服务,利用PostgreSQL的LISTEN/NOTIFY机制进行数据库变更通知,并通过WebSocket实时推送给Vue前端,实现了高效的端到端响应式数据更新。这种方案解决了传统轮询方式的资源浪费和延迟问题,提升了应用性能和用户体验。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注