React16_4整理-实战
# 前言
前面做了那么多准备,开发一个门户应用。在开发过程中,使用更多技术点。比如Styled-Components、Immutable.js、redux-immutable、页面性能优化、react-router等等等。
总结的时候会把常用的hooks以及react性能优化、基础的项目上线流程简单说一下。
# 项目目录搭建
安装 Create-React-App
:npm install -g create-react-app
运行 create-react-app
命令 新建一个 react 项目
安装第三方模块儿 Styled-Components
:yarn add styled-components
# Styled-Components
在react
中 你在一个地方引入css
就会在全局使用
使用第三方模块儿Styled-Components
对组件中的样式
进行管理
,使得每一个组件的样式只对自己生效
最终会返回
这个样式的组件
给你,你可以直接
使用
这个自定义样式
的组件
。
# Styled-Components 的使用
将css
文件改为js
文件,并且修改
用引入
的css为js
。
在js文件这样写:
/* injectGlobal 表示注入全局样式 */
import {injectGlobal} from 'styled-components'
injectGlobal`
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background: green;
}
`
使用 reset.css
去官网下载 reset.css :https://meyerweb.com/eric/tools/css/reset/index.html
。reset.css 能够清空
html 标签在所有浏览器的默认样式
了,也就是默认样式归0
。
给某一个组件单独设置样式:先写样式文件
,并且导出样式对应的控件
,最后你使用
那个控件包裹
你的内容
即可。
import styled from 'styled-components';
export const HeaderWarpper = styled.div`
height: 58px;
background: #f0f0f0;
border-bottom: 1px solid #fff;
`;
import React, {Component} from 'react';
import { HeaderWarpper } from './style.js'
class Header extends Component {
render (){
return (
<HeaderWarpper>
Header
</HeaderWarpper>
)
}
}
export default Header;
模板标签以及模板字符串
function tag (str) {
console.log(str);
}
tag`123456`//最后会输出["123456"] 使用这种方式调用会将传递进去的参数包装成一个数组
# 制作字体图标
iconfont.cn 可以用来制作自己需要的字体图标。
# 使用 combineReducers 完成对数据的拆分管理
一般情况下,一个文件的代码超过 300 行,就说你的设计肯定是有问题的。
reducer.js 中如果存放过多的数据,就需要拆分了,比如将不同组件需要使用的 reducer 放到不同组件下的 store 文件夹下,然后使用 combineReducers 整合一下,给不同的 reducer 加一个别名,使用的时候通过 state.别名.value 来用。
/* 这个是根目录 store目录下的reducer ,用来整合其它的reducer */
import { combineReducers } from 'redux';
import headerReducer from '../common/header/store/reducer';
// export default combineReducers({
// header:headerReducer
// })
const reducer = combineReducers({
header: headerReducer
})
export default reducer;
/* 之前使用是直接state.focused,定义了别名后 可以这么用了*/
const mapStateToPorps = (state) => {
return {
focused: state.header.focused
}
}
按需引入时可以给引入的对象起一个别名,使用as
关键字
import {reducer as headerReducer} from '../common/header/store';
const reducer = combineReducers({
header: headerReducer
})
export default reducer;
# actionCreators 与 constants 的拆分
如果你引入的文件里是按需导出的,但是你又向一下子全部导入,你可以使用通配符加 as 来进行全部导入,不需要加花括号,加花括号其实是通过解构的方式进行赋值。
import * as actionCreators from './store/actionCreators';
//使用的时候就可以通过 actionCreators.成员的方式了。
如果你想一下子把某一个文件夹下的 js 文件全部导出,你可以在该文件夹下新建一个 index.js 文件,然后在这个文件中引入当前文件夹下所有的 js,之后以整体导出的方式导出去。
最后你在别的地方可以直接导入这个文件夹,你可以直接导入也可以按需(解构)的方式导入。
import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as constants from './constants';
/* 整体导出 */
export {reducer, actionCreators, constants}
/* 以解构的方式按需导入 */
import {actionCreators} from './store/';
# 使用 Immutable.js 来管理 store 中的数据
immutabel.js
是 facebook 花三年
时间开发
的一个项目
,也是一个第三方
的模块儿
。
使用 immutabel.js
可以生成一个 immutabel
的对象,immutabel
表示不可改变
安装immutabel
:yarn add immutable
immutable
库,将一个 js 对象转换为一个immutable
对象,它的底层
还是采用虚拟dom
加diff算法
,这样改值得时候性能特别好
/*fromJS可以将一个js对象转换为一个immutable对象*/
import {fromJS} from 'immutable';
/* 这时候 defaultState是一个immutable对象*/
const defaultState = fromJS({
focused: false
});
export default (state = defaultState, action) => {
// 当state对象为一个普通的对象时可以这么做,
// 但是state对象已经是一个immutable对象了,所以不能返回一个普通对象
// if (action.type === constants.SERCH_FOCUS){
// return {
// focused: action.focused
// }
// }
if (action.type === constants.SERCH_FOCUS){
/*只能够调用set方法了*/
/* immutable对象的set方法,会结合之前immutable对象的值
和设置的值,返回一个全新的immutable对象,也就是减少了你克隆state的那一步。
*/
return state.set('focused', action.focused);
}
}
// 这里面传递的是一个immutable对象而不是一个简单的{},所以不能够直接.的方式取值了,要使用get的方式取值
const mapStateToPorps = (state) => {
return {
// focused:state.header.focused
focused: state.header.get('focused')
}
}
# 使用 redux-immutable 统一数据的格式
安装redux-immutable
:yarn add redux-immutable
使用 redux-immutable
来整合所有的reducer
/* 之前整合所有的 reducer 是 引入redux中的 combineReducers*/
import { combineReducers } from 'redux';
import {reducer as headerReducer} from '../common/header/store';
/* 现在改用 redux-immutable中的combineReducers*/
import { combineReducers } from 'redux-immutable';
/* 这里面的state不是一个immutable对象,而 header是一个immutable对象*/
const mapStateToPorps= (state) => {
return {
// focused:state.header.focused
focused: state.header.get('focused')
}
}
/*
现在改用 redux-immutable中的combineReducers之后 state也是一个immutable对象了 ,这样对数据的操作就统一了
*/
const mapStateToPorps = (state) => {
return {
// focused:state.header.focused
// focused:state.header.get('focused')
focused:state.get('header').get('focused')
// 你也可以使用getIn这个方法,与上面等价
focused:state.getIn(['header','focused'])
}
}
# 使用 ajax 获取数据
你可以将数据放到 public
目录下的api
文件夹,以json
的格式存放。因为底层
是一个node服务器
,它会以public文件夹
为网站根目录
。
这样一来你往网站根目录中存放数据
,请求
是网站根目录
下的资源
,就可以获取对应的数据
。
与后端开发的时候,先自己模拟数据,但是在这之前要和后端约定
好,约定好返回的数据格式
,例如{sucess:true,data:[]}
,这样的。
这样你才能够在模拟数据的时候写的 ajax 与真正上线的时候请求后端的接口统一
。
immutable
会将state
中的数组成员转换为immutable
类型的数组
,所以当你set
的时候,如果是给一个数组成员赋值
,那么对应的值也得是immutable
类型的数组,不可以将普通数组赋值给immutable
类型的数组成员。
immutable
对象转
换为一个普通
的 js 对象:list.toJS();
在使用 immutable 对象的 set 方法时,每次只能够改变一个值,如果你想同时改变两个值,可以这样做。
state.merge(
{
list:action.data,
totalPage:action:totalPage
}
);
//上面的写法要比下面的好一些,性能更好
state.set("list",action.data).set("totalPage",action:totalPage)
# 什么是路由,如何在 React 中使用路由功能
安装 react路由
第三方模块儿:react-router-dom
,安装命令:yarn add react-router-dom
引入路由: Provider
里面最好嵌套一个 div,因为里面只准有一个节点
,BrowserRouter
也是一样
。
import { BrowserRouter,Route } from 'react-router-dom';
简单使用路由
import React, { Component } from 'react';
import Header from './components/header'
import store from './store'
import { Provider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
class App extends Component {
render(){
return (
<Provider store = {store}>
<div>
<Header/>
{/*定义路由作用区域*/}
<BrowserRouter>
<div>
{/* exact 表示当你匹配的path完完全全与route中的path相等时才可以匹配成功 */}
<Route path = '/' exact render = {() => {return <div>home</div>}}></Route>
{/*定义路由*/}
<Route path = '/detail' exact render = {() => {return <div>detail</div>}}></Route>
</div>
</BrowserRouter>
</div>
</Provider>
)
}
}
export default App;
通过路由返回 组件
import React, { Component } from 'react';
import Header from './components/header'
import store from './store'
import { Provider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import Home from './pages/home';
import Detail from './pages/detail';
class App extends Component {
render(){
return (
<Provider store = {store}>
<div>
<Header/>
<BrowserRouter>
<div>
{/* exact 表示当你匹配的path完完全全与route中的path相等时才可以匹配成功 */}
<Route path = '/' exact component = {Home}></Route>
<Route path = '/detail' exact component = {Detail}></Route>
</div>
</BrowserRouter>
</div>
</Provider>
)
}
}
export default App;
# 向 styled-components 组件中传值
styled-components
中页面向style.js
中定义的样式组件传值
,页面直接给对应的组件添加属性
,style.js
的组件里,可以通过${(props)=>(props.imgUrl)}
的方式获取到页面传递过来的值。
# immutable 中的 fromJS 与 List 方法
immutable
中除了fromJS
可以将一个数组转换为一个immutable
对象外,还有一个List
方法也能够将数组
转换为一个immutable
对象,但是这个list
方法只能够把数组的外层
变成immutable
对象,不能递归
将数组内的对象也变成immutable
对象。
# 不必拘于形式
UI
组件中允许
存在少量
的逻辑
,并不是什么 JS 都不能放在里面。
# 页面性能优化及路由跳转
# 页面性能优化
之前使用 shouldComponentUpdate
来检查
是否与当前页面状态
有关来决定是否重新渲
染当前组件
,return true
表示重绘
否则就不重绘
。
react
中有一个PureComponent
,它与Component
的区别是它内部
自动实现
了shouldComponentUpdate
这个生命周期内检查
功能,所以你只需要把所有的Component替换成PureComponent
就可以减少
每一个组件内
都写shouldComponentUpdate
来检查
了。
之所以可以使用PureComponent
来大大的提升性能
,因为框架
中的数据格式
都是统一
的,都是immutable
对象,就是因为这样所以使用PureComponent
才一点问题都没有。
如果数据格式不统一
那么你会遇到坑
。所以使用immutable很重要
。
如果你不使用 immutable,那么你就是用Component
然后自己写shouldComponentUpdate
吧。
# 路由跳转
单页应用
就是只会加载一次 html,整个页面都是路由js
文件来控制
的。重新加载 html 是比较耗性能
的。
如果你想使用 a 标签来进行页面跳转,请不要那样做,最好使用 react-route-dom
中的Link
来替换a
标签的跳转
功能,它会跳转
到真正的路由
中去,而不会先去重新加载 html 再跳转到真正的路由中去。
import {Link} from 'react-route-dom';
/* 不要使用a标签来进行页面跳转 */
<a href='/detail'>
...
</a>
/* 将上面的写法 改成这样的 */
<Link key = {index} to = '/detail'>
...
</Link>
# 页面路由参数的传递
动态路由
import {Link} from 'react-route-dom';
/* 跳转到详情页的时候 直接传递id过去 */
<Link key = {index} to = {'/detail/'+"5"}>
...
</Link>
/* 这种写法要改成下面这种写法了 */
<Route path = '/detail' exact component = {Detail} ></Route>
/* 传递参数就需要 设置一个占位 :id 不然匹配不到,因为这个是完全匹配 */
<Route path = '/detail/:id' exact component = {Detail} ></Route>
/* 页面中可以通过 match.params.id 来进行获取 */
console.log(this.props.match.params.id);//5
queryString的方式传递数据
import {Link} from 'react-route-dom';
/* 跳转到详情页的时候 直接传递?id=xx过去 */
<Link key = {index} to = {'/detail?id='+"5"}>
...
</Link>
/* 还是这种写法,依然可以进行匹配 */
<Route path = '/detail' exact component = {Detail} ></Route>
/* 页面中可以通过 location.search 来进行获取,但是需要你自己来解析一下 */
console.log(this.props.location.search);//?id=5
# 获取 styled.Components 中的 dom
styled.Components
中的dom
对原生dom
进行了包裹
,所以不再使用ref
而是使用 innerRef
。
/* 可以获取 styled.Components组件中包裹的input标签内的真实dom*/
<Input placeholder = "账号" innerRef = {(input) => {this.account = input}} />
/* 打印真实的账号文本框中的值 */
console.log(this.account.value);
# 路由跳转 Redirect
在组件中 直接进行路由跳转
import {Redirect} from 'react-router-dom';
render(){
return <Redirect to = "/" />
}
# 代码优化
合适的行间距,代码尾部加分号,switch
中代码过多,就写一个方法,把代码放到方法中,然后再 switch 的 case 中调用方法即可。
# 异步组件以及 witchRouter 路由方法的使用
页面中所有的 js 代码都在 bundle.js
中,这会造成这个js
文件异常的大
,所以需要使用第三方的异步组件
。
安装第三方异步组件 react-loadable
:yarn add react-loadable
使用react-loadable
/* 该组件目录下新建一个 loadble.js */
import Loadable from 'react-loadable';
import React from 'react';
const LoadableComponent = Loadable({
/* 要异步加载的 组件 ./ 表示当前目录下的 ./index.js 文件 */
loader: () => import('./'),
loading () { /* 异步加载时的 动画页面 */
return (<div>正在加载...</div>)
},
});
/* 将这个组件返回回去 */
export default class App extends React.Component {
render() {
return <LoadableComponent/>;
}
}
/* 你也可以返回一个无状态的组件回去 */
export default () => <LoadableComponent />
/* 原本是这样的 */
import Detail from './pages/detail'
<Route path = '/detail/:id' exact component = {Detail} />
/*现在改为 在App.js 文件中将引入的组件 改为引入这个异步组件 */
/*loadable 将原本引入的组件包装成了异步的组件 */
import Detail from './pages/detail/loadble.js'
<Route path='/detail/:id' exact component = {Detail} />
使用了react-loadable
之后就不能
够直接获取路由传递
过来的参数
了,这时候就需要使用react-router-dom
中的 withRouter
方法。
import {withRouter} from 'react-router-dom';
/* 原来是这样的 */
export connect(mapStateToPoprs, mapDispatchToPorps)(Detail);
/* 改为这样 让生成的容器组件 有能力获取传递过来的参数 */
export connect(mapStateToPoprs,mapDispatchToPorps)(withRouter(Detail));
使用react-loadable
会当对应的组件代码与bundle.js
分离,这样就能够大大减少bundle.js
中的代码了。
# 总结
本片文章是基于16.4的,最新的版本主要以16.8为标准吧,废除了一些生命周期函数,但主要的componentDidMount、shouldComponentUpdate、componentDidUpdate、componentWillUnmount都没变化。 加入react hooks,hooks的使用降低了理解成本,也让react组件更像是函数。它没有什么魔法,就是数组,还是两个数组,一个存所有state,一个存所有的state的setter。
# 常用的hooks就以下这些:
useState
用两个数组实现的,一个存所有的state,另一个存state的setter。
所以如果是在循环和判断的逻辑下用useState,会导致state和setter错乱,最好在函数顶部使用。
useEffect
治病的过程中产生了额外的影响。
useEffect 就是在你初始化之后,再次render之前,产生的额外影响。比如 数组的slice 和 splice函数。
useEffect 函数就是hooks中副作用函数,第一个参数就是callback函数,第二个参数是一个数组,就是你要监听的变量。如果你放一个空数组,它会起到一个componentDidMount的作用,没有监听任何变量,那么就不存在任何变化,那么就只会执行一次。
如果你想销毁state,可以在useEffect函数return的第一个函数。
useLayoutEffect
useEffect是在浏览器渲染后执行,useLayoutEffect是在浏览器渲染前执行。
官方不建议你是用它,但是官方还是出了这个hooks。
useLayoutEffect 主要在useEffect函数结果不满意的时候才被用到,一般是当处理dom改变带的副作用,这个hook执行时,浏览器并未对dom进行渲染,相对于useEffect执行要早。
useEffect 和 useLayoutEffect
useEffect 和 useLayoutEffect在mount和update阶段执行方法一样,传参不一样。
useEffect异步执行,而useLayoutEffect同步执行。
Effect 用 HookPassive 标记useEffect,用HookLayout标记useLayoutEffect。
useMemo
老的是memo,新的hooks中是useMemo。传递⼀个函数和依赖项,当依赖项发⽣变 化,执⾏函数,该函数需要返回⼀个值
类似于vue中的computed 计算属性,但是写法不同。
useCallback
返回一个函数,当监听的数据发生变化,才会执行回调函数。
useRef
相当于一个id的作用,id能够找真实dom,这个可以用来找虚拟dom,可以拿到fiber对象。
useReducer
redux、mbox是完整独立的状态管理方案。未必一定是为react打造的,它们只是和react进行了结合。react本身关注的是渲染UI和响应各种事件,为了提高效率,所以官方才出了这个hook。
聚合参数,以达到开发效率的提升以及代码的简洁。
useReducer在re-render时候不会改变存储位置,state作为props传给子component不会产生diff的效率问题(useMemo优化)
useReducer也许是替代useState,由于数据并未能共享,所以 并不能替代redux、mbox。
useContext
useContext能够产生一个Provider,能够使得你的Provider标签下面所有的组件共享一组数据。类似于一个局部的window哟。
自定义Hook
自定义的hook其实并不是hook,只是一个自定义的函数,只是它用了use标识开头,看起来像是符合了Hook的规则。
useXXX 和vue3的composition api很像。
# class和hook的对比
官方不考虑废除class。class只是一种编程方式,在16.0之后依然是使用fiber和新的diff算法,效率上并不比class差,提升效率使用的是fiber。
hooks延申的写法太多,也许页面中一堆useState,可能显得比较乱,有时很难搞清楚这些state之间的关联,当然你可以尝试根据相应的业务逻辑做一下聚合。容易发生内存泄露。
class语法虽然难以理解,都是代码逻辑比较清晰,它面向对象嘛。
hook往往作为那种单节点组件,比如几十个表单域。最后使用class来写一个整个表单的数据流的入口。
useEffect是浅监听。
# react性能优化
- 避免跨层级的移动
- sholudComponentUpdate
- Memo/useMemo
- useReducer
# 基础的项目上线流程大概这样
后端和前端定了一些接口之后,前端同学去写前端的代码,后端去写后端接口。
以后端是php为例
后端的开发目录一般在htdocs
下,接口在 htdocs 的 api 目录下,这里写各种各样的 php 代码去调用数据库。
前端可以将模拟的api
文件夹下的数据删除掉了,因为后端同学已经把接口写好了。
前端同学 使用npm run build
命令,打包所有的文件到一个build
目录下,这时候前端的任务已经完成了。
后端的同学 就会把build
目录的文件全部粘贴到 htdocs 目录下,这时候前端的代码就已经放到后端的项目中了,这个时候就完成了项目的上线。
当前端的项目放到了后端的项目里面,这时候就应该去访问后端的项目了。
后端开启服务器,你去访问,成功访问,因为你之前弄的 api 目录和后端的 api 目录一致,所以就可以直接运行了。