むしゃくしゃしてやった。
後悔はしている。(時間かけすぎた)
ということで、タイトルの通りなのですが、
GASのWebアプリケーションにReact + Reduxを組み込んだものを作ってみました。
その時の知見を書き綴っていこうと思います。
以下のコードはリアルタイムチャットを作成してみたときのコードの一部です。
認証の機能と、リアルタイムなデータのやりとりのために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ですね。
下記を参考にしました。
さいごに
コードの全貌が見たいという方は、GASスタンドというサービスで今回のWebアプリケーションを販売しているので、是非買ってください(宣伝)