9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

React + Firebase + MaterialUI --(webpack + CDN)--> 神

Last updated at Posted at 2019-09-05

TL;DR

webpackのexternalsを活用することでファイルサイズが大きくなりがちなReact + Firebase + MaterialUIプロジェクトのサイズを削減することができる。

Source Code
Sample Page

背景

ReactやFirebaseなどの便利ライブラリは基本的にサイズが大きい。例えば、

File Size
firebase-app.js 11.22K
firebase-auth.js 157.33K
firebase-firestore.js 355.59K
firebase-storage.js 38.15K
react.production.min.js 13.00K
react-dom.production.min.js 111.45K
material-ui.production.min.js 295.05K

仮にこれを全部使う場合はバンドル後の出力サイズが約1メガバイトを超えるのは避けられない。Firebase Hostingの無料枠内で運用したい場合、1回のアクセスで1メガバイトもの転送量を消費してしまうのは痛い。

webpack + CDN

そのため、ファイルサイズの大きい外部ライブラリはUNPKGなどのCDNから<script>タグを通じて利用したいことがある。しかし単純にこの手法を使おうとすると、古いバージョンのJavascriptでグローバルに定義された変数と格闘しながらコーディングすることになる。それはしんどいので、どうにかしてNPMやTypescriptなどの現代技術を活用できるようにしたい。そこで登場するのがwebpackである。

webpackを使った開発では基本的にすべてのライブラリはNPMを通じて管理するが、設定ファイルでexternalsを指定することでファイル出力時にライブラリをグローバル変数から参照させることができる。

初期設定

ここからはTypescriptで開発することを前提とする。Javascriptを使用する場合は適宜読み替えていただきたい。

必要なライブラリをダウンロード

echo '{}' > package.json
npm i -D firebase @types/react @types/react-dom typescript webpack webpack-cli webpack-dev-server ts-loader

Typescriptの設定

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "allowJs": true,
    "jsx": "react",
    "sourceMap": true,
    "outDir": "./public/js",
    "strict": true,
    "esModuleInterop": true,
  }
}

webpackの設定

webpack.config.js
module.exports = {
  devtool: 'inline-source-map',
  entry: './src/index.tsx',
  output: {
    path: path.resolve(__dirname, 'public/js'),
    filename: 'bundle.js' 
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  module: {
    rules: [
      { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }
    ]
  },
  devServer: {
    contentBase: path.join(__dirname, 'public'),
    watchContentBase: true,
    open: true
  },
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM',
    'firebase': 'firebase',
    'firebase/auth': '',
    'firebase/storage': '',
    'firebase/firestore': '',
    '@material-ui/core': 'MaterialUI'
  }
}

ここで注目していただきたいところはexternalsである。firebase以外は素直な感じであるがfirebaseは少し厄介である。firebase/*は読み込まれていれば十分で新たにグローバル変数を作るわけではないので空文字を返している。(憶測だが)firebase-app.jsで作られたグローバル変数firebaseに対してその他のfirebase-*.jsが機能を追加している。このことはアプリケーションの書くときに意識する必要があるので、開発中に謎のエラーに遭遇したら真っ先にfirebaseコードの読み込み関係を疑おう。

package.json の script

package.json
{
  "scripts": {
    "build": "webpack --mode production",
    "start": "bash start.sh"
  },
  ...
}
start.sh
#!/bin/bash

webpack --mode development --watch &
webpack-dev-server

start.shの中身を直接package.jsonに書かず別プロセスとして実行することで、^Cでサーバを止めたときにバックグラウンド実行されているwebpack --watchも終了する。

サンプルコード (React only)

public/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>React App</title>
</head>
<body>
  <div id="root"></div>
  <script defer crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
  <script defer crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
  <script defer src="js/bundle.js"></script>
</body>
</html>

scriptdeferオプションを指定することでダウンロードが並行して実行される。(たぶん)

src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'

import App from './App'

ReactDOM.render(<App />, document.getElementById('root'))
src/App.tsx
import React from 'react'

const App: React.FC = () => {
  return (
    <h2>Hello React!!!</h2>
  )
}

export default App

npm run startを実行すればちゃんとReactが動いていることが確認できると思う。

サンプルコード (React + Firebase + MaterialUI)

public/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>React App</title>
</head>
<body>
  <div id="root"></div>
  <script defer crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
  <script defer crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
  <script defer crossorigin defer src="//unpkg.com/@material-ui/core@latest/umd/material-ui.production.min.js"></script>
  <script defer src="https://www.gstatic.com/firebasejs/6.5.0/firebase-auth.js"></script>
  <script defer src="js/bundle.js"></script>
  <script src="https://www.gstatic.com/firebasejs/6.5.0/firebase-app.js"></script>
</body>
</html>

ここで注意してほしいことは**firebase-app.jsscriptタグではdeferが指定されていない**点である。これはグローバル変数firebasefirebase-app.js内でのみで定義されることを保証するため、スクリプトの中で一番はじめに呼ばれるようにするためである。最後の行に持ってきているのは他のスクリプトのダウンロードを邪魔しないようにするためである。

src/App.tsx
import React from 'react'
import firebase from 'firebase'
import { Button, Container } from '@material-ui/core'

const firebaseConfig = {
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  ...
};

firebase.initializeApp(firebaseConfig)

const App: React.FC = () => {
  const [user, setUser] = React.useState<firebase.User | null>()
  React.useEffect(() => firebase.auth().onAuthStateChanged(setUser), [])

  const provider = new firebase.auth.GoogleAuthProvider()
  const auth = firebase.auth()

  return <Container maxWidth="sm">
    <h2>React + Firebase + MaterialUI using CDN</h2>
    <Button {...{
      variant: "contained",
      color: user ? 'default' : 'primary',
      onClick: user ?
        () => auth.signOut().catch(console.error) :
        () => auth.signInWithPopup(provider).catch(console.error)
    }}>{user ? 'sign out' : 'sign in'}</Button>
    <p>
      <a href="https://google.com">Document</a>
    </p>
    {user ?
      <ul>
        {Object.entries(user.toJSON())
          .filter(([_, x]) => typeof x === 'string' || typeof x === 'number' || typeof x === 'boolean')
          .map(([s, x]) => <li key={s}>{`${s}:\t${x}`}</li>)}
      </ul> :
      <p>Sign in, PLEASE!</p>
    }
  </Container>
}

export default App

注目ポイントimport部分である。import firebase from 'firebase/app'ではなくimport firebase from 'firebase'などとしているのはCDNと対応が取れるようにするためである。一般的には、サブディレクトリのみインポートすることは読み込むコード量が減り良いことなのだが、今回はまるごとCDNからコードを取得するのでルートディレクトリからインポートするようにする。

9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?