既然 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 | // index.js |
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 | render() { |
可以看出,它其实也是一个组件,只不过是 React 自带的,如果嫌麻烦,可以在 import 的时候就把它导进来。
上面的例子中,使用到了 ref 语法,虽然它能简化 e.target 这种写法,但是不推荐使用。
JSX 的差值表达式是使用一个大括号来定义的,包括触发事件(驼峰格式)的函数引用,注意 this 的作用域,然后下面是一些 JSX 的语法补充。
1 | // 注释,利用 {} 里面是 JS 表达式的原理,仅开发可见 |
在禁用转义中使用了双花括号,这里第二个花括号已经是 JS 的对象了,所以,并没有太大含义。
由于语言的歧义,除了 class 用 className 代替,在 label 中的 for 要用 htmlFor 来替代。
即使你不使用 JSX 语法,React 提供来一个函数:
1 | React.createElement( |
它与 JSX 的效果是一样的,也可以说 JSX 就是它的简写版本。
组件之间传值
页面都是有一个个的组件组成的,那就肯定避免不了传值问题,父组件向子组件传值相对很简单,直接通过标签的自定义属性即可,子组件只需要使用 this.props.attrName
就可以获取到,但是是只读的。
很多时候,我们需要在子组件中修改父组件的一些数据,当然这肯定是不能允许直接改的,一般是通过调用父组件的一个方法来实现数值的修改。
虽然子组件调用父组件的函数直接想不好搞,但是可以曲线救国,把父组件的方法直接传给子组件不就得了,子组件拿到了就可以在子组件的函数里直接调用;在传递的时候记得父组件里使用 bind 改变一下 this 指向。
单向数据流保证了数据的安全稳定,否则都不好定位是谁改了然后又导致了什么。
类型校验
因为不管是什么类型都可以往子组件传递,甚至方法,为了避免出错,还是校验一下比较好,方法就是:
1 | import PropTypes from 'prop-types'; |
如果类型不对会给你一个警告,虽然并不影响程序的运行。
更多高级的用法例如或者并且等判断参考官方文档:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html
Redux
父子组件传值还算是简单,当项目复杂起来,不可能只是父子之间的传值,如果还用现在的方法就会很复杂,所以需要引入 Redux 来完成不同组件之间的传值。
原理也很简单,把数据统一放到一个 Store 对象中统一存储,然后需要的组件去监控这里面某个变量的值,当发生变化时,做出相应的改变。
上图就是 Redux 的数据流,也是按照单向数据流设计,组件必须通过 Action 才可以操作 Store 的数据。
第一步,创建一个 Store,例如 ./store/index.js & reducer.js
:
1 | // index.js |
仓库创建好了,下面就可以使用了:
1 | import React, { Component } from 'react'; |
为了避免 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 | import React from 'react' |
这里使用 Provider 组件将 store 进行了传递,这样 Provider 下的所有子组件都能共享了。
1 | import { connect } from 'react-redux' |
子组件获取 store 使用的是 connect 这个函数,在 export 的时候使用。
connect 函数需要传入两个参数,也就是如何做连接,就是做了两个映射,这样都省的订阅了,store 变化数据就实时刷新了。
并且,这样组件基本不包含任何的逻辑代码了,可以做成无状态组件了;也可以这么理解,connect 函数将 UI 组件和逻辑部分进行了拼装,返回了容器组件。
State与Props
当 State 或者 Props 发生改变时,render 函数就会执行,这样 Dom 就会被重新渲染。
在前面组件里我们说的是改变数据必须用 set 函数,使用的是传统的传入一个对象()利用 K-V 的形式来进行更新;不过还有性能更好的方法就是异步函数:
1 | function handleChange(e) { |
不过因为异步,在使用的时候,数值尽量固定化,也是因为异步,所以它还会有第二个参数,是回调,根据需要使用。
在 setState 使用异步函数的时候,函数其实会传给你一个 prevState 参数,它就是原来的值。
1 | this.setState((prevState)=>({value: prevState.value + newVal})) |
至于 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 很不妥。
生命周期
对于这类描述生命周期的内容,没有什么比图更直观了:
这两幅图基本已经描述的够清楚了(虽然版本有点老),还有一个是官方的图: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 | import axios from 'axios' |
一般情况,我们发送 Ajax 请求的过程要放到 componentDidMount 中。
建议使用 axios 当然需要安装(yarn add axios
)
路由
关于页面的路由,这里就使用 react-router-dom 这个模块来完成,文档参考这里
1 | import {BrowserRouter, Route} from 'react-router-dom' |
简单说就是根据请求路径的不同来决定显示的内容,使用 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 | import { createStore, applyMiddleware } from 'redux'; |
使用多个中间件,例如 Redux Dev Tools 具体方法参考 Github 的文档。
使用 Redux-thunk 后在 action 中就不一定非要是 js 的对象了,可以返回一个函数:
1 | // before |
上方展示的也是封装后的 Action 写法,还是建议把 Action 封装到一个 js 文件,统一管理。
看起来好像比之前麻烦了,不过以经验来看,当项目越来越大后,这种方式更加容易管理和测试。
Redux-saga
与 Redux-thunk 类似,Redux-saga 也是一个类似的 Redux 中间件,做异步代码拆分的,他们两个可以互相替代,使用方法也可以参考一下 Github 的文档。
它使用单独的 JS 文件来管理 Ajax 请求,然后 run 一下。
1 | // sagas.js example |
也就是说当你 dispatch 时,mySaga 也会收到你的 action,然后如果类型匹配,就会执行对应的函数,例如 fetchUser。
可以看出 Redux-saga 是更加复杂的,相应的功能也更强大,上面也是最基本的使用。
异步加载
默认情况下,会把项目的所有 JS 打包成一个 JS,这样页面只需要加载一次 JS,但是,如果逻辑很多,第一次加载肯定很慢,对于大项目,更期望只加载用到的 JS,使用的是 react-loadable 这个组件。
1 | // loadable.js |
以上是基于官方示例做的简单修改,下面是使用:
1 | // import Detail from './pages/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
你可能需要魔法上网~~