本文简单探讨socket即时通讯及基于flex布局的高性能框架AsyncDisplayKit在项目中的应用,代码较多,简单介绍下其中的部分模块,文章持续更新中...
- demo请私信
以下是效果移动端涉及主要三方框架
Texture、Alamofire、Socket.IO-Client-Swift、SwiftyJSON、FMDB、SSZipArchive...
node端涉及主要三方框架
socket.io、mongoose、redis、compression、apn...
- 关于AsyncDisplayKit
AsyncDisplayKit是一个UI框架,他就是对UIKit的封装,最初诞生于 Facebook,为了缓解主线程的压力,提升用户体验。它最大的特点就是"异步",将消耗时间的渲染、图片解码、布局以及其它 UI 操作等等全部移出主线程,这样主线程就可以对用户的操作及时做出反应,来达到流畅运行的目的。我们在项目中使用最多的就是tableview,接下来以tableview为例,简单介绍下ASDK在项目中的应用,更多ASDk内容 https://texturegroup.org
import AsyncDisplayKit
class HomeViewController: ASViewController {
fileprivate var dataSource: [CharItem] = []
private let tableNode = ASTableNode(style: .plain)
init() {
super.init(node: ASDisplayNode())
node.addSubnode(tableNode)
tableNode.dataSource = self
tableNode.delegate = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "首页"
tableNode.view.allowsMultipleSelectionDuringEditing = false
requestData()
}
func requestData() {
NetAlamofireTool.netWork(Constants.allUsers, parameters: ["":""], method: .get) { (result) in
for item in result[RET_DATA]["allUser"].arrayValue {
DLog(result, fileName: "HomeViewController", methName: "allUsers")
let charItem = CharItem(CharItemID: item["_id"].stringValue, name: item["name"].stringValue)
charItem content = "今天不逼自己学会72变,日后谁能代你承受81难"
charItem.avatar = URL.init(string: item["avatar"].stringValue)
self.dataSource.append(charItem)
}
self.tableNode.reloadData()
}
}
}
extension HomeViewController: ASTableDelegate, ASTableDataSource {
func numberOfSections(in tableNode: ASTableNode) -> Int {
return 1
}
func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {
let model = dataSource[indexPath.row]
let block: ASCellNodeBlock = {
return WJCellNode(model: model)
}
return block
}
func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) {
tableNode.deselectRow(at: indexPath, animated: false)
let meVC = MeViewController()
navigationController?.pushViewController(meVC, animated: true)
}
}
看完以上代码,会不会感觉这和tabeview 很相似,实现了和tableview的无缝连接,我们会发现少了我们常用的一个API heightForRowAt , ASDK帮我们摆脱计算高度的烦恼,基于flex布局,ASDK内部进行高度处理。关于自定义cell,ASDK是基于block回调的方式创建cell,不需要cell重用,内部已实现。
- 接下来介绍几个常用控件:
ASCollectionNode -> UICollectionView
ASPagerNode -> UIPageViewController
ASTableNode -> UITableView
ASViewController -> UIViewController
ASNavigationControllerv -> UINavigationController
ASTabBarController ->UIKit的 UITabBarController
ASImageNode ->UIImageView
ASTextNode -> UITextView - 自定义cell及布局方式在项目中的应用
import AsyncDisplayKit
class WJCellNode: ASCellNode {
private let avatarNode = ASNetworkImageNode()
private let titleNode = ASTextNode()
private let subTitleNode = ASTextNode()
private let timeNode = ASTextNode()
private let hairlineNode = ASDisplayNode()
init() {
super.init()
automaticallyManagesSubnodes = true
avatarNode.cornerRadius = 4.0
avatarNode.cornerRoundingType = .precomposited
avatarNode.image = UIImage(named: "user_me")
titleNode.attributedText = Common.attributeTitle()
titleNode.maximumNumberOfLines = 1
timeNode.attributedText = Common.attributeTime()
subTitleNode.attributedText = Common.attributeSubTitle()
subTitleNode.maximumNumberOfLines = 1
hairlineNode.backgroundColor = UIColor(white: 0, alpha: 0.15)
hairlineNode.style.preferredSize = CGSize(width: 9, height: Constants.lineHeight)
}
override func didLoad() {
super.didLoad()
}
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
avatarNode.style.preferredSize = CGSize(width: 48.0, height: 48.0)
titleNode.style.flexGrow = 1.0
subTitleNode.style.flexGrow = 1.0
subTitleNode.style.flexShrink = 1.0
subTitleNode.style.spacingAfter = 12
timeNode.style.spacingAfter = 16
let avatarLayout: ASLayoutSpec
avatarLayout = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 12), child: avatarNode)
avatarLayout.style.preferredSize = CGSize(width: 72.0, height: 76.0)
let topStack = ASStackLayoutSpec.horizontal()
topStack.alignItems = .center
topStack.children = [titleNode, timeNode]
var bottomElements: [ASLayoutElement] = []
bottomElements.append(subTitleNode)
let bottomStack = ASStackLayoutSpec.horizontal()
bottomStack.children = bottomElements
let stack = ASStackLayoutSpec.vertical()
stack.style.flexGrow = 1.0
stack.style.flexShrink = 1.0
stack.spacing = 6
stack.children = [topStack, bottomStack]
let layout = ASStackLayoutSpec.horizontal()
layout.alignItems = .center
layout.children = [avatarLayout, stack]
layout.style.preferredSize = CGSize(width: Constants.screenWidth, height: 72)
let absLayout = ASAbsoluteLayoutSpec(children: [layout, hairlineNode])
hairlineNode.style.layoutPosition = CGPoint(x: 76, y: 72 - Constants.lineHeight)
hairlineNode.style.preferredSize = CGSize(width: Constants.screenWidth - 76, height: Constants.lineHeight)
return absLayout
}
}
实现的效果
Socket.IO-Client-Swift的使用方式
import UIKit
import SocketIO
public typealias success = (_ json: String) -> Void
public typealias fail = (_ json: String) -> Void
let user:User = WJDBMannager.getUser()
let manager = SocketManager(socketURL: URL(string: Constants.base_Url)!, config: [.log(true), .connectParams(["token":user.Token ?? ""])])
var socket = manager.defaultSocket
public final class CornerSocketManager {
public static func connectWithToken(token:String,success: @escaping success,fail: @escaping fail) {
print("++++++++++++++++++++++++++++++++++++++++++")
print("token==",token)
socket.on(clientEvent: .connect) {data, ack in
success("连接成功...")
}
socket.connect(timeoutAfter: 15, withHandler: {
fail("连接超时...")
})
socket.connect()
}
}
- 消息的发送
let bodies:[String:String] = [
"latitude":"0",
"longitude":"0",
"msg":"消息消息消息消息",
"type":"txt"
]
let params:[String:Any] = [
"chat_type":"chat",
"from_user":"xu",
"to_user":"xu",
"type":"0",
"bodies":bodies
]
// 消息发送 根据发送的参数type类型确定是文本消息、定位、图片、视频流以及发送方
socket.emitWithAck("chat", params).timingOut(after: 20) {data in
print("结果返回==",data)
}
- node端消息模块的处理
var chat = function (io) {
io.on('connection', function (socket) {
var token = socket.handshake.query.auth_token;
var userName = vfglobal.token_Map[token];
socket.broadcast.emit('onArr', {'user': userName});
vfglobal.socket_Map[userName] = socket;
vfGroupUser.findAll(userName, function (data) {
data.findIndex(function (T, number, data) {
socket.join(T.group_name);
});
});
socket.on('chat', function (message, callback) {
var messageBody = message.bodies;
if (messageBody.type == 'image') {
var imageBuffer = messageBody.fileData;
console.log('imageBuffer==',imageBuffer)
var imageName = messageBody.fileName;
var imageType = path.extname(imageName);
var uuid = vfglobal.util.generateUUID();
var newImageName = uuid + imageType;
var savePath = "./public/images/chatImages/" + newImageName;
imageScale = 1;
var width = 200;
var height = 300;
fs.writeFile(savePath, imageBuffer, function(err) {
if(err) {vfglobal.MyLog(err)}
else {
message.bodies.size = {'width' : 200, 'height' : 300};
message.bodies.fileRemotePath = "images/chatImages/" + newImageName;
message.bodies.fileData = null;
if (imageScale == 1) {
message.bodies.thumbnailRemotePath = message.bodies.fileRemotePath;
sendMessage(message, callback, imageBuffer);
}
else {
var index = imageName.indexOf('.');
var length = imageName.length;
var type = imageName.substring(index + 1, length);
gm(imageBuffer)
.resize(width, height)
.toBuffer(type, function (err, buffer) {
fs.writeFile("./public/images/chatImages/s_" + newImageName, buffer, function (err) {
if (!err) {
message.bodies.thumbnailRemotePath = "images/chatImages/s_" + newImageName;
sendMessage(message, callback, buffer);
}
})
});
}
}
});
}
else if (messageBody.type == 'video') {
var audioBuffer = messageBody.fileData;
var audioName = messageBody.fileName;
var audioType = path.extname(audioName);
var uuid = vfglobal.util.generateUUID();
var newAudioName = uuid + audioType;
fs.writeFile("./public/video/" + newAudioName, audioBuffer, function (err) {
if (err){} else {
message.bodies.fileRemotePath = "video/" + newAudioName;
message.bodies.fileData = null;
sendMessage(message, callback);
}
})
}
else if(messageBody.type == 'location') {
var url = '高德地图API'
request.GetRequest(url, function (err, res) {
if (err) {} else {
var imgData = '';
var contentType = res.headers['content-type'];
var isBackImg = contentType.indexOf("image") >= 0;
res.on("data", function (chunk) {
imgData += chunk;
});
res.on("end", function () {
if (isBackImg) {
var uuid = vfglobal.util.generateUUID();
var imageFileName = uuid + '.PNG';
fs.writeFile("./public/images/mapImages/" + imageFileName, imgData, "binary", function(err){
if(err){}else {
message.bodies.fileRemotePath = "images/mapImages/" + imageFileName;
sendMessage(message, callback);
}
});
}
});
}
});
}
});
});
}