React核心使用总结

既然 Vue 都学了,难道不顺便把 React 收了么?它的大名就不需要说了,全球最火的前端框架,也是个啥都能写的货。
相比 Vue 它会更灵活,这就意味着它更复杂,对于主力后端的我来说,也就点到为止,那些深层次的东西就不挖的太深了,App (React Native)相关的构建也以后再说,这里主要是 Web 应用的开发。

准备环境

创建一个 React App 就使用专业一点的 Create React App 脚手架吧,安装方式官网都有写,不多说。
之前官方文档会让安装这个库,现在直接可以使用 npm 自带的 npx 指令(它是 npm 5.2+ 附带的 package 运行工具)来创建了。

下面来看一下创建出来的目录机构,相比 Vue,看着好像精简一点,默认使用 yarn 构建;webpack 相关的文件就不多说了,其中有用的就是 public 和 src 文件夹。
public 文件夹很简单,放了一些 logo,还有一个网站入口文件 index.html,这个文件也非常简单,就是一个基本骨架。
看文件夹名字,src 是主要战场,里面有个 index.js 是入口 js 文件,可以说是按照这个文件来一句一句执行代码。
可以看到里面导入了一个 serviceWorker 的模块,这个是 PWA(Progressive Web App)是做 App 适配相关的,有个有意思的特点是加载后,断网情况下可查看已经加载过的内容。

PWA 的核心目标就是提升 Web App 的性能,改善 Web App 的用户体验。媲美 native 的流畅体验,将网络之长与应用之长相结合。

这 public 中的 manifest.json 文件就是来定义将此 Web App 添加快捷方式放到桌面上的图标等配置,用来尽量模拟原生 App 的体验。

不过 PWA 毕竟不是现在的重点,这个先放一放,到 React Native 再说。

另外,public 文件夹还有另一个作用,就是项目启动后,会自动把这个文件夹作为资源服务,所以这里的 html 也就是主入口了,所以你在这里面放 json 文件,可以作为接口的模拟。

组件

React 中也是有组件的概念,就是一个个的模块,例如 App.js 就是一个模块,通过 index.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
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
// js 中的这种标签写法是 JSX 语法
ReactDOM.render(<App />, document.getElementById('root'));


// App.js
import React from 'react';
import './App.css';

function App() {
return (
<div className="App">
Mps~
{
this.state.list.map((item, index) => {
return <div key={index}>{item}</div>
})
}
</div>
);
}

export default App;


// 较老版本默认的 App.js
import React, { Component } from 'react'; // Component 是 React 的子模块

class App extends Component {
constructor(props) {
// 固定调用
super(props);
this.state = {
val: ''
};

// 推荐在构造中改变 this 指向,如果用得到
this.method = this.method.bind(this)
}

// render 函数返回的即最终组件内容,使用了 JSX
render() {
return {
<div> MPS~ </div>
}
}
}

ReactDOM 这个模块是用来将我们写的组件挂载到 index.html 中的,至于为什么需要引入 React,因为使用到了 JSX。

无论你使用函数还是 class 定义,在 React 中是等效的,不过看起来函数更加的简洁,并且它还可以接受一个 props 对象来传递数据;不过嘛,虽然简洁了,但也牺牲了一些特性。
如果是用的类定义方式,那么 props 的接收就要放到构造函数(constructor)里,效果是一样的。
另外 props 是只读的,不可修改;另外,也不要直接修改 state 的内容,要改也是拷贝一份,然后使用 set 方法修改。

无状态组件

为了更加方便管理,一般会将 UI 部分单独拆离出来,也就是把 render 函数单独搞出来,这样在上层的组件中 render 之间返回 <xxxUI /> 就可以了,但是 UI 中用到的变量怎么办,就是父子组件的传值了。
在需要传递方法给子组件执行的时候,如果方法需要传递参数,应该怎么写呢,之间加括号显然不妥,只能使用箭头函数:
onClick={() => {this.props.method(p)}}

像这种抽取出来的 UI 组件只有视图,那么就可以称为无状态组件,还记得脚手架给我们生成的默认文件的那个函数,那就是一个无状态组件了,它的性能更高。

JSX

虽然 React 并不强制使用 JSX,但是基本所有的 React 都这用吧,毕竟是真的好用,目前只能说说用到的一些功能。

JSX 语法不仅仅是可以便捷的使用 HTML 标签,自定义的标签也是可以的,例如 <App />
不过需要注意,自定义的标签开头必须大写,也就是你 import 导入的时候命名就得符合这个规则。

JSX 规定返回的内容必须包裹这一个标签内,但是这样就会多了一层结构,对样式可能不友好,类似的,它也有相应的特殊标签来规避:Fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<input value={this.state.val}
ref={(input) => {this.input = input}}
onChange={this.handleChange.bind(this)}/>
</React.Fragment>
);
}


handleChange(e) {
// 必须通过 set 函数 dom 才会刷新
this.setState({
// e.target = this.input
val: e.target.value,
// 展开运算符,添加新元素
list: [...this.state.list, newVal]
})
}

可以看出,它其实也是一个组件,只不过是 React 自带的,如果嫌麻烦,可以在 import 的时候就把它导进来。
上面的例子中,使用到了 ref 语法,虽然它能简化 e.target 这种写法,但是不推荐使用。
JSX 的差值表达式是使用一个大括号来定义的,包括触发事件(驼峰格式)的函数引用,注意 this 的作用域,然后下面是一些 JSX 的语法补充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 注释,利用 {} 里面是 JS 表达式的原理,仅开发可见
{/* xxxx */}
{
// xxxxx
}

// 样式,避免与类定义混淆,使用 className
import './demo.css'

function App() {
return (
<h1 className='xxx'></h1>
)
}

// 禁用转义
function createMarkup() {
return {__html: 'First &middot; Second'};
}
function MyComponent() {
return <div dangerouslySetInnerHTML={createMarkup()} />;
// return <div dangerouslySetInnerHTML={{__html: 'First &middot; Second'}} />;
}

在禁用转义中使用了双花括号,这里第二个花括号已经是 JS 的对象了,所以,并没有太大含义。
由于语言的歧义,除了 class 用 className 代替,在 label 中的 for 要用 htmlFor 来替代。


即使你不使用 JSX 语法,React 提供来一个函数:

1
2
3
4
5
6
7
8
React.createElement(
type,
[props],
[...children]
)

// e.g.
React.createElement('div', {}, 'show');

它与 JSX 的效果是一样的,也可以说 JSX 就是它的简写版本。

组件之间传值

页面都是有一个个的组件组成的,那就肯定避免不了传值问题,父组件向子组件传值相对很简单,直接通过标签的自定义属性即可,子组件只需要使用 this.props.attrName 就可以获取到,但是是只读的。
很多时候,我们需要在子组件中修改父组件的一些数据,当然这肯定是不能允许直接改的,一般是通过调用父组件的一个方法来实现数值的修改。
虽然子组件调用父组件的函数直接想不好搞,但是可以曲线救国,把父组件的方法直接传给子组件不就得了,子组件拿到了就可以在子组件的函数里直接调用;在传递的时候记得父组件里使用 bind 改变一下 this 指向。

单向数据流保证了数据的安全稳定,否则都不好定位是谁改了然后又导致了什么。

类型校验

因为不管是什么类型都可以往子组件传递,甚至方法,为了避免出错,还是校验一下比较好,方法就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import PropTypes from 'prop-types';
// 前面省略了

App.propTypes = {
n1: propTypes.string.isRequired,
n2: propTypes.func,
n3: propTypes.number,
n4: propTypes.array,
n5: propTypes.bool
}

App.defaultProps = {
n1: 'null'
}

export default App;

如果类型不对会给你一个警告,虽然并不影响程序的运行。
更多高级的用法例如或者并且等判断参考官方文档:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html

Redux

父子组件传值还算是简单,当项目复杂起来,不可能只是父子之间的传值,如果还用现在的方法就会很复杂,所以需要引入 Redux 来完成不同组件之间的传值。
原理也很简单,把数据统一放到一个 Store 对象中统一存储,然后需要的组件去监控这里面某个变量的值,当发生变化时,做出相应的改变。

img

上图就是 Redux 的数据流,也是按照单向数据流设计,组件必须通过 Action 才可以操作 Store 的数据。
第一步,创建一个 Store,例如 ./store/index.js & reducer.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
// index.js
import { createStore } from 'redux'
import reducer from './reducer'

// 有 Redux Dev 插件可用,需要在这配置
const store = createStore(reducer)

export default store


// reducer.js
const defaultState = {} // 具体存储
export default (state = defaultState, action) => {
// state = 上一次的 state 数据
// 要根据 action 的内容来更新 state,switch....case
return state
}

// 用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})

仓库创建好了,下面就可以使用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Component } from 'react';
import store from './store'; // ./store/index.js

class App extends Component {
constructor(props) {
super(props);
store.getState();
// store 数据发生改变后执行
store.subscribe(() => console.log(store.getState()))
}

change(newVal) {
const action = {
type: 'change_value',
value: 'newVal'
};
store.dispatch(action);
}
}

为了避免 Action#type 手误写错,可以专门起一个 js 文件来定义,类似枚举。同理,Action 方法也是类似。
由于 reducer 中的 state 不能直接修改,每次根据 action 更新时需要拷贝一个,一不小心就容易漏掉,这个时候就可以使用 immutable-js 这个库来做,生成不可变对象,经过它的转换,就可以通过使用 get 方法获取或者 set 方法重新生成一个不可变对象。
为了统一样式,可以使用 redux-immutable 库来进行处理。

PS:Redux = Reducer + Flux

React-redux

通过 React-redux 可以让我们更方便的使用 Redux,官方主页是这样写的:Official React bindings for Redux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react'
import ReactDOM from 'react-dom'

import { Provider } from 'react-redux'
import store from './store'

import App from './App'

const rootElement = document.getElementById('root')
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
)

这里使用 Provider 组件将 store 进行了传递,这样 Provider 下的所有子组件都能共享了。

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
import { connect } from 'react-redux'

// class App ...

// 将 store(state)的数据映射到 props
const mapStateToProps = (state /*, ownProps*/) => {
return {
counter: state.counter
}
}

// 将要调用 dispatch 的方法映射
const mapDispatchToProps = (dispatch) => {
return {
change(e) {
const action = {};
dispatch(action);
}
}
}

export default connect(
mapStateToProps,
mapDispatchToProps
)(App)

子组件获取 store 使用的是 connect 这个函数,在 export 的时候使用。
connect 函数需要传入两个参数,也就是如何做连接,就是做了两个映射,这样都省的订阅了,store 变化数据就实时刷新了。
并且,这样组件基本不包含任何的逻辑代码了,可以做成无状态组件了;也可以这么理解,connect 函数将 UI 组件和逻辑部分进行了拼装,返回了容器组件。

State与Props

当 State 或者 Props 发生改变时,render 函数就会执行,这样 Dom 就会被重新渲染。

在前面组件里我们说的是改变数据必须用 set 函数,使用的是传统的传入一个对象()利用 K-V 的形式来进行更新;不过还有性能更好的方法就是异步函数:

1
2
3
4
5
6
7
8
9
10
function handleChange(e) {
let val = e.target.value;

this.setState(()=> {
return {value: val};
})
}

// 如果只有一个 return,箭头函数还可以简写
this.setState(()=>({value: val}))

不过因为异步,在使用的时候,数值尽量固定化,也是因为异步,所以它还会有第二个参数,是回调,根据需要使用。
在 setState 使用异步函数的时候,函数其实会传给你一个 prevState 参数,它就是原来的值。

1
2
3
this.setState((prevState)=>({value: prevState.value + newVal}))
// ES6 语法简写
{val: val} == {val}

至于 setState 为什么建议使用异步,因为这样如果有多个修改并且间隔很短,React 就可以合并成一个操作,避免连续多次的虚拟 DOM 比对与渲染。

虚拟DOM

不管是 React 还是 Vue,都是使用虚拟 DOM 来控制页面的渲染(刷新),原因就是 JS 渲染一次真实的 DOM 需要调用浏览器 API,性能损耗过高,如果频繁渲染,体验肯定不好。
对于这一问题,有几个方案,现在采用的基本都是虚拟 DOM

  • 数据改变,重新生成新的 DOM,替换老的 DOM
  • 数据改变,重新生成新的 DOM,与老的 DOM 比对,只替换差异部分
  • 加载时构建虚拟 DOM,数据改变,虚拟 DOM 随之改变,对比之前老的虚拟 DOM,确定差异部分,操作 DOM 更新差异。

这里的这个虚拟 DOM 可以理解为就是个 JS 对象,用这个 JS 对象来描述真实的 DOM,正是因为是 JS 对象,所以速度很快,性能就大幅提高了。
在 React 中,真实的 DOM 是按照虚拟 DOM 来渲染的,也正是因为虚拟 DOM 的存在,所以 React Native 得以存在,虚拟 DOM 渲染为真实 DOM 就是浏览器,渲染为原生 App 组件,这就成移动端的应用了。

虚拟 DOM 的比对 Diff 使用的是同层比对,从上往下,只要发现一层中的某个节点不一样,此节点下面所有的结构都会被重新渲染,虽然下面可能都没有变,但是这样的比对速度会更快。
之所以要设置 Key 值也是为了提高比对速度,当 key 是唯一的时候,那么就是 key 与 DOM 一对一的对应关系,当发现某个节点的 key 没有变时,可以直接复用了(先根据 key 拿到对应的节点的信息,然后校验是否一致,如果不一致说明 key 是不稳定的,这种缓存就没法用了),所以,用 index 当作 key 很不妥。

生命周期

对于这类描述生命周期的内容,没有什么比图更直观了:

img

img

这两幅图基本已经描述的够清楚了(虽然版本有点老),还有一个是官方的图:http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
根据官方图,getDerivedStateFromProps 这个方法中 16.4 发生了变化,需要特别注意。
以及 componentWillReceiveProps 与 componentWillUpdate、componentWillMount 已经被标注为过时。

在 React 中所有的组件都要继承 Component,大部分的生命周期函数中 Component 中都有默认实现,唯独 render 没有,所以这就是为什么你必须写 render 函数的原因(函数式写法有点特别)。
因为当父组件的 render 被执行时,子组件的 render 也会被执行,即使这时候 props 并没有变化,这样就会带来不必要的性能开销,所以可以使用 shouldComponentUpdate 这个生命周期函数来优化(或者继承 PureComponent,它默认实现了这个方法,不过为了避免 bug,需要配合 immutable-js 使用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import axios from 'axios'

class App extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return nextProps.xxx !== this.props.xxx;
}

componentDidMount() {
axios.get('/index')
.then((res) => {
if (res.data.ret && res.data.data) {
// something
}
})
.catch()
}

// ...
}

一般情况,我们发送 Ajax 请求的过程要放到 componentDidMount 中。
建议使用 axios 当然需要安装(yarn add axios

路由

关于页面的路由,这里就使用 react-router-dom 这个模块来完成,文档参考这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {BrowserRouter, Route} from 'react-router-dom'

function App() {

if (this.props.loginStatus) {
return <Redirect to='/' />
}

return (
<Provider store={store}>
<ResetStyle/>
<GlobalStyle/>

<Header />
<BrowserRouter>
<Route path='/' exact component={Home}></Route>
<Route path='/detail/:id' exact render={() => <div>detail</div>}></Route>
</BrowserRouter>
</Provider>
);
}

export default App;

简单说就是根据请求路径的不同来决定显示的内容,使用 exact 来进行完全匹配。
老的版本可能不允许你 Provider 或者 BrowserRouter 有多个元素,那时候用 div 裹一下就好。

另外,它还提供了一个 Link 模块,可以用来替代 a 标签,做单页面应用,避免加载过多的 HTML。
在进行匹配的时候,可以使用类似 /:id 来匹配变量,然后在组件里通过 this.props.match.params.id 获取,这就是动态路由了。
另外一种是匹配 ?传值,一样的,只不过不需要写 /:id 了,如果有 ?id=xx 会自动进行填充,获取方法不太一样,要自己手动处理字符串 this.props.location.search 并不是很推荐。

Redux-thunk

Redux-thunk 是 Redux 的一个中间件,用来『整理』Action 中的 Ajax 请求等处理,说明文档可以参考 Github 的主页

1
2
3
4
5
6
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';

// Note: this API requires redux@>=3.1.0
const store = createStore(rootReducer, applyMiddleware(thunk));

使用多个中间件,例如 Redux Dev Tools 具体方法参考 Github 的文档
使用 Redux-thunk 后在 action 中就不一定非要是 js 的对象了,可以返回一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// before
export const initAction = (newVal) => ({
type: 'change_value',
value: newVal
});

// after
export const getAction = () => {
return (dispatch) => {
axios.get('/do')
.then((res) => {
dispatch(initAction(res.data));
})
}
}


// 使用
componentDidMount() {
const action = getAction();
// 可以接受一个函数了
store.dispatch(action);
}

上方展示的也是封装后的 Action 写法,还是建议把 Action 封装到一个 js 文件,统一管理。
看起来好像比之前麻烦了,不过以经验来看,当项目越来越大后,这种方式更加容易管理和测试。

Redux-saga

与 Redux-thunk 类似,Redux-saga 也是一个类似的 Redux 中间件,做异步代码拆分的,他们两个可以互相替代,使用方法也可以参考一下 Github 的文档
它使用单独的 JS 文件来管理 Ajax 请求,然后 run 一下。

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
// sagas.js example
import { put, takeEvery } from 'redux-saga/effects'

function* fetchUser(action) {
// 以下内容怕出错可以放到 try...catch 里
const res = yield axios.get('/do');
const action = initAction(res.data);
// 执行完成后传给 reducer
yield put(action);
}

function* mySaga() {
// ES6 Generator 函数
yield takeEvery("ACTION_TYPE_NAME", fetchUser);
}

export default mySaga;


// 使用
componentDidMount() {
// action 只是包含一个 type 类型的对象即可
const action = getAction();
store.dispatch(action);
}

也就是说当你 dispatch 时,mySaga 也会收到你的 action,然后如果类型匹配,就会执行对应的函数,例如 fetchUser。
可以看出 Redux-saga 是更加复杂的,相应的功能也更强大,上面也是最基本的使用。

异步加载

默认情况下,会把项目的所有 JS 打包成一个 JS,这样页面只需要加载一次 JS,但是,如果逻辑很多,第一次加载肯定很慢,对于大项目,更期望只加载用到的 JS,使用的是 react-loadable 这个组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
// loadable.js
import Loadable from 'react-loadable';
import React from 'react'

const LoadableComponent = Loadable({
// 当前的 index.js 做异步
loader: () => import('./'),
loading () {
return <div>正在加载...</div>
}
});

export default () => <LoadableComponent/>

以上是基于官方示例做的简单修改,下面是使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// import Detail from './pages/detail' 之前
import Detail from './pages/detail/loadable'

function App() {
return (
<Provider store={store}>
<ResetStyle/>
<GlobalStyle/>

<BrowserRouter>
<Route path='/detail/:id' exact component={Detail}/>
</BrowserRouter>
</Provider>
);
}

export default App;


// 使用路由的情况下,需要对组件特殊处理
import { withRouter } from 'react-router-dom';
// ...
export default connect(mapState, mapDispatch)(withRouter(Detail));

可以看出,只需要把 import 语句改成引入编写的 loadable 文件即可,但是,如何使用了 BrowserRouter 路由,这样就会有问题,所以在 export 的时候需要使用 withRouter 包裹一下。

总结

总结一下一般的模块开发套路,首先创建组件,如果需要较多的数据就要创建独立的 Store,然后使用 Redux + React-redux 来进行管理,子组件在 export 的时候使用 connect 函数连接 store。
样式的编写可以采用 styled-components,这样数据全部来自相关的 store,使用 mapStateToProps 来映射到 props 中,相关的事件通过 mapDispatchToProps 映射到 props 中,这样改变数据就使用相应的 action 即可。
为了将逻辑写在 action 中,例如异步请求,使用了 Redux-thunk 或者 Redux-saga 来将 action 变为一个函数,将数据处理好后直接调用 dispatch 即可,为了方便 action 管理,将所有 action 的创建抽取到 actionCreators.js 文件中,相应的,常量也应该抽取到一个单独的文件,便于 actionCreators.js 和 reducer.js 的使用。
存储为了不必要的麻烦,使用 immutable-js 将其变为不可变对象,为了风格的统一使用 redux-immutable 将 store 的 state 也变为不可变对象。
这样下来,store 文件夹里就有了不少文件,为了方便导入,可以建一个 index.js 做聚合。
每一个模块都应该有自己的 store 数据仓储,所以把相关联的 store 文件夹放真自己的模块下,在主仓储使用 combineReducers 函数来组合。

因为数据变化 render() 就会执行,子组件也会重新渲染,即使子组件需要的数据没有变化,为了优化这个问题导致的页面重新渲染,除了在 shouldComponentUpdate 中判断外,React 自然也想到了这一点,所以它还有一个 PureComponent,只要继承它,就自动帮你实现了 shouldComponentUpdate 的内容,不过需要也使用 immutable-js,否则可能会有 bug。

其他

关于动画,这个我是真的不想看,CSS 看着就头疼,想做的看下 react-transition-group 这个库吧。
关于 UI 设计,可以看看 Ant Design 这个库。

正常情况下,如果你在 JS 中导入了 CSS,那么这个 CSS 是全局生效的,所以并不推荐这样使用,例如可以使用 styled-components 来管理,它使用 JS 来编写样式,这样在 WebStorm 不识别,可以安装他们的一个插件。

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~