likes
comments
collection
share

(20)性能优化和项目上线——PureComponent、异步组件、withRouter 方法 | React.js 项目实战:PC 端“简书”开发

作者站长头像
站长
· 阅读数 51
转载请注明出处,未经同意,不可修改文章内容。

🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。

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 路由方法

❌我们直接来观察一个问题:

(20)性能优化和项目上线——PureComponent、异步组件、withRouter 方法 | React.js 项目实战:PC 端“简书”开发

从视频的操作中我们可以看到:当我们刷新整个页面时,控制台给我们显示出,项目瞬间就同时加载了 5 个 .js 文件。然后,当我们后续再进“详情页”、“登录页”、“写文章”页时,项目就不再为我们加载 .js 文件了。

什么意思呢?它表示的就是,就目前我们的项目而言,只要页面刷新,不管你需不需要,它都会将整个项目“组件”的 .js 文件全部加载出来

现在项目不复杂、代码量不多,倒看不出来差异。实际工作中,商业级项目的大篇幅代码,如此这般,必然带来严重的性能问题!

  • ❓怎么解决呢?

这就需要引入“异步组件”的概念。

简单来说,就是通过一些方法,让“组件”按需加载——你不要一股脑的给我加载完,我访问“首页”,你就给我“首页”的代码;我点击某个“详情页”时,你再给我加载详情页的 .js 文件。

由于“异步组件”的底层稍微复杂,前期我们还不需要去研究它,先考虑用“工具”解决这个痛点才是当务之急!

🏆幸运的是,“先驱者”们已为我们准备好了一个第三方“模块”——react-loadable

  • ❓怎么用?

答:

1️⃣打开 react-loadable 官方文档: (20)性能优化和项目上线——PureComponent、异步组件、withRouter 方法 | React.js 项目实战:PC 端“简书”开发

2️⃣在 qdywxs-jianshu 项目中安装这个“模块”: (20)性能优化和项目上线——PureComponent、异步组件、withRouter 方法 | React.js 项目实战:PC 端“简书”开发

3️⃣我们现在的需求是——“详情页”的代码,只有进入到“详情页”才加载。因此,我们需要在“详情页”中使用 react-loadable。

3️⃣-①:在 pages 目录下的 detail 文件夹下创建一个 loadable.js 文件; (20)性能优化和项目上线——PureComponent、异步组件、withRouter 方法 | React.js 项目实战:PC 端“简书”开发

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; 

返回页面查看(❌会报出一个“错误”——组件获取不到路由参数!): (20)性能优化和项目上线——PureComponent、异步组件、withRouter 方法 | React.js 项目实战:PC 端“简书”开发

❓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,且“详情页”异步加载了):

(20)性能优化和项目上线——PureComponent、异步组件、withRouter 方法 | React.js 项目实战:PC 端“简书”开发

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
评论
请登录