前端模块
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中的数据会重置会初始状态, 所以最好将一些持续的状态保存在
localstorage
和sessionstorage
里面
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推出的新生命周期函数, 代替了 componentWillReceiveProps
和 componentWillMount
( 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用的也不多, 果然自己还是太菜了...