资讯专栏INFORMATION COLUMN

组件复用那些事儿 - React 实现按需加载轮子

K_B_Z / 1387人阅读

摘要:同时,懒加载按需加载概念至关重要。时至今日,这些实现懒加载脚本的代码仍有学习意义。代码实战下面让我们动手实现一个按需加载轮子。同样,对于组件也可以使用无状态组件函数式组件实现这样无疑更加简洁。

组件化在当今前端开发领域中是一个非常重要的概念。著名的前端类库,比如 React、Vue 等对此概念都倍加推崇。确实,组件化复用性(reusability)和模块性(modularization)的优点对于复杂场景需求具有先天优势。组件就如同乐高积木、建筑石块一般,一点点拼接构成了我们的应用。

同时,懒加载(Lazy-loading)/按需加载概念至关重要。它对于页面性能优化,用户体验提升提供了新思路。在必要情况下,我们请求的资源更少、解析的脚本更少、执行的内容更少,达到效果也就越好。

这篇文章将从懒加载时机、组件复用手段、代码实例三方面来分析,happy reading!

按需加载场景设计分析

一个典型的页面如下图:

它包含了以下几个区块:

一个头部 header;

图片展示区;

地图展现区;

页面 footer。

对应代码示例:

</>复制代码

  1. const Page = () => {
  2. };

当用户来访时,如果不滚动页面,只能看见头部区域。但在很多场景下,我们都会加载所有的 JavaScript 脚本、 CSS 资源以及其他资源,进而渲染了完整页面。这明显是不必要的,消耗了更多带宽,延迟了页面 load 时间。为此,前端历史上做过很多懒加载探索,很多大公司的开源作品应势而出:比如 Yahoo 的 YUI Loader,Facebook 的 Haste, Bootloader and Primer等。时至今日,这些实现懒加载脚本的代码仍有学习意义。这里不再展开。

如下图,在正常逻辑情况下,代码覆盖率层面,我们看到 1.1MB/1.5MB (76%) 的代码并没有应用到。

另外,并不是所有资源都需要进行懒加载,我们在设计层面上需要考虑以下几点:

不要按需加载首屏内容。这很好理解,首屏时间至关重要,用户能够越早看到越好。那么如何定义首屏内容?这需要结合用户终端,站点布局来考虑;

预先懒加载。我们应该避免给用户呈现空白内容,因此预先懒加载,提前执行脚本对于用户体验的提升非常明显。比如下图,在图片出现在屏幕 100px 时,提前进行图片请求和渲染;

懒加载对 SEO 的影响。这里面涉及到内容较多,需要开发者了解搜索引擎爬虫机制。以 Googlebot 为例,它支持 IntersectionObserver,但是也仅仅对视口里内容起作用。这里不再详细展开,感兴趣的读者可以通过测试页面以及测试页面源码,并结合 Google 站长工具:Fetch as Google 进行试验。

React 组件复用技术

提到组件复用,大多开发者应该对高阶组件并不陌生。这类组件接受其他组件,进行功能增强,并最终返回一个组件进行消费。React-redux 的 connect 即是一个 currying 化的典型应用,代码示例:

</>复制代码

  1. const MyComponent = props => (
  2. {props.id} - {props.name}
  3. );
  4. // ...
  5. const ConnectedComponent = connect(mapStateToProps, mapDispatchToProps)( MyComponent );

同样,Function as Child Component 或者称为 Render Callback 技术也较为常用。很多 React 类库比如 react-media 和 unstated 都有广泛使用。以 react-media 为例:

</>复制代码

  1. const MyComponent = () => (
  2. {matches =>
  3. matches ? (
  4. The document is less than 600px wide.

  5. ) : (

    The document is at least 600px wide.

  6. )
  7. }
  8. );

Media 组件将会调用其 children 进行渲染,核心逻辑为:

</>复制代码

  1. class Media extends React.Component {
  2. ...
  3. render() {
  4. React.Children.only(children)
  5. }
  6. }

这样,子组件并不需要感知 media query 逻辑,进而完成复用。

除此之外,还有很多组件复用技巧,比如 render props 等,这里不再一一分析。感兴趣的读者可以在我的新书中找到相关内容。

代码实战

下面让我们动手实现一个按需加载轮子。首先需要设计一个 Observer 组件,这个组件将会去检测目标区块是否在视口之中可见。为了简化不必要的逻辑,我们使用 Intersection Observer API,这个方法异步观察目标元素的可视状态。其兼容性可以参考这里。

</>复制代码

  1. class Observer extends Component {
  2. constructor() {
  3. super();
  4. this.state = { isVisible: false };
  5. this.io = null;
  6. this.container = null;
  7. }
  8. componentDidMount() {
  9. this.io = new IntersectionObserver([entry] => {
  10. this.setState({ isVisible: entry.isIntersecting });
  11. }, {});
  12. this.io.observe(this.container);
  13. }
  14. componentWillUnmount() {
  15. if (this.io) {
  16. this.io.disconnect();
  17. }
  18. }
  19. render() {
  20. return (
  21. // 这里也可以使用 findDOMNode 实现,但是不建议
  22. {
  23. this.container = div;
  24. }}
  25. >
  26. {Array.isArray(this.props.children)
  27. ? this.props.children.map(child => child(this.state.isVisible))
  28. : this.props.children(this.state.isVisible)}
  29. );
  30. }
  31. }

如上,该组件具有 isVisible 状态,表示目标元素是否可见。this.io 表示当前 IntersectionObserver 实例;this.container 表示当前观察元素,它通过 ref 来完成目标元素的获取。

componentDidMount 方法中,我们进行 this.setState.isVisible 状态的切换;在 componentWillUnmount 方法中,进行垃圾回收。

很明显,这种复用方式为前文提到的 Function as Child Component。

注意,对于上述基本实现,我们完全可以进行自定义的个性化设置。IntersectionObserver 支持 margins 或者 thresholds 的选项。我们可以在 constructor 里实现配置项目初始化,在 componentWillReceiveProps 生命周期函数中进行更新。

这样一来,针对前文页面内容,我们可以进行 Gallery 组件和 Map 组件懒加载处理:

</>复制代码

  1. const Page = () => {
  2. {isVisible => }
  3. {isVisible => }
  4. }

我们将 isVisible 状态进行传递。相应消费组件可以根据 isVisible 进行选择性渲染。具体实现:

</>复制代码

  1. class Map extends Component {
  2. constructor() {
  3. super();
  4. this.state = { initialized: false };
  5. this.map = null;
  6. }
  7. initializeMap() {
  8. this.setState({ initialized: true });
  9. // 加载第三方 Google map
  10. loadScript("https://maps.google.com/maps/api/js?key=", () => {
  11. const latlng = new google.maps.LatLng(38.34, -0.48);
  12. const myOptions = { zoom: 15, center: latlng };
  13. const map = new google.maps.Map(this.map, myOptions);
  14. });
  15. }
  16. componentDidMount() {
  17. if (this.props.isVisible) {
  18. this.initializeMap();
  19. }
  20. }
  21. componentWillReceiveProps(nextProps) {
  22. if (!this.state.initialized && nextProps.isVisible) {
  23. this.initializeMap();
  24. }
  25. }
  26. render() {
  27. return (
  28. {
  29. this.map = div;
  30. }}
  31. />
  32. );
  33. }
  34. }

只有当 Map 组件对应的 container 出现在视口时,我们再去进行第三方资源的加载。

同样,对于 Gallery 组件:

</>复制代码

  1. class Gallery extends Component {
  2. constructor() {
  3. super();
  4. this.state = { hasBeenVisible: false };
  5. }
  6. componentDidMount() {
  7. if (this.props.isVisible) {
  8. this.setState({ hasBeenVisible: true });
  9. }
  10. }
  11. componentWillReceiveProps(nextProps) {
  12. if (!this.state.hasBeenVisible && nextProps.isVisible) {
  13. this.setState({ hasBeenVisible: true });
  14. }
  15. }
  16. render() {
  17. return (
  18. Some pictures

  19. Picture 1
  20. {this.state.hasBeenVisible ? (
  21. ) : (
  22. )}
  23. Picture 2
  24. {this.state.hasBeenVisible ? (
  25. ) : (
  26. )}
  27. );
  28. }
  29. }

也可以使用无状态组件/函数式组件实现:

</>复制代码

  1. const Gallery = ({ isVisible }) => (
  2. Some pictures

  3. Picture 1
  4. {isVisible ? (
  5. ) : (
  6. )}
  7. Picture 2
  8. {isVisible ? (
  9. ) : (
  10. )}
  11. );

这样无疑更加简洁。但是当元素移出视口时,相应图片不会再继续展现,而是复现了 placeholder。

如果我们需要懒加载的内容只在页面生命周期中记录一次,可以设置 hasBeenVisible 参数:

</>复制代码

  1. const Page = () => {
  2. ...
  3. {(isVisible, hasBeenVisible) =>
  4. // Gallery can be now stateless
  5. }
  6. ...
  7. }

或者直接实现 ObserverOnce 组件:

</>复制代码

  1. class ObserverOnce extends Component {
  2. constructor() {
  3. super();
  4. this.state = { hasBeenVisible: false };
  5. this.io = null;
  6. this.container = null;
  7. }
  8. componentDidMount() {
  9. this.io = new IntersectionObserver(entries => {
  10. entries.forEach(entry => {
  11. if (entry.isIntersecting) {
  12. this.setState({ hasBeenVisible: true });
  13. this.io.disconnect();
  14. }
  15. });
  16. }, {});
  17. this.io.observe(this.container);
  18. }
  19. componentWillUnmount() {
  20. if (this.io) {
  21. this.io.disconnect();
  22. }
  23. }
  24. render() {
  25. return (
  26. {
  27. this.container = div;
  28. }}
  29. >
  30. {Array.isArray(this.props.children)
  31. ? this.props.children.map(child => child(this.state.hasBeenVisible))
  32. : this.props.children(this.state.hasBeenVisible)}
  33. );
  34. }
  35. }
更多场景

上面我们使用了 Observer 组件去加载资源。包括了 Google Map 第三方内容和图片。我们同样可以完成“当组件出现在视口时,才展现元素动画”的需求。

仿照 React Alicante 网站,我们实现了类似的按需执行动画需求。具体可见 codepen 地址。

IntersectionObserver polyfilling

前面提到了 IntersectionObserver API 的兼容性,这自然就绕不开 polyfill 话题。

一种处理兼容性的选项是“渐进增强”(progressive enhancement),即只有在支持的场景下实现按需加载,否则永远设置 isVisible 状态为 true:

</>复制代码

  1. class Observer extends Component {
  2. constructor() {
  3. super();
  4. this.state = { isVisible: !(window.IntersectionObserver) };
  5. this.io = null;
  6. this.container = null;
  7. }
  8. componentDidMount() {
  9. if (window.IntersectionObserver) {
  10. this.io = new IntersectionObserver(entries => {
  11. ...
  12. }
  13. }
  14. }
  15. }

这样显然不能实现按需的目的,我更加推荐 w3c 的 IntersectionObserver polyfill:

</>复制代码

  1. class Observer extends Component {
  2. ...
  3. componentDidMount() {
  4. (window.IntersectionObserver
  5. ? Promise.resolve()
  6. : import("intersection-observer")
  7. ).then(() => {
  8. this.io = new window.IntersectionObserver(entries => {
  9. entries.forEach(entry => {
  10. this.setState({ isVisible: entry.isIntersecting });
  11. });
  12. }, {});
  13. this.io.observe(this.container);
  14. });
  15. }
  16. ...
  17. }

当浏览器不支持 IntersectionObserver 时,我们动态 import 进来 polyfill,这就需要支持 dynamic import,此为另外话题,这里不再展开。

最后试验一下,在不支持的 Safari 浏览器下,我们看到 Network 时间线如下:

总结

这篇文章介绍涉及到组件复用、按需加载(懒加载)实现内容。更多相关知识,可以关注作者新书。
同时这篇文章截取于 José M. Pérez 的 Improve the Performance of your Site with Lazy-Loading and Code-Splitting,部分内容有所改动。

广告时间:
如果你对前端发展,尤其对 React 技术栈感兴趣:我的新书中,也许有你想看到的内容。关注作者 Lucas HC,新书出版将会有送书活动。

Happy Coding!

PS: 作者 Github仓库 和 知乎问答链接 欢迎各种形式交流。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/52220.html

相关文章

  • 组件复用那些事儿 - React 实现按需加载轮子

    摘要:同时,懒加载按需加载概念至关重要。时至今日,这些实现懒加载脚本的代码仍有学习意义。代码实战下面让我们动手实现一个按需加载轮子。同样,对于组件也可以使用无状态组件函数式组件实现这样无疑更加简洁。 组件化在当今前端开发领域中是一个非常重要的概念。著名的前端类库,比如 React、Vue 等对此概念都倍加推崇。确实,组件化复用性(reusability)和模块性(modularization...

    lidashuang 评论0 收藏0
  • 组件复用那些事儿 - React 实现按需加载轮子

    摘要:同时,懒加载按需加载概念至关重要。时至今日,这些实现懒加载脚本的代码仍有学习意义。代码实战下面让我们动手实现一个按需加载轮子。同样,对于组件也可以使用无状态组件函数式组件实现这样无疑更加简洁。 组件化在当今前端开发领域中是一个非常重要的概念。著名的前端类库,比如 React、Vue 等对此概念都倍加推崇。确实,组件化复用性(reusability)和模块性(modularization...

    dackel 评论0 收藏0
  • React 设计模式和场景分析

    摘要:这一周连续发表了两篇关于的文章组件复用那些事儿实现按需加载轮子应用设计之道化妙用其中涉及到组件复用轮子设计相关话题,并配合相关场景实例进行了分析。 showImg(https://segmentfault.com/img/remote/1460000014482098); 这一周连续发表了两篇关于 React 的文章: 组件复用那些事儿 - React 实现按需加载轮子 React ...

    avwu 评论0 收藏0

发表评论

0条评论

K_B_Z

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<