用生命谱写代码的赞歌

0%

React 开发简易查询豆瓣电影 App

React 开发简易 App

结合 React 技术开发简易的查询豆瓣电影 App ,开发过程中使用 Node.js 搭建代理服务器,跨域请求豆瓣电影的一些 API 接口。

应用详细代码访问我的 github 地址

路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default class AppContainer extends React.Component {
render() {
return (
<div>
<h1>App</h1>
<ul>
<li><Link to="/about">About</Link></li>
<li><Link to="/inbox">Inbox</Link></li>
</ul>
{/* 路由匹配的内容 */}
{this.props.children}
</div>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default class Routers extends React.Component {
render() {
return (
<Router history={hashHistory}>
<Route path="/" component={App}>
<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
<Route path="messages/:id" component={Message} />
</Route>
</Route>
</Router>
)
}
}
ReactDOM.render(
<Routers/>,
document.getElementById('app')
)

添加首页

当 URL 为 / 时,我们想渲染一个在 App 中的组件。不过在此时,App 的 render 中的 this.props.children 还是 undefined。这种情况我们可以使用 IndexRoute 来设置一个默认页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default class Routers extends React.Component {
render() {
return (
<Router history={hashHistory}>
<Route path="/" component={App}>
{/* 当 url 为/时渲染 Dashboard */}
<IndexRoute component={Dashboard} />
<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
{/* 使用 /messages/:id 替换 messages/:id */}
<Route path="/messages/:id" component={Message} />
{/* 跳转 /inbox/messages/:id 到 /messages/:id */}
<Redirect from="messages/:id" to="/messages/:id" />
</Route>
</Route>
</Router>
)
}
}

进入和离开的hook

1
2
3
4
5
6
7
<Route
path="about"
component={Inbox}
onEnter={() => console.log('进入了Inbox路由')}
onLeave={() => console.log('离开了Inbox路由')}
>
</Route>

路由匹配原理

路径语法

路由路径是匹配一个(或一部分)URL 的 一个字符串模式。大部分的路由路径都可以直接按照字面量理解,除了以下几个特殊的符号:

  • :paramName – 匹配一段位于 /、? 或 # 之后的 URL。 命中的部分将被作为一个参数
  • () – 在它内部的内容被认为是可选的
    • – 匹配任意字符(非贪婪的)直到命中下一个字符或者整个 URL 的末尾,并创建一个 splat 参数
1
2
3
<Route path="/hello/:name">         // 匹配 /hello/michael 和 /hello/ryan
<Route path="/hello(/:name)"> // 匹配 /hello, /hello/michael 和 /hello/ryan
<Route path="/files/*.*"> // 匹配 /files/hello.jpg 和 /files/path/to/hello.jpg

History

  • browserHistory
    • 去掉 URL 地址中的#标记
    • 刷新后无法找到原网页
  • hashHistory
    • 兼容低版本的 IE6/7
    • 可直接刷新页面
  • createMemoryHistory
    • 服务器渲染

如果你在这个 app 中使用 <Link to="/">Home</Link> , 它会一直处于激活状态,因为所有的 URL 的开头都是 / 。 这确实是个问题,因为我们仅仅希望在 Home 被渲染后,激活并链接到它。

如果需要在 Home 路由被渲染后才激活的指向 / 的链接,请使用 <IndexLink to="/">Home</IndexLink>

组件内部跳转

1
2
3
4
5
6
7
8
9
10
static contextTypes = {
router: PropTypes.object
}
componentDidMount() {
// 在组件内利用context特性使用代码跳转
setTimeout(() => {
this.context.router.push('/home')
}, 3000)
// service.getMovieListData()
}

组件外部跳转

虽然在组件内部可以使用 this.context.router 来实现导航,但许多应用想要在组件外部使用导航。

1
2
3
4
5
6
7
8
9
import {hashHistory} from 'react-router'
// 在组件外使用hashHistory跳转路由
export default {
getMovieListData() {
setTimeout(function() {
hashHistory.push('/home')
}, 3000)
}
}

在 React 中使用 CSS

直接在组件中引入对应的css, 每个组件根标签设置一个独立的class类名

等待效果切换

使用this.state = {}, 通过修改state的值来切换等待效果

通过代理服务器请求数据

自己的网页访问自己的代理服务器也存在跨域问题, 只要浏览器与服务器端口号和 ip 不一致就会存在跨域问题

  • fetch
    • 语法简洁, 更加语义化
    • 未来取代XMLHttpRequest对象
    • 基于标准Promise实现, 支持async/await
    • 同构方便, 使用isomorphic-fetch, 在浏览器及Node中均能使用

Promise 实现

Promise/A+规范解决异步回调深层嵌套问题
Promise/A+规范本质是一种书写格式的改变

  • Angular: $q 服务
  • Node: q 模块, co, then
  • ES6: Promise, yield
  • ES7: async await

promise有三种状态

  • Unfulfilled(Pending未完成, 初始状态)
  • Fulfilled(Resloved已完成)
  • Failed(Rejected失败, 拒绝)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 生成Promise实例
var promise = new Promise((resolve, reject) => {
fetch(url).then(response => {
if (response.ok) {
return response.json()
} else {
console.error(`服务器忙,请稍后再试;\r\nCode:${response.status}`)
}
}).then(data => {
resolve(data) // 操作成功
}).catch(err => {
reject(err) // 操作失败
})
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});

// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});

promise 对象使用时必须通过 then 方法调用改变状态, 上一个 then 里面的输入值就是下一个 then 里面的输出值

一般来说,不要在 then 方法里面定义 Reject 状态的回调函数(即 then 的第二个参数),总是使用 catch 方法

如果 then 的第二个参数存在, 发生错误走 then 的第二个参数, 否则走 catch

渲染电影列表页面

循环渲染子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 循环渲染电影列表数据, 使用ref属性获取DIV元素
renderMovieList = () => {
return (
<div ref={div => (this.theDiv = div)} className="movieList_container">
{this.state.movieListData.map(this.renderItem)}
<div className={this.state.isBottom ? 'movieList_show' : 'movieList_hide'}>正在玩命加载中, 请稍候......</div>
</div>
)
}
// 电影列表的每个项
renderItem = (item) => {
return (
<div className="movieList_item" key={item.id} onClick={() => this.goDetail(item.id)}>
<img src={item.images.small} alt=""/>
<div>
<h1>{item.title}</h1>
<span>{item.year}</span>
</div>
</div>
)
}

通过 fetch 请求数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 请求数据的方法
fetch = (movieType) => {
// 如果电影类型发生改变, 重新设置state
if (this.state.messages.movieType !== movieType) {
console.log(6)
// 电影类型改变后, 重新设置movieList_container的滚轮高度, 并清空绑定事件
this.theDiv.scrollTop = 0
this.theDiv.onscroll = null
this.setState({
// 是否显示加载效果
isLoading: true,
// 是否到底了
isBottom: false,
// 电影列表数据
movieListData: [],
// 向服务器发送的数据都写在message对象中
messages: {
movieType: movieType,
count: 10,
start: 0,
pageIndex: 1
}
})
return
}
console.log(7)
// ES6深拷贝对象
let messages = Object.assign({}, this.state.messages)
let movieListData = [].concat(this.state.movieListData)
// 修改分页信息
messages.movieType = movieType
messages.start = (messages.pageIndex - 1) * messages.count
messages.pageIndex++

// 将messages对象转化json字符串格式方便传递参数
const message = JSON.stringify(messages)
// 接收service发送的fetch请求返回的promise对象
const promise = service.getMovieListData(message)
// 通过promise.then获取resolve的数据
promise.then(
data => {
console.log(data)
// 如果movieListData中已经有数据, 则进一步追加数据
if (movieListData.length > 0) {
movieListData = movieListData.concat(data.subjects)
} else {
movieListData = data.subjects
}
// 接收到返回的数据后, 修改state状态
this.setState({
isLoading: false,
isBottom: false,
movieListData: movieListData,
messages: messages
})
}
).catch(err => {
console.error(err)
})
}

添加 CSS 样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.movieList_container {
height: 100%;
overflow-y: scroll;
}

.movieList_item {
display: -webkit-flex;
display: -moz-flex;
display: -ms-flex;
display: -o-flex;
display: flex;
height: 10rem;
}

.movieList_item img {
width: 5rem;
height: 9rem;
}

.movieList_item div {
display: flex;
flex-direction: column;
justify-content: space-evenly;
}

.movieList_hide {
display: none;
}

.movieList_show {
background-color: lawngreen;
text-align: center;
}

请求接口改造和对象深拷贝

请求接口改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import config from '../js/config.js'

export default {
// 获取电影列表数据
getMovieListData(message) {
// console.log(7)
console.log(message)
// 返回一个Promise对象
return new Promise((resolve, reject) => {
// 如果有多个参数, message是对象字符串, 否则message就是参数本身
const url = `${config.HTTP}${config.SERVER_PATH}:${config.PORT}/api/getMovieListData?message=${message}`
console.log(url)
/*console.log(8)
setTimeout(() => {
console.log(9)
reject(new Error('something bad happened'))
}, 2000)
console.log(10)*/
fetch(url)
.then(response => {
if (response.ok) {
return response.json()
} else {
console.error(`服务器忙,请稍后再试;\r\nCode:${response.status}`)
}
})
.then(data => {
console.log(data)
resolve(data)
})
.catch(err => {
reject(err)
})
})
}
}

对象深拷贝

1
2
3
// ES6深拷贝对象
let messages = Object.assign({}, this.state.messages)
let movieListData = [].concat(this.state.movieListData)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// js对象简易深拷贝
var obj = {
name: 'tom',
age: 18,
data: {
sex: '男',
dog: 'jim'
}
}

var str = JSON.stringify(obj)
var obj1 = JSON.parse(str)

console.log(obj1)
console.log(obj === obj1)
console.log(obj.data === obj1.data)

电影类别切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
componentWillReceiveProps(nextProps) {
console.log(2)
this.fetch(nextProps.params.movieType)
}
componentDidUpdate(prevProps, prevState) {
// 根据isLoading判断movieType类型是否发生改变, 然后重新发送url请求
if (this.state.isLoading) {
console.log(3)
this.fetch(this.state.messages.movieType)
} else {
console.log(4)
// 将电影列表绑定的onscroll事件清空, 以便再次绑定, 否则会重复绑定
this.theDiv.onscroll = null
this.addEventListener()
}
}
1
2
3
4
5
6
7
8
9
10
11
changeMovieType = (movieType) => {
this.setState({
movieType: movieType
})
}

<div className="movie_menu">
<Link onClick={() => this.changeMovieType('in_theaters')} className={this.state.movieType === 'in_theaters' ? 'movie_current' : ''} to="/movieList/in_theaters">正在热映</Link>
<Link onClick={() => this.changeMovieType('coming_soon')} className={this.state.movieType === 'coming_soon' ? 'movie_current' : ''} to="/movieList/coming_soon">即将上映</Link>
<Link onClick={() => this.changeMovieType('top250')} className={this.state.movieType === 'top250' ? 'movie_current' : ''} to="/movieList/top250">Top250</Link>
</div>

不可变数据结构

Immutable 与 SImmutable

页面下滑加载更多数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 为滚动的容器添加滚动监听事件
addEventListener = () => {
this.theDiv.onscroll = e => {
if (e.target.scrollHeight === e.target.scrollTop + e.target.offsetHeight) {
console.log(5)
if (this.state.isBottom) {
return
}
this.fetch(this.state.messages.movieType)
this.setState({
isBottom: true
})
}
}
}

用户加载数据提示, 并且防止多次触发滚动事件

下滑加载数据提示

1
2
3
4
5
6
7
8
9
// 循环渲染电影列表数据, 新添加一个div标签
renderMovieList = () => {
return (
<div ref={div => (this.theDiv = div)} className="movieList_container">
{this.state.movieListData.map(this.renderItem)}
<div className={this.state.isBottom ? 'movieList_show' : 'movieList_hide'}>正在玩命加载中, 请稍候......</div>
</div>
)
}

防止多次触发滚动事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 为滚动的容器添加滚动监听事件, 判断是否到底了
addEventListener = () => {
this.theDiv.onscroll = e => {
if (e.target.scrollHeight === e.target.scrollTop + e.target.offsetHeight) {
console.log(5)
// 如果到底了, 直接返回, 防止手贱多次触发滚动事件
if (this.state.isBottom) {
return
}
this.fetch(this.state.messages.movieType)
this.setState({
isBottom: true
})
}
}
}

切换电影类别时重置数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 请求数据的方法
fetch = (movieType) => {
// 如果电影类型发生改变, 重新设置state
if (this.state.messages.movieType !== movieType) {
console.log(6)
// 电影类型改变后, 重新设置movieList_container的滚轮高度, 并清空绑定事件
this.theDiv.scrollTop = 0
this.theDiv.onscroll = null
this.setState({
// 是否显示加载效果
isLoading: true,
// 是否到底了
isBottom: false,
// 电影列表数据
movieListData: [],
// 向服务器发送的数据都写在message对象中
messages: {
movieType: movieType,
count: 10,
start: 0,
pageIndex: 1
}
})
return
}
...
...
...
}

电影详细页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import React from 'react'
import PropTypes from 'prop-types'
import Loading from '../components/Loading.jsx'
import service from '../services/movieListData.js'

import '../styles/movieDetail.css'

export default class MovieDetailContainer extends React.Component {
constructor(props) {
super(props)
this.state = {
// 是否显示加载效果
isLoading: true,
// 电影详细数据
movieDetailData: []
}
}
static propTypes = {
params: PropTypes.object
}
// static contextTypes = {
// router: PropTypes.object
// }
componentDidMount() {
// 在组件内利用context特性使用代码跳转
// setTimeout(() => {
// this.context.router.push('/home')
// }, 3000)
// service.getMovieListData()
this.fetch(this.props.params.id)
}
// 请求数据的方法
fetch = (id) => {
const promise = service.getMovieDetailData(id)
promise.then(
data => {
console.log(data)
this.setState({
isLoading: false,
movieDetailData: data,
})
}
).catch(err => {
console.error(err)
})
}
// 渲染等待效果
renderLoading = () => {
return (
<div className="movieDetail_container">
<Loading/>
</div>
)
}
// 渲染电影详细数据
renderMovieDetail = () => {
return (
<div className="movieDetail_container">
<div className="movieDetail_image">
<img src={this.state.movieDetailData.images.large} alt=""/>
</div>
<h1>{this.state.movieDetailData.title}</h1>
<p>{this.state.movieDetailData.year}</p>
<p>{this.state.movieDetailData.summary}</p>
</div>
)
}
render() {
if (this.state.isLoading) {
return this.renderLoading()
}
return this.renderMovieDetail()
}
}

电影搜索页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import React from 'react'
import PropTypes from 'prop-types'
import Loading from '../components/Loading.jsx'
import service from '../services/movieListData.js'

import '../styles/movieList.css'

export default class MovieSearchContainer extends React.Component {
constructor(props) {
super(props)
this.state = {
// 是否显示加载效果
isLoading: true,
// 是否到底了
isBottom: false,
// 电影列表数据
movieListData: [],
// 向服务器发送的数据都写在message对象中
messages: {
keyword: this.props.params.keyword,
count: 10,
start: 0,
pageIndex: 1
}
}
}
static propTypes = {
params: PropTypes.object
}
static contextTypes = {
router: PropTypes.object
}
componentDidMount() {
this.fetch(this.state.messages.keyword)
}
componentWillReceiveProps(nextProps) {
this.fetch(nextProps.params.keyword)
}
componentDidUpdate(prevProps, prevState) {
if (this.state.isLoading) {
this.fetch(this.state.messages.keyword)
} else {
this.theDiv.onscroll = null
this.addEventListener()
}
}
// 为滚动的容器添加滚动监听事件
addEventListener = () => {
this.theDiv.onscroll = e => {
if (e.target.scrollHeight === e.target.scrollTop + e.target.offsetHeight) {
if (this.state.isBottom) {
return
}
this.fetch(this.state.messages.keyword)
this.setState({
isBottom: true
})
}
}
}
// 请求数据的方法
fetch = (keyword) => {
// 判断搜索的电影是否存在
if (this.state.messages.keyword !== keyword) {
this.theDiv.scrollTop = 0
this.theDiv.onscroll = null
this.setState({
// 是否显示加载效果
isLoading: true,
// 是否到底了
isBottom: false,
// 电影列表数据
movieListData: [],
// 向服务器发送的数据都写在message对象中
messages: {
keyword: keyword,
count: 10,
start: 0,
pageIndex: 1
}
})
return
}
console.log(7)
// ES6深拷贝对象
let messages = Object.assign({}, this.state.messages)
let movieListData = [].concat(this.state.movieListData)
// 修改分页信息
messages.keyword = keyword
messages.start = (messages.pageIndex - 1) * messages.count
messages.pageIndex++

// 将messages对象转化json字符串格式方便传递参数
const message = JSON.stringify(messages)
const promise = service.searchMovieListData(message)
promise.then(
data => {
console.log(data)
if (movieListData.length > 0) {
movieListData = movieListData.concat(data.subjects)
} else {
movieListData = data.subjects
}
this.setState({
isLoading: false,
isBottom: false,
movieListData: movieListData,
messages: messages
})
}
).catch(err => {
console.error(err)
})
}
// 跳转到电影详细页面
goDetail = (id) => {
this.context.router.push(`/movieDetail/${id}`)
}
// 渲染loading效果
renderLoading = () => {
return (
<div className="movieList_container">
<Loading/>
</div>
)
}
// 渲染电影列表每一项, key值与MovieList页面重复报错, 所有添加Math.random()
renderItem = (item) => {
return (
<div className="movieList_item" key={item.id + Math.random()} onClick={() => this.goDetail(item.id)}>
<img src={item.images.small} alt=""/>
<div>
<h1>{item.title}</h1>
<span>{item.year}</span>
</div>
</div>
)
}
// 循环渲染电影列表数据
renderMovieList = () => {
return (
<div ref={div => (this.theDiv = div)} className="movieList_container">
{this.state.movieListData.map(this.renderItem)}
<div className={this.state.isBottom ? 'movieList_show' : 'movieList_hide'}>正在玩命加载中, 请稍候......</div>
</div>
)
}
render() {
if (this.state.isLoading) {
return this.renderLoading()
}
return this.renderMovieList()
}
}

封装 Loading 通用组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react'
import Loading from 'react-loading'

import './Loading.css'

export default class ComponentName extends React.Component {
constructor(props) {
super(props)
this.state = {

}
}
render() {
return (
<div className="loading_component">
<Loading className="loading_loading" type="spin" color="red"/>
</div>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
.loading_component {
height: 100%;
/* width: 100%; */
display: flex;
justify-content: center;
align-items: center;
}

.loading_loading {
height: 100px!important;
width: 100px!important;
}

结合 antd 实现表单提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import React from 'react'
import PropTypes from 'prop-types'
import { message as Message, Form, Input, Tooltip, Icon, Select, Button } from 'antd'
import service from '../services/aboutService.js'

import '../styles/about.scss'
import 'antd/dist/antd.css'

const FormItem = Form.Item
const Option = Select.Option

class AboutContainer extends React.Component {
constructor(props) {
super(props)
this.state = {

}
}
static propTypes = {
form: PropTypes.object
}
handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFieldsAndScroll((err, values) => {
if (err) {
console.error(err)
return
}
this.sendFeedback(values)
// if (!err) {
// console.log('Received values of form: ', values)
// }
})
}
// 重置反馈表单
handleReset = () => {
this.props.form.resetFields()
}
// 发送反馈意见
sendFeedback = (message) => {
const messageObj = JSON.stringify(message)
const promise = service.sendFeedback(messageObj)
promise.then(
data => {
if (data.status === 'OK') {
this.handleReset()
Message.success('您的意见反馈我们已经收到, 请耐心等待问题的解决!')
}
}
).catch(
err => {
console.error(err)
}
)
}
render() {
const { getFieldDecorator } = this.props.form
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 14 },
},
}
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 24,
offset: 0,
},
sm: {
span: 14,
offset: 6,
},
},
}
const prefixSelector = getFieldDecorator('prefix', {
initialValue: '86',
})(
<Select style={{ width: 60 }}>
<Option value="86">+86</Option>
<Option value="87">+87</Option>
</Select>
)
return (
<Form onSubmit={this.handleSubmit}>
<FormItem
{...formItemLayout}
label={(
<span>
反馈信息&nbsp;
<Tooltip title="What do you want other to call you?">
<Icon type="question-circle-o" />
</Tooltip>
</span>
)}
hasFeedback
>
{getFieldDecorator('feedback', {
rules: [{ required: true, message: 'Please input your feedback!', whitespace: true }],
})(
<Input />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="联系电话"
>
{getFieldDecorator('phone', {
rules: [{ pattern: /^[0-9]{11}$/, required: true, message: 'Please input your phone number!' }],
})(
<Input addonBefore={prefixSelector} style={{ width: '100%' }} />
)}
</FormItem>
<FormItem
{...formItemLayout}
label={(
<span>
联系姓名&nbsp;
<Tooltip title="What do you want other to call you?">
<Icon type="question-circle-o" />
</Tooltip>
</span>
)}
hasFeedback
>
{getFieldDecorator('name', {
rules: [{ required: true, message: 'Please input your name!', whitespace: true }],
})(
<Input />
)}
</FormItem>
<FormItem {...tailFormItemLayout}>
<Button type="primary" htmlType="submit">Register</Button>
</FormItem>
</Form>
)
}
}

// antd的Form标准用法
export default Form.create()(AboutContainer)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import config from '../js/config.js'

export default {
// fetch的采用post提交发送反馈意见
sendFeedback(message) {
return new Promise((resolve, reject) => {
console.log(message)
const url = `${config.HTTP}${config.SERVER_PATH}:${config.PORT}/api/sendFeedback`
console.log(url)
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `message=${message}`
})
.then(response => { // promise第一步返回response
if (response.ok) {
return response.json()
} else {
console.error(`服务器忙,请稍后再试;\r\nCode:${response.status}`)
}
})
.then(data => { // 第二步通过resolve传递数据到AboutContainer组件中
console.log(data)
resolve(data)
})
.catch(err => {
reject(err)
})
})
}
}

代码异步加载与部署

修改路由配置文件, 使用 getComponent 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import React from 'react'
import {Router, Route, Redirect, browserHistory, IndexRoute} from 'react-router'

import AppContainer from '../containers/AppContainer.jsx'
import HomeContainer from '../containers/HomeContainer.jsx'
// 以下组件采用动态按需加载 getComponent
// import MovieContainer from '../containers/MovieContainer.jsx'
// import AboutContainer from '../containers/AboutContainer.jsx'
// import MovieListContainer from '../containers/MovieListContainer.jsx'
// import MovieDetailContainer from '../containers/MovieDetailContainer.jsx'
// import MovieSearchContainer from '../containers/MovieSearchContainer.jsx'

// 路由配置信息IndexRoute表示首次渲染的组件
export default class Routers extends React.Component {
constructor(props) {
super(props)
this.state = {

}
}
render() {
return (
<Router history={browserHistory}>
<Route path='/' component={AppContainer}>
<IndexRoute component={HomeContainer}/>
<Route path='home' component={HomeContainer}/>
<Route
path='movie'
getComponent={
(nextState, callback) => {
require.ensure([], require => {
callback(null, require('../containers/MovieContainer.jsx').default)
}, 'movie')
}
}
onEnter={() => console.log('进入了movie路由')}
onLeave={() => console.log('离开了movie路由')}
>
<IndexRoute
getComponent={
(nextState, callback) => {
require.ensure([], require => {
callback(null, require('../containers/MovieListContainer.jsx').default)
}, 'movieList')
}
}
/>
{/* 绝对路由带有'/',不带'/'的路由基于父组件的路径 */}
<Route path='/movieList/:movieType'
getComponent={
(nextState, callback) => {
require.ensure([], require => {
callback(null, require('../containers/MovieListContainer.jsx').default)
}, 'movieList')
}
}
/>
<Route path='/movieDetail/:id'
getComponent={
(nextState, callback) => {
require.ensure([], require => {
callback(null, require('../containers/MovieDetailContainer.jsx').default)
}, 'movieDetail')
}
}
/>
<Route path='/movieSearch/:keyword'
getComponent={
(nextState, callback) => {
require.ensure([], require => {
callback(null, require('../containers/MovieSearchContainer.jsx').default)
}, 'movieSearch')
}
}
/>
{/* 路由重定向 */}
<Redirect from='movieList' to='/movieList'/>
<Redirect from='movieDetail' to='/movieDetail'/>
<Redirect from='movieSearch' to='/movieSearch'/>
</Route>
<Route path='about'
getComponent={
(nextState, callback) => {
require.ensure([], require => {
callback(null, require('../containers/AboutContainer.jsx').default)
}, 'about')
}
}
/>
</Route>
</Router>
)
}
}

部署服务器

  1. 使用 FileZila 软件登录自己的服务器
  2. 将 webpack2 打包好的生产文件上传到服务器
  3. 使用 gitbash 的 ssh root@xxx.xx.x.xx 命令登录服务器
  4. 进入上传的生产文件目录, 使用pm2模块启动服务 pm2 start index.js
  5. 接下来就可以访问服务器地址的指定端口了