如何在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:
- Install Strophe.js via npm:
npm install strophe.js --save
- 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
- XMPP Server: Ensure your server supports WebSocket connections (Prosody, Openfire, and ejabberd all do). Update the WebSocket URL in
xmpp.service.tsto match your server's configuration. - MAM (Message Archives): To fetch old messages, enable XEP-0313 on your XMPP server and grant users permission to access their archives.
- Type Safety: For better type support, you can expand the
strophe.d.tsfile with more specific type definitions for Strophe objects. - 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
相关产品推荐
相关产品推荐

