GASでReact + Redux(+ Material UI)なWebアプリケーションを作ってみた

Google Apps Script

むしゃくしゃしてやった。
後悔はしている。(時間かけすぎた)

ということで、タイトルの通りなのですが、
GASのWebアプリケーションにReact + Reduxを組み込んだものを作ってみました。

GAS + Firebaseでリアルタイムチャットアプリ | GASスタンド
GAS + Firebaseでリアルタイムチャットアプリ GASとFirebase(Firestore)を組み合わせてリアルタイムチャットを作ってみました。 認証はFirebaseのAuthenticationでGoogleログインのみを想定しています。 UIはReact + Redux + Material UI で...

その時の知見を書き綴っていこうと思います。

以下のコードはリアルタイムチャットを作成してみたときのコードの一部です。
認証の機能と、リアルタイムなデータのやりとりのためにFirebaseを使っています。

GAS側のコード

GETリクエストを受け取って、index.htmlを返すシンプルなものですね。
今回のWebアプリケーションでは、これ以外のGASのコードはほとんど書くことがありません。

// code.gs

function doGet(e) {
  return HtmlService.createTemplateFromFile('index').evaluate()
  .setTitle('Simple Chat')
  .addMetaTag('viewport', 'width=device-width, initial-scale=1.0');
}

index.html

百聞は一見にしかず、まずは全貌を一気にお見せしましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Reactサンプル</title>
  <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
  <script crossorigin src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
  <script crossorigin src="https://unpkg.com/@material-ui/core@latest/umd/material-ui.production.min.js"></script>  
  <script crossorigin src="https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js"></script> 
  <script crossorigin src="https://www.gstatic.com/firebasejs/8.10.0/firebase-auth.js"></script>
  <script crossorigin src="https://www.gstatic.com/firebasejs/8.10.0/firebase-firestore.js"></script>
  <script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.1.1/redux.min.js"></script>
  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
</head>
<body>
  <div id="root" />
  <?!= HtmlService.createTemplateFromFile('firebase_init').evaluate().getContent(); ?>
  <?!= HtmlService.createTemplateFromFile('store').evaluate().getContent(); ?>

  <?!= HtmlService.createTemplateFromFile('Login').evaluate().getContent(); ?>
  <?!= HtmlService.createTemplateFromFile('Header').evaluate().getContent(); ?>
  <?!= HtmlService.createTemplateFromFile('Content').evaluate().getContent(); ?>
  <?!= HtmlService.createTemplateFromFile('Footer').evaluate().getContent(); ?>

  <script type="text/babel">
    const App = () => {
      const { Button } = MaterialUI;
      const [leftPaneOpen, setLeftPaneOpen] = React.useState(true);

      const [isAlreadyLoginCheck, setIsAlreadyLoginCheck] = React.useState(false);
      const [isLogin, setIsLogin] = React.useState(false);

      React.useEffect(() => {
        firebase.auth().onAuthStateChanged((user) => {
          if (user) {
            setIsAlreadyLoginCheck(true);
            setIsLogin(true);
          } else {
            setIsAlreadyLoginCheck(true);
            setIsLogin(false);
          }
        });
      }, []);

      const onLogin = () => {
        setIsLogin(true);
      };

      const AppComponent = () => {
        if (!isAlreadyLoginCheck) return <div></div>;
        if (isLogin) {
          return (
            <React.Fragment>
              <Header />
              <Content leftPaneOpen={leftPaneOpen} />
              <Footer />
            </React.Fragment>
          );
        } else {
          return <Login onLogin={onLogin} />
        }
      }

      return <AppComponent />
    };

    ReactDOM.render(
      <App />,
      document.getElementById('root')
    );    
  </script>
</body>
</html>

お分かりいただけたでしょうか?

…わかってますよ、claspでローカル開発した方が楽なことくらい、、、

…少しずつ解説していきます。

必要なライブラリのロード

まずは <head>〜</head> 内で必要なライブラリをscriptタグでロードします。
上記コードでロードしているのは下記です
・ReactとReactDOMは必須です
・JSXをトランスパイルするためにbabelのロードも必須です
・必要に応じてCSSのライブラリを導入します。今回はMaterial UIとIconのフォントですね
・ReduxやFirebaseもロードしています

各コンポーネントのインポート

開発時はこんな感じで、コンポーネント毎にファイルを別にしています。

別ファイルにしたコンポーネントはGASで以下のようにロードし、importっぽいことをしています。

<?!= HtmlService.createTemplateFromFile('Footer').evaluate().getContent(); ?>

JSXはbabelでトランスパイル

JSXを扱う場合は以下のように <script type="text/babel"> としてやる必要があります。

<script type="text/babel">
〜中略〜
</script>

先程お見せした各コンポーネントも、全て <script type="text/babel"> で括っています。
以下は例として Footer.html です。

<script type="text/babel">
  const Footer = () => {
    const { makeStyles, Divider } = MaterialUI;

    const classes = makeStyles({
      root: {
        width: '100%',
        position: 'absolute',
        bottom: 0,
      },
      flexContainer: {
        display: 'flex',
        justifyContent: 'center',
      },
    })();

    return (
      <div className={classes.root}>
        <Divider />
        <div className={classes.flexContainer}>
          <div>powered by Google Apps Script</div>
        </div>
      </div>
    );
  }
</script>

Redux

Reduxも以下のようにして、扱うことができます。(トランスパイルする必要がないので、babelは使ってないです)

<script>
function leftPaneReducer(state = { open: window.innerWidth > 600 ? true : false }, action) {
  switch (action.type) {
    case 'TOGGLE_LEFT_PANE':
      return { open: !state.open };
    default:
      return state;
  }
}

const store = Redux.createStore(leftPaneReducer);

</script>

storeがグローバル汚染しているのが気になりますが、、、
あとは使いたいコンポーネントでstore.subscribe()してやればOKですね。

下記を参考にしました。

Getting Started with Redux | Redux
Introduction > Getting Started: Resources to get started learning and using Redux

さいごに

コードの全貌が見たいという方は、GASスタンドというサービスで今回のWebアプリケーションを販売しているので、是非買ってください(宣伝)

GAS + Firebaseでリアルタイムチャットアプリ | GASスタンド
GAS + Firebaseでリアルタイムチャットアプリ GASとFirebase(Firestore)を組み合わせてリアルタイムチャットを作ってみました。 認証はFirebaseのAuthenticationでGoogleログインのみを想定しています。 UIはReact + Redux + Material UI で...
タイトルとURLをコピーしました