前言:最近在学习Redux+react+Router+Nodejs全栈开发高级课程,这里对实践过程作个记录,方便自己和大家翻阅。最终成果github地址:https://github.com/66Web/react-antd-zhaoping,欢迎star。
一、登录注册
页面文件结构
- 基础组件放在Component文件夹下面
- 页面组件放在Container文件夹下面
- 页面入口处获取用户信息,决定跳转到哪个页面
web开发模式
- 整体前后端交互通过JSON实现
- 基于cookie用户验证
- express使用cookie,需要依赖cookie-parser
npm install cookie-parser --save
- cookie类似于一张身份卡,登录后服务器端返回,带着cookie即可以访问受限资源
- cookie的管理浏览器会自动处理
- 开发流程
二、页面实现
- component->logo目录下:创建logo.js基础组件,设置顶部Logo位置与样式
- container->login目录下:创建login.js业务组件
- 应用antd-mobile的组件实现登录页面
import {List, InputItem, WingBlank, WhiteSpace, Button } from 'antd-mobile'
-
跳转路由:this.props.history.push('路由')
register = () => { // console.log(this.props) this.props.history.push('/register') }
-
整个页面实现代码
import React from 'react' import Logo from '../../component/logo/logo' import {List, InputItem, WingBlank, WhiteSpace, Button } from 'antd-mobile' class Login extends React.Component{ register = () => { // console.log(this.props) this.props.history.push('/register') } render(){ return ( <div> <Logo></Logo> <WingBlank> <List> <InputItem>用户</InputItem> <WhiteSpace /> <InputItem>密码</InputItem> </List> <WhiteSpace /> <Button type="primary">登录</Button> <WhiteSpace /> <Button type="primary" onClick={this.register}>注册</Button> </WingBlank> </div> ) } } export default Login
- container->register目录下:创建register.js业务组件
- 单选选项RadioItem需要先定义再使用
import {List, InputItem, Radio, WingBlank, WhiteSpace, Button } from 'antd-mobile' const RadioItem = Radio.RadioItem
-
通过判断state中的type控制显示不同的单选选项
constructor(props){ super(props) this.state = { type: 'genius' //或者boss } } <RadioItem checked={this.state.type == 'genius'}> 牛人 </RadioItem> <WhiteSpace /> <RadioItem checked={this.state.type == 'boss'}> Boss </RadioItem>
-
整个页面实现代码
import React from 'react' import Logo from '../../component/logo/logo' import {List, InputItem, Radio, WingBlank, WhiteSpace, Button } from 'antd-mobile' const RadioItem = Radio.RadioItem class Register extends React.Component{ constructor(props){ super(props) this.state = { type: 'genius' //或者boss } } render(){ return ( <div> <Logo></Logo> <WingBlank> <InputItem>用户名</InputItem> <WhiteSpace /> <InputItem>密码</InputItem> <WhiteSpace /> <InputItem>确认密码</InputItem> <WhiteSpace /> <RadioItem checked={this.state.type == 'genius'}> 牛人 </RadioItem> <WhiteSpace /> <RadioItem checked={this.state.type == 'boss'}> Boss </RadioItem> <WhiteSpace /> <Button type="primary">注册</Button> </WingBlank> </div> ) } } export default Register
- index.js项目入口文件中:通过Route匹配登录注册路由及组件
import Login from './container/login/login' import Register from './container/register/register' ReactDOM.render( <Provider store={store}> <BrowserRouter> <div> <Route path='/login' component={Login}></Route> <Route path='/register' component={Register}></Route> </div> </BrowserRouter> </Provider>, document.getElementById('root'));
三、判断路由
- server目录下:创建server.js服务端主入口文件
const express = require('express') const userRouter = require('./user') const app = express() //开启中间件 app.use('/user', userRouter) app.listen(9093, function(){ console.log('Node app start at port 9093') })
-
server目录下:创建user.js用户独立中间件
const express = require('express') const Router = express.Router() //路由对象 Router.get('/info', function(req, res){ return res.json({code:1}) //测试 }) module.exports = Router //对外暴露接口
-
component目录下:创建authroute验证登录组件,通过axios调用服务端接口获取测试数据
import React from 'react' import axios from 'axios' class AuthRoute extends React.Component{ componentDidMount() { //获取测试信息 axios.get('/user/info') .then(res => { console.log(res) if(res.status == 200){ console.log(res.data) } }) } render(){ return null } } export default AuthRoute
四、用户信息校验&跳转登录
- 坑:普通非路由组件获取不到this.props.history对象
- 解决:利用react-router-dom提供的withRouter可以使其显示
import {withRouter} from 'react-router-dom' @withRouter
-
利用this.props.location.pathname可以获取到当前路由,判断若是登录或注册不执行获取用户信息操作
const publicList = ['/login', 'register'] const pathname = this.props.location.pathname; if(publicList.indexOf(pathname) > -1){ return null }
-
如果当前路由页不是登录或注册,且获取不到用户信息(未登录),利用this.props.history.push('/login')跳转到登录页
五、注册交互
- state中设置初始状态
this.state = { user: '', pwd: '', repeatpwd: '', type: 'genius' //或者boss }
-
Input和RadioItem共用handleChange():通过箭头函数传参,改变state中指定key的value
handleChange(key, val){ this.setState({ [key]: val }) } <InputItem onChange={v => this.handleChange('user', v)} >用户名</InputItem> <RadioItem checked={this.state.type == 'boss'} onChange={() => this.handleChange('type', 'boss')} > Boss </RadioItem>
-
注册按钮提交:测试打印state,成功改变
六、注册请求发送
- redux目录下:创建user.redux.js,专门执行用户信息操作的redux
- state存储用户注册信息,通过redux给注册提交给服务端
- 关键:使用dispatch调用axios.post异步提交数据
import axios from 'axios' //action type const REGISTER_SUCCESS = 'REGISTER_SUCCESS' const ERROR_MSG = 'ERROR_MSG' const initState = { isAuth: false, msg: '', user: '', pwd: '', type: '' } //reducer export function user(state=initState, action){ switch(action.type){ case REGISTER_SUCCESS: return {...state, msg:'', isAuth: true, ...action.payload} case ERROR_MSG: return {...state, isAuth:false, msg:action.msg} default: return state } return state } //action function registerSuccess(data){ return {type:REGISTER_SUCCESS, payload:data} } function errorMsg(msg){ return { msg, type:ERROR_MSG } } export function register({user,pwd,repeatpwd,type}){ if (!user||!pwd||!type) { return errorMsg('用户名密码必须输入') } if (pwd!==repeatpwd) { return errorMsg('密码和确认密码不同') } //redux异步操作数据 return dispatch=>{ axios.post('/user/register',{user,pwd,type}) .then(res=>{ if (res.status==200&&res.data.code===0) { dispatch(registerSuccess({user,pwd,type})) }else{ dispatch(errorMsg(res.data.msg)) } }) } }
-
reducer.js中:合并并传入user(reducer)
//合并所有reducer,并且返回 import {combineReducers} from 'redux' import { user } from './redux/user.redux' export default combineReducers({user})
-
register.js中:使用connect连接组件和redux,传入state.user和register方法,调用this.props.register(this.state)提交注册
import {connect} from 'react-redux' import {register} from '../../redux/user.redux' @connect( state => state.user, {register} ) handleRegister = () => { this.props.register(this.state) // console.log(this.state) }
七、数据库模型的建立
- server目录下:创建model.js
- 通过mongoose连接Express和MongoDB
- 建立User数据模型:通过mongoose.Schema生成model模型,通过mongoose.model生成Entity实体
- 导出mongoose的工具函数库:getModel,供外部获取模型
const mongoose = require('mongoose') //链接mongo 并且使用react-chat这个集合 const DB_URL = 'mongodb://localhost:27017/react-chat' mongoose.connect(DB_URL) const models = { user: { 'user':{type:String, require:true}, 'pwd': {type:String, require:true}, 'type':{type:String, require:true}, //头像 'avatar':{type:String}, //个人简介或者职位简介 'desc':{type:String}, //职位 'title':{type:String}, //如果是boss 还有两个字段 'company':{type:String}, 'money':{type:String} }, chat: { } } //批量动态生成model for(let m in models){ mongoose.model(m, new mongoose.Schema(models[m])) } //mongoose的工具函数库 module.exports = { getModel:function(name){ return mongoose.model(name) } }
-
server->user.js中:引用model,通过getModel获取user数据模型,在'/list'路由下查找并显示所有数据
const model = require('./model') const User = model.getModel('user') Router.get('/list', function(req, res){ User.find({}, function(err, doc){ return res.json(doc) }) })
八、Express注册功能实现
- body-parser中间件插件解析表单数据
npm install body-parser --save
-
server.js中:使用body-parser中间件解析post传来的JSON数据
const bodyParser = require('body-parser') const cookieParser = require('cookie-parser') //开启中间件 app.use(cookieParser()) app.use(bodyParser.json()) //解析post 传来的json数据
-
user.js中:使用Router.post发送注册请求
-
先查找user,若存在则用户名重复
-
如不存在,继续使用注册信息创建新用户文档
//用户注册 Router.post('/register', function(req, res){ console.log(req.body) const {user, pwd, type} = req.body User.findOne({user:user}, function(err, doc){ if(doc){ return res.json({code:1, msg:'用户名重复'}) } User.create({user, pwd, type}, function(err, doc){ if(err){ return res.json({code:1, msg:'后端出错了'}) } return res.json({code:0}) }) }) })
九、注册跳转&密码加密实现
- 注册跳转
- src目录下:创建utils.js提供工具函数getRedirectPath(),判断用户信息返回跳转地址
export function getRedirectPath({type, avatar}){ //根据用户信息 返回跳转地址 //判断user.type :bose /genius //判断user.avtar :bossinfo /geniusinfo let url = (type === 'boss') ? '/boss' : '/genius' if(!avatar){ url += 'info' } return url }
-
user.redux.js中:在初始状态中添加redirectTo跳转地址字段,reducer注册成功后调用工具函数获得跳转地址
import {getRedirectPath} from '../utils' const initState = { redirectTo:'', …… } case REGISTER_SUCCESS: return {...state, msg:'', redirectTo:getRedirectPath(action.payload), isAuth: true, ...action.payload}
-
register.js中:判断this.props.redirectTo是否有值,有通过react-router-dom的Redirect组件进行跳转,否则返回null
import {Redirect} from 'react-router-dom' {this.props.redirectTo ? <Redirect to={this.props.redirectTo}/> : null}
-
密码加密
-
安装utility第三方插件
npm install utility --save
-
user.js中:定义加密方法md5Pwd(),使用两层utility的md5加密方法,再加一个自定义的字符串
const utils = require('utility') //使用加密方法将pwd加密后再储存 User.create({user, type, pwd:md5Pwd(pwd)}, function(err, doc){ …… }) //密码加密 function md5Pwd(pwd){ const salt = 'imooc_is_good_3957x8yza6!@#IUHJh~~' return utils.md5(utils.md5(pwd+salt)) }
十、登录功能实现
- user.redux.js中:添加登录成功的相关reducer和异步action
//action type const LOGIN_SUCCESS = 'LOGIN_SUCCESS' //user reducer中 case LOGIN_SUCCESS: return {...state, msg:'', redirectTo:getRedirectPath(action.payload), isAuth: true, ...action.payload} //登录成功 同步action function loginSuccess(data){ return {type:LOGIN_SUCCESS, payload:data} } //登录 异步action export function login({user,pwd}){ if(!user||!pwd){ return errorMsg('用户名密码必须输入') } //redux异步操作数据 return dispatch=>{ axios.post('/user/login',{user,pwd}) .then(res=>{ if (res.status==200&&res.data.code===0) { dispatch(loginSuccess(res.data.data)) }else{ dispatch(errorMsg(res.data.msg)) } }) } }
-
user.js中:通过Router.post调用'/login'接口,根据接收到的用户名和密码查询数据
//用户登录 Router.post('/login', function(req, res){ const {user, pwd} = req.body User.findOne({user, pwd:md5Pwd(pwd)},{'pwd':0}, function(err, doc){ if(!doc){ return res.json({code:1, msg:'用户名或者密码错误'}) } return res.json({code:0, data:doc}) }) })
-
login.js中:使用connect连接组件与redux,登录互动实现同注册
-
调用props中的login异步action执行登录操作
-
判断props中的redirectTo和msg,跳转连接或显示错误提示
-
实现代码
import React from 'react' import Logo from '../../component/logo/logo' import { InputItem, WingBlank, WhiteSpace, Button } from 'antd-mobile' import {Redirect} from 'react-router-dom' import {connect} from 'react-redux' import {login} from '../../redux/user.redux' @connect( state => state.user, {login} ) class Login extends React.Component{ constructor(props){ super(props) this.state = { user: '', pwd: '' } } handleChange(key, val){ this.setState({ [key]: val }) } register = () => { // console.log(this.props) this.props.history.push('/register') } handleLogin = () => { this.props.login(this.state) } render(){ return ( <div> {this.props.redirectTo ? <Redirect to={this.props.redirectTo}/> : null} <Logo></Logo> <WingBlank> {this.props.msg ? <p className='error-msg'>{this.props.msg}</p> : null} <InputItem onChange={v => this.handleChange('user', v)} >用户名</InputItem> <WhiteSpace /> <InputItem type='password' onChange={v => this.handleChange('pwd', v)} >密码</InputItem> <WhiteSpace /> <Button type="primary" onClick={this.handleLogin}>登录</Button> <WhiteSpace /> <Button type="primary" onClick={this.register}>注册</Button> </WingBlank> </div> ) } } export default Login
十一、cookie保存登录状态
- user.js中:登录注册成功后,将userid存储到cookie中
- 注册后改为使用model.save方法创建model获取_id
- 用户刷新页面校验用户信息:获取cookie中的userid,通过find by id的方式查找用户数据
- 查找条件中加入过滤:不显示密码和版本号,语法为{'pwd': 0},此处0表示不显示
//查询条件 不显示密码和版本号 const _filter = {'pwd':0, '__v':0} //用户登录成功后 res.cookie('userid', doc._id) //用户注册后存储cookie model.save 获得id const userModel = new User({user, type, pwd:md5Pwd(pwd)}) userModel.save(function(e, d){ if(e){ return res.json({code:1, msg:'后端出错了'}) } const {user, type, _id} = d res.cookie('userid', _id) return res.json({code:0, data:{user, type, _id}}) }) //用户信息 Router.get('/info', function(req, res){ //用户cookie校验 const {userid} = req.cookies if(!userid){ return res.json({code:1}) } User.findOne({_id: userid}, _filter, function(err, doc){ if(err){ return res.json({code:1, msg:'后端出错了'}) } if(doc){ return res.json({code:0, data:doc}) } }) })
-
user.redux.js中:定义loadData存储数据相关的reducer和同步action
//action type const LOAD_DATA = 'LOAD_DATA' //user reducer中添加 case LOAD_DATA: return {...state, ...action.payload} //同步action export function loadData(userinfo){ return {type:LOAD_DATA, payload:userinfo} }
-
authroute.js中:使用connect连接组件和redux
-
在判断当前为登录状态后,调用loadData同步action,将/user/info获取到的用户信息存入redux中
-
注意:@connect一定要写在@withRouter后
import {connect} from 'react-redux' import {loadData} from '../../redux/user.redux' @withRouter @connect( null, {loadData} ) if(res.data.code == 0){ //登录 this.props.loadData(res.data.data) }
注:项目来自慕课网