最近项目没那么急,就花时间多了解一下typescript和node.js,当然学习最好的方式做实例应用。
之前在做叽歪助手站内通知系统时简单了解了一下node.js,但是由于项目要求通知系统的灵活、扩展,关键很多交互不是在Web客户端。叽歪助手的通知系统更多的是系统通知,例如支付通知,订单通知等,都是触发的服务端事件。虽然放在客户端也可以,但是系统通知的第一要求是稳定可靠,放在客户端不可控因素太多,最后还是放在服务端,因此放弃了用node.js来搭建通知系统的方案,最终选择基于SignalR来搭建了一个自宿主的通知服务器,Web应用端作为连接客户端用C#来消费,Web终端只是通知的接收,没有任何通知触发。有关用C#+owin搭建自宿主服务可以参考:http://jiang.jonvie.com/post/2018/06/14/49b32cda4dff7fcb
其实node.js搭建通知服务,应该也可以做到让.net的服务端C#去触发通知事件,但是拐弯较多(可能我设想的方案有问题),但是node.js搭建大量js客户端去消费的服务端还是不错的。这次为了加深对node.js的了解和熟悉,同时学习了解一下typescript,就选择用这两者结合做一个demo来熟悉了解特性,说不定未来哪天会用到。
好了,这里我不去复制网上的文字,只谈我的个人理解:
1、关于node.js,我认为是一个轻量的js运行环境,或者说是javascript的虚拟机。通常我们熟悉跑js的环境就是浏览器,对,这里我就把node当成另外一个独立运行的程序,并提供了js的运行环境。在win下,理论上一个独立运行的exe是可以干很多事情的,node的天下就是提供了很多的开放的包,让js可以通过这些包的功能和接口,用你最熟悉的js语言,让node去干以前在浏览器里面干不了的事情,甚至是让node去监听一个端口,打开http服务,这样你的js一个文件通过node去运行就让node成了一个http服务器(后面马上要用到)。
2、关于typescript,我认为就是换了一个语言习惯和思维去写js,然后在用编译工具tsc把ts源码翻译成js源码。也就是ts并不是直接执行,最终执行的都是js。大概就是js是个很好工具大家都认同,但是写起来真的太灵活(太灵活的东西真的很麻烦)。于是就用ts来强制规定一下按一个套路来写便于管理。另外呢,便于写习惯C#和Java的猿们,可以方便的用自己习惯接近的语法来写js。C#和Java猿们,必须要跟浏览器、nodejs等换进交流时,不懂他们的语言javascript没关系,可以用跟你方言较接近的typescript来整,再通过tsc来翻译成js丢到js环境里去运行。
嗯,我认为的就这样,下面实际看看实现思路和代码,这里我是用vscode作为开发工具,一般项目开发用vs,用习惯了,也便于偷懒。但是一般学习和开发小玩意,还是喜欢用vscode这样的打开快的文本类工具,可以更深入了解编译执行过程。下面来开发(这里略过全部需要工具环境的安装搭建过程,提到要用的包自行百度去安装):
1、首先做及时通讯要建立一个通讯的中间服务器来接收客户响应,并根据客户端的响应来做消息推送,这个其实nodejs很有优势,这里也就如前文所说,用typescript在node下开发一个小的http服务监听,并提供长连接的访问和监听,这里需要用到socket.io(自行去百度安装)。node.js的运行方式是cmd下启动,同时运行一个启动入口文件(这里定义为来启动整个应用,这里将入口文件定义为AppServer.ts,生成后为同名js后缀文件。
import { createServer, Server, IncomingMessage, ServerResponse, request } from 'http';
import { readFile } from 'fs';
import io from 'socket.io';
import {User, UserGroup} from './Com/User';
const hostname: string = "localhost";//提供http服务的主机名或者ip
const port: number = 3000;//http服务的端口
var count: number = 0;//记录并统计当前服务的连接数
var mainUsers:UserGroup=new UserGroup();//定义个链接用户组的实例,用于保存连接用户信息
//创建一个http服务器对象
var server: Server = createServer((req: IncomingMessage, res: ServerResponse) => {
console.log(req.url);
readFile('./index.html', function (err: NodeJS.ErrnoException, data: Buffer) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data, 'utf-8');
});
});
//启动服务器对象监听
server.listen(port, hostname, () => {
console.log(`服务已经启动: http://${hostname}:${port}/`);
});
//通过IO监听当前http连接服务,创建一个socket实例
var ioSer=io.listen(server);
//监听socket的客户端的连接事件,用于处理新的客户端打开连接的事件操作
ioSer.sockets.on('connection', function (socket: io.Socket) {
count++;
//为当前的连接客户端创建一个user实例对象,只对当前的连接有效
let user=new User(socket.client.id);
user.UserName='anymouse';
//将当前的连接客户端信息添加到全局的连接用户清单
mainUsers.addOrUpdateUser(user);
console.log(`${socket.client.id} 已连接,连接ID:${mainUsers.get(socket.client.id).ConnID} 连接数:${count}`);
//用户连接时,给所全局有连接客户端推送一个online事件
//向全局所有的连接客户端广播当前的连接数
ioSer.emit('online', { number: count});
//向当前客户端推送updateUser事件
//上线后给当前客户端发送用户列表,仅向当前打开的连接客户端推送一次用户列表数据
socket.emit('updateUser', mainUsers.all());
//socket.emit('online', { number: count});
//socket.broadcast.emit('online', { number: count });
//监听客户端发送的updateUser事件,用于处理用户端修改user资料
socket.on('updateUser',function(data){
user.UserName=data.userName;
//将更新的用户资料修改到全局的用户清单
mainUsers.addOrUpdateUser(user);
//有用户修改用户信息后,通知诶所有连接客户端
ioSer.emit('updateUser', mainUsers.all());
});
//监听客户端的toUser事件,并将用户消息转发给指定的目标用户
socket.on('toUser',function(data){
let toId:string=data.ToUserMsg.ConnID;
//如果指定的用户还在线,通过定向广播触发toMe事件,将消息定向转发给用户
//客户端需要监听toMe事件处理发送给自己的消息
if (ioSer.sockets.connected[toId]) {
var msg={ToUserMsg:{ConnID:toId},FromUserMsg:{ConnID:user.ConnID,UserName:user.UserName},MsgContent:data.MsgContent}
ioSer.sockets.connected[toId].emit('toMe',msg);
}
});
//监听用户连接断开
socket.on('disconnect', function () {
count--;
mainUsers.deleteUser(socket.client.id);
console.log(`${socket.client.id} 已断开,连接数:${count}`);
ioSer.emit('online', { number: count});
});
});
上面用到了自定义对象User,UserGroup,我们这里定义文件User.ts
export class User {
public ConnID: string = "";
public UserName: string = "";
public UserID: string = "";
public UserSpace: string = "";
public Room: string = "";
constructor(_connID: string) {
this.ConnID = _connID;
}
}
export class UserGroup{
private _list:{[key: string]: User}={}
//获取当前实例的用户列表
public all():{[key: string]: User}{
return this._list;
}
public get(connId:string):User{
return this._list[connId];
}
public addOrUpdateUser(u:User):void{
this._list[u.ConnID]=u;
}
public deleteUser(connId:string):void{
delete this._list[connId];
}
public hasUser(u:User): boolean {
if (this._list[u.ConnID] == undefined)
return false;
else
return true;
}
}
除了引用的包,这里就是全部服务端代码,接下来就是客户端的处理html界面文件,我们这里也是放在服务端运行。在appserver.ts里
readFile('./index.html', function (err: NodeJS.ErrnoException, data: Buffer) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data, 'utf-8');
});
这一段就是讲index.html界面文件提供给访问客户端,在html里面引入了3个包,1、jquery,这个太厉害了,必须要用大家懂的,tmpl是用来在客户端格式化html模版的,剩下那个是客户端打开连接必须的。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Socket.IO Example</title>
</head>
<script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="http://ajax.microsoft.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js"></script>
<script src="http://localhost:3000/socket.io/socket.io.js"></script>
<script type="text/x-jquery-tmpl" id="msgTemp">
<div {{html Style.Box}}>
<div {{html Style.ID}}>${FromUserMsg.UserName}</div>
<div {{html Style.Content}}>{{html MsgContent}}</div>
</div>
</script>
<body>
<h1>TS+nodeJS实现及时通讯</h1>
<h2>在线用户<span id="count">0</span></h2>
<div id="msg" style="width:450px;height: 600px;border: 1px solid #000000;overflow:scroll;padding: 5px;"></div>
<div>
<div>用户名:<input type="text" id="userName"value="" />
发消息给:
<select id="userID">
<option value="">请选择</option>
</select>
</div>
<div>消息内容:<textarea id="msgText"></textarea></div>
<button id="send">发送</button>
</div>
<script>
var testData={UserMsg:{ConnID:"userid"},MsgContent:"一个文本消息"}
var path=window.location.pathname;
var thisUserName="anymouse";
console.log(path);
var count = document.getElementById('count');
var socket = io.connect('http://localhost:3000'+path);
socket.on('online',function(data){
count.innerHTML = data.number
});
socket.on('updateUser',function(data){
//console.log(data);
$("#userID").find('option').remove();
Object.keys(data).forEach(function(key){
//console.log(key,data[key]);
$("#userID").append(`<option value='${data[key].ConnID}'>${data[key].UserName}[${data[key].ConnID}]</option>`);
});
});
socket.on('toMe',function(data){
if(data.ToUserMsg.ConnID!=data.FromUserMsg.ConnID)
{
addMsg(data,false);
}
});
function addMsg(dataJson,isme)
{
dataJson.Style={Box:"",ID:"",Content:""};
if(isme)
{
dataJson.Style.Box='style="text-align: right;"';
dataJson.Style.ID='style="text-align: right;"';
dataJson.Style.Content='style="text-align: right;"';
}
$("#msgTemp").tmpl(dataJson).appendTo("#msg");
$("#msg").scrollTop($("#msg")[0].scrollHeight);
}
//addMsg(testData,false);
//addMsg(testData,true);
$(function(){
if($("#userName").val()=="")
{
$("#userName").val(thisUserName);
}
$("#send").click(function(){
if($("#userName").val()!="" && thisUserName!=$("#userName").val())
{
thisUserName=$("#userName").val();
socket.emit('updateUser', {userName:$("#userName").val()});
}
if($('#msgText').val()!="")
{
let msg={
ToUserMsg:{ConnID:$('#userID').val()},
FromUserMsg:{ConnID:"",UserName:thisUserName},
MsgContent:$('#msgText').val()
};
socket.emit('toUser',msg);
addMsg(msg,true);
}
});
});
</script>
</body>
</html>
好了,以上三个文件就构成了一个完成的聊天程序,这里聊天是通过服务器中转做的模拟点对点,计数通知都是全局的,基本全局发消息,私发消息的关键点都有用到。当然还有指定群组发消息等场景,基于以上demo,在参考官方文档,自己可以再琢磨一下。以上demo源码:
https://github.com/jonvie/nodechar