Udacity's Redux course
where you will build a content and comment web app.
Users will be able to post content to predefined categories,
comment on their posts and other users' posts,
and vote on posts and comments.
Users will also be able to edit and delete posts and comments.
# 打开服务器端口
cd api-server
node install
node server
# 打开前端页面
cd readable
node install
npm start
yarn add antd babel-plugin-import
#File: /readable/config/webpack.config.dev.js
// Process JS with Babel.
{
test: /\.(js|jsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
// 改动: 添加 antd 按需加载文件处理插件
plugins: [
//['react-html-attrs'],//添加babel-plugin-react-html-attrs组件的插件配置
// 引入样式为 css
['import', { libraryName: 'antd', style: 'css' }],
// 改动: 引入样式为 less
// ['import', { libraryName: 'antd', style: true }],
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
},
},
安装
npm install css-loader style-loader react-css-modules
所有局部的样式都放到 src/styles/*.css 统一管理。
其它所有目录包括 第三方组件 中的样式都是全局的。
修改readable/config/webpack.config.dev.js 添加 exclude\include\modules:true\localIdentName
{
test: /\.css$/,
exclude: /src|styles\.css/, //path.resolve(__dirname, 'src/styles'),
use: [
require.resolve('style-loader'), {
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
}
}, {
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebookincubator/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
autoprefixer({
browsers: [
'>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9', // React doesn't support IE8 anyway
],
flexbox: 'no-2009'
})
]
}
}
]
},
{
test: /\.css$/,
include: /src|styles\.css/, // path.resolve(__dirname, 'src/styles'),
use: [
require.resolve('style-loader'), {
loader: require.resolve('css-loader'),
options: {
importLoaders: 1, // 改动
modules: true, // 新增对css modules的支持
localIdentName: '[name]__[local]__[hash:base64:5]', //
}
}, {
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebookincubator/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
autoprefixer({
browsers: [
'>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9', // React doesn't support IE8 anyway
],
flexbox: 'no-2009'
})
]
}
}
]
},
替换 App.js 代码
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom'
import ListView from './components/ListView'
class App extends Component {
render() {
return (
<Router>
<div>
<Route exact path='/' component={ListView}/>
</div>
</Router>
);
}
}
export default App
import React from 'react'
export default class ListView extends React.Component {
render() {
return <div>ListView</div>
}
}
Welcome to the Udacity Readable API!
Use an Authorization header to work with your own data:
fetch(url, { headers: { 'Authorization': 'whatever-you-want' }})
The following endpoints are available:
GET /posts
USAGE:
Get all of the posts. Useful for the main page when no category is selected.
使用 axios 获取数据
axios({
method: 'get',
url: 'http://localhost:3001',
headers: {
'Accept': 'application/json',
'Authorization': '1234'
}
}).then(res => console.log(res))
})
查看数据形式:
这里取data数据用state allpost 接收
.then(res => { this.setState({allpost: res.data}) })
在页面中用列表显示出来
render() {
let {allpost} = this.state
return (
<ul>
{allpost.map((post) => (
<li key={post.id}>
<div>
<p>{'author: ' + post.author}</p>
<p>Body: {post.body}</p>
<p>category: {post.category}</p>
<p>delete: {post.delete}</p>
<p>id: {post.id}</p>
<p>{'timestamp: ' + new Date(post.timestamp).toLocaleString()}</p>
<p>{'title: ' + post.title}</p>
<p>{'Vote score: ' + post.voteScore}</p>
</div>
</li>
))}
</ul>
)
}
ListView.js此时全部代码: 这是到这里为止中所做的更改
import React from 'react'
import axios from 'axios'
const api = 'http://localhost:3001'
let token = localStorage.token
if (!token)
token = localStorage.token = Math.random().toString(36).substr(-8)
console.log(token)
const headers = {
'Accept': 'application/json',
'Authorization': token
}
export default class ListView extends React.Component {
constructor(props) {
super(props)
this.state = {
allpost: []
}
}
componentDidMount() {
axios({
method: 'get',
url: `${api}/posts`,
headers: {
...headers
}
}).then(res => {
this.setState({allpost: res.data})
console.log(res)
})
}
render() {
let {allpost} = this.state
console.log(allpost)
return (
<ul>
{allpost.map((post) => (
<li key={post.id}>
<div>
<p>{'author: ' + post.author}</p>
<p>Body: {post.body}</p>
<p>category: {post.category}</p>
<p>delete: {post.delete}</p>
<p>id: {post.id}</p>
<p>{'timestamp: ' + new Date(post.timestamp).toLocaleString()}</p>
<p>{'title: ' + post.title}</p>
<p>{'Vote score: ' + post.voteScore}</p>
</div>
</li>
))}
</ul>
)
}
}
<Table columns={columns}
rowKey={record => record.id}
dataSource={this.state.data}
pagination={this.state.pagination}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
列描述数据对象,是 columns 中的一项,Column 使用相同的 API
const columns = [{
title: 'Vote',
width: '5%',
dataIndex: 'index',
render: (text, record) => (
<span>
<Icon type="like-o" onClick={()=>_voteForLink()} style={{cursor: 'pointer'}} />
<span className="ant-divider" />
<Icon type="dislike-o" onClick={()=>_voteForLink()} style={{cursor: 'pointer'}} />
</span>
),
}, {
title: 'Score',
dataIndex: 'voteScore',
sorter: (a, b) => a.voteScore - b.voteScore,
width: '7%',
}, {
title: 'Title',
dataIndex: 'title',
width: '30%',
}, {
title: 'Date',
dataIndex: 'timestamp',
width: '10%',
sorter: (a, b) => a.timestamp - b.timestamp,
}, {
title: 'Author',
dataIndex: 'author',
width: '10%',
}, {
title: 'Comments',
dataIndex: 'comments'
},{
title: 'Action',
key: 'action',
render: (text, record) => (
<span>
<Link to="/">Editor</Link>
<span className="ant-divider" />
<Link to="/">Delete</Link>
</span>
),
}];
先把 API 请求拆分出来。在文件夹 utils 下面新建 Api.js,把 api 相关代码放进来
import axios from 'axios';
const api = 'http://localhost:3001'
let token = localStorage.token
if (!token)
token = localStorage.token = Math.random().toString(36).substr(-8)
console.log(token)
const headers = {
'Accept': 'application/json',
'Authorization': token
}
/**
* GET /posts
* USAGE:
* Get all of the posts. Useful for the main page when no category is selected.
*/
export const fetchPost = () => {
this.setState({ loading: true });
return axios({
method: 'get',
url: `${api}/posts`,
headers: {
...headers
}
}).then(res => {
const pagination = { ...this.state.pagination }
pagination.total = res.data.length
this.setState({
loading: false,
data: res.data,
pagination,
})
console.log(res)
})
}
发现有很多与 state 和 setState 相关的代码,现在编写 actions
新建 actions 文件夹,创建 posts.js,
新建一个 ALL_POSTS 常量,是可以将它们传递给 Reducer 函数。
然后创建 actions creators, 它将接收不同属性的对象 数据、分页、等待加载中
import * as API from '../utils/Api'
import * as ActionType from './constants'
// export const ALL_POSTS = 'ALL_POSTS'
export const AllPostsAction = (data) => {
return {
type: ActionType.ALL_POSTS,
data,
}
}
export const FetchPosts = () => {
return dispatch => {
API.fetchPost().then(response => {
dispatch(AllPostsAction(response.data.assets))
})
}
}
在这里新建了一个文件 constants.js 常量文件,方便
export const ALL_POSTS = 'ALL_POSTS'
reducer 负责创建应用程序的初始状态,之后该状态会保存在 store 中,
reducer 接收两个参数,第一个参数是当前状态,reducer 总是要返回一个状态,
第二个参数是派发(dispatch)的 action,此action 用于决定要对状态作出何种变更。
然后返回更新后的副本
function reducer (state, action) {
switch (action.type) {
case 'SUBMIT_USER' :
return Object.assign({}, state, {
user: action.user
})
}
}
Object.assign() 方法用于将所有可枚举的属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
第一个参数是目标对象,后面的参数是(多个)源对象,返回目标对象,
所以我们的应用是返回状态或结构化数据
但是此数据的形状应该是怎么样的呢? 是否应该是一个对象数组,还是包含一切数据的单个对象
具体的形状取决于功能。
无论状态的形状是什么样的,这是在开始编写reducer之前就应该决定的事情
需要花时间思考应用程序如何使用该数据,以及哪种格式最合适
只有在知道 状态 形状之后,才能构建 reducer 让它以正确的格式返回数据
现在我们有 action,我们来构建 reudcer 来指定我们的状态将如何根据这些 action 而改变
新建一个 reducers 文件夹,创建 posts.js 文件与 actions 的文件名对应,
import 我们的 actions 中创建的常量, 创建一个初始化状态 initialState 对象,
我们的 reducer 函数传入 initialState 和 action 参数,并处理更改状态,
但是不直接修改,而是更新副本。
import * as ActionType from './constants'
// import ALL_POSTS from '../actions/posts'
/**
* reducer 将指定我们 store 的形状,我们将初始状态粘贴到这里
* 当 dispatch ALL_POSTS action 时,我们的 store 的状态如何变化
*/
const AllPostsReducer = (state=[], action) => {
const { data } = action
switch (action.type) {
case ActionType.ALL_POSTS:
return {
...state, // 对象扩展语法,与之前的状态相同
data, // 修改状态
}
default:
return state
}
}
store 负责多件事情:
- holds the app's state 保存了应用程序的状态
- dispatches actions 派发action
- calls reducers 派发 action 之后调用 reducer 函数
- receives / stores new state 它还负责接收并存储新状态
const immaStore = Redux.createStore()
将 reducer 作为输入,而返回新的 store 对象
immaStore.getState() 返回 store 中的当前状态
immaStore.dispatch() 接收action对象并将它传递给 reducer 函数
immaStore.subscribe() 接收listener函数,在状态发生变更时调用
npm install redux
先安装 redux 包,修改index.js文件
# File: readable/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css'
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { createStore } from 'redux'
import reducer from './reducers/posts'
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
console.log(store)
ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();
查看 store 的属性,
dispatch 用来派发具体动作的
getState 返回Redux store 的当前状态
subscribe 侦听 store 中的变更
使 redux 应用与 redux 开发工具兼容
这样可以在应用程序中可以看到正在发生什么样的操作
我们的状态在根据这些操作发生哪些变化等等
我们要做的是,在 index.js 文件中,向 createStore 传递第二个参数
window.__REDUX__DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
如果 REDUX_DEVTOOLS_EXTENSION 存在于 window 对象,则直接调用它
它的作用是使我们可以使用 redux 开发工具
现在我们有了 actions、reducer 和 store,现在我们将它们连接到 react 应用,
看看他们是如何协同工作的
- 在index.js中我们将 store 传递给应用组件,现在应用将接收 store 作为 props
- 将完善应用组件以及该组件将渲染的UI代码
- 在组件渲染完成之后,调用 componentDidMount 方法,我们想做的是从 props 抓取 store
- 然后我们想订阅 redux store 中发生的任何变化,然后在发生任何变化时,我们想做的是调用 setState
- 我们继续从stroe 中获取 store 将它放入本地组件 state 中,这将导致重新渲染
安装 react-redux,重启服务器
想之前提到的,我们要将 store 传递给 应用组件
但这问题是,如果应用组件有很多组件,且每个都需要 store 怎么办?
无论它们是需要发出任何东西还是需要访问 redux store
问题是,每当这些子组件需要进行预 redux store相关的操作时
我们都需要将 store 向下传递给所有这些子组件
所以我们可以使用 provider 组件,它位于 react-redux 绑定上
那么我们可以将主要根组件包含在 provider 内
然后我们不将 store 传递给应用,而是将它传递给 provider
那么在将来,每当应用渲染的任何组件或应用本身需要访问 redux store 或发出操作时都能更容易
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css'
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { createStore } from 'redux'
import reducer from './reducers/posts'
import { Provider } from 'react-redux'
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
console.log(store)
ReactDOM.render(
<Provider store={ store }>
<App />
</Provider>, document.getElementById('root'));
registerServiceWorker();
Provider 组件为我们提供了一个非常便利的方式,来将 store 传递给所有子组件
但是,我们实际上仍需要一种方式来访问该 store 的 context,
而 react-redux 绑定正好能给我们这样做的 connect 方法
connect 将返回 inccurred 函数
使用connect 我们可以传入 store 状态的特定部分,
以及调度给我们的组件访问 props 的操作
ReactDOM.render(
<Provider store={ store }>
<App />
</Provider>, document.getElementById('root'));
我们将APP 组件包裹在 provider 中并传入 store
任何需要派发或需要从 store 获取状态的组件都可以使用 react redux 的 connect 函数
在我们的ListView.js 中引入 connect 函数,然后就不只是导出 ListView
我们还要导出 connect 并注意我们将调用(invoke)它
它会向我们返回一个全新的函数,然后我们可以传递组件,
用日志记录 this.props
import React from 'react'
import { connect } from 'react-redux'
class ListView extends React.Component {
render() {
console.log('Props', this.props)
return (
<h1>HelloWorld</h1>
)
}
}
export default connect()(ListView)
如果需要在一个组件内派发 action,需要做的事连接该组件,然后就可以调用 dispatch 了
mapStateToProps 函数要做的是将我们的 Redux 状态映射到组件 props
那么此函数将接收我们的状态作为第一个参数,此函数返回的将传递给 ListView 组件
可以稍微调整数据格式
那么在 mapStateToProps 中,它将返回我们想要从 Redux store 获取并传递给组件的状态
我们可以按想要的方式重新格式化数据的形状
将 redux 状态映射到特定组件的 props,我们可以用日志记录我们的应用组件在接收的props
那么我们来导入 action 创建器,这样我们就能看到在 dispatch action时应用是什么样子的
这些都来自我们的 actions 文件
我们创建 mapDispatchToProps 函数,它将 dispatch 方法映射到特定的 props
所以我们将向此方法传递 dispatch,而返回的就像在 mapStateToProps中一样,
将作为 props 传递给我们的组件
doThing = () => {
this.props.dispatch(addRecipe({}))
}
function mapDispatchToProps(dispatch){
return {
selectRecipe: (data) => dispatch(addRecipe(data)),
remove: (data) => dispatch(removeFromCalendar(data)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
doThing = () => {
this.props.selectRecipe({})
}
现在我们的组件 props 上将有一个 selectRecipe 方法和一个 remove 方法
然后当这些函数调用时,它们将自动为我们派发
再次说明,这是一种可选的方法,如果不想用也可以不用
我们在这里接收 selectRecipe 和 remove 将在被调用时自动派发
我们基本上将 action 创建器包裹在 mapDispatchToProps 中的dispatch 内
以使我们的组件更简洁一些。
因为我们现在可以直接调用 this.props 加上 mapDispatchToProps 中的方法就可以了
可以看到此时已经没有 dispatch 了,而是改变为了 fetchAllPosts
# File: src/actions/constants.js
export const ALL_POSTS = 'ALL_POSTS'
# File: src/utils/Api.js
import axios from 'axios';
const api = 'http://localhost:3001'
let token = localStorage.token
if (!token)
token = localStorage.token = Math.random().toString(36).substr(-8)
console.log(token)
const headers = {
'Accept': 'application/json',
'Authorization': token
}
/**
* GET /posts
* USAGE:
* Get all of the posts. Useful for the main page when no category is selected.
*/
export const fetchPost = () => {
return axios({
method: 'get',
url: `${api}/posts`,
headers: {
...headers
}
})
}
# File: src/actions/posts.js
import * as API from '../utils/Api'
import * as ActionType from './constants'
export const setPost = (posts) => {
return {
type: ActionType.ALL_POSTS,
posts,
}
}
export const addPost = () => {
return dispatch => {
API.fetchPost().then(data => dispatch(setPost(data)))
}
}
# File: src/reducers/posts.js
import * as ActionType from '../actions/constants'
/**
* reducer 将指定我们 store 的形状,我们将初始状态粘贴到这里
* 当 dispatch ALL_POSTS action 时,我们的 store 的状态如何变化
*/
const AllPostsReducer = (state=[], action) => {
switch (action.type) {
case ActionType.ALL_POSTS:
return action.posts
default:
return state
}
}
export default AllPostsReducer
# File: ListView.js
import React from 'react'
import { connect } from 'react-redux'
import { addPost } from '../actions/posts'
class ListView extends React.Component {
componentDidMount() {
this.props.addPost();
}
render() {
console.log('Props', this.props)
const { posts } = this.props
console.log('posts', posts)
return (
<div>
<ul>
{posts && (posts.map((post) => (
<li key={post.id}>
<div>
<p>{'author: ' + post.author}</p>
<p>Body: {post.body}</p>
<p>category: {post.category}</p>
<p>delete: {post.delete}</p>
<p>id: {post.id}</p>
<p>{'timestamp: ' + new Date(post.timestamp).toLocaleString()}</p>
<p>{'title: ' + post.title}</p>
<p>{'Vote score: ' + post.voteScore}</p>
</div>
</li>
)))}
</ul>
</div>
)
}
}
const mapStateToProps = (state, props) => {
console.log('state', state)
console.log('props', props)
return { posts: state.data };
}
export default connect(mapStateToProps, {addPost})(ListView)
mapDispatchToProps 的使用是包裹 action creator 的 dispatch 方法,在组件中直接调用 mapDispatchToProps 方法就可以此次修改的代码可以查看
# File: src/App.js
import React, { Component } from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom'
import ListView from './components/ListView'
class App extends Component {
render() {
return (
<Router>
<div>
<Route exact path='/' component={ListView}/>
</div>
</Router>
);
}
}
export default App
# File: src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css'
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { createStore, applyMiddleware } from 'redux'
import reducer from './reducers/posts'
import { Provider } from 'react-redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunk from 'redux-thunk'
// const store = createStore(
// reducer,
// window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
// console.log(store)
const store = createStore(
reducer,
composeWithDevTools(
applyMiddleware(thunk)
)
)
ReactDOM.render(
<Provider store={ store }>
<App />
</Provider>, document.getElementById('root'));
registerServiceWorker();
# File: src/components/ListView.js
import React from 'react'
import { Icon, Table } from 'antd';
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import { addPost } from '../actions/posts'
import * as API from '../utils/Api'
const columns = [{
title: 'Vote',
width: '5%',
dataIndex: 'index',
render: (text, record) => (
<span>
<Icon type="like-o" onClick={()=>_voteForLink()} style={{cursor: 'pointer'}} />
<span className="ant-divider" />
<Icon type="dislike-o" onClick={()=>_voteForLink()} style={{cursor: 'pointer'}} />
</span>
),
}, {
title: 'Score',
dataIndex: 'voteScore',
sorter: (a, b) => a.voteScore - b.voteScore,
width: '7%',
}, {
title: 'Title',
dataIndex: 'title',
width: '30%',
}, {
title: 'Date',
dataIndex: 'timestamp',
width: '10%',
sorter: (a, b) => a.timestamp - b.timestamp,
}, {
title: 'Author',
dataIndex: 'author',
width: '10%',
}, {
title: 'Comments',
dataIndex: 'comments'
},{
title: 'Action',
key: 'action',
render: (text, record) => (
<span>
<Link to="/">Editor</Link>
<span className="ant-divider" />
<Link to="/">Delete</Link>
</span>
),
}];
const _voteForLink = async () => {
console.log('voteForLink')
}
class ListView extends React.Component {
constructor(props) {
super(props)
this.state = {
pagination: {},
loading: false,
}
}
handleTableChange = (pagination, filters, sorter) => {
const pager = { ...this.state.pagination };
pager.current = pagination.current;
this.setState({
pagination: pager,
});
API.fetchPost().then(res => {
const pagination = {
...this.state.pagination
}
pagination.total = res.data.length
this.setState({loading: false, pagination})
})
}
componentDidMount() {
this.props.fetchAllPosts();
}
render() {
const { posts } = this.props
if (!posts){
return <div>Loading...</div>
}
return (
<Table columns={columns}
rowKey={record => record.id}
dataSource={posts}
pagination={this.state.pagination}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
)
}
}
const mapStateToProps = (state, props) => {
return { posts: state.data };
}
const mapDispatchToProps = (dispatch) => {
return{
fetchAllPosts: (data) => dispatch(addPost())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ListView)
所以这里有两个问题:
- API 和 setState 的使用
- loading: true 应该怎么使用
在 API.js 中增加 vote 功能的 api 请求代码:
/**
* POST /posts/:id
* USAGE:
* Used for voting on a post
* PARAMS:
* option - String: Either "upVote" or "downVote"
*/
export const VotePost = (postId, option, callback) => {
axios({
method: 'post',
url: `${api}/posts/${postId}`,
data: {option: option},
headers: { ...headers }
}).then(() => callback())
}
在 actions posts.js 中增加 vote
// vote 投票
export const voteAction = (postId, option) => {
return {
type: ActionType.VOTE,
postId,
option,
}
}
export const voteChange = (postId, option, callback) => {
console.log(callback)
return dispatch => {
API.VotePost(postId, option, callback).then(data => dispatch(voteAction(data)))
}
}
在 reducers posts.js 中增加 ActionType.VOTE
case ActionType.VOTE:
console.log('$$ reducer posts')
const newState = { ...state }
if(action.option==='upVote'){
newState[action.postId]['voteScore'] = ++newState[action.postId]['voteScore']
}else if(action.option==='downVote'){
newState[action.postId]['voteScore'] = --newState[action.postId]['voteScore']
}
return newState
最后在 ListView.js 中连接 reducers 和 actions
import React from 'react'
import { Icon, Table } from 'antd';
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import { addPost, voteChange } from '../actions/posts'
import * as API from '../utils/Api'
class ListView extends React.Component {
constructor(props) {
super(props)
this.state = {
pagination: {},
loading: false,
}
// this.loading = this.state.loading
}
handleTableChange = (pagination, filters, sorter) => {
const pager = { ...this.state.pagination };
pager.current = pagination.current;
this.setState({
loading: true,
pagination: pager,
});
API.fetchPost().then(res => {
const pagination = {
...this.state.pagination
}
pagination.total = res.data.length
this.setState({ loading: false, pagination})
// console.log('排序',res.data.length)
})
}
componentDidMount() {
this.props.fetchAllPosts();
// console.log(this.props.fetchAllPosts())
}
_voteForLink = async (postId, option) => {
this.props.vote(postId.id, option,()=>{
window.location.reload()
// this.props.history.push('/')
})
}
render() {
// console.log('Props', this.props)
const { posts } = this.props
if (!posts){
// this.loading = true
return <div>Loading ...</div>
}
console.log('posts', posts)
const columns = [{
title: 'Vote',
width: '5%',
dataIndex: 'index',
render: (text, record) => (
<span>
<Icon type="like-o" onClick={()=>{
const id = record.id;
this._voteForLink({id}, 'upVote')}} style={{cursor: 'pointer'}} />
<span className="ant-divider" />
<Icon type="dislike-o" onClick={()=>{
const id = record.id;
this._voteForLink({id}, 'downVote')}} style={{cursor: 'pointer'}} />
</span>
),
}, {
title: 'Score',
dataIndex: 'voteScore',
sorter: (a, b) => a.voteScore - b.voteScore,
width: '7%',
}, {
title: 'Title',
dataIndex: 'title',
width: '30%',
}, {
title: 'Date',
dataIndex: 'timestamp',
width: '10%',
sorter: (a, b) => a.timestamp - b.timestamp,
render: text => <span>{(new Date(text)).toLocaleString()}</span>,
}, {
title: 'Author',
dataIndex: 'author',
width: '10%',
}, {
title: 'Comments',
dataIndex: 'comments'
},{
title: 'Action',
key: 'action',
render: (text, record) => (
<span>
<Link to="/">Editor</Link>
<span className="ant-divider" />
<Link to="/">Delete</Link>
</span>
),
}];
return (
<Table columns={columns}
rowKey={record => record.id}
dataSource={posts}
pagination={this.state.pagination}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
)
}
}
const mapStateToProps = (state, props) => {
// console.log('state', state)
// console.log('props', props)
return { posts: state.data };
}
const mapDispatchToProps = (dispatch) => {
return{
fetchAllPosts: (data) => dispatch(addPost()),
vote: (postId, option, callback) => dispatch(voteChange(postId, option, callback)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ListView)
# File: src/containers/Layout.js
import React from 'react'
import { Layout, Menu, Breadcrumb } from 'antd';
import ListView from '../components/ListView'
const { Header, Content, Footer } = Layout;
class LayoutView extends React.Component {
render() {
return(
<Layout className="layout">
<Header>
<div className="logo" />
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={['']}
style={{ lineHeight: '64px' }}
>
<Menu.Item key="1">react</Menu.Item>
<Menu.Item key="2">redux</Menu.Item>
<Menu.Item key="3">udacity</Menu.Item>
</Menu>
</Header>
<Content style={{ padding: '0 50px' }}>
<Breadcrumb style={{ margin: '12px 0' }}>
<Breadcrumb.Item>Home</Breadcrumb.Item>
<Breadcrumb.Item>All Posts</Breadcrumb.Item>
{/* <Breadcrumb.Item></Breadcrumb.Item> */}
</Breadcrumb>
<div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
<h1>All Posts</h1>
<ListView/>
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>
Custer Tian ©2017 Created by Custer Tian
</Footer>
</Layout>
)
}
}
export default LayoutView
import React, { Component } from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom'
import Layout from './containers/Layout'
class App extends Component {
render() {
return (
<Router>
<div>
<Route exact path='/' component={Layout}/>
</div>
</Router>
);
}
}
export default App
#File: src/utils/API.js
/**
* GET /categories
* USAGE:
* Get all of the categories available for the app. List is found in categories.js.
* Feel free to extend this list as you desire.
*/
export const fetchCategories = () => {
return axios ({
method: 'get',
url: `${api}/categories`,
headers: { ...headers }
})
}
#File: src/actions/constants.js
export const ALL_CATEGORIES = 'ALL_CATEGORIES' // 获取所有分类
#File: src/actions/categories.js
// 获取所有分类
export const CategoriesAction = (categories) => {
return {
type: ActionType.ALL_CATEGORIES,
categories,
}
}
export const CategoriesFunc = () => {
return dispatch => {
API.fetchCategories().then(data => dispatch(CategoriesAction(data)))
}
}
#File: src/reducers/categories.js
import * as ActionType from '../actions/constants'
const reducer = (state=[], action) => {
switch(action.type){
case ActionType.ALL_CATEGORIES:
return action.categories
default:
return state
}
}
export default reducer
要创建一个 store, Redux 的 createStore 方法只能接受一个 reducer
我们将在更高的一个层级上创建一个 reducer
它使用 组合 来调用其他两个 reducer
#File: src/reducers/reducers.js
import { combineReducers } from 'redux'
import Categories from './categories'
import Posts from './posts'
const rootReducer = combineReducers({
categories: Categories,
posts: Posts,
})
export default rootReducer
#File: src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css'
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducers/reducers' // 修改1
import { Provider } from 'react-redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunk from 'redux-thunk'
const store = createStore(
rootReducer, // 修改2
composeWithDevTools(
applyMiddleware(thunk)
)
)
ReactDOM.render(
<Provider store={ store }>
<App />
</Provider>, document.getElementById('root'));
registerServiceWorker();
const mapStateToProps = (state, props) => {
// console.log('state', state)
// console.log('props', props)
return { posts: state.data };
}
=====>>>>
const mapStateToProps = (state, props) => {
// console.log('state', state)
// console.log('props', props)
return { posts: state.posts.data };
}
或者可以改成:
const mapStateToProps = ({posts}) => {
// console.log('state', state)
// console.log('props', props)
return { posts: posts.data };
}
# File: src/containers/Layout.js
// 展示组件
import React from 'react'
import { connect } from 'react-redux'
import { Layout, Menu, Breadcrumb } from 'antd';
import ListView from '../components/ListView'
import { CategoriesFunc } from '../actions/categories'
const { Header, Content, Footer } = Layout;
class LayoutView extends React.Component {
componentDidMount(){
this.props.fetchCategories()
}
render() {
const { categories } = this.props
// console.log(categories)
if(categories){
console.log(categories.categories)
}
return(
<Layout className="layout">
<Header>
<div className="logo" />
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={['']}
style={{ lineHeight: '64px' }}
>
<Menu.Item key="categories">Categories:</Menu.Item>
{categories
&&(categories.categories.map((item) => (
<Menu.Item key={item.name}>{item.name}</Menu.Item>
)))}
{/* <Menu.Item key="1">react</Menu.Item>
<Menu.Item key="2">redux</Menu.Item>
<Menu.Item key="3">udacity</Menu.Item> */}
</Menu>
</Header>
<Content style={{ padding: '0 50px' }}>
<Breadcrumb style={{ margin: '12px 0' }}>
<Breadcrumb.Item>Home</Breadcrumb.Item>
<Breadcrumb.Item>All Posts</Breadcrumb.Item>
{/* <Breadcrumb.Item></Breadcrumb.Item> */}
</Breadcrumb>
<div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
<h1>All Posts</h1>
<ListView/>
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>
Custer Tian ©2017 Created by Custer Tian
</Footer>
</Layout>
)
}
}
const mapStateToProps = (state,props) => {
// console.log(categories.data)
// console.log('state', state)
// console.log('props', props)
return { categories: state.categories['data'] };
}
const mapDispatchToProps = (dispatch) => {
return{
fetchCategories: (data) => dispatch(CategoriesFunc()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(LayoutView)
#File: src/utils/API.js
/**
* GET /:category/posts
* USAGE:
* Get all of the posts for a particular category
*/
export const fetchCategoryPosts = (category) => {
return axios({
method: 'get',
url: `${api}/${category}/posts`,
headers: {...headers}
})
}
#File: src/actions/constants.js
export const CATEGORY_POSTS = 'CATEGORY_POSTS' // 获取一个分类下的所有posts
#File: src/actions/categories.js
// 获取一个分类下的所有posts
export const CategoryPostsAction = (category,posts) => {
return {
type: ActionType.CATEGORY_POSTS,
category,
posts
}
}
export const CategoryPostsFunc = () => {
return dispatch => {
API.fetchCategoryPosts().then(data => dispatch(CategoryPostsAction(data)))
}
}
#File: src/reducers/posts.js
case ActionType.CATEGORY_POSTS:
return action.categoryPosts
#File: src/containers/Layout.js
....
<div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
<Button type="primary" onClick={this.showModal.bind(this)} icon="plus" style={{float: 'right'}} ghost>Add Post</Button>
{this.props.visible?<AddPost/>:<p></p>}
{this.state.curCategory?<h1>{this.state.curCategory}</h1>:<h1>All Posts</h1>}
<ListView posts={this.props.posts}/>
</div>
.....
确定使用这个 异步关闭 - 点击确定后异步关闭对话框,例如提交表单。
import { Modal, Button } from 'antd';
class App extends React.Component {
state = {
ModalText: 'Content of the modal',
visible: false,
confirmLoading: false,
}
showModal = () => {
this.setState({
visible: true,
});
}
handleOk = () => {
this.setState({
ModalText: 'The modal will be closed after two seconds',
confirmLoading: true,
});
setTimeout(() => {
this.setState({
visible: false,
confirmLoading: false,
});
}, 2000);
}
handleCancel = () => {
console.log('Clicked cancel button');
this.setState({
visible: false,
});
}
render() {
const { visible, confirmLoading, ModalText } = this.state;
return (
<div>
<Button type="primary" onClick={this.showModal}>Open</Button>
<Modal title="Title"
visible={visible}
onOk={this.handleOk}
confirmLoading={confirmLoading}
onCancel={this.handleCancel}
>
<p>{ModalText}</p>
</Modal>
</div>
);
}
}
ReactDOM.render(<App />, mountNode);
新建一个 AddPost.js 文件来展示 这个对话框
#File: src/containers/AddPost.js
import React from 'react'
import { connect } from 'react-redux'
import { Modal } from 'antd';
import { closeNewPostModal } from '../actions/modalvisible'
class AddPost extends React.Component {
state = {
ModalText: 'Content of the modal',
confirmLoading: false,
}
handleOk = () => {
this.setState({
ModalText: 'The modal will be closed after two seconds',
confirmLoading: true,
});
setTimeout(() => {
this.setState({
confirmLoading: false,
});
this.props.closeModal()
}, 2000);
}
handleCancel = () => {
console.log('Clicked cancel button');
this.props.closeModal()
}
render() {
const { confirmLoading, ModalText } = this.state;
return (
<div>
<Modal title="Title"
visible={this.props.visible}
onOk={this.handleOk}
confirmLoading={confirmLoading}
onCancel={this.handleCancel}
>
<p>{ModalText}</p>
</Modal>
</div>
);
}
}
const mapStateToProps = (state,props) => {
// console.log('state', state.modalvisible.newPostModalVisible)
return{ visible: state.modalvisible.newPostModalVisible }
}
const mapDispatchToProps = (dispatch) => {
return { closeModal: (data) => dispatch(closeNewPostModal()) }
}
export default AddPost
先增加 actions 常量 constants.js
#File: src/actions/constants.js
export const SHOW_NEW_POST_MODAL = 'SHOW_NEW_POST_MODAL' // 显示Post Modal
export const CLOSE_NEW_POST_MODAL = 'CLOSE_NEW_POST_MODAL' // 关闭Post Modal
再增加 actions creator 代码
#File: src/actions/modalvisible.js
import * as ActionType from './constants'
export function newPost() {
return {
type: ActionType.SHOW_NEW_POST_MODAL
}
}
export function closeNewPostModal() {
return {
type: ActionType.CLOSE_NEW_POST_MODAL
}
}
newPost() 打开Modal
closeNewPostModal() 关闭Modal
#File: src/reducers/modalvisible.js
import * as ActionType from '../actions/constants'
const initialState = {
newPostModalVisible: false,
}
const modalVisibleReducer = (state=initialState, action) => {
switch (action.type) {
case ActionType.SHOW_NEW_POST_MODAL:
return Object.assign({}, state, { newPostModalVisible: true })
case ActionType.CLOSE_NEW_POST_MODAL:
return Object.assign({}, state, { newPostModalVisible: false })
default:
return state
}
}
export default modalVisibleReducer
再修改 reducers.js 文件
#File: src/reducers/reducers.js
import { combineReducers } from 'redux'
import Categories from './categories'
import Posts from './posts'
import modalVisibleReducer from './modalvisible'
const rootReducer = combineReducers({
categories: Categories,
posts: Posts,
modalvisible: modalVisibleReducer,
})
export default rootReducer
首先 增加 mapStateToProps
visible: state.modalvisible.newPostModalVisible,
和 mapDispatchToProps
newPostModal: (data) => dispatch(newPost()),
const mapStateToProps = (state, props) => {
// console.log(categories.data)
// console.log('state', state.modalvisible.newPostModalVisible)
// console.log('props', props)
return {
posts: state.posts.data,
categories: state.categories['data'],
categoryPosts: state.categoryPosts,
visible: state.modalvisible.newPostModalVisible,
};
}
const mapDispatchToProps = (dispatch) => {
return{
fetchAllPosts: (data) => dispatch(addPost()),
fetchCategories: (data) => dispatch(CategoriesFunc()),
fetchCategoryPosts: (category) => dispatch(CategoryPostsFunc(category)),
newPostModal: (data) => dispatch(newPost()),
}
}
在组件中使用 visible 和 newPost() 函数
showModal(e) {
this.props.newPostModal()
}
·····
<Button type="primary" onClick={this.showModal.bind(this)} icon="plus" style={{float: 'right'}} ghost>Add Post</Button>
{this.props.visible?<AddPost/>:<p></p>}
效果如下:
可以看到 newPostModal: (data) => dispatch(newPost()),
控制 Modal 的打开
this.props.newPostModal()
showModal = (e) => {
this.props.newPostModal()
}
·····
<Button type="primary" onClick={this.showModal(this)} icon="plus" style={{float: 'right'}} ghost>Add Post</Button>
{this.props.visible?<AddPost/>:<p></p>}
不知道为什么 会自动调用 this.props.newPostModal(), 总是自动打开 Modal
handleCancel = () => {
console.log('Clicked cancel button');
this.props.closeModal()
}
·····
const mapStateToProps = (state,props) => {
// console.log('state', state.modalvisible.newPostModalVisible)
return{ visible: state.modalvisible.newPostModalVisible }
}
const mapDispatchToProps = (dispatch) => {
return { closeModal: (data) => dispatch(closeNewPostModal()) }
}
观察 Layout.js 和 AddPost.js 中的 visible state
在不同组件中使用同一个 state 状态控制 Modal 可见还是隐藏
当用户访问一个展示了某个列表的页面,想新建一项但又不想跳转页面时,可以用 Modal 弹出一个表单,用户填写必要信息后创建新的项。
通过使用 onFieldsChange 与 mapPropsToFields,可以把表单的数据存储到上层组件或者 Redux、dva 中,更多可参考 rc-form 示例。
import React from 'react'
import {connect} from 'react-redux'
import {Modal, Form, Input, Select} from 'antd';
import {closeNewPostModal} from '../actions/modalvisible'
const Option = Select.Option;
const FormItem = Form.Item;
// 展示组件
const NewPostForm = Form.create()((props) => {
const {categories, visible, onCancel, onCreate, form, confirmLoading} = props;
const {getFieldDecorator} = form;
return (
<Modal
visible={visible}
title="Create a new post"
kText="Create"
onCancel={onCancel}
onOk={onCreate}
confirmLoading={confirmLoading}>
<Form layout="vertical">
<FormItem label="Title For Post">
{getFieldDecorator('title', {
rules: [
{
required: true,
message: 'Please input the title of post!'
}
]
})(<Input/>)}
</FormItem>
<FormItem label="Post Content">
{getFieldDecorator('body', {
rules: [
{
required: true,
message: 'Please input the Post Body!'
}
]
})(<Input type="textarea"/>)}
</FormItem>
<FormItem label="Author">
{getFieldDecorator('author', {
rules: [
{
required: true,
message: 'Please input the Author!'
}
]
})(<Input/>)}
</FormItem>
<FormItem label="Category" hasFeedback>
{getFieldDecorator('select', {
rules: [
{
required: true,
message: 'Please select your post category!'
}
]
})(
<Select placeholder="Please select a category">
{categories
&&(categories.categories.map((item) => (
<Option key={item.name} value={item.name}>{item.name}</Option>
)))}
</Select>
)}
</FormItem>
</Form>
</Modal>
);
});
class AddPost extends React.Component {
state = {
confirmLoading: false
}
handleOk = () => {
this.setState({confirmLoading: true});
setTimeout(() => {
this.setState({confirmLoading: false});
this.props.closeModal()
}, 2000);
}
handleCancel = () => {
console.log('Clicked cancel button');
this.props.closeModal()
}
saveFormRef = (form) => {
this.form = form;
}
render() {
const { categories, visible } = this.props
const {confirmLoading} = this.state
return (
<div>
<NewPostForm
ref={this.saveFormRef}
categories={categories}
visible={visible}
onCancel={this.handleCancel}
onCreate={this.handleOk}
confirmLoading={confirmLoading}/>
</div>
);
}
}
const mapStateToProps = (state, props) => {
// console.log('state', state.categories['data'].categories)
return {
categories: state.categories['data'],
visible: state.modalvisible.newPostModalVisible,
}
}
const mapDispatchToProps = (dispatch) => {
return {
closeModal: (data) => dispatch(closeNewPostModal())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AddPost)
#File: src/actions/constants.js
export const ADD_NEW_POST = 'ADD_NEW_POST' // 增加新的 post
#File: src/actions/posts.js
// 创建新的post
export const newPost = (newPost) => {
return {
type: ActionType.ADD_NEW_POST,
newPost
}
}
export const addNewPost = (value, callback) => {
return dispatch => {
API.createPost(value, callback).then(data=>dispatch(newPost(data)))
}
}
#File: src/reducers/posts.js
case ActionType.ADD_NEW_POST:
return Object.assign({}, state, action.newPost)
handleOk = () => {
const form = this.form;
form.validateFields((err, values) => {
if (err) {
return;
}
values.id = Math.random().toString(20)
values.timestamp = Date.now()
this.props.addNewPost(values,()=>{
console.log('Received values of form: ', values);
form.resetFields();
// this.setState({ visible: false });
this.setState({confirmLoading: true});
this.props.closeModal()
setTimeout(() => {
// this.setState({confirmLoading: false});
// this.props.closeModal()
window.location.reload()
}, 2000);
})
});
}
#File: readable/src/utils/Api.js
export const DeletePost = (postId, callback) => {
+ const request = axios ({
+ method: 'delete',
+ url: `${api}/posts/${postId}`,
+ headers: {...headers},
+ }).then(()=>callback())
+ return request
+}
#File: readable/src/actions/constants.js
export const DELETE_POST = 'DELETE_POST' // 删除post
#File: readable/src/actions/posts.js
//删除post
+export const deletePost = (postId) => {
+ return {
+ type: ActionType.DELETE_POST,
+ postId,
+ }
+}
+export const deletePostAction = (postId, callback) => {
+ console.log('deletePostAction')
+ return dispatch => {
+ API.DeletePost(postId, callback).then(data=>dispatch(deletePost(data)))
+ }
+}
#File: readable/src/reducers/posts.js
switch (action.type) {
case ActionType.ALL_POSTS:
+ // console.log('$$ all posts', action.posts)
// return {
// ...state, // 对象扩展语法,与之前的状态相同
// data, // 修改状态
// }
- return action.posts
+ // return action.posts
+ return action.posts.data.filter(post=>post.deleted===false)
case ActionType.ADD_NEW_POST:
return Object.assign({}, state, action.newPost)
+ case ActionType.DELETE_POST:
+ // console.log('$$ delete posts', state)
+ return Object.assign({}, state, {deleted: true})
+ // return state.data.filter((post) => {
+ // return post.id !== action.postId
+ // })
+
case ActionType.VOTE:
console.log('$$ reducer posts')
const newState = { ...state }
File: readable/src/components/ListView.js
#File: readable/src/containers/PostDetail.js
import React from 'react'
class PostDetail extends React.Component {
render() {
return(
<div> PostDetail </div>
)
}
}
export default PostDetail
#File: readable/src/App.js
import React, { Component } from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom'
import Layout from './containers/Layout'
import PostDetail from './containers/PostDetail'
class App extends Component {
render() {
return (
<Router>
<div>
<Route exact path='/' component={Layout}/>
<Route exact path='/:category/:post_id' component={PostDetail}/>
</div>
</Router>
);
}
}
export default App
#File: readable/src/components/ListView.js
{
title: 'Title',
dataIndex: 'title',
width: '25%',
render: (text, record) => (<Link to={`/${record.category}/${record.id}`}>{text}</Link>),
},
ListView.js 代码修改 现在效果图为:
点击 title 链接 进入 详情页面 如下图:
详情页面的展示 以 卡片的形式
#File: readable/src/containers/PostDetail.js
import React from 'react'
import { Link } from 'react-router-dom'
import {Card} from 'antd'
import {Layout, Menu, Breadcrumb} from 'antd'
const {Header, Content, Footer} = Layout
class PostDetail extends React.Component {
render() {
return (
<Layout className="layout">
<Header>
<div className="logo"/>
<Menu theme="dark" mode="horizontal" defaultSelectedKeys={['2']} style={{
lineHeight: '64px', fontSize: '20px'
}}>
<Menu.Item key="1">Readable[Detail]</Menu.Item>
</Menu>
</Header>
<Content style={{
padding: '0 50px'
}}>
<Breadcrumb style={{
margin: '12px 0'
}}>
<Breadcrumb.Item><Link to='/'>Home</Link></Breadcrumb.Item>
<Breadcrumb.Item><Link to='/'>Posts</Link></Breadcrumb.Item>
<Breadcrumb.Item>detail</Breadcrumb.Item>
</Breadcrumb>
<div style={{
background: '#fff',
padding: 24,
minHeight: 280
}}>
<Card style={{
width: 300
}}>
<p>Card content</p>
<p>Card content</p>
<p>Card content</p>
</Card>
</div>
</Content>
<Footer style={{
textAlign: 'center'
}}>
Custer Tian ©2017 Created by Custer Tian
</Footer>
</Layout>
)
}
}
export default PostDetail
Title: Udacity is the best place to learn React
Body: Everyone says so after all.
timestamp: 2016/6/29 上午10:21:12
Vote score: 6
Author: thingtwo
comments number: 2
没有实现字数校验框
这里在详细信息下面简单实现一个评论框:
<div style={{
margin: '24px 0'
}}>
<TextArea rows={6} placeholder="请输入评论..." size="large"/>
<Button type="primary" ghost style={{
margin: '24px 0',
float: 'right'
}}>comment</Button>
</div>
在评论框下面放留言信息,使用带连接线的树形控件
<Tree showLine defaultExpandedKeys={['0-0-0']} onSelect={this.onSelect}>
<TreeNode title="parent 1" key="0-0">
<TreeNode title="parent 1-0" key="0-0-0">
<TreeNode title="leaf" key="0-0-0-0"/>
<TreeNode title="leaf" key="0-0-0-1"/>
<TreeNode title="leaf" key="0-0-0-2"/>
</TreeNode>
<TreeNode title="parent 1-1" key="0-0-1">
<TreeNode title="leaf" key="0-0-1-0"/>
</TreeNode>
<TreeNode title="parent 1-2" key="0-0-2">
<TreeNode title="leaf" key="0-0-2-0"/>
<TreeNode title="leaf" key="0-0-2-1"/>
</TreeNode>
</TreeNode>
</Tree>
这里的简单实现: