diff --git a/README.md b/README.md index 9a4a2ae..dd132c4 100644 --- a/README.md +++ b/README.md @@ -3,27 +3,28 @@ React-test ## 介绍 -这是一个react测试项目,旨在通过webpack打包生成用于restful API的one page react项目 +这是一个react测试项目,旨在通过webpack打包生成用于restful API的one page react项目. +该demo展示了异步登录,页面检测登录自动跳转,不同页面渲染等相关操作. +须安装[webpack](https://github.com/webpack/webpack),[redux-devtools-extension](https://github.com/zalmoxisus/redux-devtools-extension) -## 技术栈 +## 目录结构 ``` -*babel -*webpack -*react -*redux -*react-redux -*react-router -*react-router-redux +webpack.config.js //webpack配置文件 +app.js //express应用程序 +-views //express模板 +-public //静态文件夹 +-client //客户端项目文件夹 +--app.js //入口文件 +--config.js //配置文件 +---actions //redux action列表 +---reducers //redux reducer列表 +---components //react组件 +---pages //页面组件 ``` -## 目录结构 +##如何运行 ``` --client //客户端项目库 ---app.js //入口文件 ---router.js //路由 ----actions //redux action列表 ----reducers //redux reducer列表 ----components //react组件 ----pages //页面组件 --public //发布目录 +npm install +webpack +npm start ``` \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..f2218da --- /dev/null +++ b/app.js @@ -0,0 +1,53 @@ +//加载依赖 +var express = require('express'); +var path = require('path'); + +var app = express(); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'html'); +app.engine('html', require('ejs-mate')); + +app.use(express.static(path.join(__dirname, 'public'))); + +//路由设置 +app.get('*',function(req,res){ + res.render('index',{}); +}); + + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + var err = {status:404}; + next(err); +}); + +// error handlers + +// production error handler +// no stacktraces leaked to user +app.use(function(err, req, res, next) { + console.log(err); + if(!err.status){ + err.status=500; + } + switch(err.status){ + case 403: + err.message = '没有权限访问'; + break; + case 404: + err.message = '找不到这个页面'; + break; + case 500: + err.message='系统出错了'; + break; + } + res.status(err.status); + res.render('error', { + title: '出错拉', + error:err + }); +}); + +module.exports = app; diff --git a/bin/www b/bin/www new file mode 100644 index 0000000..c38896a --- /dev/null +++ b/bin/www @@ -0,0 +1,89 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || 8080); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + console.log('Listening on ' + bind); +} \ No newline at end of file diff --git a/client/actions/auth.js b/client/actions/auth.js new file mode 100644 index 0000000..ceedd36 --- /dev/null +++ b/client/actions/auth.js @@ -0,0 +1,32 @@ +/** + * Action - auth + * 验证方法 + */ +//加载依赖 +import { push } from 'react-router-redux'; //router跳转方法 + +//导入常量 +import { LOGIN_STATUS_IN } from '../actions/login'; + +/* + * action 创建函数 + */ + +/** + * [authLoginStatus description] + * @param {string} login_status 登录状态 + * @param {boolen} check_status 须检测的状态,true为检测是否登录(当未登录时,跳转到登录页),false为检测是否未登录(当登录时,跳转到dashboard页) + * @return {function} [description] + */ +export function authLoginStatus(login_status,check_status){ + return function(dispatch) { + //检测登录,当未登录时,跳转到登录页 + if(check_status&&login_status!==LOGIN_STATUS_IN){ + dispatch(push('/')); + } + //检测未登录,当登录时,跳转到dashboard页 + if(!check_status&&login_status===LOGIN_STATUS_IN){ + dispatch(push('/dashboard/')); + } + } +} \ No newline at end of file diff --git a/client/actions/login.js b/client/actions/login.js new file mode 100644 index 0000000..107b8c2 --- /dev/null +++ b/client/actions/login.js @@ -0,0 +1,124 @@ +/** + * Action - login + * 登录,控制登录操作 + */ +//加载依赖 +import fetch from 'isomorphic-fetch'; +import { + API_LOGIN, API_LOGOUT +} +from '../config'; +import { + push +} +from 'react-router-redux'; //router跳转方法 + +/* + * action 类型 + */ +//登入请求 +export const LOGIN_REQUEST = 'LOGIN_REQUEST'; +export const LOGIN_RECEIVE = 'LOGIN_RECEIVE'; +export const LOGIN_ERROR = 'LOGIN_ERROR'; +//登出请求 +export const LOGOUT_REQUEST = 'LOGOUT_REQUEST'; +export const LOGOUT_RECEIVE = 'LOGOUT_RECEIVE'; +export const LOGOUT_ERROR = 'LOGOUT_ERROR'; +//状态 +export const LOGIN_STATUS_OUT = 'out'; //登出 +export const LOGIN_STATUS_IN = 'in'; //登入 +export const LOGIN_STATUS_LOAD = 'load'; //加载 +export const LOGIN_STATUS_ERROR = 'error'; //出错 +/* + * action 创建函数 + */ +//登录 +export function loginRequest() { + return { + type: LOGIN_REQUEST + } +} + +export function loginReceive(data) { + return { + type: LOGIN_RECEIVE, + info: data + } +} + +export function loginError(data) { + return { + type: LOGIN_ERROR, + msg: data + } +} + +export function loginPost(data) { + return function(dispatch) { + if (!data.name || !data.password) { + dispatch(loginError('没有输入用户名或密码')); + return false; + } + dispatch(loginRequest()); + let headers = new Headers(data); + let request = new Request(API_LOGIN, { + method: 'GET', + headers: headers + }) + fetch(request).then(response => response.json()) + .then(json => { + if (json.head.status === 200) { + dispatch(loginReceive(json.body)); + //跳转到登录后首页 + dispatch(push('/dashboard/')); + } else { + dispatch(loginError(json.head.msg)); + } + }).catch(error => { + console.log(error); + dispatch(loginError('网络错误,请重试')); + }) + } +} + +//登出 +export function logoutRequest() { + return { + type: LOGOUT_REQUEST + } +} + +export function logoutReceive(data) { + return { + type: LOGOUT_RECEIVE, + info: data + } +} + +export function logoutError(data) { + return { + type: LOGOUT_ERROR, + msg: data + } +} + +export function logoutPost() { + return function(dispatch) { + dispatch(logoutRequest()); + let request = new Request(API_LOGOUT, { + method: 'GET' + }); + fetch(request).then(response => response.json()) + .then(json => { + if (json.head.status === 200) { + dispatch(logoutReceive(json.body)); + //跳转到登录后首页 + dispatch(push('/')); + } else { + dispatch(logoutError(json.head.msg)); + } + }).catch(error => + dispatch(loginError('网络错误,请重试')) + ) + } +} \ No newline at end of file diff --git a/client/app.js b/client/app.js index cf20eec..dbdd9f5 100644 --- a/client/app.js +++ b/client/app.js @@ -1,83 +1,55 @@ /** * app */ -import React, { PropTypes } from 'react' -import ReactDOM from 'react-dom' -import { createStore,applyMiddleware} from 'redux' -import { Provider, connect } from 'react-redux' -import thunk from 'redux-thunk' -// React component -class Counter extends React.Component { - render () { - const { value, onIncreaseClick } = this.props - return ( -
- {value} - -
- ) - } -} - -Counter.propTypes = { - value: PropTypes.number.isRequired, - onIncreaseClick: PropTypes.func.isRequired -} +//加载依赖 +import React from 'react'; +import ReactDOM from 'react-dom'; +import { createStore, applyMiddleware } from 'redux'; +import { Provider,Counter } from 'react-redux'; +import { Router, Route, browserHistory,IndexRoute } from 'react-router'; +import { syncHistory } from 'react-router-redux'; +import thunk from 'redux-thunk'; +import {reducer} from './reducers/index'; -// Action -const increaseAction = {type: 'increase'} +//加载页面 +import Layout from './pages/layout'; //公共组件 +import Home from './pages/home'; //首页 +import Dashboard from './pages/dashboard'; //控制面板 -// Reducer -function counter (state = {count: 0}, action) { - let count = state.count - switch (action.type) { - case 'increase': - return {count: count + 1} - default: - return state - } -} +// Sync dispatched route actions to the history +const reduxRouterMiddleware = syncHistory(browserHistory); //插入中间件 let createStoreWithMiddleware = applyMiddleware( - thunk + thunk, + reduxRouterMiddleware )(createStore) -//载入redux debug插件 -function configureStore(reducer,initialState) { - const store = createStoreWithMiddleware(reducer, initialState, - window.devToolsExtension ? window.devToolsExtension() : undefined - ); - return store; +if(process.env.NODE_ENV==='production'){ + var store = createStoreWithMiddleware(reducer,{}); } -// Store -//let store = createStore(counter); -let store = configureStore(counter,{count:0}); - -// Map Redux state to component props -function mapStateToProps (state) { - return { - value: state.count +else{ + //载入redux debug插件 + function configureStore(initialState) { + const store = createStoreWithMiddleware(reducer, initialState, + window.devToolsExtension ? window.devToolsExtension() : undefined + ); + return store; } + // Store + var store = configureStore({}); } -// Map Redux actions to component props -function mapDispatchToProps (dispatch) { - return { - onIncreaseClick: () => dispatch(increaseAction) - } -} - -// Connected Component -let App = connect( - mapStateToProps, - mapDispatchToProps -)(Counter) - +//创建路由 ReactDOM.render( - + + + + + + , document.getElementById('page') ) \ No newline at end of file diff --git a/client/components/login.js b/client/components/login.js new file mode 100644 index 0000000..7bca242 --- /dev/null +++ b/client/components/login.js @@ -0,0 +1,45 @@ +/** + * Page - index + * 首页 + */ +import React, {PropTypes,Component} from 'react'; +import { LOGIN_STATUS_OUT,LOGIN_STATUS_IN,LOGIN_STATUS_LOAD,LOGIN_STATUS_ERROR } from '../actions/login'; + +//封装app组件 +class Login extends Component { + render() { + const {status,msg} = this.props; + var Tip='',Button=; + switch(status){ + case LOGIN_STATUS_ERROR: + Tip=
提示信息:{msg}
+ break; + case LOGIN_STATUS_LOAD: + Button=; + break; + } + return ( +
this.handleSubmit(e)}> + {Tip} +

+

+

{Button}

+
+ ); + } + + handleSubmit(e) { + e.preventDefault(); + this.props.onLoginSubmit({ + name:this.refs.name.value, + password:this.refs.password.value + }); + } +} +//组件限制 +Login.propTypes={ + onLoginSubmit:PropTypes.func.isRequired, + status:PropTypes.string.isRequired, + msg:PropTypes.string +} +export default Login; \ No newline at end of file diff --git a/client/config.js b/client/config.js new file mode 100644 index 0000000..4d507cf --- /dev/null +++ b/client/config.js @@ -0,0 +1,7 @@ +/** + * 配置信息 + */ +//API配置 +const API_PATH='/api/'; +export const API_LOGIN=API_PATH+'login.json'; +export const API_LOGOUT=API_PATH+'loginout.json'; \ No newline at end of file diff --git a/client/pages/dashboard.js b/client/pages/dashboard.js new file mode 100644 index 0000000..c3c18bd --- /dev/null +++ b/client/pages/dashboard.js @@ -0,0 +1,33 @@ +/** + * Page - index + * 首页 + */ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { push } from 'react-router-redux'; + +//加载action +import { authLoginStatus } from '../actions/auth'; +import { logoutPost,LOGIN_STATUS_IN } from '../actions/login'; + +//加载reducer +import {propMap} from '../reducers/index'; + +//封装组件 +class Dashboard extends Component { + componentDidMount(){ + const { login,dispatch,routing } = this.props; + //检测是否未登录 + dispatch(authLoginStatus(login.status,true)); + } + render() { + const { login,dispatch } = this.props; + return ( +
+

欢迎光临控制面板,当前登录用户{login.info.name},当前登录角色{login.info.role}

+

{e.preventDefault();dispatch(logoutPost());}}>退出

+
+ ); + } +} +export default connect(propMap)(Dashboard); \ No newline at end of file diff --git a/client/pages/home.js b/client/pages/home.js new file mode 100644 index 0000000..a989bbf --- /dev/null +++ b/client/pages/home.js @@ -0,0 +1,37 @@ +/** + * Page - index + * 首页 + */ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { push } from 'react-router-redux'; + +//加载组件 +import Login from '../components/login'; + +//加载action +import { authLoginStatus } from '../actions/auth'; +import { loginPost,LOGIN_STATUS_IN } from '../actions/login'; + +//加载reducer +import {propMap} from '../reducers/index'; + +//封装组件 +class Index extends Component { + componentDidMount(){ + const { login,dispatch } = this.props; + //检测是否登录 + dispatch(authLoginStatus(login.status,true)); + } + render() { + const { login,dispatch } = this.props; + var LoginBlock=dispatch(loginPost(data)) } status={login.status} msg={login.msg} />; + return ( +
+

这是一个测试程序,当前登录状态 {login.status}

+ {LoginBlock} +
+ ); + } +} +export default connect(propMap)(Index); \ No newline at end of file diff --git a/client/pages/layout.js b/client/pages/layout.js new file mode 100644 index 0000000..8ba84ba --- /dev/null +++ b/client/pages/layout.js @@ -0,0 +1,18 @@ +/** + * Page - layout + * 框架 + */ +import React, { Component } from 'react'; + +//封装组件 +class Layout extends Component { + render() { + return ( +
+

这是一个APP

+ {this.props.children} +
+ ); + } +} +export default Layout; \ No newline at end of file diff --git a/client/reducers/index.js b/client/reducers/index.js new file mode 100644 index 0000000..cff9967 --- /dev/null +++ b/client/reducers/index.js @@ -0,0 +1,26 @@ +/** + * Reducer - index + * 汇总 + */ +//加载reducers +import login from './login'; + +import { combineReducers } from 'redux'; +import { routeReducer } from 'react-router-redux'; + +/** + * 组织注入的object + * @param {object} state state数据 + * @return {object} 返回的prop数据 + */ +export function propMap(state){ + return { + routing:state.routing, + login:state.login + } +} + +export const reducer = combineReducers({ + login, + routing: routeReducer +}) \ No newline at end of file diff --git a/client/reducers/login.js b/client/reducers/login.js new file mode 100644 index 0000000..65c36e8 --- /dev/null +++ b/client/reducers/login.js @@ -0,0 +1,55 @@ +/** + * Reducer - login + * 登录 + * 生成变量:login + */ +//导入action常量 +import { LOGIN_REQUEST, LOGIN_RECEIVE, LOGIN_ERROR, LOGOUT_REQUEST, LOGOUT_RECEIVE, LOGOUT_ERROR,LOGIN_STATUS_OUT,LOGIN_STATUS_IN,LOGIN_STATUS_LOAD,LOGIN_STATUS_ERROR } from '../actions/login'; + +//初始化state +let init_state = { + status: LOGIN_STATUS_OUT, + info: { + name: '', + role: '' + }, + msg: '' +} + +export default function login(state = init_state, action) { + switch (action.type) { + case LOGIN_REQUEST: + return Object.assign({}, state, { + status: LOGIN_STATUS_LOAD + }); + break; + case LOGIN_RECEIVE: + return Object.assign({}, state, { + status: LOGIN_STATUS_IN, + info: action.info + }); + break; + case LOGIN_ERROR: + return Object.assign({}, state, { + status: LOGIN_STATUS_ERROR, + msg: action.msg + }); + break; + case LOGOUT_REQUEST: + return Object.assign({}, state, { + status: LOGIN_STATUS_LOAD + }); + break; + case LOGOUT_RECEIVE: + return Object.assign({}, state, init_state); + break; + case LOGOUT_ERROR: + return Object.assign({}, state, { + status:LOGIN_STATUS_ERROR, + msg: action.msg + }); + break; + default: + return state; + } +} diff --git a/client/router.js b/client/router.js deleted file mode 100644 index de11bdc..0000000 --- a/client/router.js +++ /dev/null @@ -1,3 +0,0 @@ -/** - * router components - */ \ No newline at end of file diff --git a/package.json b/package.json index 93ad0f1..e566f91 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,30 @@ { - "name": "react-test", - "version": "1.0.0", - "description": "react test", - "main": "webpack.config.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "devDependencies": { - "babel-core": "^6.5.2", - "babel-loader": "^6.2.3", - "babel-preset-es2015": "^6.5.0", - "babel-preset-react": "^6.5.0", - "react": "^0.14.7", - "react-dom": "^0.14.7", - "react-redux": "^4.4.0", - "react-router": "^2.0.0", - "redux": "^3.3.1", - "webpack": "^1.12.13" - }, - "dependencies": { - "redux-thunk": "^1.0.3" - } + "name": "react-test", + "version": "1.0.0", + "description": "react test", + "main": "webpack.config.js", + "scripts": { + "start": "node ./bin/www" + }, + "author": "bakajinsei", + "license": "ISC", + "dependencies": { + "ejs-mate": "^2.3.0", + "express": "~4.11.1" + }, + "devDependencies": { + "babel-core": "^6.5.2", + "babel-loader": "^6.2.3", + "babel-preset-es2015": "^6.5.0", + "babel-preset-react": "^6.5.0", + "isomorphic-fetch": "^2.2.1", + "react": "^0.14.7", + "react-dom": "^0.14.7", + "react-redux": "^4.4.0", + "react-router": "^2.0.0", + "react-router-redux": "^3.0.0", + "redux": "^3.3.1", + "redux-thunk": "^1.0.3", + "webpack": "^1.12.13" + } } diff --git a/public/api/login.json b/public/api/login.json new file mode 100644 index 0000000..b0c0dbd --- /dev/null +++ b/public/api/login.json @@ -0,0 +1,10 @@ +{ + "head":{ + "status":200, + "msg":"登录成功" + }, + "body":{ + "name":"test", + "role":"管理员" + } +} \ No newline at end of file diff --git a/public/api/loginout.json b/public/api/loginout.json new file mode 100644 index 0000000..097b1b8 --- /dev/null +++ b/public/api/loginout.json @@ -0,0 +1,7 @@ +{ + "head":{ + "status":200, + "msg":"登出成功" + }, + "body":null +} \ No newline at end of file diff --git a/views/error.html b/views/error.html new file mode 100644 index 0000000..45bb282 --- /dev/null +++ b/views/error.html @@ -0,0 +1,7 @@ + + +React Test +
+

<%= error.message %>

+
+ \ No newline at end of file diff --git a/views/index.html b/views/index.html new file mode 100644 index 0000000..5965695 --- /dev/null +++ b/views/index.html @@ -0,0 +1,8 @@ + + +React Test + +
+ + + \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index fe4ae60..0701e5a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -20,5 +20,12 @@ module.exports = { } } ] - } + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + 'NODE_ENV': '"development"' + } + }) + ] }