wodax-login-qrcode.react
Version:
login component with qrcode for WodaX applications
793 lines (694 loc) • 21.1 kB
JavaScript
// @flow
import React, { createElement, PureComponent } from 'react';
import QRCode from 'qrcode.react';
import PropTypes from 'prop-types';
const StateQrCodeLogin = {
ready: `ready`, // 准备阶段,二维码未获取,界面准备阶段
loginFailed: `loginFailed`, // 登录反馈阶段:二维码扫描登录失败
loginSuccess: `loginSuccess`, // 登录反馈阶段:二维码扫描登录成功
scanComplete: `scanComplete`, // 登录反馈阶段: 二维码已经扫描,等待用户授权
codeOutWork: `codeOutWork`, // 登录反馈阶段: 二维码失效
networkLost: `networkLost`, // 登录反馈阶段:长连接丢失
qrCodeGetting: `qrCodeGetting`, // 信息获取:二维码获取...
qrCodeGetSuccess: `qrCodeGetSuccess`, // 信息获取:二维码获取成功
qrCodeGetFailed: `qrCodeGetFailed`, // 信息获取:二维码获取失败
};
const ErrorType = {
eventSourceError: 0, // eventSource error
};
const noop = () => {};
let GlobalDebugEnable = false;
const debugLog = function(type, ...args) {
try {
if (GlobalDebugEnable) {
console[type || `log`].apply(console, [' [WODAX_DEBUG]: '].concat(args));
} else {
noop();
}
} catch (e) {
console.error(e);
}
};
class WodaXLoginWithQrCode extends PureComponent {
// 静态方法:暴露状态枚举
static getEnumStateQrCodeLogin = () => StateQrCodeLogin;
state = {
// 默认状态
currentQrLoginState: StateQrCodeLogin.ready,
// 初始二维码值,二维码值来源于内部封装服务或者外部服务
qrcode: ``,
// 接收到的事件存储起来
sseEvents: [],
};
componentDidMount(): * {
this.updateQrCode();
}
componentWillUnmount(): * {
this.uninstallEventSource();
}
render() {
const {
className,
style,
titleRender,
qrCodeRender,
toolTipRender,
additionLayerRender,
refreshQrCodeRender,
sseMessageRender,
} = this.props;
return (
<div
className={className}
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
...style,
}}
>
{/*标题渲染*/}
{titleRender(this)}
{/*显示二维码*/}
{qrCodeRender(this)}
{/*显示提示信息*/}
{toolTipRender(this)}
{/*附加层渲染处理*/}
{additionLayerRender(this)}
{/*刷新渲染*/}
{refreshQrCodeRender(this)}
{/*接收实时数据渲染*/}
{sseMessageRender(this)}
</div>
);
}
/**
* 获取或更新二维码
*/
updateQrCode = () => {
const { options, fetchQrCodeHandler } = this.props;
const { qrcode: oldQrCode } = this.state;
const { useEventSourceForFetchQrCode } = options;
this.setState(
{
currentQrLoginState: StateQrCodeLogin.qrCodeGetting,
},
() => {
if (!useEventSourceForFetchQrCode) {
(async () => {
if (fetchQrCodeHandler) {
try {
const response = await fetchQrCodeHandler(this);
const { code, data } = response;
if (code === 0) {
if (oldQrCode === data) {
this.setState({
currentQrLoginState: StateQrCodeLogin.qrCodeGetSuccess,
});
} else {
this.uninstallEventSource();
this.setState(
{
currentQrLoginState: StateQrCodeLogin.qrCodeGetSuccess,
qrcode: data,
},
() => {
this.installEventSource({
qrcode: data,
});
}
);
}
} else {
this.uninstallEventSource();
this.setState({
currentQrLoginState: StateQrCodeLogin.qrCodeGetFailed,
});
}
} catch (e) {
console.error(e);
}
}
})();
} else {
this.uninstallEventSource();
this.installEventSource();
}
}
);
};
installEventSource = (config = {}) => {
const { options: defaultOptions } = WodaXLoginWithQrCode.defaultProps;
const { options, onReceiveMessageHandler, debugEnable } = this.props;
// 设置全局DebugEnable
GlobalDebugEnable = debugEnable;
// 获取默认设置的EventSource可以处理的消息类型
const { clientEventSource } = {
...defaultOptions,
...options,
};
const {
url,
params,
messageTypes,
withCredentials,
saveAllEvents,
} = clientEventSource;
const paramsString = Object.entries({ ...params, ...config })
.map(([key, value]) => `${key}=${value}`)
.join(`&`);
// 获取服务地址
const serviceUrl = encodeURI(
`${url}${paramsString.length > 0 ? `?` : ``}${paramsString}`
);
debugLog(`log`, `---> SSE url`, serviceUrl);
if (EventSource) {
// 判断当前存在的sse的url是否已经创建过
if (this.sse) {
const { url } = this.sse;
if (url === serviceUrl) {
return;
} else {
this.uninstallEventSource();
}
}
// 创建 EventSource 实例
debugLog(`log`, `--> installEventSource`);
this.sse = new EventSource(serviceUrl, {
withCredentials: !!withCredentials,
});
// 注册事件监听
let sseMsgTypes = messageTypes || [];
sseMsgTypes = sseMsgTypes.includes(`message`)
? sseMsgTypes
: sseMsgTypes.concat([`message`]);
sseMsgTypes.forEach(type => {
this.sse.addEventListener(
type,
message => {
this.setState(prevState => {
let events = (saveAllEvents ? prevState.sseEvents : []).concat(
message
);
return {
sseEvents: events,
};
});
onReceiveMessageHandler && onReceiveMessageHandler(message, this);
},
false
);
});
this.sse.onopen = e => {
debugLog(`log`, `---> SSE Open`);
onReceiveMessageHandler && onReceiveMessageHandler(e, this);
};
// 连接异常时会触发 error 事件并自动重连
this.sse.onerror = e => {
const { target } = e;
const { readyState } = target;
if (EventSource.CLOSED === readyState) {
debugLog(`log`, `---> SSE Disconnected`);
} else if (EventSource.CONNECTING === readyState) {
debugLog(`log`, `---> SSE Connecting...`);
}
onReceiveMessageHandler && onReceiveMessageHandler(e, this);
this.onErrorEventHandler({
type: ErrorType.eventSourceError,
data: e,
});
};
} else {
console.error(`Your browser doesn't support SSE`);
}
};
uninstallEventSource = () => {
const { debugEnable } = this.props;
if (this.sse) {
try {
debugLog(`log`, `--> uninstallEventSource`);
this.sse.close();
} catch (e) {
debugLog(`error`, `--> uninstallEventSource [error]`, e);
}
}
};
/**
* error 处理器
* @param errorInfo
*/
onErrorEventHandler = errorInfo => {
try {
const { type, data } = errorInfo;
if (type === ErrorType.eventSourceError) {
} else {
}
} catch (e) {
console.error(e);
}
};
}
/**
* 封装默认的渲染函数
* @private
*/
const _default = {
/**
* 最上部分标题渲染
* @param instance React Object
* @returns {*}
*/
titleRender: instance => {
const { options: defaultOptions } = WodaXLoginWithQrCode.defaultProps;
const { options } = instance.props;
const {
title,
titleStyle,
titleClassName,
appLogo,
appLogoClassName,
appLogoStyle,
} = { ...defaultOptions, ...options };
return (
<div
className={titleClassName}
style={{
padding: '4px',
...titleStyle,
}}
wodaxuiname={`TitleContainer`}
>
<span>{title}</span>
{appLogo !== `` ? (
<div
className={appLogoClassName}
style={appLogoStyle}
wodaxuiname={`appLogoContainer`}
>
<img src={appLogo} alt={appLogo} />
</div>
) : null}
</div>
);
},
/**
* 二维码显示渲染
* @param instance React Object
* @returns {*}
*/
qrCodeRender: instance => {
const { options: defaultOptions } = WodaXLoginWithQrCode.defaultProps;
const { options } = instance.props;
const {
qrCodeContainerClassName,
qrCodeContainerStyle,
qrCodeContainerOpacity,
qrCodeConfig,
} = {
...defaultOptions,
...options,
};
const { qrcode, currentQrLoginState } = instance.state;
const opacity = [
StateQrCodeLogin.loginFailed,
StateQrCodeLogin.loginSuccess,
StateQrCodeLogin.scanComplete,
StateQrCodeLogin.codeOutWork,
StateQrCodeLogin.networkLost,
StateQrCodeLogin.qrCodeGetting,
StateQrCodeLogin.qrCodeGetFailed,
].includes(currentQrLoginState)
? qrCodeContainerOpacity[`min`]
: qrCodeContainerOpacity[`max`];
return (
<div
className={qrCodeContainerClassName}
style={{
padding: '4px',
...qrCodeContainerStyle,
opacity: opacity,
}}
wodaxuiname={`qrCodeContainer`}
>
{qrcode !== `` && (
<QRCode
value={qrcode}
renderAs={qrCodeConfig.renderAs}
size={qrCodeConfig.size}
bgColor={qrCodeConfig.bgColor}
fgColor={qrCodeConfig.fgColor}
level={qrCodeConfig.level}
includeMargin={qrCodeConfig.includeMargin}
/>
)}
</div>
);
},
/**
* 提示信息显示渲染
* @param instance React Object
* @returns {*}
*/
toolTipRender: instance => {
const { options: defaultOptions } = WodaXLoginWithQrCode.defaultProps;
const { options } = instance.props;
const {
toolTipContainerClassName,
toolTipContainerStyle,
toolTipContainerProps,
} = {
...defaultOptions,
...options,
};
const { currentQrLoginState } = instance.state;
const uiMapRender = () => {
let uiMap = {};
// eslint-disable-next-line array-callback-return
Object.entries(StateQrCodeLogin).map(([key, value]) => {
[`Text`, `ClassName`, `Style`].forEach(attribute => {
if (
{ ...defaultOptions, ...options }.hasOwnProperty(
`${key}${attribute}`
)
) {
uiMap[key] = uiMap[key] || {};
uiMap[key] = {
[`${attribute}`]: { ...defaultOptions, ...options }[
`${key}${attribute}`
],
...uiMap[key],
};
}
});
});
const eleProps = uiMap[currentQrLoginState];
return eleProps
? createElement(
'div',
{
className: toolTipContainerClassName,
style: toolTipContainerStyle,
wodaxuiname: `toolTipContainer`,
...toolTipContainerProps,
},
<div
className={eleProps[`ClassName`]}
style={{
padding: '4px',
...eleProps[`Style`],
}}
>
<span>{eleProps[`Text`]}</span>
</div>
)
: null;
};
return uiMapRender();
},
/**
* 附加层处理,完全由外部绘制
* @param instance
* @returns {null}
*/
additionLayerRender: instance => {
return null;
},
/**
* 刷新操作显示渲染
* @param instance React Object
* @returns {*}
*/
refreshQrCodeRender: instance => {
const { options: defaultOptions } = WodaXLoginWithQrCode.defaultProps;
const { options } = instance.props;
const {
refreshContainerShowEveryTime,
refreshContainerProps,
refreshButtonCaption,
refreshButtonElement,
refreshButtonProps,
} = {
...defaultOptions,
...options,
};
const { currentQrLoginState } = instance.state;
const enableRenderElement =
[
// StateQrCodeLogin.ready,
StateQrCodeLogin.loginFailed,
StateQrCodeLogin.codeOutWork,
// StateQrCodeLogin.qrCodeGetting,
// StateQrCodeLogin.qrCodeGetSuccess,
StateQrCodeLogin.qrCodeGetFailed,
].indexOf(currentQrLoginState) > 0 || refreshContainerShowEveryTime;
return (
enableRenderElement &&
createElement(
`div`,
{
wodaxuiname: `RefreshContainer`,
...refreshContainerProps,
},
createElement(
refreshButtonElement,
{
wodaxuiname: `RefreshButton`,
onClick: () => {
instance.updateQrCode();
},
...refreshButtonProps,
},
refreshButtonCaption
)
)
);
},
/**
* 实时信息接收处理
* @param instance React Object
* @returns {null}
*/
sseMessageRender: instance => {
const { sseEvents } = instance.state;
const { options: defaultOptions } = WodaXLoginWithQrCode.defaultProps;
const { options } = instance.props;
const { showSSEMessage } = {
...defaultOptions,
...options,
};
return instance.sse && sseEvents.length > 0 && showSSEMessage ? (
<div
wodaxuiname={`sseMessageRender`}
children={sseEvents.map(eventMsg => {
// TODO: 根据事件的数据,来处理
return <p>{eventMsg.data}</p>;
})}
/>
) : null;
},
/**
* 获取二维码接口
* @param instance WodaXLoginWithQrCode 实例
* @returns {Promise<R>|Promise}
*/
fetchQrCodeHandler: instance =>
new Promise((resolve, reject) => {
resolve({
code: 0,
data: `http://ops.woda.ink/wodax/umi/${Date.now()}`,
});
}),
/**
* 接收SSE发过来的数据
* @param data SSE发送的数据
* @param instance WodaXLoginWithQrCode 实例
*/
onReceiveMessageHandler: (data, instance) => {
// debugLog(`log`, `onReceiveMessageHandler -->`, data);
},
};
WodaXLoginWithQrCode.propTypes = {
className: PropTypes.string,
Style: PropTypes.object,
// 是否开启日志调试
debugEnable: PropTypes.bool,
options: PropTypes.shape({
// 基础参数: {eventSource使用}
// 是否使用EventSource来重新获取最新QrCode
useEventSourceForFetchQrCode: PropTypes.bool,
// EventSource 的配置
clientEventSource: PropTypes.shape({
// EventSource 主要服务url
url: PropTypes.string,
// EventSource 附带参数
params: PropTypes.object,
// EventSource 可以处理的消息类型
messageTypes: PropTypes.array,
// withCredentials
withCredentials: PropTypes.bool,
// EventSource 是否保存所有接收到的事件
saveAllEvents: PropTypes.bool,
}),
// 基础参数: {Title使用}
// 显示的Title内容
title: PropTypes.string,
titleStyle: PropTypes.object,
titleClassName: PropTypes.string,
// 基础参数: {AppLogo使用}
// 显示App的Logo
appLogo: PropTypes.string,
appLogoClassName: PropTypes.string,
appLogoStyle: PropTypes.object,
// 基础参数: {二维码配置}
// QrCode 二维码配置
// see: https://github.com/zpao/qrcode.react
qrCodeConfig: PropTypes.shape({
renderAs: PropTypes.string,
size: PropTypes.number,
bgColor: PropTypes.string,
fgColor: PropTypes.string,
level: PropTypes.string,
includeMargin: PropTypes.bool,
}),
qrCodeContainerClassName: PropTypes.string,
qrCodeContainerStyle: PropTypes.object,
qrCodeContainerOpacity: PropTypes.shape({
min: PropTypes.number,
max: PropTypes.number,
}),
// 基础参数: {提示信息}
toolTipContainerClassName: PropTypes.string,
toolTipContainerStyle: PropTypes.object,
toolTipContainerProps: PropTypes.object,
codeOutWorkText: PropTypes.string,
codeOutWorkClassName: PropTypes.string,
codeOutWorkStyle: PropTypes.object,
loginFailedText: PropTypes.string,
loginFailedClassName: PropTypes.string,
loginFailedStyle: PropTypes.object,
loginSuccessText: PropTypes.string,
loginSuccessClassName: PropTypes.string,
loginSuccessStyle: PropTypes.object,
qrCodeGetFailedText: PropTypes.string,
qrCodeGetFailedClassName: PropTypes.string,
qrCodeGetFailedStyle: PropTypes.object,
scanCompleteText: PropTypes.string,
scanCompleteClassName: PropTypes.string,
scanCompleteStyle: PropTypes.object,
// 基础参数: {刷新,重新获取二维码}
// 是否一直显示刷新区域
refreshContainerShowEveryTime: PropTypes.bool,
// 刷新最外层的div的Props
refreshContainerProps: PropTypes.object,
// 指定按钮的标题
refreshButtonCaption: PropTypes.string,
// 指定按钮的选择的元素
refreshButtonElement:
PropTypes.string |
PropTypes.element |
PropTypes.elementType |
PropTypes.func,
// 指定按钮的Props
refreshButtonProps: PropTypes.object,
// 基础参数: {实时信息接收处理}
// 是否默认显示sse回显信息
showSSEMessage: PropTypes.bool,
}),
// 数据请求接口,可以覆盖
fetchQrCodeHandler: PropTypes.func.isRequired,
// 数据反馈接口。外接应用,可以自己处理所有来自服务器的消息
onReceiveMessageHandler: PropTypes.func,
// 渲染接口, 可以覆盖
titleRender: PropTypes.func.isRequired,
qrCodeRender: PropTypes.func.isRequired,
toolTipRender: PropTypes.func.isRequired,
additionLayerRender: PropTypes.func,
refreshQrCodeRender: PropTypes.func.isRequired,
sseMessageRender: PropTypes.func.isRequired,
};
WodaXLoginWithQrCode.defaultProps = {
className: `wodax-login-with-qrcode`,
Style: {},
// 是否开启日志调试
debugEnable: false,
// 可选项
options: {
// 基础参数: {eventSource使用}
// 是否使用EventSource来重新获取最新QrCode
useEventSourceForFetchQrCode: false,
clientEventSource: {
// EventSource 主要服务url
url: `https://stream.wikimedia.org/v2/stream/recentchange`,
// EventSource 附带参数
params: {},
// EventSource 可以处理的消息类型
messageTypes: [`message`],
// withCredentials
withCredentials: false,
// EventSource 是否保存所有
saveAllEvents: false,
},
// 基础参数: {Title使用}
title: `打开App,扫码登录`,
titleClassName: `qr-login-title`,
appLogo: ``,
// QrCode 二维码配置
qrCodeConfig: {
renderAs: `canvas`,
size: 128,
bgColor: `#FFFFFF`,
fgColor: `#000000`,
level: `L`,
includeMargin: false,
},
qrCodeContainerOpacity: {
min: 0.01,
max: 1,
},
// 提示信息
toolTipContainerClassName: `qr-login-tooltip`,
toolTipContainerStyle: {
padding: '4px',
position: 'absolute',
},
codeOutWorkText: `二维码已失效`,
loginFailedText: `登录失败`,
loginSuccessText: `登录成功`,
qrCodeGetFailedText: `获取二维码失败`,
scanCompleteText: `扫描成功! 请在手机上确认登录`,
// 基础参数: {刷新区域,重新获取二维码}
// 是否一直显示刷新区域
refreshContainerShowEveryTime: false,
refreshContainerProps: {
style: {
padding: '4px',
position: 'absolute',
marginTop: '40px',
},
},
// 点击刷新
refreshButtonCaption: `请点击刷新`,
refreshButtonElement: `button`,
refreshButtonProps: {
style: {
padding: '4px',
},
},
// 基础参数: {实时信息接收处理}
// 是否默认显示sse回显信息
showSSEMessage: false,
},
// 数据请求接口,可以覆盖
fetchQrCodeHandler: _default.fetchQrCodeHandler,
// 数据反馈接口。外接应用,可以自己处理所有来自服务器的消息
onReceiveMessageHandler: _default.onReceiveMessageHandler,
// 渲染接口, 可以覆盖,重新写
titleRender: _default.titleRender,
qrCodeRender: _default.qrCodeRender,
toolTipRender: _default.toolTipRender,
additionLayerRender: _default.additionLayerRender,
refreshQrCodeRender: _default.refreshQrCodeRender,
sseMessageRender: _default.sseMessageRender,
};
export default WodaXLoginWithQrCode;