banner
他山之石

他山之石

webpack4とreact-router 4を使用してコード分割を実現する

Webpack のコード分割機能は、コードを異なるバンドルに分離し、必要に応じてこれらのファイルをロードまたは並行してロードすることができます。

image

コード分割とは#

私たちは、webpack を使用して react アプリケーションをパッケージ化する際、webpack がアプリ全体を 1 つの js ファイルにパッケージ化することを知っています。ユーザーが初回画面にアクセスすると、全体の js ファイルが一度に読み込まれるため、初回画面のレンダリング速度が遅くなる問題が発生します。そこで、webpack はコード分割の機能を開発しました。この機能は、コードを異なるバンドルに分離し、必要に応じてこれらのファイルをロードまたは並行してロードすることができます。コード分割は、より小さなバンドルを取得し、リソースのロード優先度を制御するために使用でき、適切に使用すれば、ロード時間に大きな影響を与えることができます。
一般的なコード分割の方法は 3 つあります:

  • エントリーポイント:entry 設定を使用して手動でコードを分離します。
  • 重複防止:CommonsChunkPlugin を使用してチャンクを重複させて分離します。
  • 動的インポート:モジュールのインライン関数呼び出しを通じてコードを分離します。
    この記事では、動的インポートの方法のみを議論します。

動的インポート#

動的コード分割に関して、webpack は 2 つの類似した技術を提供しています。動的インポートに対して、最初の方法であり、優先される方法は、ECMAScript 提案に準拠した import () 構文を使用することです。2 つ目は、webpack 特有の require.ensure を使用することです。まずは、最初の方法を試してみましょう……

注意:import () 呼び出しは内部で promises を使用します。古いバージョンのブラウザで import () を使用する場合は、polyfill ライブラリ(例えば es6-promise や promise-polyfill)を使用して Promise を shim してください。

以下では、react-router 4 を使用して react のコード分割を実現します。

react のコード分割と遅延読み込み#

react-router4 を使用してコード分割を行う際、コミュニティには react-loadable のような成熟したサードパーティライブラリが実装されています。ここでは、サードパーティライブラリを使用せずにコード分割を実現する方法を紹介します。

ここでは、あなたが react、react-router4、webpack について基本的な理解を持ち、簡単な開発環境を構築できることを前提とします。まず、私が構築したプロジェクトのエントリーファイルを見てみましょう。

src 下 index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('app'));

src 下 App.js:

import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Switch, BrowserRouter } from 'react-router-dom';
import Routes from './routes';

export default () => (
  <BrowserRouter>
    <Switch>
      {
        Routes.map(({name, path, exact = true, component }) => (
          <Route path={path} exact={exact} component={component} key={name} />
        ))
      }
    </Switch>
  </BrowserRouter>
)

App.js では、react-router-dom を使用してルーティングを行い、routes.js ファイルをルーティング設定ファイルとして使用しています。次に、ルーティング設定を使用してルーティングコンポーネントを生成します。次に、ルーティング設定 routes.js を見てみましょう。

import AsyncComponent from './components/acync-component';

export default [
  {
    name: 'ホーム',
    icon: 'home',
    path: '/',
    component: AsyncComponent(() => import('./containers/home'))
  },
  {
    name: '詳細',
    icon: 'detail',
    path: '/detail',
    component: AsyncComponent(() => import('./containers/detail'))
  }
]

routes.js では、ルーティングコンポーネントに必要なパラメータを設定しています。ルーティングパラメータでは、非同期コンポーネント AsyncComponent を使用していることに注意してください。ここではコンポーネントをインポートしていないのではなく、AsyncComponent に関数を渡しています。この関数は、*AsyncComponent(() => import ('./containers/home'))* コンポーネントが作成されるときに動的にインポートされます。同時に、関数を引数として渡すこの書き方は、webpack にここでコード分割が必要であることを認識させることができます。

import () を使用するには、Babel プリプロセッサーと Syntax Dynamic Import Babel Plugin のようなものを使用する必要があります。import () は promise を返すため、async 関数と一緒に使用できます。async 関数を使用するには、babel-plugin-transform-runtime を使用する必要があります。

babel プラグインをインストール:

npm i -D babel-plugin-syntax-dynamic-import babel-plugin-transform-runtime

本プロジェクトで使用している他の babel プラグインには、babel-core、babel-loader、babel-preset-env、babel-preset-react などがあり、主に jsx コンパイルに使用されます。
.babelrc を設定
ルートディレクトリに.babelrc を新規作成し、以下のように設定します:

{
  "presets":["react","env"],
  "plugins": ["syntax-dynamic-import", "transform-runtime"]
}

非同期コンポーネント AsyncComponent#

本プロジェクトの AsyncComponent は src/components/async-component/index.js にあり、コードは以下の通りです:

import React, { Component } from 'react';

export default (loadComponent, placeholder = '読み込み中。。。。') => {
  return class AsyncComponent extends Component {
    
    constructor() {
      super();

      this.state = {
        Child: null
      }

      this.unmount = false;
    }

    componentWillUnmount() {
      this.unmount = true;
    }

    async componentDidMount() {
      const { default: Child } = await loadComponent();

      if (this.unmount) return;

      this.setState({
        Child
      })
    }

    render() {
      const { Child } = this.state;

      return (
        Child ? <Child {...this.props} /> : placeholder
      )
    }
  }
}

まず、これは高階コンポーネントであり、新しいコンポーネントを返します。2 つの引数を受け取ります。1 つは動的に読み込む必要があるコンポーネントのメソッド、もう 1 つは動的読み込み時のプレースホルダーで、Loading コンポーネントを渡すこともできます。

返された AsyncComponent 内部では、constructor で state を Child として初期化し、値を null に設定し、this.unmount = false を定義して、コンポーネントがアンマウントされたかどうかを示します。

async を使用して非同期メソッドを定義し、componentDidMount で、await を使用して渡された最初の引数を非同期に実行し、現在のルートのコンポーネントを動的に読み込みます。

注意:
ES6 モジュールの import () メソッドを呼び出す際は、モジュールの.default 値を指す必要があります。これが promise が処理された後に返される実際の module オブジェクトだからです。

したがって、ここでは ES6 のオブジェクト分割を使用してモジュールの default を取得し、Child に割り当てます。
次に、コンポーネントがアンマウントされた状態を確認し、アンマウントされている場合は戻ります。
次に、Child を state に設定します。
render メソッドでは、state から Child を取得し、三項演算子を使用して Child が true かどうかを判断します。true の場合は Child コンポーネントをレンダリングし、this.props を注入します。そうでない場合はプレースホルダーをレンダリングします。
コンポーネントが componentWillUnmount されると、this.unmount を true に設定します。

テスト検証#

containers 内に home と detail という 2 つのフォルダーを新規作成し、それぞれのフォルダー内に index.js を作成して 2 つのルートコンポーネントを作成します。コードは以下の通りです:
home/index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import { Link } from 'react-router-dom';
import '../../style.css';
import icon from '../../assets/404.png';

export default class Home extends React.Component {
  render() {
    return (
      <div>
        <h1>hello webpack</h1>
        <img src={icon} alt=""/>
        <Link to="/detail">詳細</Link>
      </div>
    )
  }
}

detail/index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import '../../style.css';
import icon from '../../assets/404.png';

export default class Detail extends React.Component {
  render() {
    return (
      <div>
        <h1>hello webpack detail</h1>
        <img src={icon} alt=""/>
      </div>
    )
  }
}

ルートディレクトリの package.json に起動スクリプトを設定:

"scripts": {
    "start": "webpack-dev-server --mode development",
    "build": "webpack --mode production"
  },

その後、npm start を実行してプロジェクトを起動します:
ブラウザで localhost:8080 にアクセスします。

image

ページが最初に main.js と 0.js を読み込んでいるのが確認できます。詳細ボタンをクリックしてhttp://localhost:8080/detail に移動します。

image

すぐに 1.js が読み込まれ、これによりコード分割が実現され、各ルートが動的に読み込まれます。初回画面のレンダリング速度が向上しました。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。