LINEアプリ内ブラウザのリンク先をデフォルトブラウザで開く方法
どうしたものかわからず target="_blank"
を試したりしていたが、
URL末尾に ?openExternalBrowser=1
をつけるだけでよかった。
React HooksのTypeScriptによる型付け
比較的よく使うuseState/useReducer/useContextのTypeScriptによる型付け
Hooksの詳細についてはこちらを参照
Hooks API Reference – React
useState
import * as React from 'react' export const UseStateExample = () => { const [count, setCount] = React.useState<number>(0) return ( <div> <div>count: {count}</div> <div> <button onClick={() => setCount(count + 1)}>++</button> </div> </div> ) } // 複数の値を1つのstateにまとめる場合 interface State { count: number message: string } const initialState: State = { count: 0, message: 'hi', } export const ComplexStateExample = () => { const [state, setState] = React.useState<State>(initialState) const { count, message } = state return ( <div> <h1>{message}</h1> <div>count: {count}</div> <div> <button onClick={() => setState({ ...state, count: count + 1, }) } > ++ </button> </div> </div> ) }
useReducer
import * as React from 'react' interface State { count: number } interface IncrementAction { type: 'increment' } interface ChangeAction { type: 'change' number: number } interface ResetAction { type: 'reset' } type Action = IncrementAction | ChangeAction | ResetAction const reducer = (state: State, action: Action) => { switch (action.type) { case 'increment': return { count: state.count + 1 } case 'change': return { count: action.number } case 'reset': return { count: 0 } default: return { ...state } } } const initialState: State = { count: 0, } export const UseReducerExample = () => { const [state, dispatch] = React.useReducer(reducer, initialState) return ( <div> <div>count: {state.count}</div> <div> <button onClick={() => dispatch({ type: 'increment' })}>++</button> <button onClick={() => dispatch({ type: 'change', number: 5 })}> to 5 </button> <button onClick={() => dispatch({ type: 'reset' })}>reset</button> </div> </div> ) }
useContext
import * as React from 'react' interface ExampleContextValue { message: string count: number } const initialContextValue: ExampleContextValue = { message: 'hello', count: 100, } const ExampleContext = React.createContext<ExampleContextValue>( initialContextValue, ) export const UseContextExample = () => { const { message, count } = React.useContext(ExampleContext) return ( <div> {message}: {count} </div> ) }
NetlifyでSPAのfallbackを行う
/
以外でリロードすると404になってしまうのを防ぐため、
netlify.tomlにfallbackの設定を追加した。
netlify.toml
# COMMENT: This a rule for Single Page Applications [[*redirects*]] from = "/*" to = "/index.html" status = 200
おまけ
Visual Studio Codeでtomlを扱いやすくするためにBetter TOMLを導入した
Google Cloud BuildでSPAのビルドとFirebase Hostingへのデプロイを自動化する
この記事はSpeee Advent Calendar 201819日目の記事です。
前日はyuta_kobayashiによる雑談の効用についてのお話でした。
今日はSPAの継続的デリバリーを手軽に実現できる、
Google Cloud Buildのお話です。
やること
最近、ちょっとしたSPAのデプロイ先としてFirebase Hostingを使うことが多くなってきたが、ローカルでFirebaseプロジェクトの設定をしたり、ビルドやデプロイを手動で実行するのが面倒なので最初にCloud Buildを設定してビルドとデプロイを自動化するようにしている。
以上を自動化する。
参考ドキュメント
ビルドトリガーを使用したビルドの自動化 | Cloud Build | Google Cloud
準備
- Firebaseで新規プロジェクトを作成
- アプリケーションはcreate-react-appで適当に作っておく
Firebaseのセットアップ
firebase-cli を使ってセットアップする
// Firebase CLIでログイン $ firebase login // SPAのプロジェクトディレクトリでfirebaseを初期化 $ firebase init // hostingを選択 ? **Which Firebase CLI features do you want to setup for this folder? Press Space** ❯◉ Hosting: Configure and deploy Firebase Hosting sites // buildディレクトリの内容をdeployする ? **What do you want to use as your public directory?** (public) build // SPAの設定 ? **Configure as a single-page app (rewrite all urls to /index.html)?** (y/N) y
デプロイの確認
念のため、Firebase Hostingにローカルからデプロイできることを確認しておく
$ yarn build $ firebase deploy
Cloud Buildの設定
APIの有効化
コンソールにアクセスしてCloud Build APIを有効にする
FirebaseはGCPと統合するとBlazeプラン(従量)に上がってしまうため、Firebaseのプロジェクトと別にプロジェクトを作ってからCloud Build APIの有効化を行うとよい。
設定
ビルドトリガーの作成
「トリガーの作成」からビルドトリガーを作成する。
cloudbuild.yamlの作成
cloudbuild.yamlを追加する。
ひとまずビルドの動作確認のため、yarn install
と yarn build
の設定のみ書いてcommitし、masterにpushする
steps: - name: 'gcr.io/cloud-builders/yarn' args: ['install'] - name: 'gcr.io/cloud-builders/yarn' args: ['build']
pushして確認
cloudbuild.yamlをcommit、pushしてトリガーの確認を行う。
Cloud Buildのビルド履歴から、ビルドの結果が閲覧できる。 各ビルドステップがグリーンになっていればトリガーが発動し、ビルドが成功している。
Firebaseデプロイのビルドステップを追加する
ここを参考にした
Firebaseのイメージを作成してgcr.ioにsubmitしておく
$ git clone https://github.com/GoogleCloudPlatform/cloud-builders-community.git $ cd cloud-builders-community/firebase/ $ gcloud builds submit --config cloudbuild.yaml .
Firebaseトークンの取得
$ firebase login:ci ... ✔ Success! Use this token to login on a CI server: [GENERATED_TOKEN] // 生成されたトークンが表示される // 後で使うのでexportしておく $ export FIREBASE_TOKEN=[GENERATED_TOKEN]
KMSでトークンを暗号化
KMSの有効化
https://console.cloud.google.com/security/kms の「セットアップ」でAPIを有効にする
Cloud BuildのサービスアカウントにKMS復号の権限を追加
トークンの暗号化
$ gcloud auth login $ gcloud config set project [PROJECT_ID] $ gcloud kms keyrings create cloudbuilder --location global $ gcloud kms keys create firebase-token --location global --keyring cloudbuilder --purpose encryption $ echo -n $FIREBASE_TOKEN | gcloud kms encrypt \ --plaintext-file=- \ --ciphertext-file=- \ --location=global \ --keyring=cloudbuilder \ --key=firebase-token | base64 [ENCRYPTED_TOKEN]
表示されたトークンをコピーしておく
cloudbuild.yamlにステップを追加
steps: - name: 'gcr.io/cloud-builders/yarn' args: ['install'] - name: 'gcr.io/cloud-builders/yarn' args: ['build'] - name: 'gcr.io/$PROJECT_ID/firebase' args: ['deploy', '--project=$PROJECT_ID'] secretEnv: ['FIREBASE_TOKEN'] secrets: # kmsKeyNameの中では$PROJECT_IDが使えないので直接書く必要あり - kmsKeyName: 'projects/[PROJECT_ID]/locations/global/keyRings/cloudbuilder/cryptoKeys/firebase-token' secretEnv: FIREBASE_TOKEN: '[ENCRYPTED_TOKEN]' # ここにさきほどコピーしたトークンをペースト
余談: create-react-appの環境変数
ビルドトリガーにはsubstitution(代入変数)という形で変数を定義できるが、そのまま環境変数としてロードされるわけではないので env
を使ってsubstitution -> 環境変数への変換を行う必要がある
ビルドトリガーで上記のようにsubstitutionを設定し、cloudbuild.yamlで下記のようにして利用する
steps: - name: 'gcr.io/cloud-builders/yarn' args: ['install'] - name: 'gcr.io/cloud-builders/yarn' args: ['build'] env: # substitutionをenvに渡す - 'REACT_APP_GOOGLE_MAPS_API_KEY=$_GOOGLE_MAPS_API_KEY' ... substitutions: _GOOGLE_MAPS_API_KEY: your-api-key
確認
以上の設定を行い、cloudbuild.yamlをcommitしてGitHubにpushすればビルドが開始される
まとめ
現在実運用しているプロジェクトでは、複数のビルドトリガーを用意し、
devブランチへのpushを開発環境に即反映、releaseブランチにpushしたら本番にデプロイといった運用をしている。
ビルドトリガーによって別の cloudbuild.yaml を設定することもできるため、環境に応じて異なるビルドステップを設定することも可能だ。
最近はGitHub Actionsが登場し、同じようなことが実現できるようになったので、そちらも近いうちに試してみたい。
明日はiida-hayatoによるハッカソンの話
お楽しみに!
TypeScriptで書いたCloud FunctionsをCloud Buildでビルド&デプロイする際の設定
何?
Cloud FunctionsをTypeScriptで書き、かつデプロイを自動化したい人向けの情報
前提
Cloud FunctionsをCloud Buildでデプロイする方法については id:selmertsx が以下のエントリで詳しく書いてくれている
TypeScriptで書いたコードをビルドしてからデプロイするための設定
cloudbuild.yaml
steps: - name: 'gcr.io/cloud-builders/npm' args: ['install'] - name: 'gcr.io/cloud-builders/npm' args: ['run', 'build'] - name: 'gcr.io/cloud-builders/gcloud' args: - beta - functions - deploy - test - --entry-point=test - --trigger-http - --stage-bucket=${_BUCKET_NAME} substitutions: _BUCKET_NAME: your-bucket
—stage-bucket
に渡すbucket名はデフォルトではsubstitutionsに設定した値になるが、Cloud Buildのトリガーに代入変数が設定されていればそちらが使われる。
create-react-app + antd-mobile + TypeScript環境へのStorybook導入
前提
- create-react-app
- TypeScript
- antd-mobile(antdもほぼ同じ方法で対応可能)
Storybookの導入
$ npm i -g @storybook/cli $ getstorybook
追加されたstoriesフォルダは /src 以下に移動する
TypeScriptの型定義を追加
$ yarn add -D @types/storybook__react @types/storybook__addon-actions
configの追加、変更
.storybook/webpack.config.js を追加する
const genDefaultConfig = require('@storybook/react/dist/server/config/defaults/webpack.config.js') const tsImportPluginFactory = require('ts-import-plugin') module.exports = (baseConfig, env) => { const config = genDefaultConfig(baseConfig, env) config.module.rules.push({ test: /\.tsx?$/, exclude: /node_modules/, include: [/src/], loader: 'ts-loader', options: { getCustomTransformers: () => ({ before: [ tsImportPluginFactory({ libraryName: 'antd-mobile', libraryDirectory: 'lib', style: 'css', }), ], }), }, }) config.resolve.extensions.push('.ts', '.tsx') return config }
.storybook/config.jsを書き換え
import { configure } from '@storybook/react' const req = require.context('../src/stories', true, /.stories.tsx$/) function loadStories() { req.keys().forEach(filename => req(filename)) } configure(loadStories, module)
storiesを.tsxに置き換え
src/stories/index.stories.tsx
import { action } from '@storybook/addon-actions' import { storiesOf } from '@storybook/react' import { Button } from 'antd-mobile' import * as React from 'react' storiesOf('Button', module) .add('with text', () => ( <Button onClick={action('clicked')}>Hello Button</Button> )) .add('with some emoji', () => ( <Button type="primary" onClick={action('clicked')}> <span role="img" aria-label="so cool"> 😀 😎 👍 💯 </span> </Button> ))
動作確認
$ yarn storybook
モバイル端末のプレビューを追加する
storybook/addons/viewport at master · storybooks/storybook
addons/viewportを使う
$ yarn add -D @storybook/addon-viewport
.storybook/addons.js
import '@storybook/addon-actions/register' import '@storybook/addon-links/register' import '@storybook/addon-viewport/register' // 追加
これでモバイル端末サイズでの確認が簡単にできるようになる
antd-mobileのthemeを変更している場合
.storybook/webpack.config.js が少し複雑になる
const genDefaultConfig = require('@storybook/react/dist/server/config/defaults/webpack.config.js') const tsImportPluginFactory = require('ts-import-plugin') const modifyVars = require('../antdTheme') const path = require('path') module.exports = (baseConfig, env) => { const config = genDefaultConfig(baseConfig, env) config.module.rules.push({ test: /\.tsx?$/, exclude: /node_modules/, include: [/src/], loader: 'ts-loader', options: { getCustomTransformers: () => ({ before: [ tsImportPluginFactory({ libraryName: 'antd-mobile', libraryDirectory: 'lib', style: true, }), ], }), }, }) config.resolve.extensions.push('.ts', '.tsx') config.module.rules.push({ test: /\.less$/, use: [ { loader: 'style-loader', }, { loader: 'css-loader' }, { loader: 'less-loader', options: { modifyVars }, }, ], include: path.resolve(__dirname, '../src/', '../node_modules/'), }) return config }
Ant Design Mobleをcreate-react-app(TypeScript)のプロジェクトで使う
Ant Design Mobileをcreate-react-app(w/ TypeScript)のプロジェクトで使う方法
create-react-appのセットアップに関しては別記事にまとめた
antd-mobileを追加
$ yarn add antd-mobile
モジュール化されたantd-mobileを利用可能にする
react-app-rewiredでCRAのconfigに変更を加える
$ yarn add -D react-app-rewired react-app-rewire-less ts-import-plugin $ touch config-overrides.js
config-overrides.js
const { getLoader } = require('react-app-rewired') const tsImportPluginFactory = require('ts-import-plugin') const rewireLess = require('react-app-rewire-less') module.exports = function override(config, env) { const tsLoader = getLoader( config.module.rules, rule => rule.loader && typeof rule.loader === 'string' && rule.loader.includes('ts-loader'), ) tsLoader.options = { getCustomTransformers: () => ({ before: [ tsImportPluginFactory({ libraryDirectory: 'lib', libraryName: 'antd-mobile', style: true, }), ], }), } config = rewireLess.withLoaderOptions({ javascriptEnabled: true, modifyVars: {}, // https://mobile.ant.design/docs/react/customize-theme })(config, env) return config }
react-app-rewiredを有効化するにはnpm scriptsも変更する必要がある
package.json
"scripts": { "start": "react-app-rewired start --scripts-version react-scripts-ts", "build": "react-app-rewired build --scripts-version react-scripts-ts", "test": "react-app-rewired test --env=jsdom --scripts-version react-scripts-ts",
上記の設定を行うことにより、antd-mobileのからimportするだけでcssも含めてモジュール化されたコンポーネントが利用可能になる
import { Button } from 'antd-mobile'
テーマの変更
テーマの変更を行う場合は、上で作成したconfig-overrides.jsのmodifyVarsにkey-valueで設定する
以下はプライマリカラーを変更する例
config = rewireLess.withLoaderOptions({ javascriptEnabled: true, modifyVars: { 'brand-primary': 'red' }, // https://mobile.ant.design/docs/react/customize-theme })(config, env) return config
設定できる全項目は下記のページから確認できる