You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Ionic 2(TypeScript)中使用Strophe.js实现XMPP聊天?

Ionic 2+ with TypeScript & Strophe.js: XMPP Chat Module Implementation

Got it, let's walk through a practical, production-ready example that covers all the XMPP operations you need—connection, message archives, presence management, and contact lists—using Ionic 2+ with TypeScript.


First: Setup Dependencies

Strophe.js doesn't have official TypeScript types, so we'll install the library and add a basic type declaration to avoid compilation errors:

  1. Install Strophe.js via npm:
npm install strophe.js --save
  1. Create a type declaration file (src/typings/strophe.d.ts) to let TypeScript recognize Strophe:
declare module 'strophe.js' {
  export = Strophe;
}

declare var Strophe: any;

Step 1: Build an XMPP Service (Core Logic)

We'll encapsulate all XMPP logic in an injectable service—this keeps your components clean and allows reuse across the app.

Create src/app/services/xmpp.service.ts:

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class XmppService {
  private connection: any;
  private userJid: string;

  // Subjects to broadcast events to components
  public connectionStatus$ = new Subject<string>();
  public incomingMessages$ = new Subject<{from: string, body: string}>();
  public rosterUpdates$ = new Subject<any[]>();
  public presenceUpdates$ = new Subject<{jid: string, status: string}>();
  public archivedMessages$ = new Subject<any[]>();

  constructor() {}

  // Establish XMPP connection via WebSocket
  connect(jid: string, password: string) {
    this.userJid = jid;
    // Replace with your XMPP server's WebSocket endpoint
    this.connection = new Strophe.Connection('wss://your-xmpp-server:5281/xmpp-websocket');
    
    this.connection.connect(
      jid,
      password,
      (status: number) => this.handleConnectionStatus(status)
    );
  }

  // Handle connection state changes
  private handleConnectionStatus(status: number) {
    switch (status) {
      case Strophe.Status.CONNECTED:
        this.connectionStatus$.next('connected');
        this.setupEventHandlers();
        this.fetchRoster();
        this.sendPresence('available');
        break;
      case Strophe.Status.DISCONNECTED:
        this.connectionStatus$.next('disconnected');
        break;
      case Strophe.Status.CONNFAIL:
        this.connectionStatus$.next('connection_failed');
        break;
      default:
        this.connectionStatus$.next(`unknown_status_${status}`);
    }
  }

  // Register handlers for incoming messages/presence
  private setupEventHandlers() {
    // Incoming chat messages
    this.connection.addHandler(
      (msg: any) => {
        if (msg.getAttribute('type') === 'chat') {
          const from = msg.getAttribute('from');
          const body = msg.getElementsByTagName('body')[0]?.textContent;
          if (body) this.incomingMessages$.next({ from, body });
        }
        return true; // Keep handler active
      },
      null,
      'message',
      'chat'
    );

    // Presence updates (online/offline)
    this.connection.addHandler(
      (presence: any) => {
        const jid = presence.getAttribute('from').split('/')[0]; // Strip resource
        const status = presence.getAttribute('type') || 'available';
        this.presenceUpdates$.next({ jid, status });
        return true;
      },
      null,
      'presence'
    );
  }

  // Fetch contact list (roster)
  fetchRoster() {
    const rosterIq = this.connection.iq({
      type: 'get',
      id: 'roster_fetch'
    }).c('query', { xmlns: 'jabber:iq:roster' });

    this.connection.sendIQ(rosterIq, (result: any) => {
      const contacts = [];
      const items = result.getElementsByTagName('item');
      for (let i = 0; i < items.length; i++) {
        const item = items[i];
        contacts.push({
          jid: item.getAttribute('jid'),
          name: item.getAttribute('name') || item.getAttribute('jid'),
          subscription: item.getAttribute('subscription'),
          status: 'unknown'
        });
      }
      this.rosterUpdates$.next(contacts);
    });
  }

  // Update user presence (online/offline/away)
  sendPresence(status: string, statusMessage?: string) {
    const presence = this.connection.presence({ type: status === 'available' ? undefined : status });
    if (statusMessage) presence.c('status').t(statusMessage);
    this.connection.send(presence);
  }

  // Send a chat message
  sendMessage(toJid: string, body: string) {
    const msg = this.connection.message({
      to: toJid,
      type: 'chat',
      body
    });
    this.connection.send(msg);
  }

  // Fetch archived messages (XEP-0313 MAM)
  fetchArchivedMessages(contactJid: string, limit: number = 50) {
    const mamIq = this.connection.iq({
      type: 'get',
      id: 'archive_fetch'
    }).c('query', {
      xmlns: 'urn:xmpp:mam:2',
      with: contactJid,
      max: limit.toString()
    });

    this.connection.sendIQ(mamIq, (result: any) => {
      const messages = [];
      const forwardedItems = result.getElementsByTagName('forwarded');
      
      for (let i = 0; i < forwardedItems.length; i++) {
        const item = forwardedItems[i];
        const msg = item.getElementsByTagName('message')[0];
        const body = msg.getElementsByTagName('body')[0]?.textContent;
        const from = msg.getAttribute('from');
        const timestamp = item.getElementsByTagName('delay')[0]?.getAttribute('stamp');
        
        if (body) {
          messages.push({
            from,
            body,
            timestamp,
            isOwn: from === this.userJid
          });
        }
      }
      this.archivedMessages$.next(messages);
    });
  }

  // Disconnect from server
  disconnect() {
    if (this.connection) this.connection.disconnect();
  }
}

Step 2: Build a Chat Component to Use the Service

Create a chat page (src/app/chat/chat.page.ts) that uses the XMPP service to handle user interactions:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { XmppService } from '../services/xmpp.service';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-chat',
  templateUrl: './chat.page.html',
  styleUrls: ['./chat.page.scss'],
})
export class ChatPage implements OnInit, OnDestroy {
  private subs: Subscription[] = [];
  connectionStatus = 'disconnected';
  contacts: any[] = [];
  selectedContact: any;
  messages: any[] = [];
  messageInput = '';
  userJid = '';
  userPassword = '';

  constructor(private xmppService: XmppService) {}

  ngOnInit() {
    // Subscribe to connection status changes
    this.subs.push(
      this.xmppService.connectionStatus$.subscribe(status => {
        this.connectionStatus = status;
      })
    );

    // Subscribe to incoming messages
    this.subs.push(
      this.xmppService.incomingMessages$.subscribe(msg => {
        if (msg.from.includes(this.selectedContact?.jid)) {
          this.messages.push({...msg, isOwn: false});
        }
      })
    );

    // Subscribe to contact list updates
    this.subs.push(
      this.xmppService.rosterUpdates$.subscribe(contacts => {
        this.contacts = contacts;
      })
    );

    // Subscribe to presence updates
    this.subs.push(
      this.xmppService.presenceUpdates$.subscribe(update => {
        const contact = this.contacts.find(c => c.jid === update.jid);
        if (contact) contact.status = update.status;
      })
    );

    // Subscribe to archived messages
    this.subs.push(
      this.xmppService.archivedMessages$.subscribe(messages => {
        this.messages = [...messages, ...this.messages];
      })
    );
  }

  connectToXmpp() {
    if (this.userJid && this.userPassword) {
      this.xmppService.connect(this.userJid, this.userPassword);
    }
  }

  selectContact(contact: any) {
    this.selectedContact = contact;
    this.messages = [];
    this.xmppService.fetchArchivedMessages(contact.jid);
  }

  sendChatMessage() {
    if (this.selectedContact && this.messageInput.trim()) {
      this.xmppService.sendMessage(this.selectedContact.jid, this.messageInput);
      this.messages.push({
        from: this.userJid,
        body: this.messageInput,
        isOwn: true
      });
      this.messageInput = '';
    }
  }

  setPresence(status: string) {
    this.xmppService.sendPresence(status);
  }

  ngOnDestroy() {
    this.subs.forEach(sub => sub.unsubscribe());
    this.xmppService.disconnect();
  }
}

Corresponding HTML Template (chat.page.html)

<ion-header>
  <ion-toolbar>
    <ion-title>XMPP Chat</ion-title>
    <ion-buttons slot="end">
      <ion-button (click)="connectToXmpp()" *ngIf="connectionStatus === 'disconnected'">
        Connect
      </ion-button>
      <ion-button (click)="setPresence('unavailable')" *ngIf="connectionStatus === 'connected'">
        Go Offline
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>
  <!-- Connection Form -->
  <ion-card *ngIf="connectionStatus !== 'connected'" class="ion-padding">
    <ion-item>
      <ion-label position="stacked">XMPP JID</ion-label>
      <ion-input [(ngModel)]="userJid"></ion-input>
    </ion-item>
    <ion-item>
      <ion-label position="stacked">Password</ion-label>
      <ion-input type="password" [(ngModel)]="userPassword"></ion-input>
    </ion-item>
    <ion-button expand="block" (click)="connectToXmpp()" [disabled]="!userJid || !userPassword">
      Connect
    </ion-button>
    <ion-text color="medium" class="ion-text-center mt-2">
      Status: {{ connectionStatus.replace('_', ' ') }}
    </ion-text>
  </ion-card>

  <!-- Chat Interface -->
  <div *ngIf="connectionStatus === 'connected'" class="chat-container">
    <ion-row>
      <!-- Contact List -->
      <ion-col size="4">
        <ion-list>
          <ion-list-header>Contacts</ion-list-header>
          <ion-item *ngFor="let contact of contacts" (click)="selectContact(contact)" [class.selected]="selectedContact?.jid === contact.jid">
            <ion-avatar slot="start">
              <ion-icon name="person-circle" [color]="contact.status === 'available' ? 'success' : 'medium'"></ion-icon>
            </ion-avatar>
            <ion-label>
              <h2>{{ contact.name }}</h2>
              <p>{{ contact.status === 'available' ? 'Online' : 'Offline' }}</p>
            </ion-label>
          </ion-item>
        </ion-list>
      </ion-col>

      <!-- Message Area -->
      <ion-col size="8">
        <div *ngIf="selectedContact; else noContact">
          <ion-toolbar>
            <ion-title>{{ selectedContact.name }}</ion-title>
          </ion-toolbar>
          <ion-list class="messages-list">
            <ion-item *ngFor="let msg of messages" [class.own-message]="msg.isOwn">
              <ion-label class="message-bubble">
                {{ msg.body }}
                <p class="message-time">{{ msg.timestamp ? (msg.timestamp | date:'short') : 'Just now' }}</p>
              </ion-label>
            </ion-item>
          </ion-list>
          <ion-footer>
            <ion-toolbar>
              <ion-input [(ngModel)]="messageInput" placeholder="Type a message..."></ion-input>
              <ion-button slot="end" (click)="sendChatMessage()" [disabled]="!messageInput.trim()">
                <ion-icon name="send"></ion-icon>
              </ion-button>
            </ion-toolbar>
          </ion-footer>
        </div>
        <ng-template #noContact>
          <ion-card class="ion-text-center ion-padding">
            <ion-icon name="chatbubbles" size="large" color="medium"></ion-icon>
            <p>Select a contact to start chatting</p>
          </ion-card>
        </ng-template>
      </ion-col>
    </ion-row>
  </div>
</ion-content>

Basic Styling (chat.page.scss)

.selected {
  background-color: var(--ion-color-light);
}

.messages-list {
  height: calc(100vh - 200px);
  overflow-y: auto;
}

.message-bubble {
  padding: 8px 12px;
  border-radius: 16px;
  max-width: 70%;
}

.own-message {
  justify-content: flex-end;
}

.own-message .message-bubble {
  background-color: var(--ion-color-primary);
  color: white;
}

.message-time {
  font-size: 10px;
  margin-top: 4px;
  opacity: 0.7;
}

Critical Setup Notes

  1. XMPP Server: Ensure your server supports WebSocket connections (Prosody, Openfire, and ejabberd all do). Update the WebSocket URL in xmpp.service.ts to match your server's configuration.
  2. MAM (Message Archives): To fetch old messages, enable XEP-0313 on your XMPP server and grant users permission to access their archives.
  3. Type Safety: For better type support, you can expand the strophe.d.ts file with more specific type definitions for Strophe objects.
  4. Security: Never hardcode credentials in your app—use secure authentication flows (like OAuth if your server supports it) or prompt users for credentials securely.

内容的提问来源于stack exchange,提问作者gaurang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:14:51