【招聘App】—— React/Nodejs/MongoDB全栈项目:登录注册

前言:最近在学习Redux+react+Router+Nodejs全栈开发高级课程,这里对实践过程作个记录,方便自己和大家翻阅。最终成果github地址:https://github.com/66Web/react-antd-zhaoping,欢迎star。


 一、登录注册

     页面文件结构

  • 基础组件放在Component文件夹下面
  • 页面组件放在Container文件夹下面
  • 页面入口处获取用户信息,决定跳转到哪个页面

     web开发模式

  • 整体前后端交互通过JSON实现
  • 基于cookie用户验证
  1. express使用cookie,需要依赖cookie-parser
    npm install cookie-parser --save
  2. cookie类似于一张身份卡,登录后服务器端返回,带着cookie即可以访问受限资源
  3. cookie的管理浏览器会自动处理
  • 开发流程

      

二、页面实现

  • component->logo目录下:创建logo.js基础组件,设置顶部Logo位置与样式
  • container->login目录下:创建login.js业务组件
  1. 应用antd-mobile的组件实现登录页面
    import {List, InputItem, WingBlank, WhiteSpace, Button } from 'antd-mobile'
  2. 跳转路由:this.props.history.push('路由')

    register = () => {
            // console.log(this.props)
            this.props.history.push('/register')
    }
  3. 整个页面实现代码

    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业务组件
  1. 单选选项RadioItem需要先定义再使用
    import {List, InputItem, Radio, WingBlank, WhiteSpace, Button } from 'antd-mobile'
    const RadioItem = Radio.RadioItem
  2. 通过判断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>
  3. 整个页面实现代码

    扫描二维码关注公众号,回复: 5033326 查看本文章
    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
  1. state存储用户注册信息,通过redux给注册提交给服务端
  2. 关键:使用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
  1. 通过mongoose连接Express和MongoDB
  2. 建立User数据模型:通过mongoose.Schema生成model模型,通过mongoose.model生成Entity实体
  3. 导出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发送注册请求

  1. 先查找user,若存在则用户名重复

  2. 如不存在,继续使用注册信息创建新用户文档

    //用户注册
    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})
            })
        })
    })
    

九、注册跳转&密码加密实现  

  • 注册跳转
  1. 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
    }
  2. 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} 
  3. 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}

      

  • 密码加密

  1. 安装utility第三方插件

    npm install utility --save
  2. 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,登录互动实现同注册

  1. 调用props中的login异步action执行登录操作

  2. 判断props中的redirectTo和msg,跳转连接或显示错误提示

  3. 实现代码

    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中
  1. 注册后改为使用model.save方法创建model获取_id
  2. 用户刷新页面校验用户信息:获取cookie中的userid,通过find by id的方式查找用户数据
  3. 查找条件中加入过滤:不显示密码和版本号,语法为{'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

  1. 在判断当前为登录状态后,调用loadData同步action,将/user/info获取到的用户信息存入redux

  2. 注意:@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)
    }

      

      


注:项目来自慕课网

猜你喜欢

转载自www.cnblogs.com/ljq66/p/10267478.html