前言:最近在学习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)
}
注:项目来自慕课网