原文地址?

medium.com

在单页面应用的时代,你可以使用React来完成几乎所有的Web应用甚至大型的Web引用。你甚至可以使用React开发一个Facebook。

当前正如你所知,Webpack完成所有的魔法,它将会将你所有的代码放到bundle.js这个文件中,供浏览器使用。这个文件将包含所有组件和所有的依赖。因此,越是庞大的应用,这个文件将会越大。当然,大文件会耗费更多的时间去下载。因此首屏幕载入会非常慢,但是事实上,很多组件都不会出现在用户所浏览的那部分页面上。

最好的实践方案是将这个单一的bundle.js拆分成多个文件,并按需要进行载入。因此,那些不被用户所浏览的页面所包含的的组件,将不会被载入。

为了实现按需载入,是有很多的解决方案的。这里我将分享如何使用HOC来完成这件事情。

什么是HOC?

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

我们可以想像到HOC就是在你的组件外进行一次封装,它会在你的组件创建时注入一些公共状态或者修改一些组件行为。一般一个HOC就是一个非常简单的函数,接收一个组件并返回另一个组件。然后你就能把HOC当成一个非常简单的组件在JSX中应用。我们可以在高阶组件文档 中详细了解高阶组件。

所以,我们要做的就是定义一个HOC,并且在所有需要进行非同步载入的组件上应用。

非同步组件的HOC

让我们先创建asyncComponent.js这样一个文件来编写我们的HOC代码。同时我比较倾向在工程内建立一个叫「hoc」的目录来存放所有的高阶组件。

让我们看看它的代码吧:

//hoc/asyncComponent.js

import React, {Component} from react;

const asyncComponent = (importComponent) => {
return class extends Component {
state = {
component: null
}

componentDidMount() {
importComponent()
.then(cmp => {
this.setState({component: cmp.default});
});
}

render() {
const C = this.state.component;
return C ? <C {...this.props}/> : null;
}
}
};

export default asyncComponent;

如你所见,它就是返回一个函数,这个函数接收另外一个函数为参数并返回一个基类是React Component的匿名类。简单说我们的asyncComponent是一个返回组件的函数。

我们可以看出importComponent是一个函数,它简单的返回我们需要引入的组件,它的代码会像下面这个样子:

const impFn = () => {
return import(./components/Card);
}

每次importComponent函数被调用的时候,React都会尝试引入这个组件,它将会自动载入一个包含该组件的chunk.js

使用asyncComponent

让我们来看看,我们该如何使用这个组件和当我们使用该组件时会发生什么。我们的例子中将在一个函数组件中使用这个高阶组件。

//components/Container.js

import React from react;
import asyncComponent from ../../hoc/asyncComponent;

const AsyncButton = asyncComponent(() => {
return import(../Button);
});

const container = () => {
return (
<div>
<h1>Here goes an async loaded button component</h1>
<AsyncButton/>
</div>
);
};

export default container;

在这里,我们定义了一个AsyncButton,而非在DOM中直接中<Button/>组件。我们已经知道我们的asyncComponent是如何定义的,我们也可以猜到AsyncButton将被作为React的组件类型来使用。但是,当这个组件被添加DOM的时候,会发生什么呢?我们就需要到asyncComponent中寻找答案。

很显然,当AsyncButton被挂载到DOM时(参考componentDidMount),它会调用我们的importComponent函数。在这个场景下,它会返回一个Button组件。在import操作完成前,这个地方的DOM将是空的。当缺失的组件完成了下载,它将会作为AsyncButton组件状态添加到AsyncButton组件中,从而触发重新渲染。现在,我们的非同步组件将会传递props并渲染Button组件。

好了,我们已经让我们的Button组件在真正被需要使用的时候才被下载和渲染。

非同步组件配合路由使用

当我们的应用拥有很多容器组件(例如页面),那就很有必要一开始只载入首先被访问页面,而剩下的页面则按需载入。我们的asyncComponent可以很好的完成这件事,你只需像我们非同步载入Button组件那样去就可以了。

这里我们来做个简单的例子。假设我们所有的路由被分拆到不同的文件中,在某个路由文件中只有两个简单的路由。Home页面和用户Profile页面,其中Home页面是首屏页面,而用户Profile不一定会被访问。

import React, {Component} from react;
import {Route, Switch} from react-router-dom;

import HomePage from ./containers/HomePage;

const AsyncProfilePage = asyncComponent(() => {
return import(./containers/ProfilePage);
});

class Routes extends Component {
render() {
return (
<Switch>
<Route exact path=/ component={HomePage}/>
<Route exact path=/profile component={AsyncProfilePage}/>
</Switch>
);
}
}

export default Routes;

因此用户的Profile页面,将在被访问的时候载入。

希望你能从这篇博文中学到一些好玩的东西,并且让我们开心的编码吧。


翻译后记:

在非SPA的时代,也有很多非同步载入JS文件的方案,例如require.js,但是这些都要使用一些小技巧(修改DOM,增加script标签等)。而随著ECMAScript 6的出现,浏览器正全力的将ES6语法和特性直接在浏览器内实现。而import()函数也是ES的modules规范(动态import)的一部分,这大大的简化了浏览器动态载入JS的难度。

React对大型项目也提出了自己代码分割方案 。这篇文章只是展示下非同步载入较为浅显的原理。

翻译水平有限,希望多提出宝贵意见。


推荐阅读:
相关文章