学习编程的最好方法就是去编程。
这篇笔记项目来自于:
https://www.udemy.com/reactjs-and-flux-learn-by-building-10-projects/ 的第11课。
最终效果如图:
这个项目是单页面应用,使用的是客户端的渲染,没有用到服务器。
package.json
{
"name": "workoutlogger",
"version": "1.0.0",
"description": "Workout Logger",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Morphues Cao",
"license": "ISC",
"devDependencies":{
"browserify":"*",
"gulp":"*",
"reactify":"*",
"vinyl-source-stream":"*",
"object-assign":"*"
},
"dependencies":{
"react":"^0.14.7",
"react-dom":"^0.14.7",
"flux":"^2.1.1"
}
}
使用gulp进行编译,复制和监控。gulpfile.js
//path %PATH%;.\node_modules\.bin
var gulp = require('gulp');
var browserify = require('browserify');
var reactify = require('reactify');
var source = require('vinyl-source-stream');
gulp.task('browserify',function(){
browserify('./src/js/main.js')
.transform('reactify') //jsx -> js
.bundle()
.pipe(source('main.js'))
.pipe(gulp.dest('dist/js'));
});
gulp.task('copy',function(){
gulp.src('src/index.html')
.pipe(gulp.dest('dist'));
gulp.src('src/css/*.*')
.pipe(gulp.dest('dist/css'));
gulp.src('src/js/vendors/*.*')
.pipe(gulp.dest('dist/js'));
});
gulp.task('default',['browserify','copy'],function(){
return gulp.watch('src/**/*.*',['browserify','copy']);
});
目录结构:
参考flux的unidirectional data flow.:
对应的文件结构就更容易理解:
index.html
<html>
<head>
<title>Workout logger</title>
<link rel="stylesheet" href="css/bootstrap.css"/>
<link rel="stylesheet" href="css/style.css"/>
</head>
<body>
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div id="app">TEST APP</div>
</div>
</div>
<script src="js/jquery.js"></script>
<script src="js/main.js"></script>
</body>
</html>
js/main.js
var App = require('./components/App');
var React = require('react');
var ReactDOM = require('react-dom');
var AppAPI = require('./utils/AppAPI.js')
var StartData = require('./startData.js');
if(localStorage.getItem('workouts') == null){
StartData.init();
}
AppAPI.getWorkouts();
ReactDOM.render(
<App />,
document.getElementById('app')
);
js/components/App.js
var React = require('react');
var AppActions = require('../actions/AppActions');
var AppStore = require('../stores/AppStore');
var AddForm = require('./AddForm.js');
var Workouts = require('./Workouts.js');
function getAppState(){
return {
showForm: AppStore.getShowForm(),
workouts: AppStore.getWorkouts()
}
}
var App = React.createClass({
getInitialState: function(){
return getAppState();
},
componentDidMount:function(){
AppStore.addChangeListener(this._onChange);
},
componentWillUnmount:function(){
AppStore.removeChangeListener(this._onChange)
},
onShowFormClick:function(e){
e.preventDefault();
AppActions.showForm();
},
render: function(){
console.log(this.state.workouts)
if(this.state.showForm){
var form = <AddForm />
}else{
var form = '';
}
return (
<div>
<h1 className="text-center page-header">WorkoutLogger</h1>
<a onClick={this.onShowFormClick} href="#" className="btn btn-primary btn-block">Add Workout</a>
<br/>
{form}
<br/>
<Workouts workouts = {this.state.workouts} />
<br/>
</div>
)
},
_onChange: function(){
this.setState(getAppState());
}
});
module.exports = App;
js/components/AddForm.js
var React = require('react');
var AppActions = require('../actions/AppActions');
var AppStore = require('../stores/AppStore');
var AddForm = React.createClass({
render: function(){
return (
<form onSubmit={this.onSubmit}>
<div className="form-group">
<select className="form-control" ref="type">
<option value="Jogging">Jogging</option>
<option value="Weight Lifting">Weight Lifting</option>
<option value="Elliptical">Elliptical</option>
<option value="Yoga">Yoga</option>
<option value="Other">Other</option>
</select>
</div>
<div className="form-group">
<input type="text" className="form-control"
ref="minutes" placeholder="Minutes"/>
</div>
<div className="form-group">
<input type="text" className="form-control"
ref="miles" placeholder="Miles(Optional)"/>
</div>
<button type="submit" className="btn btn-default btn-block">Log Workout</button>
</form>
)
},
onSubmit: function(e){
e.preventDefault();
var workout = {
id: this.generateId(),
type:this.refs.type.value.trim(),
minutes:this.refs.minutes.value.trim(),
miles:this.refs.miles.value.trim(),
date:new Date()
}
//console.log(workout);
AppActions.addWorkout(workout);
},
generateId: function(){
var id='';
var possible = '0123456789';
for(var i=0;i<5;i++){
id += possible.charAt(Math.floor(Math.random() * possible.length));
}
return id;
}
});
module.exports = AddForm;
js/components/Workouts.js
var React = require('react');
var AppActions = require('../actions/AppActions');
var AppStore = require('../stores/AppStore');
var Workout = require('./Workout');
var Workouts = React.createClass({
render: function(){
return (
<ul className="list-group">
{
this.props.workouts.map(function(workout,i){
// console.log(workout);
return(
<Workout workout={workout} key={i} />
)
})
}
</ul>
)
}
});
module.exports = Workouts;
js/components/Workout.js
var React = require('react');
var AppActions = require('../actions/AppActions');
var AppStore = require('../stores/AppStore');
var Workout = React.createClass({
render: function(){
if(this.props.workout.miles != ''){
var miles = ' | '+this.props.workout.miles + 'Miles';
}else{
var miles = '';
}
return (
<li className="list-group-item">
{this.props.workout.type} - {this.props.workout.minutes} Minutes {miles}
<a href="#" onClick={this.onDelete.bind(this,this.props.workout.id)}
className="delete">X</a>
</li>
)
},
onDelete:function(i,j){
AppActions.removeWorkout(i);
}
});
module.exports = Workout;
js/actions/AppActions.js
var AppDispatcher = require('../dispatcher/AppDispatcher');
var AppConstants = require('../constants/AppConstants');
var AppActions = {
showForm: function(){
// console.log('AppAction: Searching for movie' + movie.title);
AppDispatcher.handleViewAction({
actionType: AppConstants.SHOW_FORM
});
},
addWorkout:function(workout){
AppDispatcher.handleViewAction({
actionType: AppConstants.ADD_WORKOUT,
workout:workout
});
},
receiveWorkouts:function(workouts){
AppDispatcher.handleViewAction({
actionType: AppConstants.RECEIVE_WORKOUTS,
workouts:workouts
});
},
removeWorkout:function(workoutId){
AppDispatcher.handleViewAction({
actionType: AppConstants.REMOVE_WORKOUT,
workoutId:workoutId
});
}
}
module.exports = AppActions;
js/dispatcher/AppDispatcher.js
var Dispatcher = require('flux').Dispatcher;
var assign = require('object-assign');
var AppDispatcher = assign(new Dispatcher(),{
handleViewAction: function(action){
var payload = {
source:"VIEW_ACTION",
action: action
}
this.dispatch(payload);
}
});
module.exports = AppDispatcher;
js/stores/AppStore.js
var AppDispatcher = require('../dispatcher/AppDispatcher');
var AppConstants = require('../constants/AppConstants');
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var AppAPI = require('../utils/AppAPI.js')
var CHANGE_EVENT = 'change';
var _workouts = [];
var _showForm = false;
var AppStore = assign({},EventEmitter.prototype,{
showForm: function(){
_showForm = true;
},
getShowForm: function(){
return _showForm;
},
addWorkout:function(workout){
_workouts.push(workout);
},
getWorkouts:function(){
return _workouts;
},
receiveWorkouts:function(workouts){
_workouts = workouts;
},
removeWorkout:function(workoutId){
// var index =_workouts.findIndex(x => x.id === workoutId);
function findIndex(_workouts,workoutId){
for(var i=0,len=_workouts.length;i<len;i++){
if(_workouts[i]._id === workoutId){
return i;
}
}
return -1;
}
var index = findIndex(_workouts,workoutId);
_workouts.splice(index,1);
},
emitChange:function(){
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback){
this.on('change',callback);
},
removeChangeListener:function(callback){
this.removeListener('change',callback);
}
});
AppDispatcher.register(function(payload){
var action = payload.action;
switch(action.actionType){
case AppConstants.SHOW_FORM:
AppStore.showForm();
AppStore.emit(CHANGE_EVENT);
break;
case AppConstants.ADD_WORKOUT:
AppStore.addWorkout(action.workout);
AppAPI.addWorkout(action.workout);
AppStore.emit(CHANGE_EVENT);
break;
case AppConstants.RECEIVE_WORKOUTS:
AppStore.receiveWorkouts(action.workouts);
AppStore.emit(CHANGE_EVENT);
break;
case AppConstants.REMOVE_WORKOUT:
AppStore.removeWorkout(action.workoutId);
AppAPI.removeWorkout(action.workoutId);
AppStore.emit(CHANGE_EVENT);
break;
}
return true;
})
module.exports = AppStore;
js/utils/AppAPI.js
var AppActions = require('../actions/AppActions');
module.exports = {
addWorkout: function(workout){
console.log('Saving Workout...');
var workouts = JSON.parse(localStorage.getItem('workouts'));
workouts.push(workout);
console.log('addWorkout:',workouts);
localStorage.setItem('workouts',JSON.stringify(workouts));
},
getWorkouts:function(){
var workouts = JSON.parse(localStorage.getItem('workouts'));
AppActions.receiveWorkouts(workouts);
},
removeWorkout:function(workoutId){
var workouts = JSON.parse(localStorage.getItem('workouts'));
for(var i=0;i<workouts.length;i++){
if(workouts[i].id ==workoutId){
workouts.splice(i,1);
}
}
localStorage.setItem('workouts',JSON.stringify(workouts));
}
}
js/startData.js
module.exports ={
init:function(){
localStorage.clear();
localStorage.setItem('workouts',JSON.stringify([
{
id:0001,
type:'Jogging',
minutes:20,
miles:2,
date: new Date()
},
{
id:0002,
type:'Yoga',
minute:40,
miles:'',
date:new Date()
}
]));
}
}