React实战博客1---登录模块

前端模块

1.登录窗口设计

​ 登录窗包括了3个模块( 登录, 注册, 重置 ), 通过嵌套路由切换状态

登录窗口

​ 登录加密使用MD5算法加盐( 盐值随便添加的这里就不写出来了~ ), 如果没有注册账号, 可以选择游客登录, 但是会失去用户操作的权限, 而且也只能看见我本人的博客, 无法获取其他用户发表的博客哦( 虽然也没有其他用户了... )

2.react嵌套路由实现窗口切换

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom'
import { Provider } from 'react-redux'

import store from './reducers'     // 全局状态监控会在后面提到

ReactDOM.render(
  <Provider store={store}>
    <Router>
      <App />
    </Router>
  </Provider>, document.getElementById('root'))


// App.js
render() {
    return (
        <div className="App">
            <Switch>
                <Redirect exact path="/" to="/sign" />
                <Route path="/sign" component={LoginPage} />
            </Switch>
        </div>
    );
}


// login.jsx
return (
      <div className="sign-bg">
        <div className="sign-window">
          {
            this.state.showTab 
            ? <div className="sign-switch">
                <Link onClick={() => this.state.switchLogin(1)} className={isLogin ? 'active' : ''} to="/sign/login">账号登录</Link>
                <Link onClick={() => this.state.switchLogin(0)} className={!isLogin ? 'active' : ''} to="/sign/register">快速注册</Link>
              </div>
            : null
          }
          <Redirect exact path="/sign" to="/sign/login" />
          <Route exact path="/sign/login" render={ () => (<Login hideTab={this.hideTab} {...this.state} />) } />
          <Route exact path="/sign/register" render={ () => (<Register {...this.state} />) } />
          <Route exact path="/sign/reset" render={ () => (<Reset backwards={this.backwards} {...this.state} />) } />
        </div>
        <div className="plice">京ICP备19108748号-1</div>
      </div>
    )
对比vue:
  • 1.路由组件: react在路由切换组件上与vue的 <router-view /> 不同的是, 每个视图必须用独立的 <Route /> 组件包裹, 而vue则指定include属性来分辨不同视图对应的组件
  • 2.视图渲染: vue的组件渲染在配置路由时就指定好了, 十分方便快捷.而react的组件渲染是写在 <Route /> 组件上的.
  • 3.手动切换路由: vue在全局注册了$router属性来实现手动切换路由, 而react则通过高阶组件 WithRouter 传入的history属性进行路由切换( 说实话,每次都要引入再使用挺烦的..这回我支持vue哈哈 )

​ 个人认为react虽然写法繁琐了一点, 但是更加灵活, 可根据具体情况渲染不同的组件, 而vue相对更加轻快, 适合快速搭建中小型项目

性能优化:

<Redirect /> 组件能少用就少用, 因为每用一次组件就重新渲染一次, 上面的代码我在 //sign 时分别重定向了2次, 显然这是浪费性能的体现, 经过改进之后将第二个 <Redirect /> 去掉了

// App.js
<div className="App">
    <Switch>
        { /*<Redirect exact path="/" to="/sign" /> */}
        <Redirect exact path="/" to="/sign/login" />
        <Route path="/sign" component={LoginPage} />
    </Switch>
</div>


// login.jsx
{/* <Redirect exact path="/sign" to="/sign/login" /> */}
<Route exact path="/sign/login" render={ () => (<Login hideTab={this.hideTab} {...this.state} />) } />
<Route exact path="/sign/register" render={ () => (<Register {...this.state} />) } />
<Route exact path="/sign/reset" render={ () => (<Reset backwards={this.backwards} {...this.state} />) } />

3.react-redux全局状态监控实现登录授权

​ react-redux是在react的基础上对redux的拓展. 实现了许多用于react-dom中的方法和高阶组件

实际应用( 取自项目 )
// reducers/index.js
import { createStore } from 'redux'

let initialState = {
  loginState: localStorage.getItem('loginState') ? localStorage.getItem('loginState') : -1,     // 登录状态 -1.未登录   0.游客登录   1. 用户登录
  salt: '****',   // MD5盐值
  userInfo: !!sessionStorage.getItem('userInfo') ? JSON.parse(sessionStorage.getItem('userInfo')) : { nickname: '' }   // 用户信息对象
}

const mainReducer = (state = initialState, action) => {
  switch(action.type) {
    case 'changeLoginState':
      localStorage.setItem('loginState', action.payload)
      return {
        ...state,
        loginState: action.payload
      }
    case 'getUserInfo':
      sessionStorage.setItem('userInfo', action.payload)
      return {
        ...state,
        userInfo: JSON.parse(action.payload)
      }
    default:
      return state
  }
}

const store = createStore(mainReducer)

export default store


// login.jsx
import { connect } from 'react-redux'

class Login extends React.Component {

  static propTypes = {
    hideTab: PropTypes.func,
    match: PropTypes.object.isRequired,
    location: PropTypes.object.isRequired,
    history: PropTypes.object.isRequired
  }

  constructor(props) {
    super(props)
  
    this.state = {
      username: '',
      password: '',
      userId: '',
      salt: '',
      emptyName: false,
      emptyPass: false,
      hideTab: null
    }
  
  }

  static getDerivedStateFromProps(props, state) {
    return {
      ...props
    }
  }

  loginToBlog() {
    let { username, password } = this.state

    let emptyName = username === ''
    let emptyPass = password === ''

    // 校验是否有空选项
    if (!emptyName && !emptyPass) {
      login({
        username,
        password
      })
        .then(res => {
          if (res.code == 0) {
            const modal = Modal.warning()
            modal.update({
              content: '该用户还未注册',
              onOk() {
                modal.destroy()
              }
            })
          }else if (res.code == 1) {    // 成功登陆, 储存用户信息
            message.success('欢迎')
            window.localStorage.setItem('user', JSON.stringify({ username, password }))
            this.props.changeLoginState(1)
            this.props.getUserInfo(JSON.stringify(res.data.userInfo))
            this.props.history.push('/main')
          }else if (res.code == 2) {   // 密码错误
            const modal = Modal.warning()
            modal.update({
              content: '密码错误!请重试',
              onOk() {
                modal.destroy()
              }
            })
          }
        })
        .catch(err => console.log(err))
    }else {
      this.setState({
        emptyName,
        emptyPass
      })
    }
  }

  changeUsername(target) {
    let username = target.value.trim()
    let emptyName = false
    this.setState({
      username,
      emptyName
    })
  }
  changePassword(target) {
    let password = target.value.trim()
    let emptyPass = false
    password = MD5(password + this.props.salt)
    this.setState({
      password,
      emptyPass
    })
  }

  render() {
    let { emptyName, emptyPass } = this.state
    return (
      <div className="login-window">
        <Input className={emptyName ? 'alert' : ''} onInput={({target}) => this.changeUsername(target)} placeholder="请输入用户名" type="text"/>
        <Input className={emptyPass ? 'alert' : ''} onInput={({target}) => this.changePassword(target)} placeholder="请输入密码" type="password"/>
        <div className="service">
          <Link to="/main" onClick={() => this.props.changeLoginState(0)}>游客进入</Link>
          <Link to="/sign/reset" onClick={this.state.hideTab}>忘记密码?</Link>
        </div>
        <button className="sign-btn" onClick={() => this.loginToBlog()}>登录</button>
      </div>
    )
  }
}

Login = withRouter(Login)

const mapLoginStateToProps = (state) => {
  return {
    loginState: state.loginState
  }
}

const mapLoginDispatchToProps = (dispatch) => {
  return {
    changeLoginState(type) {
      dispatch({
        type: 'changeLoginState',
        payload: type
      })
    },
    getUserInfo(data) {
      dispatch({
        type: 'getUserInfo',
        payload: data
      })
    }
  }
}

Login = connect(mapLoginStateToProps, mapLoginDispatchToProps)(Login)
对比vue:
  • 1.react中使用 createStore 创建一个store实例, vue使用 Vuex.store() 创建store实例, 创建方法一致, 但是传参却有很大不同
  • 2.react并没有将同步和异步方法分割开, 统一通过action属性判断并调用相关方法, vue则通过mutations和actions分别操作同步和异步方法
  • 3.react注册修改状态的方法比较零散, 一般写在各个组件或者当前视图的主页面下( 因为方法写好后要通过 connect() 方法注册到当前组件下 ). 而vue则更为集中, 一般把所有的mutations和actions分别写在一个汇总文件下, 再输出模块到各个文件里面
  • 4.与vue相同, 在刷新之后store中的数据会重置会初始状态, 所以最好将一些持续的状态保存在 localstoragesessionstorage 里面

​ react中的一些方法命名还是非常清楚明白的, 我们可以从上面的例子清楚看到 connect() 方法传入2个参数, mapStateToProps 是将store中的对象映射到当前组件的props中, 而 mapDispatchToProps 就是将一些方法映射到props中, 个人认为这样在日后维护代码的时候会比统一管理舒服一点吧, 这也是react灵活多变的体现

登录授权控制

​ 目前我只做了登录状态和用户操作权限的控制, 因为项目进行到目前为止还没有太多需要权限控制的操作, 所以我仅仅只判断了登录状态( 状态码上面示例有写 )

// app.js
class App extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      loginState: 1
    }
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.loginState < 0 && prevState.loginState >= 0) {   // 避免递归
      nextProps.history.replace('/sign/login')
      return {
        ...prevState,
        ...nextProps
      }
    }else {
      return prevState
    }
  }

  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.loginState !== nextState.loginState
  }
  
  render() {
    return (
      <div className="App">
        <Switch>
          <Redirect exact path="/" to="/sign/login" />
          <Route path="/sign" component={LoginPage} />
          <Route path="/main" component={MainPage} />
          <Route path="/add" component={AddPage} />
          <Route path="/user" component={UserPage} />
        </Switch>
      </div>
    );
  }
}

App = withRouter(App)

const mapStateToProps = (state) => {
  return {
    loginState: state.loginState
  }
}

App = connect(mapStateToProps)(App)

export default App;
聊一聊getDerivedStateFromProps

getDerivedStateFromProps 是react16推出的新生命周期函数, 代替了 componentWillReceivePropscomponentWillMount ( 16版本以后使用这两个旧生命周期会被提示添加前缀 UNSAFE_ )具体如何请移步到这篇简书 React新生命周期--getDerivedStateFromProps

​ 其实我也还不太习惯这个方法, 因为如果要在初始化组件时发起网络请求, 就会出现请求2次的情况( 每发生一次数据变化就会调用一次 ), 所以在性能优化方面需要做得更好

后端模块

// app.js  入口文件
const Koa = require('koa');
const KoaBodyParser = require('koa-bodyparser');   // 格式化http参数
const KoaStatic = require('koa-static');         // 静态文件路径

const path = require('path');

// 创建一个Koa对象表示web app本身:
const app = new Koa();
// 跨域处理
const cors = require('koa2-cors');     // 开启跨域
app.use(cors({
    origin: function (ctx) {
      return ctx.header.origin;      // 建议不要写'*', 别问我为什么知道
    },
    exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
    maxAge: 5,
    credentials: true,
    allowMethods: ['GET', 'POST', 'DELETE'],
    allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}))
app.use(KoaBodyParser());
app.use(KoaStatic(path.join(__dirname, '/client/public/index.html')));

const router = require('./routers');
app.use(router.routes());
app.use(router.allowedMethods());

// 在端口7458监听:
app.listen(7458);
console.log('app started at port 7458...');


// routers.js
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/data')
const db = mongoose.connection
db.on('error', console.error.bind(console, '连接数据库失败'));

let Schema, User, Article
db.on('open', function() {
  Schema = new mongoose.Schema({
    username: String, 
    password: String,
    nickname: String,
    email: String,
    phone: String,
  });
  User = mongoose.model('users', Schema);
  Article = mongoose.model('articles', Schema);
})

const Router = require('koa-router');
const router = new Router();

// 登录接口
router.post('/login', async(ctx, next) => {
  let body = ctx.request.body
  let { username, password } = body

  // code: 0 未注册, 1 成功, 2 密码错误
  let user = null 
  try {
    user = await User.find({'username': username})
    ctx.response.type = 'application/json';
    if (user.length > 0) {
      if (user[0].password == password) {
        ctx.response.body = {
          code: 1,
          msg: 'success',
          data: {
            userInfo: {
              nickname: user[0].nickname
            }
          }
        };
      }else {
        ctx.response.body = {
          code: 2,
          msg: '密码错误'
        };
      }
    }else {
      ctx.response.body = {
        code: 0,
        msg: '请先注册账户'
      };
    }
  } catch (error) {
    console.log(error)
    ctx.response.status = 500
    return next()
  }
})

// 注册接口
router.post('/register', async(ctx, next) => {
  let body = ctx.request.body
  let { username, password, nickname, email, phone } = body
  ctx.response.type = 'application/json'

  // 搜索数据库是否已经有该用户名
  let users = null
  try {
    users = await User.find({'username': username})
    users = await User.find({'nickname': nickname})
    if (users.length > 1 || (users[0] && users[0].username == username)) {
      ctx.response.body = { code: 0, msg: '该用户名已被注册!' }
      return
    }
    if (users.length > 1 || (users[0] && users[0].nickname == nickname)) {
      ctx.response.body = { code: 0, msg: '该昵称已存在!' }
      return
    }
  } catch (error) {
    console.log(error)
    ctx.response.status = 500
    return next()
  }

  // 注册新用户
  let doc = null
  try {
    doc = await User.create({ username, password, nickname, email, phone })
  } catch (error) {
    console.log(error)
    ctx.response.status = 500
    return next()
  }
  ctx.response.body = { code: 1, msg: 'success', data: doc }
})

// 重置密码接口
router.post('/reset', async(ctx, next) => {
  let body = ctx.request.body
  let { username, password, email, phone } = body
  ctx.response.type = 'application/json'

  // 搜索数据库是否已经有该用户名
  let users = null
  try {
    users = await User.findOneAndUpdate({ username, email, phone }, { password })
    if (!!users) {
      ctx.response.body = { code: 1, msg: 'success', data: users }
    }else {
      ctx.response.body = { code: 0, msg: '手机及邮箱验证失败', data: {} }
    }
  } catch (error) {
    console.log(error)
    ctx.response.status = 500
    return next()
  }
})


module.exports = router

​ 后端小白, 目前只能写一些增删改查的接口功能, 返回的数据参考RESTful设计, 对我来说相比增删改查更难的反而是es7的async await方法, 实在是不太熟悉, trycatch用的也不多, 果然自己还是太菜了...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343