auction_server.ts
/**
* Created by Administrator on 2019/1/4/004.
*/
import * as express from 'express'
import {Server} from "ws";
/*app :声明服务器端提供的http服务的*/
const app = express();
export class Product {
constructor(
public id: number,
public title: string,
public price: number,
public rating: number,
public desc: string,
public categories: Array<string>
) {
}
}
export class Comment {
constructor(
public id: number,
public productId: number,
public timestamp: string,
public user: string,
public rating: number,
public content: string
) {
}
}
const products: Product[] = [
new Product(1, '第一个商品', 1.99, 2.5, '这是第一个商品', ['电子产品', '硬件设备']),
new Product(2, '第二个商品', 2.99, 3.5, '这是第二个商品', ['图书']),
new Product(3, '第三个商品', 3.99, 4.5, '这是第三个商品', ['硬件设备']),
new Product(4, '第四个商品', 4.99, 3.5, '这是第四个商品', ['电子产品', '硬件设备']),
new Product(5, '第五个商品', 5.99, 2.5, '这是第五个商品', ['电子产品']),
new Product(6, '第六个商品', 6.99, 3.5, '这是第六个商品', ['图书'])
];
const comments: Comment[] = [
new Comment(1, 1, '2017-02-02 22:22:22', '张三', 3, '东西不错'),
new Comment(2, 1, '2017-03-02 22:22:22', '李四', 4, '东西挺不错'),
new Comment(3, 1, '2017-04-02 22:22:22', '王五', 2, '东西还行'),
new Comment(4, 2, '2017-04-02 22:22:22', '赵六', 3, '东西挺好'),
new Comment(5, 2, '2017-05-02 22:22:22', '孙七', 2, '东西一般'),
];
app.get('/',(req, res) => {
res.send("Hello Express!")
});
app.get('/api/products',(req, res) => {
res.json(products)
});
app.get('/api/product/:id',(req, res) => {
res.json(products.find((product) => product.id == req.params.id))
});
app.get('/api/product/:id/comments',(req,res) => {
res.json(comments.filter((comment: Comment) => comment.productId == req.params.id))
});
const server = app.listen(8000,"localhost",() => {
console.log("服务器已经启动,地址是:http://localhost:8000");
});
const wsServer = new Server({port:8085});
wsServer.on("connection",websocket => {
websocket.send("这个消息是服务器主动推送的");
websocket.on("message",message => {
console.log("接收到消息:"+message);
})
});
/*定时推送*/
setInterval(() => {
if(wsServer.clients){
wsServer.clients.forEach(client => {
client.send("这是定时推送");
})
}
},2000);
product.service.ts
import { Injectable } from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ProductService {
/* 初始化服务数据*/
constructor(private http: HttpClient) { }
getProducts(): Observable<any> {
/*return this.products;*/
return this.http.get('/api/products');
}
getProduct(id: number): Observable<any> {
/*return this.products.find((product) => product.id == id );*/
return this.http.get('/api/product/' + id);
}
getCommentsForProductId(id: number): Observable<any> {
/*return this.comments.filter((comment: Comment) => comment.productId == id);*/
return this.http.get('/api/product/' + id + '/comments');
}
getAllCategories(): string[] {
return ['电子产品', '硬件设备', '图书'];
}
}
export class Product {
constructor(
public id: number,
public title: string,
public price: number,
public rating: number,
public desc: string,
public categories: Array<string>
) {
}
}
export class Comment {
constructor(
public id: number,
public productId: number,
public timestamp: string,
public user: string,
public rating: number,
public content: string
) {
}
}
product.component.ts
product.component.html
product-detail.component.ts
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {Comment, Product, ProductService} from '../shared/product.service';
@Component({
selector: 'app-product-detail',
templateUrl: './product-detail.component.html',
styleUrls: ['./product-detail.component.css']
})
export class ProductDetailComponent implements OnInit {
product: Product;
comments: Comment[];
/* 星级评论*/
newRating = 5;
newComment = '默认好评';
isCommentHidden = true;
constructor(private routeInfo: ActivatedRoute,
private productService: ProductService) { }
ngOnInit() {
const productId: number = this.routeInfo.snapshot.params['productId'];
/*http服务:异步服务,服务器数据也许未返回,属性可能undefined
* 解决:模板加 ?
* */
/*3.调用方法获得商品*/
/* this.product = this.productService.getProduct(productId);*/
/*模板多处使用product,此处手动订阅*/
this.productService.getProduct(productId).subscribe(
product => this.product = product
);
/*4.调用方法获取评论*/
/*使用数组的方法comments,此处手动订阅*/
this.productService.getCommentsForProductId(productId).subscribe(
comments => this.comments = comments
);
}
...
}
product-detail.component.html
<div class="thumbnail">
<img src="http://placehold.it/820x230"/>
<div>
<h4 class="pull-right">{{product?.price}}元</h4>
<h4>{{product?.title}}</h4>
<h4>{{product?.desc}}</h4>
</div>
<div>
<p class="pull-right">{{comments?.length}}</p>
<p>
<app-stars [rating]="product?.rating"></app-stars>
</p>
</div>
</div>
<div class="well">
<div>
<button class="btn btn-success" (click)="isCommentHidden = !isCommentHidden">发表评论</button>
</div>
<!--星级评论-->
<div [hidden]="isCommentHidden">
<div>
<app-stars [(rating)]="newRating" [readonly]="false"></app-stars>
</div>
<div>
<textarea [(ngModel)]="newComment"></textarea>
</div>
<div>
<button class="btn" (click)="addComment()">提交</button>
</div>
</div>
<div class="row" *ngFor="let comment of comments">
<hr>
<div class="col-md-12">
<app-stars [rating]="comment.rating"></app-stars>
<span>{{comment.user}}</span>
<span class="pull-right">{{comment.timestamp}}</span>
<p></p>
<p>{{comment.content}}</p>
</div>
</div>
</div>
效果图
商品搜索功能
服务器端:
...
app.get('/api/products',(req,res) => {
let result = products;
let params = req.query;
if(JSON.stringify(params)!="{}"){
if(params.title) {
result = result.filter((p) => p.title.indexOf(params.title) != -1);
}
if(params.price && result.length>0) {
result = result.filter((p) => p.price <= parseInt(params.price));
}
if(params.category !== "-1" && result.length>0) {
console.log("3");
result = result.filter((p) => p.categories.indexOf(params.category) != -1);
}
}
res.json(result);
});
...
客户端:
app.module.ts
import {HttpClientModule} from '@angular/common/http';
...
imports: [
...
HttpClientModule
],
...
search.component.ts
import { Component, OnInit } from '@angular/core';
import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms';
import {ProductService} from '../shared/product.service';
@Component({
selector: 'app-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.css']
})
export class SearchComponent implements OnInit {
/*建数据模型*/
formModel: FormGroup;
categories: string[];
constructor(private productService: ProductService, fb: FormBuilder) {
fb = new FormBuilder();
this.formModel = fb.group({
title: ['', Validators.minLength(3)],
price: [null, this.positiveNumberValidator],
category: ['-1']
});
}
ngOnInit() {
this.categories = this.productService.getAllCategories();
}
/*正数校验器---》商品价格搜索框*/
positiveNumberValidator(control: FormControl): any {
if (!control.value) {
return null;
}
const price = parseInt (control.value, 10);
if (price > 0) {
return null;
} else {
return { positiveNumber: true};
}
}
onSearch() {
if (this.formModel.valid) {
console.log(this.formModel.value);
this.productService.searchEvent.emit(this.formModel.value);
}
}
}
product.service.ts
import { Component, OnInit } from '@angular/core';
import {Product, ProductService} from '../shared/product.service';
import {Observable} from 'rxjs/index';
@Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.css']
})
export class ProductComponent implements OnInit {
/*搜索商品*/
/*关键字*/
/*private keyword: string;
private titleFilter: FormControl = new FormControl();*/
private products: Observable<Product[]>;
private imgUrl = 'http://placehold.it/320x150';
/*注入服务*/
constructor(private productService: ProductService) {
/*this.titleFilter.valueChanges
.subscribe(
value => this.keyword = value
);*/
}
/*调用方法获得商品*/
ngOnInit() {
this.products = this.productService.getProducts();
/*订阅流,事件发送到流*/
this.productService.searchEvent.subscribe(
params => this.products = this.productService.search(params)
);
}
}
product.service.ts
import {EventEmitter, Injectable} from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http';
import {Observable} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ProductService {
/*EventEmitter:既可以作为事件订阅者,也可以作为事件发送者*//*搜索事件的流*/
searchEvent: EventEmitter<ProductSearchParams> = new EventEmitter();
/* 初始化服务数据*/
constructor(private http: HttpClient) { }
getProducts(): Observable<any> {/*return this.products;*/
return this.http.get('/api/products');
}
getProduct(id: number): Observable<any> {/*return this.products.find((product) => product.id == id );*/
return this.http.get('/api/product/' + id);
}
getCommentsForProductId(id: number): Observable<any> {/*return this.comments.filter((comment: Comment) => comment.productId == id);*/
return this.http.get('/api/product/' + id + '/comments');
}
getAllCategories(): string[] {
return ['电子产品', '硬件设备', '图书'];
}
search(params: ProductSearchParams): Observable<any> {
return this.http.get('/api/products', {params: this.encodeParams(params)});
}
private encodeParams(params: ProductSearchParams) {
return Object.keys(params)
.filter(key => params[key])
.reduce((sum: HttpParams, key: string) => {
sum = sum.append(key, params[key]);
return sum;
}, new HttpParams());
}
}
export class ProductSearchParams {
constructor(
public title: string,
public price: number,
public category: string
) {
}
}
export class Product {
constructor(
public id: number,
public title: string,
public price: number,
public rating: number,
public desc: string,
public categories: Array<string>
) {
}
}
export class Comment {
constructor(
public id: number,
public productId: number,
public timestamp: string,
public user: string,
public rating: number,
public content: string
) {
}
}
效果图
商品关注功能
点击关注按钮->创建连接
product-detail.component.html
<!--商品关注-->
<div class="thumbnail">
<button class="btn btn-default btn-lg"
[class.active] = "isWatched"
(click)="watchProduct()">
{{isWatched?'取消关注':'关注'}}
</button>
<label>最新出价:{{currentBid}}元</label>
</div>
web-socket.service.ts
import { Injectable } from '@angular/core';
import {Observable} from 'rxjs/index';
@Injectable({
providedIn: 'root'
})
export class WebSocketService {
ws: WebSocket;
constructor() { }
createObservableSocket(url: string, id: number): Observable<any> {
this.ws = new WebSocket(url);
return new Observable(
observable => {
this.ws.onmessage = (event) => observable.next(event.data);
this.ws.onerror = (event) => observable.error(event);
this.ws.onclose = (event) => observable.complete();
this.ws.onopen = (event) => this.sendMessage({productId: id});
}
);
}
sendMessage(message: any) {
this.ws.send(JSON.stringify(message));
}
}
app.module.ts
import {WebSocketService} from './shared/web-socket.service';
...
providers: [ProductService, WebSocketService],
...
product-detail.component.ts
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {Comment, Product, ProductService} from '../shared/product.service';
import {WebSocketService} from "../shared/web-socket.service";
@Component({
selector: 'app-product-detail',
templateUrl: './product-detail.component.html',
styleUrls: ['./product-detail.component.css']
})
export class ProductDetailComponent implements OnInit {
product: Product;
comments: Comment[];
newRating = 5;
newComment = '默认好评';
isCommentHidden = true;
/* 商品关注*/
isWatched = false;
currentBid: number;
constructor(private routeInfo: ActivatedRoute,
private productService: ProductService,
private wsService: WebSocketService) { }
ngOnInit() {
const productId: number = this.routeInfo.snapshot.params['productId'];
this.productService.getProduct(productId).subscribe(
product => {
this.product = product;
this.currentBid = product.price;
}
);
this.productService.getCommentsForProductId(productId).subscribe(
comments => this.comments = comments
);
}
addComment () {
const comment = new Comment(0, this.product.id, new Date().toISOString(), 'someone', this.newRating, this.newComment);
this.comments.unshift(comment);
const sum = this.comments.reduce((sum, comment) => sum + comment.rating, 0);
this.product.rating = sum / this.comments.length;
this.newComment = '默认好评';
this.newRating = 5;
this.isCommentHidden = true;
}
/* 商品关注*/
watchProduct () {
this.isWatched = !this.isWatched;
/*给一个服务器地址,createObservableSocket返回可观测的流,
客户端订阅流接收服务端发送的消息*/
this.wsService.createObservableSocket('ws://localhost:8085',this.product.id)
.subscribe();
}
}
将productId放入集合,生成新的商品出价,定时推送给客户端
look this :https://ask.csdn.net/questions/696871
服务器端
...
/*将productId放入一个集合*/
/*subscription:客户端,关注商品*/
const subscription = new Map<any, number[]>();
const wsServer = new Server({port:8085});
wsServer.on("connection",websocket => {
/*websocket.send("这个消息是服务器主动推送的");*/
websocket.on("message",message => {
console.log("接收到消息:"+message);
/*接收到的消息转换,字符串-->javascript对象*/
let messageObj = JSON.parse(<string>message);
let productIds = subscription.get(websocket) || [];
subscription.set(websocket, [...productIds, messageObj.productId]);
})
});
/**/
const currentBids = new Map<number, number>();
/*定时推送给客户端*/
setInterval(() => {
/*生成新的商品出价*/
products.forEach(p => {
let currentBid = currentBids.get(p.id) || p.price;
let newBid = currentBid + Math.random()*5;
currentBids.set(p.id, newBid);
})
/*循环客户端,推送感兴趣商品价格*/
subscription.forEach((productIds: number[], ws) => {
if(ws.readyState === 1){
let newBids = productIds.map( pid => ({
productId: pid,
bid: currentBids.get(pid)
}));
ws.send(JSON.stringify(newBids));
}else{/*客户端断开连接*/
subscription.delete(ws);
}
});
},2000);
客户端
product-detail.component.ts
...
watchProduct () {
this.isWatched = !this.isWatched;
/*给一个服务器地址,createObservableSocket返回可观测的流,
客户端订阅流接收服务端发送的消息*/
this.wsService.createObservableSocket('ws://localhost:8085', this.product.id)
.subscribe(
products => {
const product = products.find( p => p.productId = this.product.id);
this.currentBid = product.bid;
}
);
}
...
web-socket.service.ts
import {map} from 'rxjs/internal/operators';
...
createObservableSocket(url: string, id: number): Observable<any> {
this.ws = new WebSocket(url);
return new Observable<string>(
observable => {
this.ws.onmessage = (event) => observable.next(event.data);
this.ws.onerror = (event) => observable.error(event);
this.ws.onclose = (event) => observable.complete();
this.ws.onopen = (event) => this.sendMessage({productId: id});
}
).pipe(map(message => JSON.parse(<string>message)));
}
...
效果图
商品取消关注功能
客户端
product-detail.component.ts
...
/* 当前订阅的流:返回的值*/
subscription: Subscription;
...
watchProduct () {
if (this.subscription) {
this.subscription.unsubscribe();
this.isWatched = false;
this.subscription = null;
} else {
this.isWatched = true;
this.subscription = this.wsService.createObservableSocket('ws://localhost:8085', this.product.id)
.subscribe(
products => {
const product = products.find( p => p.productId = this.product.id);
this.currentBid = product.bid;
}
);
}
}
...
效果图