(20)性能优化和项目上线——PureComponent、异步组件、withRouter 方法 | React.js 项目实战:PC 端“简书”开发
转载请注明出处,未经同意,不可修改文章内容。
🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。
1 PureComponent
由于我们项目的每个组件都调用了“connect 方法”和 store 作“连接”。
❌这会无形中产生一个问题: 只要 store 中的任意“数据”发生改变,那么整个项目的每一个组件都会被重新地“渲染”(即,render 函数都会被重新执行)!
而实际上,有些“数据”发生改变,其实和某些组件是不相关的。若都无条件地重新渲染,无疑会增加很多无意义的“虚拟 DOM”比对,从而影响页面的性能。
- ❓怎么解决这个问题呢?
由“前置知识”可知,我们可以用 shouldComponentUpdate 这个“生命周期函数”来写一些性能优化相关的代码——只有当“变化”的“数据”与本组件相关,本组件才重新渲染;否则,不重新渲染。
🏆幸运的是,React 也考虑到了这个问题,为了不让我们在每个组件都手动去增加 shouldComponentUpdate 的相关判断,React Fiber(React16 和之后的版本)为我们提供了一个内置的组件 PureComponent(纯组件)。它在底层为我们实现了一个 shouldComponentUpdate,大大减少了我们的工作量!
❗️❗️❗️但要注意:之所以我们可以用 PureComponent,是因为我们的项目里对“数据”管理时,使用了一个框架——immutable.js。
如果,你在之后的项目中未使用 immutable.js 来管理“数据”,则不要随意用 PureComponent!取而代之,请自行去写 shouldComponentUpdate 来优化性能。否则,会出现莫名的 bug。
- ❓怎么使用 PureComponent 呢?
答:很简单,把每个组件 index.js
文件开头的 Component
替换为 PureComponent
。
如,打开 pages 目录下 detail 文件夹中的 index.js
文件:
import React, {PureComponent} from "react"; // ❗️❗️❗️
import {
DetailWrapper,
Header,
Content
} from "./style.js";
import {connect} from "react-redux";
import {actionCreators} from "./store";
class Detail extends PureComponent { // ❗️❗️❗️
render() {
return(
<DetailWrapper>
<Header>{this.props.title}</Header>
<Content
dangerouslySetInnerHTML={{__html: this.props.content}}
/>
</DetailWrapper>
)
}
componentDidMount() {
this.props.getDetail(this.props.match.params.id);
}
}
const mapStateToProps = (state) => {
return {
title: state.getIn(["detail", "title"]),
content: state.getIn(["detail", "content"])
}
}
const mapDispatchToProps = (dispatch) => {
return {
getDetail(id) {
const action = actionCreators.getDetailData(id);
dispatch(action)
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Detail);
💡篇幅原因,其他各组件,大家自行在代码中修改即可。
2 “异步组件”和 withRouter 路由方法
❌我们直接来观察一个问题:
从视频的操作中我们可以看到:当我们刷新整个页面时,控制台给我们显示出,项目瞬间就同时加载了 5 个 .js
文件。然后,当我们后续再进“详情页”、“登录页”、“写文章”页时,项目就不再为我们加载 .js
文件了。
什么意思呢?它表示的就是,就目前我们的项目而言,只要页面刷新,不管你需不需要,它都会将整个项目“组件”的 .js
文件全部加载出来。
现在项目不复杂、代码量不多,倒看不出来差异。实际工作中,商业级项目的大篇幅代码,如此这般,必然带来严重的性能问题!
- ❓怎么解决呢?
这就需要引入“异步组件”的概念。
简单来说,就是通过一些方法,让“组件”按需加载——你不要一股脑的给我加载完,我访问“首页”,你就给我“首页”的代码;我点击某个“详情页”时,你再给我加载详情页的 .js
文件。
由于“异步组件”的底层稍微复杂,前期我们还不需要去研究它,先考虑用“工具”解决这个痛点才是当务之急!
🏆幸运的是,“先驱者”们已为我们准备好了一个第三方“模块”——react-loadable。
- ❓怎么用?
答:
1️⃣打开 react-loadable 官方文档:
2️⃣在 qdywxs-jianshu
项目中安装这个“模块”:
3️⃣我们现在的需求是——“详情页”的代码,只有进入到“详情页”才加载。因此,我们需要在“详情页”中使用 react-loadable。
3️⃣-①:在 pages 目录下的 detail 文件夹下创建一个 loadable.js
文件;
import Loadable from 'react-loadable';
/* 3️⃣-③:先删除下面这行代码,我们暂时不做详细的“Loding”组件;
import Loading from './my-loading-component';
*/
// ❗️3️⃣-⑦:引入 React,以便使用 JSX 语法;
import React from "react";
const LoadableComponent = Loadable({ // 3️⃣-③:这行代码创建了一个“异步组件”;
/*
3️⃣-④:下面这行代码指,将要把哪个“组件”放到这个“异步组件”中进行“异步”加载。
先注释掉下面这行代码,因为我们要加载的当前目录下的 index.js(即,Detail 组件)~
loader: () => import('./my-component'),
*/
loader: () => import("./"), /*
❗️❗️❗️注意这里边的 import 和最上边的“import 引入”
是不一样的东西。这个 import 是“异步加载”的一个新语法!
*/
/*
3️⃣-⑤:下边这个 loading 是指,当我们从“首页”进入“详情页”,
页面需要加载这个“详情页”,“加载”的过程中,我们可以在页面上
临时显示一些内容,如“别慌,我正在加载~”等;
注释掉下面这行代码,我们没单独写这个“Loading 组件”,而是直接用 JSX 写一些“内容”即可。
loading: Loading,
*/
loading() {
// 3️⃣-⑥:下边就要用到 JSX 的语法,所以我们必须先去上边把 React 引入进来;
return(
// 3️⃣-⑧:接下来就可以用 JSX 语法来写加载过程中“临时显示”的一些内容;
<div>别慌,我正在加载~</div>
)
}
});
/*
3️⃣-⑨:最后,导出了一个“无状态组件”。我们可以通过“无状态”组件的特性来简写下边的代码,
以提高性能;
export default class App extends React.Component {
render() {
return <LoadableComponent/>;
}
}
*/
export default () => <LoadableComponent/>
通过以上操作后,“Detail 组件”就变成了一个“异步组件”,它就可以实现“异步加载”的需求。
4️⃣继续修改,打开 src 目录下的 App.js
文件:
import React, { PureComponent } from "react";
import {GlobalStyle} from "./style";
import {GlobalIconStyle} from "./statics/iconfont/iconfont";
import {BrowserRouter, Route} from "react-router-dom";
import Header from "./common/header";
import Home from "./pages/home";
/*
❗️❗️❗️4️⃣-①:以前,我们是从 detail 页面引入 Detail 组件,然后
在下边直接加载 Detail 组件。
但,现在我们需要去加载“异步组件”,故引入的路径要变为 ./pages/detail/loadable.js。
从这个“路径”引入的“组件”为“异步组件”!
注释掉下面这行代码~
import Detail from "./pages/detail";
*/
import Detail from "./pages/detail/loadable.js";
import Login from "./pages/login";
import Write from "./pages/write";
import { Provider } from "react-redux";
import store from "./store";
class App extends PureComponent {
render() {
return (
<div>
<GlobalStyle />
<GlobalIconStyle />
<Provider store={store}>
<BrowserRouter>
<div>
<Header />
<Route path="/" exact component={Home}></Route>
{/* 4️⃣-②:通过上边的一通操作,下边加载的“Detail 组件”就是一个“异步组件”! */}
<Route path="/detail/:id" exact component={Detail}></Route>
<Route path="/login" exact component={Login}></Route>
<Route path="/write" exact component={Write}></Route>
</div>
</BrowserRouter>
</Provider>
</div>
);
}
}
export default App;
返回页面查看(❌会报出一个“错误”——组件获取不到路由参数!):
❓5️⃣为什么会报这个错误呢,怎么解决呢?
5️⃣-①:打开 src 目录下的 App.js
文件;
import React, { PureComponent } from "react";
import {GlobalStyle} from "./style";
import {GlobalIconStyle} from "./statics/iconfont/iconfont";
import {BrowserRouter, Route} from "react-router-dom";
import Header from "./common/header";
import Home from "./pages/home";
// ❗️5️⃣-②:这里引入的是一个“异步组件”;
import Detail from "./pages/detail/loadable.js";
import Login from "./pages/login";
import Write from "./pages/write";
import { Provider } from "react-redux";
import store from "./store";
class App extends PureComponent {
render() {
return (
<div>
<GlobalStyle />
<GlobalIconStyle />
<Provider store={store}>
<BrowserRouter>
<div>
<Header />
<Route path="/" exact component={Home}></Route>
{/*
❗️❗️❗️5️⃣-③:还记得 Route 是什么东西吗?
Route 就是“一条条的路由规则”:
path 指页面要跳转的路径;
exact 指“路径”必须“精确”地匹配才“跳转”;
component 指将“渲染”的内容替换为等号后边的“组件”。
❗️❗️❗️发现没有?由于上一步引入的是“异步组件”,那这里“渲染”的也就是
上边 loadable.js 中转换后的“异步”Detail 组件。简言之,这里的 Route
对应的是“异步”Detail 组件。
既然如此,pages 目录下 detail 文件夹里的 index.js 文件中的“Detail 组件”
是获取不到 Route 里的 id 参数的!
*/}
<Route path="/detail/:id" exact component={Detail}></Route>
<Route path="/login" exact component={Login}></Route>
<Route path="/write" exact component={Write}></Route>
</div>
</BrowserRouter>
</Provider>
</div>
);
}
}
export default App;
5️⃣-③:打开报错的文件——pages 目录下 detail 文件夹里的 index.js
文件。
import React, {PureComponent} from "react";
import {
DetailWrapper,
Header,
Content
} from "./style.js";
import {connect} from "react-redux";
import {actionCreators} from "./store";
// 🏆🏆🏆5️⃣-⑤:幸运的是,react-router-dom 为我们提供了一个 withRouter 方法;
import {withRouter} from "react-router-dom";
class Detail extends PureComponent {
render() {
return(
<DetailWrapper>
<Header>{this.props.title}</Header>
<Content
dangerouslySetInnerHTML={{__html: this.props.content}}
/>
</DetailWrapper>
)
}
componentDidMount() {
this.props.getDetail(this.props.match.params.id); /*
5️⃣-④:既然这个文件里的
“Detail 组件”获取不到
Route 里的 id 参数,那
页面肯定就会报错了;
❓怎么解决呢?
怎样才能使这个文件的“Detail 组件
获取到 Route 里的“参数”和内容呢?
*/
}
}
const mapStateToProps = (state) => {
return {
title: state.getIn(["detail", "title"]),
content: state.getIn(["detail", "content"])
}
}
const mapDispatchToProps = (dispatch) => {
return {
getDetail(id) {
const action = actionCreators.getDetailData(id);
dispatch(action)
}
}
}
/*
5️⃣-⑥:然后用 withRouter 改写下边代码,使本文件的“Detail 组件”也能够
获取到 Route 里的“参数”和内容!
export default connect(mapStateToProps, mapDispatchToProps)(Detail);
*/
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Detail));
返回页面查看(成功解决 bug,且“详情页”异步加载了):
3 关于代码的优化
OK,先由衷恭喜你学到此处🆙🆙🆙!
随着本篇的结束,整个“简书”项目的逻辑代码就算告一段落了。后续大伙儿可以跟着我们这期间反复练习的“套路”,自行拓展这个项目。我相信,如果你是完完整整认真跟下来的话,你肯定有信心和能力接手真实工作中的中、大型项目。
❗️当然,代码中还有一些需要优化的点,我为了把知识点和流程讲的更清楚,故代码的有些地方会显得“啰嗦”。
3.1 之前提过的“优化”的地方
我们之前就提过的需要“优化”的点(主要是 ES6 语法相关),这里依然适用。大的方向还是那些地方(小的方向,就是格式的一些优化,如“分号”的添加,“换行符”的添加等),所以希望你融汇贯通后,在实际工作中适当去注意那些优化的点,尽量写出优美的代码。
现实工作中,团队几乎都有自己的一套编码规范或要求,平时多严格要求自己,规范化地写代码,你一定能走得更远!
3.2 reducer.js
中用“switch 语句”替换“多条 if 语句”
在每个组件的 reducer.js
文件中,都用到了多条 if 语句作条件判断。我这样写是为了更好的讲解逻辑,但实际编码过程中,请用“switch 语句”作相应替换,以节省性能!
🔗前置知识:《JavaScript 初识——④ 流程控制语句》——switch 语句~
如,打开 pages 目录下 home 文件夹中 store 里的 reducer.js
文件:
import {fromJS} from "immutable";
import {INIT_HOME_DATA, ADD_HOME_DATA, CHANGE_SHOW_TO_TOP} from "./actionTypes";
const defaultState = fromJS({
labelList: [],
articleList: [],
panelsList: [],
articlePage: 1,
showToTop: false
})
export default (state=defaultState, action) => {
/*
❗️❗️❗️1️⃣将以下的“if 语句”用“switch 语句”改写:!
if(action.type === INIT_HOME_DATA) {
return state.merge({
labelList: action.labelList,
articleList: action.articleList,
panelsList: action.panelsList
})
};
if(action.type === ADD_HOME_DATA) {
return state.merge({
"articleList": state.get("articleList").concat(action.moreArticleList),
"articlePage": action.nextPage
})
};
if(action.type === CHANGE_SHOW_TO_TOP) {
return state.set("showToTop", action.show);
}
return state;
*/
// 🏆🏆🏆2️⃣改写为:
switch(action.type) {
case INIT_HOME_DATA:
return state.merge({
labelList: action.labelList,
articleList: action.articleList,
panelsList: action.panelsList
});
case ADD_HOME_DATA:
return state.merge({
"articleList": state.get("articleList").concat(action.moreArticleList),
"articlePage": action.nextPage
});
case CHANGE_SHOW_TO_TOP:
return state.set("showToTop", action.show);
default:
return state;
}
};
❗️3️⃣上边改写后的代码依然不是最优的代码,如你所见,前两个 case 里的逻辑代码还是太多了。实际编码过程中,我们要有意识地将这些大片大片的代码抽象成函数来调用:
import {fromJS} from "immutable";
import {INIT_HOME_DATA, ADD_HOME_DATA, CHANGE_SHOW_TO_TOP} from "./actionTypes";
const defaultState = fromJS({
labelList: [],
articleList: [],
panelsList: [],
articlePage: 1,
showToTop: false
})
// 3️⃣-②:在这里分别编写 initHomeData 和 addHomeData 函数;
const initHomeData = (state, action) => {
return state.merge({
labelList: action.labelList,
articleList: action.articleList,
panelsList: action.panelsList
});
};
const addHomeData = (state, action) => {
return state.merge({
"articleList": state.get("articleList").concat(action.moreArticleList),
"articlePage": action.nextPage
});
}
// 3️⃣-①:将前两个 case 里的代码抽象成“函数”;
export default (state=defaultState, action) => {
switch(action.type) {
case INIT_HOME_DATA:
/*
3️⃣-①-1:抽象成 initHomeData 函数,并调用;
return state.merge({
labelList: action.labelList,
articleList: action.articleList,
panelsList: action.panelsList
});
*/
return initHomeData(state, action);
case ADD_HOME_DATA:
/*
3️⃣-①-2:抽象成 addHomeData 函数,并调用;
return state.merge({
"articleList": state.get("articleList").concat(action.moreArticleList),
"articlePage": action.nextPage
});
*/
return addHomeData(state, action);
case CHANGE_SHOW_TO_TOP:
return state.set("showToTop", action.show);
default:
return state;
}
};
💡篇幅原因,我就不挨个去优化这些代码了,编程重要的是思路,掌握好思路,事半功倍!
❗️❗️❗️但要注意:实际编码中,我并不提倡一开始就各种想优化代码、提高性能。拿到一个需求,你我首要做的都应该是先去实现这个需求,需求实现后,再考虑函数封装、性能优化的东西。
切勿“本末倒置”!
3.3 actionTypes.js
中,给变量加一个“命名空间”
以“命名空间”的形式来规范“常量”的编写是一种最佳实践,所以一开始我们就应该这样去做!
如,打开 pages 目录下 home 文件夹中 store 里的 actionTypes.js
文件:
/*
❗️❗️❗️用“命名空间”改写“等号”右边的字符串:
export const INIT_HOME_DATA = "init_home_data";
export const ADD_HOME_DATA ="add_home_data";
export const CHANGE_SHOW_TO_TOP="change_show_to_top";
*/
// 🏆🏆🏆改写为:
export const INIT_HOME_DATA = "home/INIT_HOME_DATA";
export const ADD_HOME_DATA ="home/ADD_HOME_DATA";
export const CHANGE_SHOW_TO_TOP="home/CHANGE_SHOW_TO_TOP";
💡篇幅原因,我也不挨个去优化这些代码了,请自行按示例进行优化!
4 项目上线
如你所见,我们前端页面的代码已全部编写完毕。在实际工作中,此时此刻,后端小伙伴也差不多编写完了相关的数据“接口”。
双方都编写好了代码,下一步我们前端应该怎么做呢?
1️⃣先把 public 目录下的 api
文件夹删除掉,因为真正上线的时候,这里面 mock 的“数据”都不需要了,后端已为我们准备好了对应的“接口”;
2️⃣终端定位到 qdywxs-jianshu
,运行:
npm run build
打包完成后, qdywxs-jianshu
项目会多出一个 build
文件夹。
3️⃣我们把 build
文件夹给到后端小伙伴即可;
4️⃣后端小伙伴会将这个 build
文件夹里的所有文件拿出来放在后端工程的 htdocs
目录下(这个目录里包含后端小伙伴编写的 api
接口文件)完成上线。
OK,青山不改,绿水长流,咱们下个项目再见~
祝好,qdywxs ♥ you!
转载自:https://juejin.cn/post/7373565544698609676