对React SSR的理解
服务端渲染是数据与模版组成的html,即 HTML = 数据 + 模版。将组件或页面通过服务器生成html字符串,再发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。页面没使用服务渲染,当请求页面时,返回的body里为空,之后执行js将html结构注入到body里,结合css显示出来;
SSR的优势:
- 对SEO友好
- 所有的模版、图片等资源都存在服务器端
- 一个html返回所有数据
- 减少HTTP请求
- 响应快、用户体验好、首屏渲染快
1)更利于SEO
不同爬虫工作原理类似,只会爬取源码,不会执行网站的任何脚本使用了React或者其它MVVM框架之后,页面大多数DOM元素都是在客户端根据js动态生成,可供爬虫抓取分析的内容大大减少。另外,浏览器爬虫不会等待我们的数据完成之后再去抓取页面数据。服务端渲染返回给客户端的是已经获取了异步数据并执行JavaScript脚本的最终HTML,网络爬中就可以抓取到完整页面的信息。
2)更利于首屏渲染
首屏的渲染是node发送过来的html字符串,并不依赖于js文件了,这就会使用户更快的看到页面的内容。尤其是针对大型单页应用,打包后文件体积比较大,普通客户端渲染加载所有所需文件时间较长,首页就会有一个很长的白屏等待时间。
SSR的局限:
1)服务端压力较大
本来是通过客户端完成渲染,现在统一到服务端node服务去做。尤其是高并发访问的情况,会大量占用服务端CPU资源;
2)开发条件受限
在服务端渲染中,只会执行到componentDidMount之前的生命周期钩子,因此项目引用的第三方的库也不可用其它生命周期钩子,这对引用库的选择产生了很大的限制;
3)学习成本相对较高 除了对webpack、MVVM框架要熟悉,还需要掌握node、 Koa2等相关技术。相对于客户端渲染,项目构建、部署过程更加复杂。
时间耗时比较:
1)数据请求
由服务端请求首屏数据,而不是客户端请求首屏数据,这是"快"的一个主要原因。服务端在内网进行请求,数据响应速度快。客户端在不同网络环境进行数据请求,且外网http请求开销大,导致时间差
- 客户端数据请求
- 服务端数据请求
2)html渲染 服务端渲染是先向后端服务器请求数据,然后生成完整首屏 html返回给浏览器;而客户端渲染是等js代码下载、加载、解析完成后再请求数据渲染,等待的过程页面是什么都没有的,就是用户看到的白屏。就是服务端渲染不需要等待js代码下载完成并请求数据,就可以返回一个已有完整数据的首屏页面。
- 非ssr html渲染
- ssr html渲染
Redux 中异步的请求怎么处理
可以在 componentDidmount 中直接进⾏请求⽆须借助redux。但是在⼀定规模的项⽬中,上述⽅法很难进⾏异步流的管理,通常情况下我们会借助redux的异步中间件进⾏异步处理。redux异步流中间件其实有很多,当下主流的异步中间件有两种redux-thunk、redux-saga。
(1)使用react-thunk中间件
redux-thunk优点:
- 体积⼩: redux-thunk的实现⽅式很简单,只有不到20⾏代码
- 使⽤简单: redux-thunk没有引⼊像redux-saga或者redux-observable额外的范式,上⼿简单
redux-thunk缺陷:
- 样板代码过多: 与redux本身⼀样,通常⼀个请求需要⼤量的代码,⽽且很多都是重复性质的
- 耦合严重: 异步操作与redux的action偶合在⼀起,不⽅便管理
- 功能孱弱: 有⼀些实际开发中常⽤的功能需要⾃⼰进⾏封装
使用步骤:
- 配置中间件,在store的创建中配置
import {createStore, applyMiddleware, compose} from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk'
// 设置调试工具
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
// 设置中间件
const enhancer = composeEnhancers(
applyMiddleware(thunk)
);
const store = createStore(reducer, enhancer);
export default store;
- 添加一个返回函数的actionCreator,将异步请求逻辑放在里面
/** 发送get请求,并生成相应action,更新store的函数 @param url {string} 请求地址 @param func {function} 真正需要生成的action对应的actionCreator @return {function} */
// dispatch为自动接收的store.dispatch函数
export const getHttpAction = (url, func) => (dispatch) => {
axios.get(url).then(function(res){
const action = func(res.data)
dispatch(action)
})
}
- 生成action,并发送action
componentDidMount(){
var action = getHttpAction('/getData', getInitTodoItemAction)
// 发送函数类型的action时,该action的函数体会自动执行
store.dispatch(action)
}
(2)使用redux-saga中间件
redux-saga优点:
- 异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中
- action摆脱thunk function: dispatch 的参数依然是⼀个纯粹的 action (FSA),⽽不是充满 “⿊魔法” thunk function
- 异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
- 功能强⼤: redux-saga提供了⼤量的Saga 辅助函数和Effect 创建器供开发者使⽤,开发者⽆须封装或者简单封装即可使⽤
- 灵活: redux-saga可以将多个Saga可以串⾏/并⾏组合起来,形成⼀个⾮常实⽤的异步flow
- 易测试,提供了各种case的测试⽅案,包括mock task,分⽀覆盖等等
redux-saga缺陷:
- 额外的学习成本: redux-saga不仅在使⽤难以理解的 generator function,⽽且有数⼗个API,学习成本远超redux-thunk,最重要的是你的额外学习成本是只服务于这个库的,与redux-observable不同,redux-observable虽然也有额外学习成本但是背后是rxjs和⼀整套思想
- 体积庞⼤: 体积略⼤,代码近2000⾏,min版25KB左右
- 功能过剩: 实际上并发控制等功能很难⽤到,但是我们依然需要引⼊这些代码
- ts⽀持不友好: yield⽆法返回TS类型
redux-saga可以捕获action,然后执行一个函数,那么可以把异步代码放在这个函数中,使用步骤如下:
- 配置中间件
import {createStore, applyMiddleware, compose} from 'redux';
import reducer from './reducer';
import createSagaMiddleware from 'redux-saga'
import TodoListSaga from './sagas'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const sagaMiddleware = createSagaMiddleware()
const enhancer = composeEnhancers(
applyMiddleware(sagaMiddleware)
);
const store = createStore(reducer, enhancer);
sagaMiddleware.run(TodoListSaga)
export default store;
- 将异步请求放在sagas.js中
import {takeEvery, put} from 'redux-saga/effects'
import {initTodoList} from './actionCreator'
import {GET_INIT_ITEM} from './actionTypes'
import axios from 'axios'
function* func(){
try{
// 可以获取异步返回数据
const res = yield axios.get('/getData')
const action = initTodoList(res.data)
// 将action发送到reducer
yield put(action)
}catch(e){
console.log('网络请求失败')
}
}
function* mySaga(){
// 自动捕获GET_INIT_ITEM类型的action,并执行func
yield takeEvery(GET_INIT_ITEM, func)
}
export default mySaga
- 发送action
componentDidMount(){
const action = getInitTodoItemAction()
store.dispatch(action)
}
为什么虚拟DOM会提高性能
虚拟DOM 相当于在js 和 真实DOM中间加了一个缓存,利用DOM Diff 算法避免了没有必要的DOM操作,从而提高性能
React-Router 4怎样在路由变化时重新渲染同一个组件?
当路由变化时,即组件的props发生了变化,会调用componentWillReceiveProps等生命周期钩子。那需要做的只是: 当路由改变时,根据路由,也去请求数据:
代码语言:javascript复制class NewsList extends Component {
componentDidMount () {
this.fetchData(this.props.location);
}
fetchData(location) {
const type = location.pathname.replace('/', '') || 'top'
this.props.dispatch(fetchListData(type))
}
componentWillReceiveProps(nextProps) {
if (nextProps.location.pathname != this.props.location.pathname) {
this.fetchData(nextProps.location);
}
}
render () {
...
}
}
利用生命周期componentWillReceiveProps,进行重新render的预处理操作。
state 是怎么注入到组件的,从 reducer 到组件经历了什么样的过程
通过connect和mapStateToProps将state注入到组件中:
代码语言:javascript复制import { connect } from 'react-redux'
import { setVisibilityFilter } from '@/reducers/Todo/actions'
import Link from '@/containers/Todo/components/Link'
const mapStateToProps = (state, ownProps) => ({
active: ownProps.filter === state.visibilityFilter
})
const mapDispatchToProps = (dispatch, ownProps) => ({
setFilter: () => {
dispatch(setVisibilityFilter(ownProps.filter))
}
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Link)
上面代码中,active就是注入到Link组件中的状态。 mapStateToProps(state,ownProps)中带有两个参数,含义是∶
- state-store管理的全局状态对象,所有都组件状态数据都存储在该对象中。
- ownProps 组件通过props传入的参数。
reducer 到组件经历的过程:
- reducer对action对象处理,更新组件状态,并将新的状态值返回store。
- 通过connect(mapStateToProps,mapDispatchToProps)(Component)对组件 Component进行升级,此时将状态值从store取出并作为props参数传递到组件。
高阶组件实现源码∶
代码语言:javascript复制import React from 'react'
import PropTypes from 'prop-types'
// 高阶组件 contect
export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
class Connect extends React.Component {
// 通过对context调用获取store
static contextTypes = {
store: PropTypes.object
}
constructor() {
super()
this.state = {
allProps: {}
}
}
// 第一遍需初始化所有组件初始状态
componentWillMount() {
const store = this.context.store
this._updateProps()
store.subscribe(() => this._updateProps()); // 加入_updateProps()至store里的监听事件列表
}
// 执行action后更新props,使组件可以更新至最新状态(类似于setState)
_updateProps() {
const store = this.context.store;
let stateProps = mapStateToProps ?
mapStateToProps(store.getState(), this.props) : {} // 防止 mapStateToProps 没有传入
let dispatchProps = mapDispatchToProps ?
mapDispatchToProps(store.dispatch, this.props) : {
dispatch: store.dispatch
} // 防止 mapDispatchToProps 没有传入
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}
render() {
return <WrappedComponent {...this.state.allProps} />
}
}
return Connect
}
高阶组件的应用场景
权限控制
利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别 和 页面元素级别
代码语言:javascript复制// HOC.js
function withAdminAuth(WrappedComponent) {
return class extends React.Component {
state = {
isAdmin: false,
}
async componentWillMount() {
const currentRole = await getCurrentUserRole();
this.setState({
isAdmin: currentRole === 'Admin',
});
}
render() {
if (this.state.isAdmin) {
return <WrappedComponent {...this.props} />;
} else {
return (<div>您没有权限查看该页面,请联系管理员!</div>);
}
}
};
}
// 使用
// pages/page-a.js
class PageA extends React.Component {
constructor(props) {
super(props);
// something here...
}
componentWillMount() {
// fetching data
}
render() {
// render page with data
}
}
export default withAdminAuth(PageA);
可能你已经发现了,高阶组件其实就是装饰器模式在 React 中的实现:通过给函数传入一个组件(函数或类)后在函数内部对该组件(函数或类)进行功能的增强(不修改传入参数的前提下),最后返回这个组件(函数或类),即允许向一个现有的组件添加新的功能,同时又不去修改该组件,属于 包装模式(Wrapper Pattern) 的一种。
什么是装饰者模式:在不改变对象自身的前提下在程序运行期间动态的给对象添加一些额外的属性或行为
可以提高代码的复用性和灵活性。
再对高阶组件进行一个小小的总结:
- 高阶组件 不是组件,是 一个把某个组件转换成另一个组件的 函数
- 高阶组件的主要作用是 代码复用
- 高阶组件是 装饰器模式在 React 中的实现
封装组件的原则
封装原则
1、单一原则:负责单一的页面渲染
2、多重职责:负责多重职责,获取数据,复用逻辑,页面渲染等
3、明确接受参数:必选,非必选,参数尽量设置以_开头,避免变量重复
4、可扩展:需求变动能够及时调整,不影响之前代码
5、代码逻辑清晰
6、封装的组件必须具有高性能,低耦合的特性
7、组件具有单一职责:封装业务组件或者基础组件,如果不能给这个组件起一个有意义的名字,证明这个组件承担的职责可能不够单一,需要继续抽组件,直到它可以是一个独立的组件即可
如何避免组件的重新渲染?
React 中最常见的问题之一是组件不必要地重新渲染。React 提供了两个方法,在这些情况下非常有用:
React.memo()
:这可以防止不必要地重新渲染函数组件PureComponent
:这可以防止不必要地重新渲染类组件 这两种方法都依赖于对传递给组件的props
的浅比较,如果props
没有改变,那么组件将不会重新渲染。虽然这两种工具都非常有用,但是浅比较会带来额外的性能损失,因此如果使用不当,这两种方法都会对性能产生负面影响。 通过使用 React Profiler,可以在使用这些方法前后对性能进行测量,从而确保通过进行给定的更改来实际改进性能。
react性能优化方案
- 重写
shouldComponentUpdate
来避免不必要的dom操作 - 使用
production
版本的react.js
- 使用
key
来帮助React
识别列表中所有子组件的最小变化
何为受控组件(controlled component)
在 HTML 中,类似 <input>
, <textarea>
和 <select>
这样的表单元素会维护自身的状态,并基于用户的输入来更新。当用户提交表单时,前面提到的元素的值将随表单一起被发送。但在 React 中会有些不同,包含表单元素的组件将会在 state 中追踪输入的值,并且每次调用回调函数时,如 onChange
会更新 state,重新渲染组件。一个输入表单元素,它的值通过 React 的这种方式来控制,这样的元素就被称为"受控元素"。
react-router 里的 Link 标签和 a 标签的区别
从最终渲染的 DOM 来看,这两者都是链接,都是 标签,区别是∶ <Link>
是react-router 里实现路由跳转的链接,一般配合<Route>
使用,react-router接管了其默认的链接跳转行为,区别于传统的页面跳转,<Link>
的“跳转”行为只会触发相匹配的<Route>
对应的页面内容更新,而不会刷新整个页面。
<Link>
做了3件事情:
- 有onclick那就执行onclick
- click的时候阻止a标签默认事件
- 根据跳转href(即是to),用history (web前端路由两种方式之一,history & hash)跳转,此时只是链接变了,并没有刷新页面而
<a>
标签就是普通的超链接了,用于从当前页面跳转到href指向的另一 个页面(非锚点情况)。
a标签默认事件禁掉之后做了什么才实现了跳转?
代码语言:javascript复制let domArr = document.getElementsByTagName('a')
[...domArr].forEach(item=>{
item.addEventListener('click',function () {
location.href = this.href
})
})
react 和 vue的共同点和区别
相同点
- 两者都是用了虚拟dom
- 都鼓励使用组件化应用
- 都可以通过cli 快速创建项目,也都有自己的状态管理工具
- 支持数据驱动试图
- 都支持服务端渲染
不同点
- 设计思想不同
react 是函数式思想,使用jsx语法,all in js
vue 是响应式思想,也是基于数据可变的,把html css js组合到一起
- 渲染方式不同
react 默认状态改变时会重新渲染所有子组件(当然也可以在shouldCompoentUpdate生命周期中控制不更新)
vue 在渲染过程中会跟踪每一个组件的依赖关系,不需要渲染整个组件树
- 性能不同
react 适合大中型项目
vue 使用中小型项目
redux的三大原则
- 单一数据源
整个应用的state被存储在一个object tree中,并且这个object tree 之存在唯一一个store中
- state是只读的
唯一改变state的方式是触发action,action是一个用于描述已经发生时间的对象,这个保证了视图和网络请求都不能直接修改state,相反他们只能表达想要修改的意图
- 使用纯函数来执行修改state 为了描述action如何改变state tree 需要编写reduce
React 数据持久化有什么实践吗?
封装数据持久化组件:
代码语言:javascript复制let storage={
// 增加
set(key, value){
localStorage.setItem(key, JSON.stringify(value));
},
// 获取
get(key){
return JSON.parse(localStorage.getItem(key));
},
// 删除
remove(key){
localStorage.removeItem(key);
}
};
export default Storage;
在React项目中,通过redux存储全局数据时,会有一个问题,如果用户刷新了网页,那么通过redux存储的全局数据就会被全部清空,比如登录信息等。这时就会有全局数据持久化存储的需求。首先想到的就是localStorage,localStorage是没有时间限制的数据存储,可以通过它来实现数据的持久化存储。
但是在已经使用redux来管理和存储全局数据的基础上,再去使用localStorage来读写数据,这样不仅是工作量巨大,还容易出错。那么有没有结合redux来达到持久数据存储功能的框架呢?当然,它就是redux-persist。redux-persist会将redux的store中的数据缓存到浏览器的localStorage中。其使用步骤如下:
(1)首先要安装redux-persist:
代码语言:javascript复制npm i redux-persist
(2)对于reducer和action的处理不变,只需修改store的生成代码,修改如下:
代码语言:javascript复制import {createStore} from 'redux'
import reducers from '../reducers/index'
import {persistStore, persistReducer} from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
const persistConfig = {
key: 'root',
storage: storage,
stateReconciler: autoMergeLevel2 // 查看 'Merge Process' 部分的具体情况
};
const myPersistReducer = persistReducer(persistConfig, reducers)
const store = createStore(myPersistReducer)
export const persistor = persistStore(store)
export default store
(3)在index.js中,将PersistGate标签作为网页内容的父标签:
代码语言:javascript复制import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux'
import store from './redux/store/store'
import {persistor} from './redux/store/store'
import {PersistGate} from 'redux-persist/lib/integration/react';
ReactDOM.render(<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
{/*网页内容*/} </PersistGate>
</Provider>, document.getElementById('root'));
这就完成了通过redux-persist实现React持久化本地数据存储的简单应用。
了解redux吗?
- redux 是一个应用数据流框架,主要解决了组件之间状态共享问题,原理是集中式管理,主要有三个核心方法:action store reduce
- 工作流程 view 调用store的dispatch 接受action传入的store,reduce进行state操作
view通过store提供的getState获取最新的数据
- redux的优点:
新增的state 对状态的管理更加明确
流程更加规范,减少手动编写代码,提高编码效率
- redux的缺点:
当数据更新是有时候组件不需要,也要重新绘制,影响效率
refs 是什么
- refs是react中引用的简写,有主语存储特定 React 元素或组件的引用的属性,它将由组件渲染配置函数返回
- 当我们需要输入框的内容,触发动画等时候可以使用refs
什么是纯函数?
纯函数是不依赖并且不会在其作用域之外修改变量状态的函数。本质上,纯函数始终在给定相同参数的情况下返回相同结果。
React和vue.js的相似性和差异性是什么?
相似性如下。
(1)都是用于创建UI的 JavaScript库。
(2)都是快速和轻量级的代码库(这里指 React核心库)。
(3)都有基于组件的架构。
(4)都使用虚拟DOM。
(5)都可以放在单独的HTML文件中,或者放在 Webpack设置的一个更复杂的模块中。
(6)都有独立但常用的路由器和状态管理库。
它们最大的区别在于 Vue. js通常使用HTML模板文件,而 React完全使用 JavaScript创建虚拟DOM。 Vue. js还具有对于“可变状态”的“ reactivity”的重新渲染的自动化检测系统。
何为 reducer
一个 reducer 是一个纯函数,该函数以先前的 state 和一个 action 作为参数,并返回下一个 state。
在React中遍历的方法有哪些?
(1)遍历数组:map && forEach
代码语言:javascript复制import React from 'react';
class App extends React.Component {
render() {
let arr = ['a', 'b', 'c', 'd'];
return (
<ul>
{
arr.map((item, index) => {
return <li key={index}>{item}</li>
})
}
</ul>
)
}
}
class App extends React.Component {
render() {
let arr = ['a', 'b', 'c', 'd'];
return (
<ul>
{
arr.forEach((item, index) => {
return <li key={index}>{item}</li>
})
}
</ul>
)
}
}
(2)遍历对象:map && for in
代码语言:javascript复制class App extends React.Component {
render() {
let obj = {
a: 1,
b: 2,
c: 3
}
return (
<ul>
{
(() => {
let domArr = [];
for(const key in obj) {
if(obj.hasOwnProperty(key)) {
const value = obj[key]
domArr.push(<li key={key}>{value}</li>)
}
}
return domArr;
})()
}
</ul>
)
}
}
// Object.entries() 把对象转换成数组
class App extends React.Component {
render() {
let obj = {
a: 1,
b: 2,
c: 3
}
return (
<ul>
{
Object.entries(obj).map(([key, value], index) => { // item是一个数组,把item解构,写法是[key, value]
return <li key={key}>{value}</li>
})
}
</ul>
)
}
}
受控组件、非受控组件
- 受控组件就是改变受控于数据的变化,数据变了页面也变了。受控组件更合适,数据驱动是react核心
- 非受控组件不是通过数据控制页面内容