- 投稿日:2020-08-23T23:40:24+09:00
【面倒臭がりさん必見】たった1つのコマンドでRails + React環境構築
突然ですが、環境構築は好きですか?
...私は苦手です。
端が丸まってくっついちゃったサランラップぐらい苦手です。恐らく、あなたがこの記事を開いたということは
少なからず環境構築に苦手意識を持っているのではないでしょうか。「もっと気軽に環境構築できたらいいのにな...」
というあなたの心の声を受け
今回は『コマンド1つでRailsとReactの環境構築ができるシェルスクリプト』をつくりました。たった1つのコマンドを叩くだけ!
あとは『Netflix』で好きな動画でも見ていれば
勝手に環境構築が終わっているという"夢の世界"がここにあります!さらに!
環境構築終了後、すぐに開発が始められるよう
Rails側のトップページとReact側のエントリーポイントも自動で生成される完全親切設計版です!
Reactのコンポーネントが初めからゴリゴリ書ける状態になっています。以下、本文
前提必須条件
Dockerがインストール済みであること
主にインストールされるもの
インストールされるもの バージョン Ruby 2.6.3 Rails 6.0.3.2 Node.js 10.x PostgreSQL 12.3 foreman(Gem) 0.87.2 インストールされるReact周りのパッケージ群
- redux
- react-redux
- react-router-dom
- redux-devtools-extension
- redux-form
- redux-thunk
- axios
- babel-plugin-root-import
- redux-toolkit
- material-ui
使い方の手順(この記事のメイン)
- 好きな名前でフォルダを作成する
- 1で作成したフォルダの中に『docker_rails_react.sh』という空ファイルを作成する
- 2で作成したファイルに下記『シェルスクリプト本体』を全文コピーして保存する
- ターミナルを起動
- 1で作成したフォルダに移動し、コマンド『bash docker_rails_react.sh』を実行する
- Netflixを見て時間を潰す(数分〜数十分間)
- 『Compiled successfully.』がターミナルに表示されたらhttp://localhost:3000/にアクセス
完※macOS動作検証済み
シェルスクリプト本体
docker_rails_react.sh
#!/bin/bash ENV APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn APP_NAME=$(basename `pwd`) echo APP_NAME: $APP_NAME UPPER_APP_NAME=`echo $APP_NAME | tr "[:lower:]" "[:upper:]"` echo UPPER_APP_NAME: $UPPER_APP_NAME # make Procfile.dev cat <<'EOF' > Procfile.dev web: bundle exec rails s -p 3000 -b '0.0.0.0' webpacker: bin/webpack-dev-server EOF # make Dockerfile cat <<EOF > Dockerfile FROM ruby:2.6.3 RUN apt-get update -qq && apt-get install -y nodejs postgresql-client # install yarn RUN apt-get update && apt-get install -y curl apt-transport-https wget && \\ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \\ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \\ apt-get update && apt-get install -y yarn # install Node.js RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && \\ apt-get install -y nodejs RUN mkdir /$APP_NAME WORKDIR /$APP_NAME COPY Gemfile /$APP_NAME/Gemfile COPY Gemfile.lock /$APP_NAME/Gemfile.lock RUN bundle install RUN bundle exec rails webpacker:install RUN bundle exec rails webpacker:install:react COPY . /$APP_NAME # Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 # Start the main process. CMD ["rails", "server", "-b", "0.0.0.0"] EOF # make Gemfile cat <<'EOF' > Gemfile source 'https://rubygems.org' gem 'rails', '~> 6.0', '>= 6.0.3.2' EOF # make Gemfile.lock touch Gemfile.lock # make docker-compose.yml cat <<EOF > docker-compose.yml version: '3' services: db: image: postgres volumes: - ./tmp/db:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: password web: build: . command: bash -c "rm -f tmp/pids/server.pid && bundle exec foreman start -f Procfile.dev" volumes: - .:/$APP_NAME ports: - "3000:3000" - "3035:3035" environment: ${UPPER_APP_NAME}_DB_HOST: db ${UPPER_APP_NAME}_DB_USERNAME: postgres ${UPPER_APP_NAME}_DB_PASSWORD: password ${UPPER_APP_NAME}_DEVELOPMENT_DB: ${APP_NAME}_development ${UPPER_APP_NAME}_TEST_DB: ${APP_NAME}_test depends_on: - db EOF # make entrypoint.sh cat <<EOF > entrypoint.sh #!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /$APP_NAME/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "\$@" EOF echo "docker-compose run web rails new . --force --no-deps -–skip-turbolinks --webpack=react --database=postgresql" docker-compose run web rails new . --force --no-deps -–skip-turbolinks --webpack=react --database=postgresql # fix config/database.yml echo "fix config/database.yml" rm -f config/database.yml cat <<EOF > config/database.yml default: &default adapter: postgresql encoding: unicode host: <%= ENV['${UPPER_APP_NAME}_DB_HOST'] %> username: <%= ENV['${UPPER_APP_NAME}_DB_USERNAME'] %> password: <%= ENV['${UPPER_APP_NAME}_DB_PASSWORD'] %> pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default database: <%= ENV['${UPPER_APP_NAME}_DEVELOPMENT_DB'] %> test: <<: *default database: <%= ENV['${UPPER_APP_NAME}_TEST_DB'] %> production: <<: *default database: <%= ENV['${UPPER_APP_NAME}_DB'] %> username: <%= ENV['${UPPER_APP_NAME}_DB_USERNAME'] %> password: <%= ENV['${UPPER_APP_NAME}_DB_PASSWORD'] %> EOF # fix config/webpacker.yml cat config/webpacker.yml | sed "s/host: localhost/host: 0.0.0.0/g" > __tmpfile__ cat __tmpfile__ > config/webpacker.yml rm __tmpfile__ # fix config/routes.rb rm -f config/routes.rb cat <<'EOF' > config/routes.rb Rails.application.routes.draw do # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html root "top#show" end EOF # make .babelrc .babelrc cat <<'EOF' > .babelrc { "plugins": [ [ "babel-plugin-root-import", { "paths": [ { "rootPathSuffix": "./app/javascript/src", "rootPathPrefix": "~/" }, ] } ] ] } EOF # fix config/webpack/environment.js rm -f config/webpack/environment.js cat <<'EOF' > config/webpack/environment.js const { environment } = require('@rails/webpacker') environment.splitChunks(); module.exports = environment EOF # make top_controller.rb cat <<'EOF' > app/controllers/top_controller.rb class TopController < ApplicationController def show end end EOF # fix application.html.erb rm -f app/views/layouts/application.html.erb cat <<EOF > app/views/layouts/application.html.erb <!DOCTYPE html> <html> <head> <title>$APP_NAME</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= javascript_pack_tag 'application' %> </head> <body style="margin: 0;"> <div id="root"> <%= yield %> </div> </body> </html> EOF # make top.html.erb mkdir -p app/views/top cat <<'EOF' > app/views/top/show.html.erb <%= javascript_packs_with_chunks_tag 'index' %> EOF # make entrypoint cat <<'EOF' > app/javascript/packs/index.jsx import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk' import { Router, Route, Switch, IndexRoute, useLocation } from 'react-router-dom'; import { createBrowserHistory } from 'history'; import { composeWithDevTools } from 'redux-devtools-extension'; // reducer import rootReducer from '~/reducers/'; // Component import Top from '~/components/tops/'; const middleWares = [thunk]; const enhancer = process.env.NODE_ENV === 'development' ? composeWithDevTools(applyMiddleware(...middleWares)) : applyMiddleware(...middleWares); const store = createStore(rootReducer, enhancer); const customHistory = createBrowserHistory(); render( <Provider store={store}> <Router history={customHistory}> <Route render={({ location }) => ( <Switch location={location}> <Route exact path='/' component={Top} /> </Switch> )}/> </Router> </Provider>, document.getElementById('root') ) EOF # add foreman-gem echo "gem 'foreman', '~> 0.87.2'" >> Gemfile docker-compose run web bundle install docker-compose build echo "install package" echo "docker-compose run web yarn add redux react-redux react-router-dom redux-devtools-extension redux-form redux-thunk axios @babel/preset-react babel-plugin-root-import @reduxjs/toolkit @material-ui/core" docker-compose run web yarn cache clean docker-compose run web yarn add redux react-redux react-router-dom redux-devtools-extension redux-form redux-thunk axios @babel/preset-react babel-plugin-root-import @reduxjs/toolkit @material-ui/core echo "docker-compose run web rake db:create" docker-compose run web rake db:create # make styles/images dir mkdir -p app/javascript/src/styles mkdir -p app/javascript/src/images # make component mkdir -p app/javascript/src/components/tops cat <<'EOF' > app/javascript/src/components/tops/index.jsx import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import { topInitial } from '~/modules/tops/'; const useStyles = makeStyles(theme => ({ successed: { color: '#036ab5', }, })); const Top = () => { const classes = useStyles(); const dispatch = useDispatch(); const topState = useSelector(state => state.top); React.useEffect(() => { dispatch(topInitial({ initial: true })); }, []); return ( <> <Typography variant="h1" align="center">Hello Rails on React</Typography> { topState.initial && <Typography variant="h5" align="center" className={ classes.successed }>SUCCESSED</Typography> } </> ) } export default Top; EOF # make module mkdir -p app/javascript/src/modules/tops cat <<'EOF' > app/javascript/src/modules/tops/index.jsx import { createSlice } from '@reduxjs/toolkit'; const initialState = { initial: false, } const top = createSlice({ name: 'top', initialState, reducers: { topInitial(state, action) { const { initial } = action.payload; return { ...state, initial: initial, } } } }) export const { topInitial } = top.actions export default top.reducer; EOF # make reducer mkdir -p app/javascript/src/reducers cat <<'EOF' > app/javascript/src/reducers/index.jsx import { combineReducers } from 'redux'; import { reducer as formReducer } from 'redux-form'; import top from '~/modules/tops/'; export default combineReducers({ form: formReducer, top, }); EOF echo "docker-compose up" docker-compose up初期生成ファイルの仕様(気になる人向け)
- 状態管理はReduxを使用
- ファイル構成はDucksパターンを使用
- Redux Toolkitを使用
- Redux Formを使用
- Material-UIを使用
- React関連のソースパスを『.babelrc』ファイルで管理
- webpackにsplitChunksを使用
- foremanを使用し、railsサーバーとwebpack-dev-serverを同一コンテナで起動
- top_controllerのshowアクションをRootに設定
- database.ymlの環境変数名にはフォルダ名が適用される
- ページタイトルにはフォルダ名が適用される
- TypeScript未対応
ディレクトリ構成(気になる人向け)
任意のフォルダ ├── app │ ├── controllers │ │ └── top_controller.rb │ ├── javascript │ │ ├── packs │ │ │ ├── application.js │ │ │ └── index.jsx │ │ └── src │ │ ├── components │ │ │ └── tops │ │ │ └── index.jsx │ │ ├── images │ │ ├── modules │ │ │ └── tops │ │ │ └── index.jsx │ │ ├── reducers │ │ │ └── index.jsx │ │ └── styles │ └── views │ ├── layouts │ │ └── application.html.erb │ └── top │ └── show.html.erb ├── config │ ├── database.yml │ ├── routes.rb │ ├── webpack │ │ └── environment.js │ └── webpacker.yml ├── docker_rails_react.sh ├── docker-compose.yml ├── Dockerfile ├── entrypoint.sh ├── Gemfile ├── Gemfile.lock ├── package.json ├── Procfile.dev └── yarn.lock※最適化されていない(無駄な処理がある)のはご容赦ください
では、楽しいRails on React生活を!
- 投稿日:2020-08-23T23:14:55+09:00
とりあえずrecoil触ってみるまで
特にコメントなし
import React from "react"; import "./styles.css"; import { RecoilRoot, atom, selector, useRecoilState, useRecoilValue } from "recoil"; const countState = atom({ key: "countState", // unique ID (with respect to other atoms/selectors) default: 0 // default value (aka initial value) }); const charCountState = selector({ key: "charCountState", // unique ID (with respect to other atoms/selectors) get: ({ get }) => { const text = get(countState); return text * 10; } }); export default function App() { console.log("111111111"); return ( <RecoilRoot> <div className="App"> <Test /> <Test2 /> </div> </RecoilRoot> ); } const Test = () => { const [count, setCount] = useRecoilState(countState); console.log("fffffffff"); return ( <div> <button onClick={() => { setCount(count + 1); }} > test </button> {count} </div> ); }; const Test2 = () => { console.log("88888888888"); const count = useRecoilValue(charCountState); return ( <div> <button>test2</button> {count} </div> ); };
- 投稿日:2020-08-23T20:25:32+09:00
ReactとFirebaseを使ってログインフォームを実装する③
ログイン機能を確認するため、先にEメールとパスワードでのログインを完了させます。その次にダイアログ表示まで実装します。
FirebaseでEメールとパスワードでのログイン設定
FIrebaseのコンソール画面の左側にあるAuthenticationをクリックします。ログイン方法からメール/パスワードをクリックして有効にするをオンにして保存しましょう。
これだけでEメールとパスワードでのログインできます。簡単です。ログインフォームをダイアログ表示にする
material-uiのダイアログを使用します。ダイアログを管理するステートを用意し、trueの時はダイアログ表示、falseで閉じます。
下記のURLを参照してください。
https://material-ui.com/components/dialogs/NavBarを編集しましょう。Reduxのstoreからログイン情報を取得して、ログインとログアウト処理を切り替えています。LoginFormにformTextを渡すことでログインとサインアップも切り替えてます。
firebase.auth().signOut()でログアウトできます。 コメントを参照してください。src/components/NavBar.jsximport React, { useState, Fragment } from 'react'; import { connect } from 'react-redux'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import Button from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; import DialogContent from '@material-ui/core/DialogContent'; import DialogTitle from '@material-ui/core/DialogTitle'; import firebase from '../config/firebase'; import LoginForm from './LoginForm'; const useStyles = makeStyles(theme => ({ root: { flexGrow: 1, }, title: { flexGrow: 1, }, })); function NavBar(props) { const classes = useStyles(); // ダイアログ表示のステート const [daialogOpen, setDaialogOpen] = useState(false); // ログインとサインアップを切り替えるステート const [formText, setFormText] = useState('ログイン'); const handleClickSignUpOpen = () => { // ダイアログ表示してformTextを新規登録にする setDaialogOpen(true); setFormText('新規登録'); }; const handleClickLoginOpen = () => { // ダイアログ表示してformTextをログインにする setDaialogOpen(true); setFormText('ログイン'); }; const handleClose = () => { // ダイアログを閉じる setDaialogOpen(false); }; const logout = () => { // firebaseからのログアウト処理 firebase .auth() .signOut() .then(() => { console.log('ログアウトしました'); }) .catch(error => { console.log(`ログアウト時にエラーが発生しました (${error})`); }); }; const onFormTextSignUp = () => { setFormText('新規登録'); }; const onFormTextLogin = () => { setFormText('ログイン'); }; const renderSwitchLoginOrSignUp = () => { // ログインフォームの下にログインとサインアップを切り替える処理 if (formText === 'ログイン') { return ( <div style={{ textAlign: 'center', margin: 20 }}> アカウントをお持ちでないですか? <span onClick={onFormTextSignUp} style={{ color: 'rgb(0, 112, 210)', cursor: 'pointer' }}> 新規登録 </span> </div> ); } else { return ( <div style={{ textAlign: 'center', margin: 20 }}> すでにアカウントをお持ちですか? <span onClick={onFormTextLogin} style={{ color: 'rgb(0, 112, 210)', cursor: 'pointer' }}> ログイン </span> </div> ); } }; const renderDialog = () => { // ログアウト時、ログインフォームをダイアログ表示するボタン配置 return ( <Fragment> <div> <Button color="inherit" onClick={handleClickLoginOpen}> ログイン </Button> </div> <div> <Button color="inherit" onClick={handleClickSignUpOpen}> 無料登録 </Button> </div> <Dialog open={daialogOpen} onClose={handleClose} fullWidth maxWidth={'sm'}> <DialogTitle className={classes.dialogTitle}>{formText} </DialogTitle> <DialogContent> <LoginForm formText={formText} /> {renderSwitchLoginOrSignUp()} </DialogContent> </Dialog> </Fragment> ); }; const renderLogout = () => { // ログインしてたら表示、ログアウトボタン return ( <Button color="inherit" onClick={() => { logout(); handleClose(); }} > ログアウト </Button> ); }; const renderAuth = () => { if (props.isLoggedIn) { // ログインしていたらログアウトボタン表示 return renderLogout(); } else { // ログアウトしていたらログインボタン表示 return renderDialog(); } }; return ( <AppBar position="static"> <Toolbar> <Typography variant="h6" className={classes.title}> News </Typography> {renderAuth()} </Toolbar> </AppBar> ); } const mapStateToProps = state => { return { isLoggedIn: state.auth.isLoggedIn, }; }; export default connect(mapStateToProps)(NavBar);ログインフォームはダイアログで表示するのでLandingPage.jsxのLoginFormコンポーネントは削除しましょう。そしてログイン状態ならLoginedPageにリダイレクトするようにします。
src/components/LandingPage.jsximport React from 'react'; import { connect } from 'react-redux'; import { Redirect } from 'react-router-dom'; function LandingPage(props) { if (props.isLoggedIn) { return <Redirect to={'/logined'} />; } else { return <div>LandingPage</div>; } } const mapStateToProps = state => { return { isLoggedIn: state.auth.isLoggedIn }; }; export default connect(mapStateToProps)(LandingPage);ここで挙動を確認してみましょう。ローカルサーバー起動します。
ヘッダーのログイン、新規登録ボタンでダイアログが表示されます。test@gmail.comなどの適当なEメールとパスワードで登録すると、ページが偏移してLoginedPageになります。
ログアウトをクリックしてLandingPageになっていたら正しく動作しています。
FirebaseのコンソールでAuthenticationを確認するとユーザーが登録されています!
おわり
お疲れさまでした!ほぼ完成です。あとはソーシャルログインを実装するためにfirebaseにそれぞれのプロバイダーapiキーなどを登録するだけです。
今回はダイアログの表示とログイン状態による表示の切り替えが主でした。material-uiを使えば簡単に実装できます。次回でログインフォームは完成です。
全4回
- 投稿日:2020-08-23T18:17:22+09:00
テキストエディタライブラリの比較検討
はじめに
以前、ノート共有型SNSの投稿フォーム実装についてご紹介という記事でreact-quillのことについて紹介させていただきました。
今回は、Reactに対応している複数のテキストエディタライブラリがあった中で、どのように比較検討しQuillを選定することになったのか書いていきたいと思います。比較したテキストエディタライブラリ
比較項目
- データ構造
- 編集前と編集後の差分を保持出来る形式であるか
- 利点
- 懸念点
react-quill
データ構造
- Deltaという独自のデータ構造
Delta形式{ ops: [ { insert: 'Gandalf', attributes: { bold: true } }, { insert: ' the ' }, { insert: 'Grey', attributes: { color: '#cccccc' } } ] }利点
- 日々メンテナンスが行われていて、開発が活発 2020/08/23時点
Toolbarのメニューは自由にカスタマイズ可能- 入力された情報を都度検知出来る
- 選択された文字の色・背景色を変更することが可能
- 写真・URLでビデオが入れられる
- moduleを使うことでカスタマイズが容易
懸念点
- 閲覧者モード(Read only mode)は提供されている機能として備わっていない
- 閲覧者モードとは、プレビューのみで編集は出来ないモードを指します。Qiitaでいうプレビューモードです。
Toolbarの位置を変更しUIを調整する & 閲覧者モードでは不要のイベントハンドラを無効にすることで対応可能Editor.js
データ構造
- JSON形式
BlockとInlineという概念があり、Block単位でオブジェクトが管理される。JSON形式{ "time": 1550476186479, "blocks": [ { "type": "header", "data": { "text": "Editor.js", "level": 2 } }, { "type": "paragraph", "data": { "text": "Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration." } }, { "type": "header", "data": { "text": "Key features", "level": 3 } }, { "type": "list", "data": { "style": "unordered", "items": [ "It is a block-styled editor", "It returns clean data output in JSON", "Designed to be extendable and pluggable with a simple API" ] } } ], "version": "2.8.1" }太字やハイパーリンク,文字の色・背景色は
Inlineで定義され、HTMLタグで表現される。JSON形式{ "type" : "list", "data" : { "style" : "unordered", "items" : [ "Hey. <b>Meet</b> the new Editor. On this page you can see it in action — try to edit this text." ] } }対応形式
公式で用意されているスタイルは以下の通りです。自作したスタイルを設定することも可能。
Block
- Header(h1, h2, ...)
- Image
- List
- Link
- Code
- Quote
- Delimiter
- Table
- Raw HTML
Inline
- Bold
- Italic
- Link
- InlineCode
- Maker
利点
- 選択文字列の色・背景色を変える機能が用意されている
- ショートカットが定義可能
Blockから他のBlockへの変換ができる(h1->h2, text->List, etc...)
Blockの入れ替えが出来る懸念点
- 閲覧者モード(Read only mode)は実装中 2020/08/23時点
- 現段階で実現するのであれば、
Block,Inlineツールの閲覧者バージョンを実装する必要があると考えられる。Blockの並び替え機能とBlockの新規追加を無効にできれば、閲覧者モードとして使えそうだが、本家のJSをオーバーライドする必要が生まれるかもしれない。私たちの結論としては、自分たちでカスタマイズするには実装コストが大きい。MediumEditor
データ構造
- HTML形式
- 各要素は
<p>タグで区切られる利点
懸念点
- Markdown記法に対応していない
- 選択した文字の色・背景色の変更は対応していない
- あくまでMediumと似せたデザインというだけで、機能は全くの別物。(例えば、マーカー機能が無いなど)
動作の様子
PELL
データ構造
- HTML形式
利点
- Markdown記法に対応していること
- サイズが軽量であること
サイズ比較
ライブラリー サイズ(昇順) pell 3.54kB medium-editor 105.kB trix 204kB quill 205kB 懸念点
- 開発が2年以上止まっている(2020/08/23時点)
- 基本的に自分でカスタマイズする方式なので、実装量が多くなりやすい
- 直っていない不具合がいくつか確認出来た
- Issuesより
trix
データ構造
- HTML形式
利点
- ファイルアップロードが可能
- アップロードAPI先も指定出来る
- 日本語入力中でもちゃんとイベント発火する
- メンションや値の埋め込みが可能
懸念点
- 選択した文字の色・背景色を変更する機能が備わっていない
おわりに
データ構造・利点・懸念点をリストアップし、私たちのアプリに必要な機能要件を満たすことが出来るのか1つ1つ比較・検討することで適切な技術選定が行えますね?
- 投稿日:2020-08-23T14:24:49+09:00
SPAをStatic Web Appsにデプロイ、本番環境で使ってみた
MicrosoftのカンファレンスMS Build 2020でAzure Static Web Appsが発表され、同時期にちょうどSPA(Single Page Application)でウェブアプリケーションを開発中だったので、まだPreview中でしたが、GitHub Actionsとの連携もできそうだったので、ぜひプロダクションで使おう!と思い現在進行形で運用しています。
なので、私みたいにHTML、CSS、JavaScript等の静的コンテンツを本番環境で運用しようとしている方向けに「Static Web Appsは使えるのか?」という記事を書きたいと思います。本番環境で動かしてみた所感
先に結論を言うと、Static Web Apps 十分使えます。様々気づきもあったので紹介していきます。まずは出来上がったものをお見せすると以下のサイトです。
【拡散希望!】
— Kei Otoi Okada (@KeiOtoiOkada) August 25, 2020
留学希望者には絶対見てほしい「日本初!AIを使った最強留学情報サイト」ついに完成しました!自信を持ってマジヤバすごいサイトになりました。ぜひ一度見て見てください!
New サイト:https://t.co/hZmJDG4Kyb#留学 #留学生 #アメリカ留学 pic.twitter.com/EX7otDPhdgこちらのサイト、留学希望者が大学を選ぶ際に、より具体的な情報を得て多くの人が留学に行ってほしい!って想いで作成しました。そうです、こちらのサイトのフロント側(静的コンテンツ部分)が丸っとStatic Web Appsに乗っかって今も現在進行形で本番環境で動いています。
Azure Static Web Appsとは
いわゆるSPA(Single Page Application)やらSSG(Static Site Generator)やらを含むHTML、CSS、JavaScript等の静的コンテンツを配信する新しいAzureサービスです。その他にもカスタムドメインやSSL証明書が無料で設定できたり、Azure Functionsと統合できたり(今回は使ってませんが)、その他もろもろ非機能要件にもしっかり対応してくれています。
また今回使いたい!と思わせてくれた機能のひとつで、ビルド環境としてGitHub Actionsが強制的に使われるように設定されているため、コンテンツを更新する度にビルドを走らせるJAMstack運用も可能となっています。Azure Static Web Appsの公式サイトはこちら
今回の構成
インフラの運用、管理するのは絶対に嫌だ!ということで考え付く最高のPaaS(Platform as a Services)を採用(Static Web Appsに関してはまだPreviewだが)しました。これでやりたくないインフラの管理はなくなり、また将来の拡張性も考え(モバイルアプリも作るかもだから)フロントエンドとバックエンドを分けて構築しました。以下が構成図です。
- ウェブアプリケーションのフロントエンドはReactを使いTypeScriptで書いています(今回はこの部分の話しかしません)
- ウェブアプリケーションのバックエンドは.NET Coreを使いC#で書いています
- データベースはSQL DBを使用しています
Static Web Appsにデプロイ完了するまでの手順(軽く)
実際のデプロイ完了する手順は以下の記事に良く書かれているのでそちらを参照していただければなと思います。
たぶん10分で試せる。Azure Static Web AppsにWebサイトをデプロイして独自ドメイン設定とFunctionsでAPI公開まで一応簡単に手順だけは紹介します。
1. GitHubリポジトリを作成し、ソースコードをPush
2. Azureポータル上でStatic Web Appsを作成し、先ほど作ったGitHubのリポジトリを参照させる
3. GitHubのリポジトリ上にgitHub/workflowsのフォルダーが自動的に作成され、ビルド&デプロイの手順が書かれたymlファイルが自動生成される
4. カスタムドメイン購入&ポータルで設定正直驚くほど簡単にデプロイが完了します。リアルに10分ぐらいで完了してしまいます。
Static Web Appsで運用してみて良かったと感じたところ
今のところ問題なく本番環境でも動いていて、Previewだがかなり良く出来ている印象。以下良かった点まとめています。
- グローバルコンテンツディストリビューションを使用
- Static Web Apps作成する際、自動的にビルド&デプロイの手順が書かれたymlファイルを定義してくれる
- Static Web Appsのルーティング定義は routes.json で記述すれば問題なし
- やっぱり無料は助かる
グローバルコンテンツディストリビューションを使用
全世界のApp Service内にデプロイ + Traffic Managerで近くのApp Serviceにルーティングしているらしく、高可用性はかなり期待できるかと。
一応Static Web Appsを作成する際に、リージョン指定をしなくてはいけないのですが、こちらはAzure FunctionsにデプロイするAPIのみ、指定したリージョンにデプロイされるものなので、静的コンテンツのデプロイ先としてはどこのリージョンを指定しても良さそうです。ですので、どのリージョンを選択しようと、アクセスした地域の一番近いApp ServiceにTraffic Managerがルーティングしてくれる仕様となっています。
具体的な裏側が気になる方は、こちらのブログ記事を読まれるとスッキリすると思います。
App Service Static Web Apps の仕組みを探る(非公式)
Static Web Apps作成する際、自動的にビルド&デプロイの手順が書かれたymlファイルを定義してくれる
最初の説明にもありましたが、Static Web Appsはビルド環境としてGitHub Actionsが強制的に使われるように設定されているため、コンテンツを更新する度にビルドを走らせるJAMstack運用も可能となっています。以下が簡単な手順です。
Static Web Appsを作成する際に、GitHub アカウントと連携するよう聞いてきます。
認証が完了すると、どの組織、リポジトリ、ブランチとSyncするか聞いてくるので、予めPushしておいたGitHubのリポジトリとブランチを選択します。ただこれで「確認および作成」ではなくて、必ず「次:ビルド >」を選択してください。
ビルドの設定の中では、「アプリの成果物の場所」を設定します。こちらにはデプロイすべきファイル群が格納されるディレクトリを設定します。今回はTypeScript、ReactでHTML/JSファイルをビルドする場合は、buildといったディレクトリにビルド結果のファイル群を格納しますので、そのディレクトリ名を指定しておきます。リポジトリのルートディレクトリにindex.htmlファイルなどを格納している場合は、この項目は空のままで構いません。「アプリの成果物の場所」が設定できると「確認および作成」を押して、Static Web Appsを実際に作成していきます。
Static Web Apps自体は1~2分で完成しますが、同時にGitHub Actionsとの連携もされ、自動的にビルド&デプロイが実行されます(こちらは5分ぐらいで完了します)。GitHubアカウントを見れば、Actionsというタブが増えているのが分かると思います。
またymlファイルも自動的に生成されていることも分かります。
ビルド&デプロイが完了したら、あとはStatic Web Appsのリソースからサイトを閲覧しにいくだけです!
無事見れました!これでコンテンツを更新する度にビルドを走らせるJAMstack運用の完成となります。
Static Web Appsのルーティング定義は routes.json で記述すれば問題なし
今回、若干引っかかったのがこのルーティングの定義。実はAzureのサービスの中に、Static Web Appsと同じように静的コンテンツを配信できるBlob StorageのStatic website hostingというサービスが存在しています。静的コンテンツを配信するという観点では同じようなことができるのですが、Static Web Appsと同じように細かいルーティング設定ができません。「じゃあStatic Web Appsはどうやってルーティング定義するんだろう?」と疑問に思ったのですが、さすが新サービス、routes.json で記述してあげれば何の問題なく動くことが分かりました。
Static Web Appsのルーティングのドキュメントはこちら
Reactの場合は public フォルダー内にroutes.jsonを配置して、ビルド&デプロイすれば自動的に読み取ってルーティングをしてくれます。
これで、index.html 以外のPathでアクセスしたときも404のNot Foundにならなくて済みます。
まとめ
まだPreviewですが、このクオリティのものを無料で使えるのはやはり助かります。またGAされたとしても、Blob Storageより少し高くて、App Serviceより安い価格が期待できると思うので、今回の私のようにSPAを構築しようとしている方にとってはStatic Web Appsを選択肢のひとつに入れてもいいのではないかなと思います。
一度触ってみたいと思う方は、Microsoftが無償で提供している学習ポータルのMS Learnがあるのでそちらから試してみると良いと思います。
Azure Static Web Apps で Angular、React、Svelte、または Vue の JavaScript アプリと API を発行する
- 投稿日:2020-08-23T13:56:21+09:00
【React】【textarea】改行のみ、文字未入力の際に送信ボタンを無効化する
都内スタートアップでフロントエンドエンジニアとしてReactを書いております。
最近業務でチャット機能を実装する際、タイトルどおりの実装が必要だったのでメモ程度に残していきます。全体イメージ
import React, {useState} from 'react'; const MessageForm = () => { const [message, setMessage] = useState(''); const isExistTextInInput = !!message.match(/[^\n]/) const sendMessage = () => { if(!isExistTextInInput) return //inputの値を送信!! } return ( <div> <textarea value={message} onChange={e => setMessage(e.target.velue)} /> <button onClick={() => isExistTextInInput && sendMessage} className={isExistTextInInput ? 'btn' : 'btn--disable'} > 送信 </button> </div> ); };解説
きもは下記変数です
const isExistTextInInput = !!message.match(/[^\n]/)
String.prototype.match()正規表現全体に一致したすべての結果を配列で返します
何もないとnullになります
!!でboolに型変換配列ならfrue, nullならfalseが返ります
条件である
[^\n]
^は、〜以外という正規表現\nは改行の正規表現なので下記条件分岐になります。
改行以外の文字列がある => 配列が返る
文字列がない、改行しかない => nullが返るまとめ
あとは
isExistTextInInputを使ってボタンのメゾット発火を無効にしたり、クラスを付け替えるだけです
めちゃめちゃ簡単なのでお試しあれキーボード操作系の
エンターシフトで送信や、","でタグを追加だったりも実装したので後にまとめようかなと思います!
- 投稿日:2020-08-23T13:48:08+09:00
【React】デフォルト値もundefinedチェックもいらないcreateContext【Typescript】
Introduction
Typescriptによる型定義とContextによるState管理で、ReactのDX(Developer Experience)は劇的に進化しました。
しかし、素晴らしい型定義によって、謎に苦しめられることもあります。
Problem
createContextにはデフォルト値を渡す必要がありますが、往々にしてそのデフォルト値はundefinedです。const AuthContext = React.createContext<authContextType | undefined>(undefined);これの何が問題なのでしょうか?
それはContextを使用する際に致命的になります。// どこかでuseContextして作られたuseAuth関数 const Auth = useAuth()!;
createContextのデフォルト値としてundefinedを指定したことで、Contextを使用する際にその値がundefinedでないかを”全ての場所で”逐一確認する必要があります。根本的に、
undefinedはアプリケーションにとって何の意味もなさない値です。
それを、わざわざ確認しなければいけないということは、バグやエラーの温床になります。Solution
デフォルト値を
undefinedとせず、未定義チェックを行えるcretaeContextのwrapperを作成して解決します。function createCtx<ContextType>() { const ctx = React.createContext<ContextType | undefined>(undefined); function useCtx() { const c = React.useContext(ctx); if (!c) throw new Error("useCtx must be inside a Provider with a value"); return c; } return [useCtx, ctx.Provider] as const; }今回の問題は、デフォルト値
undefinedのContextに対して、useContextをしてしまうとでundefinedがuseContextに紛れ込んでしまうことでした。そこで、wrapper関数に未定義チェック関数を定義し、
useContextがundefinedである場合はエラーをthrowします。これで、wrapper関数
createCtxで定義されたuseCtxは、<ContextType>で渡されるジェネリック型を返します。
無事、useContextからundefinedを駆逐しました。Example
ユーザー管理、ログイン、ログアウトなどを管理するContextのExampleを作ってみました。
type User = { id: string; name: string; }; type authContextType = { user: User | null; signIn: () => void; signUp: () => void; signOut: () => void; }; function createCtx<ContextType>() { const ctx = React.createContext<ContextType | undefined>(undefined); function useCtx() { const c = React.useContext(ctx); if (!c) throw new Error("useCtx must be inside a Provider with a value"); return c; } return [useCtx, ctx.Provider] as const; } const [useAuth, SetAuthProvider] = createCtx<authContextType>(); const AuthProvider: React.FC = (props) => { const auth = useAuthCtx(); return <SetAuthProvider value={auth}>{props.children}</SetAuthProvider>; }; const useAuthCtx = (): authContextType => { const [user, setUser] = React.useState<User | null>(null); const signIn = () => { // Some sign in action }; const signUp = () => { // Some sign up action }; const signOut = () => { // Some sign out action }; return { user, signIn, signUp, signOut }; }; export const App = () => { const auth = useAuth(); return ( <AuthProvider> <button onClick={() => {auth.signIn()}}>ログイン</button> <button onClick={() => {auth.signUp()}}>サインアップ</button> </AuthProvider> ); };Sample
【Qiita記事準備中】 React.createContextで管理するFirebase Authentication
Happy Hacking!!?
苦情受付中...✍️
- 投稿日:2020-08-23T09:23:36+09:00
React アプリに Power BI レポートやダッシュボードを埋め込む
本記事では、Microsoft からリリースされている React 用の Power BI クライアントについて見ていきます。
powerbi-client-react とは
React Component として powerbi-client がラップされたもので、簡単に React アプリケーションに Power BI を埋め込めるというものでした。これは便利。では早速スクラッチで作ってみましょう。
React アプリを作成
今回は Create React App というツールを使ってアプリを作成します。
1. npx で create-react-app を実行。テンプレートは自分の好みで typescript を指定。
npx create-react-app react-powerbi-embed --template typescript2. 作成が完了したらフォルダに移動してアプリを一旦実行してアプリが起動する事を確認。
cd react-powerbi-embed yarn start3. powerbi-client-react を追加。
npm install powerbi-client-reactPower BI レポートの選定
埋め込むレポートを用意します。どのレポートでもいいのですが、ここでは既存のアプリにあるレポートを使ってみます。
1. Power BI より「アプリ」を選択し、「アプリの取得」ボタンをクリック。一覧より任意のアプリを選択。ここでは COVID-19 US Tracking Report を選択。
3. アドレスより report の Id をコピーしておく。Id は url の reports 直後にある GUID。
アクセストークンの取得
Power BI の埋め込みに使えるアクセストークンは種類がいくつかあるため、この記事では説明しません。機会があれば別の記事で紹介しますが、今回は PowerShell を使って一時的に使えるものを取得します。
1. PowerShell を起動して Power BI 用モジュールを追加。
Install-Module MicrosoftPowerBIMgmt2. Power BI に接続。ログインが求められるので認証情報を入力。
Connect-PowerBIServiceAccount3. アクセストークンの取得。出力されたトークンの Bearer 以降をコピー。
Get-PowerBIAccessToken -AsStringトークンは一定期間で無効となるため、エラーが出るようになったらまた再発行してください。
Power BI レポートの埋め込み
GitHub ページにあるサンプルを使っていきます。
1. 作成した react アプリを Visual Studio Code で開き、App.tsx を選択。
2. import を追加。GitHub の例では powerbi-clien-react だけ追加しているが、powerbi-client も必要。
import { PowerBIEmbed } from 'powerbi-client-react'; import { models, Report} from 'powerbi-client';3. 既存の header 要素の内容を削除して、代わりにサンプルを張り付け。PowerBIEmbed は powerbi-client-react が提供する React コンポーネント。
<PowerBIEmbed embedConfig = {{ type: 'report', // Supported types: report, dashboard, tile, visual and qna id: '<Report Id>', embedUrl: '<Embed Url>', accessToken: '<Access Token>', tokenType: models.TokenType.Embed, settings: { panes: { filters: { expanded: false, visible: false } }, background: models.BackgroundType.Transparent, } }} eventHandlers = { new Map([ ['loaded', function () {console.log('Report loaded');}], ['rendered', function () {console.log('Report rendered');}], ['error', function (event) {console.log(event.detail);}] ]) } cssClassName = { "report-style-class" } getEmbeddedComponent = { (embeddedReport) => { this.report = embeddedReport as Report; }} />4. embedConfig の id と accessToken に取得した値を張り付け。また tokenType を models.TokenType.Aad を指定。
embedConfig = {{ type: 'report', // Supported types: report, dashboard, tile, visual and qna id: '<取得したレポート ID>', embedUrl: 'https://app.powerbi.com/reportEmbed', accessToken: '<取得したアクセストークン>', tokenType: models.TokenType.Aad, settings: { panes: { filters: { expanded: false, visible: false } }, background: models.BackgroundType.Transparent, } }}5. getEmbeddedComponent ではローカル変数であろう report に対して埋め込まれたレポートを取得しているが、React ではステート使う事が多いため、React の import を変更し useState を読み込み。
import React, { useState } from 'react';6. function 直下にステート管理用のコードを追加。
const [report, setReport] = useState<Report>();7. 指定した setReport を使うように getEmbeddedComponent を変更。
getEmbeddedComponent = { (embeddedReport) => { setReport(embeddedReport as Report); }}8. cssClassName に "report-style-class" が指定されているが、そのようなスタイルは存在しないため、App.css に以下を追加。
.report-style-class { height: 69vh; margin: 1% auto; width: 60%; }9. サンプルではレポートの背景が透明に指定されているが、見辛いため Default に変更。
background: models.BackgroundType.Defaultアプリを確認
コードが追加されたら、自動更新されたアプリを確認してみましょう。レポートが埋め込まれている事が分かります。
イベントの取得
eventHandlers には各種イベントの処理を指定できます。サンプルではイベント発生時にコンソールにログを出力するようになっています。
eventHandlers = { new Map([ ['loaded', function () {console.log('Report loaded');}], ['rendered', function () {console.log('Report rendered');}], ['error', function (event) { if(event!=undefined) console.log(event.detail);}] ]) }ブラウザで F12 キーを押下して、コンソールを確認してください。
ダッシュボードの埋め込み
Power BI の埋め込みはレポート以外にも、ダッシュボードやタイル、QnA も埋め込めます。ここではダッシュボードを試してみます。
1. Power BI で任意のダッシュボードを追加。ここではレポートから複数のタイルをピン止めしたダッシュボードを作成。
2. レポートと同様、アドレスから Id をコピー。
3. Visual Studio に戻り、embedConfig の以下項目を変更。
- id: ダッシュボードの ID に変更
- embedUrl: 'https://app.powerbi.com/dashboardEmbed' に変更
4. アプリを確認してダッシュボードが表示されていることを確認。
まとめ
今回は React 用の PowerBI クライアントを簡単に紹介しました。より詳細は実際のコードを見ていただくとともに、GitHub にあるデモアプリも使ってみてください。
- 投稿日:2020-08-23T03:21:58+09:00
React フック API の使い方メモ
何番煎じだって話ですが、タイトルの通りです。
公式サイトにも使い方はありますが、
表現が自分の中でよく飲み込めないので自分用にサンプルとわかりにくいところだけ書き起こしてみました。useState
ここに関しては公式を読めば特に補足はいらないかなと思います。
UseStateSample.jsximport React, { useState } from 'react' const UseStateSample = () => { const [count, setCount] = useState(0) return ( <> <h2>Use State Sample</h2> Count: {count} <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> </> ) } export default UseStateSample;useEffect
基本機能はレンダリングが行われるタイミングでここの処理を実行します。下記例ではcountの初期値を0に設定していますが、useEffect内で+1されているので1で画面に表示されます。
UseStateSample.jsximport React, { useState, useEffect } from 'react' const UseEffectSample = () => { const [count, setCount] = useState(0) useEffect(() => { setCount((count) => count + 1) }, []); return ( <> Count: {count} </> ) } export default UseEffectSample;重要なポイントとしては以下3点かなと。
セッタ(上記例でいうところのsetCount)はその時点での値を引数として受け取る
setCount((count) => count + 1)上記のcountのことです。
例ではuseStateで0を入れているので初回は0が入ってきます。第2引数に依存する値を配列で指定できる`
これも実は上記例ですでに使用している機能です。
上記例は下記のように空配列を指定してあげないと、無限にレンダリングが起こります。useEffect(() => { setCount((count) => count + 1) }, []);useEffectでは第2引数に指定がないとprops(上述ではない)かstateが更新されるたびに実行されることになるからです。
countを更新した際に再レンダリングされないようにするために空配列を指定しています。
空配列が指定されるとuseEffectはマウント時とアンマウント時にしか実行されません。useEffectはレンダリング後に実行される
上記例では特に問題になりませんが下記のようにするとエラーが発生します。
UseStateSample.jsximport React, { useState, useEffect } from 'react' const UseEffectSample = () => { const [count, setCount] = useState() useEffect(() => { setCount({ value: 1 }) }, []); return ( <> Count: {count.value} </> ) } export default UseEffectSample;
UseEffectSample.jsx:12 Uncaught TypeError: Cannot read property 'value' of undefined
valueなんて値はないよって言ってます。これはuseEffectが実行される前にレンダリングされてcount.valueにアクセスされていることを意味します。useContext
私の現在の理解では複数のコンポーネント間で共有できるオブジェクトを提供するもの です。
わかりやすいところでいくと一番最初に挙げたStateを複数コンポーネント間で共有することができるようになります。
ちょっとアレンジした例を紹介。
以下ではcreateContextでuseContextを実装し、UsingSharedState1とUsingSharedState2でuseContextを宣言し、その値を参照及び変更できるようにした例です。index.jsximport UseContextSample from '../components/UseContextSample' import UsingSharedState1 from '../components/UsingSharedState1' import UsingSharedState2 from '../components/UsingSharedState2' import styles from '../styles/Home.module.css' export default function Home() { return ( <div className={styles.container}> <main className={styles.main}> <UseContextSample> <UsingSharedState1/> <UsingSharedState2/> </UseContextSample> </main> </div> ) }UseContextSample.jsximport React, { useState } from 'react' export const UseContext = React.createContext({ value: 0, changeValue: () => {} }); const UseContextSample = ({ children }) => { const [count, setCount] = useState(0) return ( <UseContext.Provider value={{value: count, changeValue: (count) => setCount(count)}}> {children} </UseContext.Provider> ) } export default UseContextSample;UsingSharedState1.jsximport React, { useContext } from 'react' import { UseContext } from './UseContextSample' const UsingSharedState1 = () => { const sharedData = useContext(UseContext); return ( <> <span>UsingSharedState1: {sharedData.value}</span> <button onClick={() => sharedData.changeValue(1)}>UsingSharedState1 Button</button> </> ) } export default UsingSharedState1;UsingSharedState2.jsximport React, { useContext } from 'react' import { UseContext } from './UseContextSample' const UseContextSample2 = ({ children }) => { const sharedData = useContext(UseContext); return ( <> <span>UsingSharedState2: {sharedData.value}</span> <button onClick={() => sharedData.changeValue(2)}>UsingSharedState2 Button</button> </> ) } export default UseContextSample2;useMemo
コンポーネントがレンダリングされる度にオブジェクトの再生成を防ぐためのもの
という認識(間違ってるかも)UseMemoSample.jsximport React, { useContext, useMemo } from 'react' import { UseContext } from './UseContextSample' const UseMemoSample = () => { const sharedData = useContext(UseContext); const array = [...Array(10000000).keys()] const memo = useMemo(() => { let temp = 0; array.map((index) =>{ temp = temp + index; }) return temp }, [xxx]); return ( <> <p>{memo}</p> </> ) } export default UseMemoSample;無理やりな例ですが、上記の例でコンポーネントがレンダリングされるたびにmemoの値を計算するのは無駄に見えますよね。
これを毎回実行しないようにするためのもの。useEffectと同様に第2引数で依存変数を定義すると、実行されるタイミングを制御できます。useCallback
useMemoの関数版。
関数の場合は実行されなくても、そのインスタンスをコンポーネントとともに生成してしまいます。
性能向上やメモリの節約(?)をするために、毎回生成する必要のないものについてはuseCallbackによって生成します。例によって、第2引数には依存変数を定義できます。一旦は以上です。
時間があるときに他のHooksも書き残しておこうと思います。


























