虽然不是第一次接触react,但之前都没有写博客记录,好记性不如烂笔头,希望以后养成多做笔记的好习惯吧。

关于react

react 作为当下最流行的前端框架,之所以能被如此多前端开发者所喜爱自然是有原因的。react有一个很重要的特性——限制(Constraint),这里的“限制”并不是贬义,因为对于大部分开发者来说,代码的可维护性和扩展性并不是很好,所以只有施加“限制”,代码的管理才会更加简单。必须按照react的规范来写代码,不然程序就跑不了,就是这么霸道。

react 还有很多优势,个人认为最重要的就是虚拟DOM和组件化。

  • 虚拟DOM:现在的框架很少有直接操作DOM的,虽然jQuery曾经称霸一方。当页面上的DOM树改变的时候,虚拟DOM机制会将前后DOM树进行对比(通过一些高效的算法),如果两个DOM树有不一样的地方,react只会针对不一样的地方进行修改,自然就提高了性能。举个栗子:现在的html结构是这样
1
2
3
4
5
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>

你想让它变成这样

1
2
3
4
5
6
<ul>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
</ul>

如果是原始的DOM操作,你需要先将开始的三个li删除,然后再将后面的四个li添加进去;可能你也会这样操作:将三个liinnerHTML修改,只需要添加一个7就行了。那么恭喜你,做的很好。react有个很重要的思想,状态管理。只要DOM节点的对应的state发生了改变,react就会将其标注为“脏状态”,在一个Event loop结束后,对这些有标注的地方进行修改,并且只对修改了的地方重新渲染,减少了很多冗余的DOM操作。具体实现可以参考 深度剖析:如何实现一个 Virtual DOM 算法

  • 组件化:简单的说下好处,可以模板化,可复用,可嵌套的UI模块,当然组件之间会存在依赖关系。可以将UI组件当作独立的js模块使用,推荐使用ES6的import来引用组件模块,当然也可以配合CommonJs、AMD、CMD等规范来require我们需要的组件模块,最重要的是处理好依赖关系。

项目

前面说了这么多,终于到重点了。这个项目是参照慕课网实战视频做的,视频地址:React高级实战 打造大众点评 WebApp

  • 启动:

    1
    2
    3
    npm i
    npm run mock
    npm start
  • 项目预览

  • 项目源码

  • 项目目录

    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
    │ package.json // 配置文件
    │ README.md
    ├─build // 打包后的文件
    ├─config // 配置文件
    ├─mock // mock数据
    │ │ server.js // 请求数据接口
    │ ├─detail
    │ ├─home
    │ ├─public
    │ ├─search
    │ └─user
    ├─public
    ├─scripts
    │ build.js // 项目打包入口
    │ start.js // 项目启动入口
    │ test.js
    └─src
    │ config.js // 常量配置
    │ index.js // 程序入口
    │ index.less
    ├─actions // actions
    ├─components // 木偶组件
    ├─containers // 智能组件,与redux连接
    ├─fetch // 获取数据
    ├─reducers // reducers
    ├─router
    │ index.jsx // 前端路由
    ├─static // 静态资源
    └─util // 工具函数

webpack配置

首先,项目采用 create-react-app 脚手架搭建,很多配置都已经帮我们写好了,但是仍需要自定义一些配置,所以我 npm run eject。对于这个命令,官方是这样解释的:

Note: this is a one-way operation. Once you eject, you can’t go back!

If you aren’t satisfied with the build tool and configuration choices, you can eject at any time. This command will remove the single build dependency from your project.

Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except eject will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.

You don’t have to ever use eject. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.

这个项目我使用less编译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
34
35
{
test: /\.(css|less)$/,
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',
}),
],
},
},
{
loader: require.resolve('less-loader') // compiles Less to CSS
},
],
},

在开发环境和产品环境都需要配置,即在webpack.config.dev.jswebpack.config.prod.js都需要配置

前端路由配置

然后就是前端页面路由配置,视频使用的是react-router2.0的版本,我用的是4.0版本,接触过的都知道,这两者差异很大。

  • history 不用props传递,使用HashRouter
  • Route 不能嵌套使用,都在同一层级
  • 想要匹配所有不存在的路由,就不传path
  • 动态参数 :param 可选参数 :param? 精确匹配 exact

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { HashRouter, Route, Switch } from 'react-router-dom'
<HashRouter>
<Switch>
<Route path='/' exact component={Home} />
<Route path='/city' component={City}/>
<Route path='/search/:category/:keyword' component={Search}/>
<Route path='/detail/:id' component={Detail}/>
<Route path='/login' component={Login}/>
<Route path='/user' component={User}/>
<Route component={NotFound}/>
</Switch>
</HashRouter>

服务器配置

我这里使用的koa2.0来做后端接口的模拟

由于我们的数据是采用静态数据模拟的,所以请求数据其实就是请求js文件,并将数据返回

代码如下:

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
const Koa = require('koa')
const Router = require('koa-router')
const path = require('path')
const static = require('koa-static')
const cors = require('koa2-cors');
const bodyParser = require('koa-body')();
const app = new Koa()
const router = new Router()
// 跨域
app.use(cors({
credentials: true,
allowMethods: ['GET', 'POST', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}))
// 访问静态资源
app.use(static(
path.join(__dirname, '/public')
))
// 主页广告
router.get('/data/homead', async (ctx) => {
ctx.body = require('./home/ad')
})
// 主页列表
const homeListData = require('./home/list')
router.get('/data/homelist/:cityname/:page', async (ctx) => {
const pageSize = 1
const { cityname, page } = ctx.params
let filterData = homeListData.filter(item =>
item.cityname === cityname
)
let hasMore = pageSize * page >= filterData.length ? false : true
ctx.body = {
result: filterData.slice(0, page * pageSize),
hasMore: hasMore
}
})
// 搜索页列表
const searchListData = require('./search/list')
router.get('/data/search/:cityname/:category/:keyword/:page', async (ctx) => {
const pageSize = 1
const { cityname, category, keyword, page } = ctx.params
let filterData
let filterData1 = searchListData.filter(item =>
item.cityname === cityname
)
if (keyword === 'all') {
filterData = filterData1.filter(item =>
item.category.includes(category)
)
} else {
filterData = filterData1.filter(item =>
item.title.includes(keyword) || item.subTitle.includes(keyword)
)
}
let hasMore = pageSize * page >= filterData.length ? false : true
ctx.body = {
result: filterData.slice(0, page * pageSize),
hasMore: hasMore
}
})
// 详情页 商家详情
const detailData = require('./detail/info')
router.get('/data/detail/info/:id', async (ctx) => {
let { id } = ctx.params
id = parseInt(id)
const filterData = detailData.filter(item =>
item.id === id
)
ctx.body = filterData
})
// 详情页 评论列表
const CommentData = require('./detail/comment')
router.get('/data/detail/comment/:id/:page', async (ctx) => {
const pageSize = 2
const { id, page } = ctx.params
let filterData = CommentData
let hasMore = pageSize * page >= filterData.length ? false : true
ctx.body = {
result: filterData.slice(0, page * pageSize),
hasMore: hasMore
}
})
// 登录
router.post('/data/login', bodyParser, async (ctx) => {
const { username, password } = ctx.request.body
if (username === 'linxun' && password === 'linxun') {
ctx.body = {
status: 1,
msg: '登录成功'
}
} else {
ctx.body = {
status: 0,
msg: '用户名或密码错误'
}
}
})
// 收藏
router.get('/data/collect/:id', async (ctx) => {
let { id } = ctx.params
id = parseInt(id, 10)
const filterData = detailData.filter(item =>
item.id === id
)
ctx.body = filterData
})
// 订单
const orderListData = require('./user/orderList')
router.get('/data/orderList', async (ctx) => {
ctx.body = orderListData
})
// 加载路由中间件
app.use(router.routes()).use(router.allowedMethods())
app.listen(8000, () => {
console.log('route-use-middleware is starting at port 8000')
})

前端数据请求

由于我们的数据是采用静态数据模拟的,所以请求数据其实就是请求js文件,并将数据返回

这里使用fetch和promise,在组件中只需要发送请求,然后在.then()中即可拿到需要的数据

代码如下:

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
import 'whatwg-fetch'
import 'es6-promise'
export function get(url) {
return fetch(url, {
headers: {
'Accept': 'application/json, text/plain, */*',
}
})
}
export function post(url, paramsObj) {
return fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: obj2params(paramsObj)
});
}
function obj2params(obj) {
let result = '', item;
for (item in obj) {
result += '&' + item + '=' + encodeURIComponent(obj[item]);
}
result = result.length > 0 ? result.slice(1) : result;
return result;
}

react与redux连接

在智能组件中

代码如下:

1
2
3
4
5
6
7
8
9
10
function mapStateToProps(state) {
return { ... } // 需要的仓库中的数据
}
import { bindActionCreators } from 'redux'
function mapDispatchToProps(dispatch) {
// actionCreater:只负责生成action (一个普通对象:必须包含type,其他自定义)
return { actionCreater: bindActionCreators(actionCreater, dispatch) }
}

主要的代码就是这些,样式和素材都是视频中的。