最近作了一个后台的项目,既然是后台管理系统,登陆的控制天然是少不了的。javascript
接到需求——后台系统!花了几乎半天搞出来了Webpack配置、搞出来了React Router、搞出来了 React 代码基本的结构,下一步就是搞所谓的“登陆逻辑”了。前端
正好 React v16 大变,而本身最近又有些时候没写过React了,便不妨借此次机会熟悉一下React的新API吧!据说React新出的 Context API 能够“取代Redux”,那此次登陆逻辑就用 Context 写吧!java
虽然离开React有些时日了,可是它新的 Context API 看起来仍是很“美味”的。因而,三两下,一套 Context 就出如今了入口文件里:后端
// 登陆以前的默认登陆信息
// 在能够看到我设计的“登陆信息”的数据结构和内容
const defaultLoginInfo = {
username: '',
token: false,
ident: false,
};
class Main extends React.Component {
constructor(props) {
super(props);
// 写登陆信息到 this.state
// 经过 toStore 判断是否也要写 Storage
this.updateLogin = (info, toStore = true) => {
this.setState(prevState => {
let nv = {...prevState.userLoginInfo, ...info};
toStore && this.storeLoginInfo(nv);
return {userLoginInfo: nv};
});
};
// 从 sesionStorage 中取回登陆信息
this.retrieveLoginInfo = () => {
let stored = sessionStorage.getItem('userLoginInfo');
// 这里须要一个 try ... catch ... ,可是为了代码易读我给删除了
return stored ? JSON.parse(stored) : {};
};
// 将登陆信息存入 Storage
this.storeLoginInfo = (val) => {
sessionStorage.setItem('userLoginInfo', JSON.stringify(val));
};
this.state = {
userLoginInfo: {
...defaultLoginInfo,
update: this.updateLogin, // 向子组件暴露方法
// 在主组件初绐化时经过覆盖默认登陆信息来生成实际用的登陆信息
...this.retrieveLoginInfo(),
exit: () => this.updateLogin({...defaultLoginInfo}), // 向子组件暴露方法 - 退出登陆
},
};
}
render() {} // .....
componentDidMount() {
this.updateLogin(this.retrieveLoginInfo(), false);
}
}
复制代码
能够说是很简陋了——这就是我用Context API 替代 Redux 的第一个做品。但它很简明,也工做得很好——直到我想为登陆部分加些新想法……设计模式
从这几点出发来看,原来的代码在 React 组件以外,一无可取……缓存
那就把代码放到全局呗……session
window 对象?(有这种想法的同窗请面壁思过)数据结构
那么如何避免使用全局变量又能解决数据存储的问题呢?那就是是“沙盒模式”。沙盒模式,是JS很是广泛的一个设计模式,它经过闭包的原理将数据维持在一个函数做用于中,而经过返回值内的函数引用这个函数包体内的变量的方式,造成闭包,而只有经过该函数的返回函数才能访问和修改该闭包内的数据,从而起来了数据保护的做用。闭包
嗯,又是那个叫“闭包”的玩意。ide
可是,咱们如今有了“模块化”。当咱们 import
一个模块的时候,这个模块的声明会保持在一个独立的做用域中,且一直存在。可使用 exports
来实现“沙盒”的效果。除了导出的函数,其它对外界都不可见。(话说 Webpack 的模块不也是用闭包来实现的吗?)
分析一下:
哪里须要发布?
哪里须要订阅?
明显,发布订阅是合适的。
// 登陆过时检测
const checkExpireTime = info => {
return Date.now() > info.expireTime && info.expireTime >= 0;
};
// 负责 Storage 操做:取回 + 存入
function retrieveLoginInfo() {
let stored = sessionStorage.getItem('userLoginInfo');
if(stored) {
try {
let info = { ...defaultLoginInfo, ...JSON.parse(stored) };
if(checkExpireTime(info) || !info.token) {
exitLogin();
return {...defaultLoginInfo};
}
return {...defaultLoginInfo, ...info};
} catch(e) {
return {...defaultLoginInfo};
}
} else {
exitLogin();
return {...defaultLoginInfo};
}
}
function storeLoginInfo(val) {
return sessionStorage.setItem('userLoginInfo', JSON.stringify(val));
}
// 广播
function broadcastLoginInfo(info) {
broadcastList.forEach(curt => {
curt(info);
});
}
// 存放 Listener
let broadcastList = [];
function registerLoginInfoBroadcast(callback) {
if(!broadcastList.includes(callback)) {
broadcastList.push(callback);
}
}
// 更新登陆信息 - 相似 Dispacher
function updateLoginInfo(info) {
if(checkExpireTime(info)) {
exitLogin();
return [false, '登陆过时,请从新登陆'];
} else {
storeLoginInfo(info);
broadcastLoginInfo(info);
return [true];
}
}
// 一些经常使用动做的提取(咱们要拒绝样本代码)
function exitLogin() {
updateLoginInfo({...defaultLoginInfo});
}
function syncLoginInfo() {
broadcastLoginInfo(retrieveLoginInfo());
}
export default {
update: updateLoginInfo,
retrieve: retrieveLoginInfo,
exit: exitLogin,
registerBroadcast: registerLoginInfoBroadcast,
sync: syncLoginInfo,
storeLoginInfo,
retrieveLoginInfo,
defaultLoginInfo,
};
复制代码
好比说,后台检测到 Token 错误,想强行清空登陆信息,要怎么操做?
import loginInfo from '@/utils/path/to/loginInfoStorage.js';
// ...
function RequestApi (respData) {
// Do some processing
if([301, 302, 303].indexOf(respData.status.code) !== -1) {
loginInfo.exit(); // 登陆出错?要自行退出登陆!
}
}
复制代码
至于 React 根组件里,状况就有些复杂了……
在根组件里:
要记得 Register Listener
// 放在 constructor 或者 componentDidMounted 里都好
loginInfo.registerBroadcast(info => {
this.updateLoginState(info, false);
});
复制代码
Listener 触发时要更新根组件的 State
this.updateLoginState = (info, toStore = true) => {
this.setState(prevState => {
let nv = {...prevState.userLoginInfo, ...info};
toStore && this.storeLoginInfo(nv);
let newState = {};
if(!prevState.useLoginModal || nv.token) {
newState.userLoginInfo = {...nv};
}
return newState;
});
};
复制代码
随 Context 传给子组件的函数也不能忘
this.state = {
userLoginInfo: {
...this.retrieveLoginInfo(),
update: this.updateLoginInfo,
exit: () => {
// 还有其它功能
loginInfo.exit();
},
},
};
复制代码
为了突出本质,以上只是我简化后的代码。完整的代码(见下文)还有登陆弹框等功能。
注意
原本不是本文讨论范围,但这里也让我颇费心思,实现得也不很好。此处不妨讲讲。
Storage 丢失后有提示,引导用户从新登陆。引导从新登陆不可以使用页面跳转,可使用弹框。
一个棘手的问题是,框能够弹出来,但框背后的管理界面UI不能变。
个人思路是:state 里的 loginInfo 分两种——真实反映实际登陆状态的 actualLoginInfo 和为UI专供的 userLoginInfo。React router 和其它UI组件的 render 根据 userLoginInfo 来作判断和渲染,登陆弹框则使用 actualLoginInfo。 下面就是我实际使用的代码了。只是这个思路很不优雅。
注意的几点:
useLoginModal
- 使用登陆弹框仍是路由跳转到一整个登陆页?useLoginModal
为 true 时显示登陆弹窗class Main extends Component {
constructor(props) {
super(props);
this.updateLoginState = (info, toStore = true) => {
this.setState(prevState => {
let nv = {...prevState.userLoginInfo, ...info};
toStore && this.storeLoginInfo(nv);
let newState = {
actualLoginInfo: {...nv},
};
if(!prevState.useLoginModal || nv.token) {
newState.userLoginInfo = {...nv};
newState.useLoginModal = !!info.token; // 登陆后默认使用登陆弹窗
}
return newState;
});
};
loginInfo.registerBroadcast(info => {
// 显示登陆弹窗就不修改登陆相关的UI状态
this.updateLoginState(info, false);
});
this.retrieveLoginInfo = loginInfo.retrieve;
this.updateLoginInfo = loginInfo.update;
// [NOTE] 须要在渲染<Route>以前读入登陆状态
// 不然刷新以后URL会由于Route未渲染而丢失
this.state = {
userLoginInfo: {
...this.retrieveLoginInfo(),
// 升级登陆信息
update: this.updateLoginInfo,
// 在UI中使用此函数来退出登陆
// config.useLoginModal
// - true 改写登陆状态、不修改登陆相关的UI状态、显示登陆弹窗
// - false 改写登陆状态、修改登陆相关的UI状态、回到登陆页面
exit: (config = {}) => {
if(config.useLoginModal || false) {
this.setState({
useLoginModal: true,
}, () => {
loginInfo.exit();
});
} else {
this.setState({
useLoginModal: false,
}, () => {
loginInfo.exit();
});
}
},
},
useLoginModal: false,
actualLoginInfo: {},
};
render() {
return (
<UserCtx.Provider value={this.state.userLoginInfo}>
<UserCtx.Consumer>
{info => (
<main styleName="main-container">
<HashRouter>
<LocaleProvider locale={zh_CN}>
<>
<Switch>
{(!info.token) && <Route path="/login" component={withRouterLogin} />}
{info.token && <Route path="/admin" component={withRouterAdmin} />}
<Redirect to={info.token ? '/admin' : '/login'} />
</Switch>
</>
</LocaleProvider>
</HashRouter>
{/* 未登陆且useLoginModal时显示登陆弹窗 */}
<Modal
title="请先登陆帐户"
visible={this.state.useLoginModal && !this.state.actualLoginInfo.token}
footer={false}
width={370}
closable={false}
>
<LoginForm />
</Modal>
</main>
)}
</UserCtx.Consumer>
</UserCtx.Provider>
);
}
}
复制代码
不少地方还有待完善:
我这个前端小菜狗就是这样在不知不觉中把登陆部分的代码抽象出来了一套发布订阅模型。