赞
踩
随着跨端技术的发展,前端开发职能不再局限于浏览器,而是具备了很多客户端开发的能力,比如桌面应用框架Electorn
,移动App
框架React native
.
一般而言,前端同学对http
协议非常熟悉,在平时的工作中使用http
与后端通信居多.但在原生客户端领域,比如Java
语言开发的安卓应用,与后端通信的方式大多采用socket
.
众所周知,http
连接是一种短连接,即客户端向服务器端发送一次请求,服务器端响应后连接即会断掉.而socket
连接是一种长连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉.
前端领域存在一个和socket
连接功能相似的通信协议,即WebSocket
.WebSocket
创建了一种持久性的连接,后端不仅能正常处理客户端发送的消息,还能主动向客户端推送消息.
后端主动推送消息的能力在一些特殊的场景中太重要了,比如App
接受到的信息通知,即时通讯接受的好友信息,另外面板上实时展现波动的金融数据.
不管是桌面应用框架Electron
,还是App
开发框架React native
,它们都拥有基于原生平台封装的WebSocket
.比起浏览器端开放的WebSocket
,原生平台提供的支持要稳定很多.
因此在使用前端技术开发客户端应用时,完全可以使用WebSocket
协议作为前后端通信的主要方式,不再需要往项目中引入http
,因为http
拥有的能力WebSocket
同样也能找到替代方案.
本文接下来将详细介绍用react hook
开发一款客户端应用时,如何在项目中搭建有效的通信机制,让WebSocket
和redux
有机结合,在不影响前端习惯的编程风格下,建立起客户端与服务器的全双工通信.
前后端约定连接建立后,使用WebSocket
协议通信的数据格式(参考如下).
{
request_id,
command,
data,
}
request_id
是一段随机生成的字符串,用来标识本次客户端向服务器请求的id
值.command
是命令关键词,类似于接口中的url
,后端根据此标识来决定返回接口数据.data
是发送的参数.前端向后端发起请求时,以上3
个参数都得携带,其他参数可根据业务需要增添.
后端主动向前端推送消息时,只需要发送command
和data
参数即可.客户端监听command
,根据值的不同进行相应的操作.
在整个项目的通信架构下,前端需要搭建好以下两种通信机制.
客户端向服务器发送请求,服务器处理请求并返回响应结果,客户端接受响应结果再做后续处理.这种机制模拟了类似于前端ajax
的通信方式,客户端除了发送请求,还要负责接受该请求的响应.
服务器主动向客户端推送消息,客户端接受消息.
以上两种机制基本满足了开发的需要,接下来在项目实战中实现以上两种机制(源代码贴在了文章结尾).
登录页如下,页面内容很简答,两个输入框,输入账号和密码.还有一个登录按钮.
鼠标点击登录按钮时,dispatch
触发LoginAction
,此时客户端向服务器发起登录请求,请求成功后进入then
的回调函数,打印出登录成功
并且路由跳转到首页home
.
import { LoginAction } from "../../redux/actions/login"; export default function Login() { const dispatch = useDispatch(); const history = useHistory(); //省略 ... //登录 const login = ()=>{ dispatch(LoginAction()).then(()=>{ console.log("登录成功!"); history.push("/home"); }) } return ( <div> <ul> <li>登录账号:</li> <li> <div><input onChange={updateUser} type="text" placeholder="请输入账号" /></div> </li> <li>登录密码:</li> <li> <div><input onChange={updatePwd} type="text" placeholder="请输入密码" /></div> </li> </ul> <button onClick={login}>立即登录</button> </div> ) }
现在进入LoginAction
函数的内部实现,探索它是如何实现发起请求 - 接受响应
(代码如下).
LoginAction
内部调用了fetch
函数,action
的type
值为"PUSH_MESSAGE"
.
从这里大概可以推断出fetch
内部调用了dispatch
,派发了一个type
为"PUSH_MESSAGE"
的action
.
另外fetch
函数返回了一个Promise
,在then
回调函数里接受后端返回的响应.
import { fetch } from "./global"; // 组装action数据类型 const loginType = (username,password)=>{ return { type:"PUSH_MESSAGE", // 实际开发中这里应该用变量替代 value:{ command:"login", data:{ username, password } } } } export const LoginAction = ()=>(dispatch,getState)=>{ const { username,password } = getState().login; return fetch({ dispatch, action:loginType(username,password) }).then((response:any)=>{ console.log("接受后端响应,请求成功!"); }) }
由此可见,fetch
函数它能实现向后端发起请求,并且在返回的then
回调函数里获取响应的能力.
fetch
函数代码如下,调用fetch
时如果传递了第三个参数loading
,就调用openLoading(dispatch)
,从而修改reducer
中定义的一个全局状态loading
,页面就可以根据loaidng
值做加载中的样式变换.
fetch
函数内部主要返回了一个Promise
,核心代码便是将resolve
赋予了action
,那么这将意味着何时调用action.resolve()
,fetch
函数返回的then
回调函数将何时执行.
代码接下来执行dispatch(action)
,将传递给fetch
函数的参数action
派发了.
从上面代码可知,派发的action.type
的值为PUSH_MESSAGE
,那么肯定在redux
中有一个地方会监听到派发的action
,并触发对后端的请求.
/** * loading 决定本次请求需不需要让页面出现加载中的样式 */ export const fetch = ({dispatch,action,loading = false}) =>{ loading && openLoading(dispatch); // 加载中 return new Promise((resolve,reject)=>{ action.resolve = resolve; action.reject = reject; dispatch(action); }).finally(()=>{ // 异步请求完成后关闭loading closeLoading(dispatch); }) } //修改全局reducers/global.ts定义的loading状态 const openLoading = (dispatch)=>{ dispatch({ type:"UPDATE_GLOBAL_STATE", value:{ type:"loading", value:true } }) } const closeLoading = (dispatch)=>{ dispatch({ type:"UPDATE_GLOBAL_STATE", value:{ type:"loading", value:false } }) }
在哪个地方去监听type
值为PUSH_MESSAGE
的action
呢?这部分代码封装在redux
中间件函数里非常合适.
中间件函数不仅能解析action
的具体参数,它还能将全局要使用的WebSocket
与redux
绑定在一起,最终实现通过派发action
达到运用WebSocket
向后端发起请求的目的.
观察下面中间件函数,即wsMiddleware()
的返回值.redux
的中间件函数会在应用每一次派发action
后,都会执行一遍.
登录action
派发后,线程会进入下面的中间件函数运行.中间件函数里编写了一个switch
,分别监听type
值为CONNECT_READY
、PUSH_MESSAGE
以及DIS_CONNECT
,分别对应建立WebSocket
连接、向后端推送数据以及断开连接三种操作.
登录操作派发的action.type
正是等于PUSH_MESSAGE
,因此会进入第二个case
结构,代表向后端发起请求.
后面我们会设置应用启动时派发type
值为CONNECT_READY
的action
,即创建WebSocket
连接.假设进行登录操作时,WebSocket
连接已经创建好了,变量名为socket
.
在case 'PUSH_MESSAGE'
包裹的代码里,代码首先解析action
的参数command
和data
,并使用uuid
随机生成一个不重复的字符串request_id
.
command
、data
和request_id
组装成参数对象message
.接下来关键的一步代码,如果发现action
携带resolve
参数,说明本次请求是由调用上面fetch
函数发起的,因此需要将该action
缓存到callback_list
.最后调用socket.send
将请求发送给后端.
前后端商议好,凡是带有request_id
的请求,后端处理完后也要将request_id
联合响应返回给前端.这样前端就能知道返回的响应对应着哪一次发起的请求.
现在前端请求已经结束了,后端一旦处理完就会向前端发起推送通知,这时候就会触发onMessage
函数.
onMessage
函数拿到后端推送的消息,取出其中的request_id
,并检查callback_list
是否缓存过该action
,如果缓存了说明该请求是通过fetch
发起,那么此时调用action.resolve(response.data)
,就能触发fetch
返回的回调函数执行并将响应结果一同传递过去.
这样整个过程串联起来就会发现,页面组件先调用action
,而action
调用fetch
,进而fetch
又触发中间件函数使用Websocket
向后端发送数据,并将请求的action
缓存在callback_list
.后端返回响应后,中间件的监听函数从callback_list
里取出缓存的action
,并调用action.resolve
从而顺利触发了fetch
的回调函数执行.因此整个环节便实现了发起请求 - 接受响应
.
const WS = window.require('ws');// 安装基于node构建的websocket库ws,并使用window.require引入 import { messageResolve } from './common'; import { v1 as uuid } from 'uuid'; //请求缓存列表 const callback_list = {}; const wsMiddleware = () => { let socket = {}; // 存储websocket连接 /** * 连接成功了 */ const onOpen = (store) => { store.dispatch({ type: 'UPDATE_GLOBAL_STATE', value: { type: 'connected', payload: true, }, }); }; /** * 收到发送过来的消息 */ const onMessage = (store, response) => { if(typeof response === "string"){ response = JSON.parse(response); } let action; if (response.request_id && (action = callback_list[response.request_id])) { delete callback_list[response.request_id]; // 该请求缓存过了 action.resolve(response.data); } messageResolve(store, response); }; /** * 连接断开了 */ const onClose = (store) => { store.dispatch({ type: 'UPDATE_GLOBAL_STATE', value: { type: 'connected', payload: false, }, }); }; //定时器 let timer = null; //返回中间件函数 return (store) => (next) => (action) => { switch (action.type) { // 建立连接 case 'CONNECT_READY': timer = setInterval(() => { if (socket != null && (socket.readyState == 1 || socket.readyState == 2)) { //已经连接成功了 timer && clearInterval(timer); timer = null; return; } socket = new WS('ws://localhost:8080'); socket.on('open', () => { onOpen(store); }); socket.on('message', (data: any) => { onMessage(store, data); }); socket.on('close', () => { onClose(store); }); }, 1000); break; // 向后台推送消息 case 'PUSH_MESSAGE': const { command, data } = action.value; const message = { command, data, request_id: uuid(), }; if (action.resolve) { callback_list[message.request_id] = action; } socket.send(JSON.stringify(message)); // 想后端推送消息 break; // 应用主动发起断开连接的操作 case 'DIS_CONNECT': socket.close(); onClose(store); break; default: next(action); } }; }; export default wsMiddleware();
上面中间件函数还监听了两种操作,分别是'CONNECT_READY'
对应的建立连接和'DIS_CONNECT'
对应的断开连接.
在看上述操作之前,先在reducers
下面创建一个存储全局通用的状态文件global.js
(代码如下).文件分别定义了四个状态,分别是connected
、token
、is_login
以及loading
.
connected
用来标记当前Websocket
有没有处于连接上,比如突然断网connected
的值会变成false
,那么界面上就可以根据该状态值做相应的视图展现.
token
和is_login
是登录成功后赋予的值,下一次客户端再发起请求时就可以将token
值塞到data
中一起发送给后端做校验.
const defaultState = { connected: false, // 是否连接上 token: '', // 登录成功返回的token is_login:false, // 已经登录了吗 loading:false //页面是否显示加载中的样式 }; export default (state = defaultState, action: actionType) => { switch (action.type) { case 'UPDATE_GLOBAL_STATE': // 修改全局状态 const { type, payload } = action.value; return { ...state, [type]: payload }; default: return state; } };
全局状态定义了四个,而与中间件函数密切相关的属性是connected
.
case 'CONNECT_READY'
负责监听建立Websocket
连接的操作(代码如下),代码块里首先定义了一个定时器,每过一秒连接一次,直到与后端连接成功为止.
连接建立后,socket
分别监听了三个函数open
、message
和close
.open
函数会在连接建立成功后触发,成功后将全局状态connected
置为true
.
close
断开连接时触发,断开时将全局状态connected
置为false
.
message
监听后端推送的过来的消息.这里的消息分为两种类型.一种是前端发起请求,后端返回响应,另一种是后端主动推送消息.
那何时何地派发type
值为'CONNECT_READY'
的action
来建立Websocket
连接呢?
/** * 连接成功了 */ const onOpen = (store) => { store.dispatch({ type: 'UPDATE_GLOBAL_STATE', value: { type: 'connected', payload: true, }, }); }; /** * 收到发送过来的消息 */ const onMessage = (store, response) => { if(typeof response === "string"){ response = JSON.parse(response); } let action; if (response.request_id && (action = callback_list[response.request_id])) { delete callback_list[response.request_id]; // 该请求缓存过了 action.resolve(response.data); } messageResolve(store, response); }; /** * 连接断开了 */ const onClose = (store) => { store.dispatch({ type: 'UPDATE_GLOBAL_STATE', value: { type: 'connected', payload: false, }, }); }; //省略 ... case 'CONNECT_READY': timer = setInterval(() => { if (socket != null && (socket.readyState == 1 || socket.readyState == 2)) { //已经连接成功了 timer && clearInterval(timer); timer = null; return; } socket = new WS('ws://localhost:8080'); socket.on('open', () => { onOpen(store); }); socket.on('message', (data: any) => { onMessage(store, data); }); socket.on('close', () => { onClose(store); }); }, 1000);
文章其实上面已经提及,建立连接应该发生在应用启动之时,因为只有当Websocket
连接成功了,后面所有的操作才有意义.
新建一个组件WsConnect
执行连接操作(代码如下).组件先判断全局状态connected
值,如果发现没有连接上,随即派发CONNECT_READY
,触发中间件的函数执行创建Websocket
连接的操作.
const WsConnect = (props) => { const dispatch = useDispatch(); const { connected } = useSelector((state)=>state.global); //建立websocket连接 if(!connected){ dispatch({ type:"CONNECT_READY" }); } return ( <div> {props.children} </div> ); } export default WsConnect;
最后将WsConnect
塞入到react
的根组件App
中,这样就能确保应用在启动之时就会派发action
建立Websocket
连接.
export default function App() {
return (
<Provider store={store}>
<WsConnect>
<Router />
</WsConnect>
</Provider>
);
}
我们再回到最初讲解的LoginAction
(代码如下),中间件函数内监听到后端响应回来时会执行action.resolve(response.data)
.
这句代码一执行就会触发fetch
返回的then
回调函数执行.
回调函数将后端返回的值赋予了全局状态token
,并将全局状态is_login
设置为true
,代表登录成功了.
const updateGlobalType = (type,value)=>{ return { type:"UPDATE_GLOBAL_STATE", value:{ type, value } } } export const LoginAction = ()=>(dispatch,getState)=>{ const { username,password } = getState().login; return fetch({ dispatch, action:loginType(username,password) }).then((response)=>{ dispatch(updateGlobalType("token",response.token)); // 存储token值 dispatch(updateGlobalType("is_login",true)); //将全局状态is_login置为true }) }
由于上面fetch
函数前面加了一个return
返回自己的执行结果,因此界面上调用dispatch(LoginAction())
也能返回一个then
回调函数(代码如下).
在回调函数里引用react-router-dom
提供的api
,登录成功后页面立马跳转到首页,至此整个登录流程完结.
import { useHistory } from "react-router-dom"; import { LoginAction } from "../../redux/actions/login"; export default function Login() { const dispatch = useDispatch(); const history = useHistory(); //省略 ... //登录 const login = ()=>{ dispatch(LoginAction()).then(()=>{ console.log("登录成功!"); history.push("/home"); }) } return ( <div> <ul> <li>登录账号:</li> <li> <div><input onChange={updateUser} type="text" placeholder="请输入账号" /></div> </li> <li>登录密码:</li> <li> <div><input onChange={updatePwd} type="text" placeholder="请输入密码" /></div> </li> </ul> <button onClick={login}>立即登录</button> </div> ) }
登录功能实践了发起请求 - 接受响应
的整体环节,接下来实现服务器主动推送消息的机制.
Demo
最终实现效果图如下.登录成功后,页面跳转到首页.在客户端没发起请求的条件下,应用会连续收到后端发送过来的推送通知,并将推送的数据渲染到首页视图上.
通过上面对中间件函数讲解可知,onMessage
专门负责处理后端推送过来的消息(代码如下).如果是后端主动推送的消息通知,代码会进入messageResolve
函数执行.
import { messageResolve } from './common'; /** * 收到发送过来的消息 */ const onMessage = (store, response) => { if(typeof response === "string"){ response = JSON.parse(response); } let action; if (response.request_id && (action = callback_list[response.request_id])) { delete callback_list[response.request_id]; // 该请求缓存过了 action.resolve(response.data); } messageResolve(store, response); };
messageResolve
函数(代码如下)一方面会派发type
为MESSAGE_INCOMMING
的action
,触发某些页面上定义的监听逻辑.
另一方面它会解析出响应的command
字段,用来判端是否触发一些公共功能.比如全局的消息通知以及版本升级的操作.
/** * 消息处理 */ export const messageResolve = (store, response) => { //将推送的消息广播全局,因为可能某些页面需要监听消息 store.dispatch({ type: 'MESSAGE_INCOMMING', value: response, }); //公共功能的开发 switch (response.command) { case 'message_inform': //消息通知,可以用弹框提醒 console.log(`后端推送一条通知:${JSON.stringify(response.data)}`); break; case 'software_upgrading'://版本升级 console.log("触发版本升级的窗口"); break; } };
首页reducer
一旦监听到messageResolve
派发的action
(代码如下),会解析出command
字段的值,如果发现command
值与"home/add_item"
相等,说明后端想在首页上实时动态添加数据.
最终首页视图会获取reducer
定义的list
状态渲染列表,当后端主动推送一条数据时,页面就会触发重新渲染.
至此后端主动推送的机制便已实现.
const defaultState = { list: [] }; export default (state = defaultState, action: actionType) => { switch (action.type) { case 'MESSAGE_INCOMMING': //监听后端推送过来的消息 if(action.value.command === "home/add_item"){ // 添加一条数据 return {...state,list:[...state.list,action.value.data]}; } return state; break; default: return state; } };
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。