基于create-react-app官方脚手架搭建dva模式的项目-权限布局的开发,之前的项目骨架已经可以按照你自己的业务和项目逻辑去组装汽车了。
这里依然以上项目骨架为基础,增加权限布局的开发。
权限设计思路:
1 用户一旦登录成功,后台会返回一个token令牌,此令牌的形式有很多种,传统的就是session-cookie机制了,也可以采用比较流行的一种令牌机制JWT(有兴趣的同学可自行学习),个人推荐已此种方式做令牌。
2 前端会把token存在浏览器端,比如sessionStorge存储机制,之后每次请求均会在请求的中带上这个token传给服务端,服务端校验此令牌,若合法则通过,视为正常用户,若不合法则返回无权限。
3 用户点击退出,或者后台返回不合法,前端则请求退出接口,并清空临时数据,返回登录界面。
4 涉及登录安全,在开发环境也增加https访问模式,修改package.json文件中scripts配置项:
"start": "cross-env PORT=9999 HTTPS=true node scripts/start.js",
下面我们在之前基础上进行开发
1 routes目录下新建login目录,里面新建index.js和index.less文件作为登录页面:
(1)不要忘了接入store数据
(2)登录页面,需要表单Form组件支持
(3)用到了样式,本项目用到classnames (可自行学习),安装:cnpm i classnames --save
(4)要用到css-module,这里有个点:直接开启module会导致和antd冲突,故此开启方式,修改webpack.config.dev.js和webpack.config.prod.js,在本文件中复制一份css-loader配置部分,改为开启less的module,这份配置排在前面
代码如下:
{ test: /\.less$/, use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options: { importLoaders: 1, modules: true, localIdentName:"[name]__[local]___[hash:base64:5]" }, }, { loader: require.resolve('postcss-loader'), options: { // Necessary for external CSS imports to work // https://github.com/facebookincubator/create-react-app/issues/2677 ident: 'postcss', plugins: () => [ require('postcss-flexbugs-fixes'), autoprefixer({ browsers: [ '>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9', // React doesn't support IE8 anyway ], flexbox: 'no-2009', }), ], }, }, { loader:require.resolve('less-loader'), options: { javascriptEnabled: true } } ], }, { test: /\.(css|less)$/, use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options: { importLoaders: 1, }, }, { loader: require.resolve('postcss-loader'), options: { // Necessary for external CSS imports to work // https://github.com/facebookincubator/create-react-app/issues/2677 ident: 'postcss', plugins: () => [ require('postcss-flexbugs-fixes'), autoprefixer({ browsers: [ '>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9', // React doesn't support IE8 anyway ], flexbox: 'no-2009', }), ], }, }, { loader:require.resolve('less-loader'), options: { javascriptEnabled: true } } ], },
关与开启module和antd冲突解决方法网上找了下,大家可参考:
https://segmentfault.com/a/1190000011225917
https://segmentfault.com/q/1010000011965218
https://www.jianshu.com/p/51ff1c8be301
https://blog.csdn.net/nongweiyilady/article/details/79939761
登录页面代码如下:
login/index.js
import React, {Component} from 'react'; import {connect} from 'dva' import {injectIntl} from 'react-intl' import {Row, Col, Form, Icon, Input, Button} from 'antd' import classnames from 'classnames'; import styles from './index.less'; const FormItem = Form.Item class Login extends Component{ loginSubmit=(e)=>{ e.preventDefault(); const {form} = this.props; form.validateFields((err, values) => { if (!err) { console.log(values); } }); } render(){ const {form} = this.props; const {getFieldDecorator} = form; return( <Row> <Col className={classnames(styles.loginFormCol)}> <Form onSubmit={this.loginSubmit} className={classnames(styles.loginForm)}> <h3>登录</h3> <FormItem> {getFieldDecorator('username', { rules: [{ required: true, message: '请输入用户名' }], })( <Input prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="用户名" /> )} </FormItem> <FormItem> {getFieldDecorator('password', { rules: [{ required: true, message: '请输入密码' }], })( <Input prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} type="password" placeholder="密码" /> )} </FormItem> <FormItem> <Button type="primary" htmlType="submit" className={classnames(styles.loginBtn)}> 登录 </Button> </FormItem> </Form> </Col> </Row> ) } } export default connect(({ })=>({ }))(injectIntl(Form.create()(Login)))
login/index.less
.loginFormCol{ height:100vh; display:flex; justify-content:center; flex-direction:column; .loginForm{ width:300px; height:190px; margin:0 auto; .loginBtn{ width:100% } } }
最终效果:
2 修改models/app.js文件,增加login.js的model
(1)增加token存储字段,用于将来存储令牌的字段,值取自sessionStorage.getItem('token'),对应增加对其的实时更新effects和reducers:updateToken和updateStore
(2)增加locationPathname字段,用于存储当前url的pathname,对应增加对其的实时更新effects和reducers:updateLocation
代码如下:
(3)models/login.js代码:
export default { namespace: 'login', state: { name:'这是login的model' }, subscriptions: { }, effects: { }, reducers: { }, };
3 src目录下增加目录layout,用于存放布局组件
(1)layout下创建auth.js,权限组件,用户包裹全局组件:
import {connect} from 'dva'; import React from 'react'; const Auth=({ children,dispatch,token,locationPathname })=>{ if(!token&&locationPathname!='/login'){ dispatch({ type:'app/logout' }) } return ( <React.Fragment> {children} </React.Fragment> ); } export default connect(({ app })=>({ token:app.get('token'), locationPathname:app.get('locationPathname'), }))(Auth)
(2)把locale.js国际化组件,移到layout目录下,目录调整如下:
4 src/route.js修改,包裹auth组件:
import React from 'react'; import { Router, Route, Switch } from 'dva/router'; import dynamic from 'dva/dynamic' import Locale from './layout/locale' import Auth from './layout/auth' import {config} from './utils' const { menuGlobal } = config function RouterConfig({ history, app }) { return ( <Auth> <Locale> <Router history={history}> <Switch> { menuGlobal.map(({path,...dynamics},index)=>( <Route key={index} path={path} exact component={dynamic({ app, ...dynamics })} /> )) } </Switch> </Router> </Locale> </Auth> ); } export default RouterConfig;
下面验证一下效果:
直接访问:https://localhost:9999/aaa , 你会发现已经跳转到了https://localhost:9999/login页面,当我们再sessionStorage中模拟一个token=123456,再次访问/aaa即可进入,当清除掉token则退出到/login,效果如下:
5 routes目录下,新建home,作为登录后的首页,目录如下:
index.js
import React, {Component} from 'react'; import {connect} from 'dva' import {Link} from 'dva/router' import {injectIntl} from 'react-intl' import {Row, Col, Form, Button} from 'antd' import classnames from 'classnames'; import styles from './index.less'; class Home extends Component{ render(){ return( <Row> <Col className={classnames(styles.home)}> 欢迎您,来到首页 </Col> <Col> <Link to={'/aaa'}><Button>去AAA页面</Button></Link> </Col> </Row> ) } } export default connect(({ })=>({ }))(injectIntl(Form.create()(Home)))
index.less
.home{ padding:20px; }
6 models目录下新建home.js
export default { namespace: 'home', state: { name:'这是home的model' }, subscriptions: { }, effects: { }, reducers: { }, };
7 修改utils/config.js 代码:
const menuGlobal=[ { id:'login', pid:'0', name:'登录', icon:'user', path: '/login', models: () => [import('../models/login')], //models可多个 component: () => import('../routes/login'), }, { id:'home', pid:'0', name:'首页', icon:'user', path: '/', models: () => [import('../models/home')], //models可多个 component: () => import('../routes/home'), }, { id:'aaa', pid:'0', name:'aaa页', icon:'user', path: '/aaa', models: () => [import('../models/aaa')], //models可多个 component: () => import('../routes/AAA'), }, { id:'bbb', pid:'0', name:'bbb页', icon:'user', path: '/aaa/bbb', models: () => [import('../models/bbb')], //models可多个 component: () => import('../routes/BBB'), }, { id:'ccc', pid:'0', name:'ccc页', icon:'user', path: '/ccc', models: () => [import('../models/ccc')], //models可多个 component: () => import('../routes/CCC'), }, ]; export default { menuGlobal }
8 我们暂时约定账号为admin,密码为123456
(1) 修改/routes/login/index.js
import React, {Component} from 'react'; import {connect} from 'dva' import {injectIntl} from 'react-intl' import {Row, Col, Form, Icon, Input, Button} from 'antd' import classnames from 'classnames'; import styles from './index.less'; const FormItem = Form.Item class Login extends Component{ loginSubmit=(e)=>{ e.preventDefault(); const {form,dispatch} = this.props; form.validateFields((err, values) => { if (!err) { dispatch({ type:'login/login', payload:{ values } }) } }); } render(){ const {form} = this.props; const {getFieldDecorator} = form; return( <Row> <Col className={classnames(styles.loginFormCol)}> <Form onSubmit={this.loginSubmit} className={classnames(styles.loginForm)}> <h3>登录</h3> <FormItem> {getFieldDecorator('username', { rules: [{ required: true, message: '请输入用户名' }], })( <Input autoComplete={'off'} prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="用户名" /> )} </FormItem> <FormItem> {getFieldDecorator('password', { rules: [{ required: true, message: '请输入密码' }], })( <Input autoComplete={'off'} prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} type="password" placeholder="密码" /> )} </FormItem> <FormItem> <Button type="primary" htmlType="submit" className={classnames(styles.loginBtn)}> 登录 </Button> </FormItem> </Form> </Col> </Row> ) } } export default connect(({ })=>({ }))(injectIntl(Form.create()(Login)))
(2) 修改models/login.js
import {message} from 'antd'; export default { namespace: 'login', state: { name:'这是login的model' }, subscriptions: { }, effects: { * login ({ payload }, {put, select}) { if(payload.values.username=='admin'&&payload.values.password=='123456'){ //登录成功 yield put({ type:'app/loginOk', payload:{ token:'123abc' } }); }else{ message.warning('用户名或密码不正确') } }, }, reducers: { }, };
(3) 修改models/app.js
import {Map, fromJS} from 'immutable'; import {routerRedux} from 'dva/router'; const initState = Map({ i18n: 'zh_CN', token:null, locationPathname:null, }) export default { namespace: 'app', state:initState, subscriptions: { setup({ dispatch, history }) { }, setupHistory ({ dispatch, history }) { history.listen((location) => { dispatch({ type: 'updateLocation', payload: { locationPathname: location.pathname }, }); dispatch({ type: 'updateToken', payload: { token: window.sessionStorage.getItem('token') }, }) }) }, }, effects: { * changeLang ({ payload: {value}, }, { put }) { yield put({ type: 'updateLang', payload: {value}}); }, * updateLocation ({ payload }, {put, select}) { yield put({type: 'updateStore', payload}); }, * updateToken ({ payload }, {put, select}) { yield put({type: 'updateStore', payload}); }, * loginOk ({ payload }, {put, select}) { window.sessionStorage.setItem('token',payload.token); yield put(routerRedux.push({ pathname: '/' })); }, * logout ({ payload }, {put, select}) { window.sessionStorage.removeItem('token'); window.location.href='/login'; }, }, reducers: { updateLang (state,{payload:{value}}) { return state.set('i18n',value); }, updateStore (state, { payload }) { return payload?state.mergeDeep(fromJS(payload)):initState }, }, };
(4) 修改layout/auth.js
import {connect} from 'dva'; import React from 'react'; const Auth=({ children,dispatch,token,locationPathname })=>{ if(!token&&locationPathname!='/login'){ dispatch({ type:'app/logout' }) }else if(token&&locationPathname=='/login'){ dispatch({ type:'app/loginOk', payload:{ token:token } }) } return ( <React.Fragment> {children} </React.Fragment> ); } export default connect(({ app })=>({ token:app.get('token'), locationPathname:app.get('locationPathname'), }))(Auth)
至此,我们看下效果: