- 投稿日:2019-10-09T22:51:51+09:00
gRPC-Web + React + Node.js + TypeScriptでシンプルなチャットサービスを作る
概要
かねてよりgRPCおよびgRPC-Webに興味があり、これを用いてシンプルなリアルタイムチャットサービスを制作し、公開した。
本稿では、その開発工程について解説する。ゴール
gRPC-Webを用いて「わいわいチャット」を作る。
https://waiwai-chat-2019.aanrii.com/
内容はシンプルなチャットアプリケーションだ。サイトを開くとまず過去ログが表示され、ほかの入室者の投稿が随時流れてくる。任意の名前で入室すると投稿欄が出現し、発言ができる。発言した内容はサイトにアクセスしている全員に、即座に共有される。過去ログは無限スクロールで遡ることができる。
フロントエンドはReactを用いたSPAとし、Netlifyを使って静的サイト生成・配信する。また、バックエンドはGKE上で動くNode.jsアプリケーションとし、かつenvoyをプロキシとして挟んで外部と通信させる。そして、フロントエンド-バックエンド間はgRPC-Web (HTTPS) で通信する。
なお、コードはここで公開している。
https://github.com/aanrii/waiwai-chat-2019やること
完成に到るまで、主に以下のことに取り組む。
- バックエンド開発
- gRPC-Web + Node.js + TypeScriptによるアプリケーション開発 (+ grpcurlによるデバッグ)
- envoy-proxyを通したフロントエンドとの接続
- GCPでの実行 (GKEへのデプロイ、およびCloud SQL + Cloud Pub/Subの導入。別稿にて解説)
- SSL有効化 (cert-managerによる証明書自動取得。別稿にて解説)
- フロントエンド開発
- gRPC-Web + React + TypeScriptによるアプリケーション開発
- Netlifyを用いた静的コンテンツ配信 (別稿にて解説)
本稿では、バックエンド・フロントエンドアプリケーションがローカル上で動くことを目標として解説を進める。
GCP上での実行とgRPCサーバの冗長化、SSL有効化などについては、別稿にて説明する。開発
Service (protobuf) の定義
gRPCアプリケーションを作るため、まずgRPCサーバ (バックエンド) のI/Fを定義する.protoファイルを作る。
proto/MessageService.protosyntax = "proto3"; import "google/protobuf/empty.proto"; service MessageService { rpc GetMessageStream(google.protobuf.Empty) returns (stream Message); rpc PostMessage(Message) returns (PostMessageResponse); } message PostMessageResponse { string status = 1; // メッセージの処理結果 } message Message { string text = 1; // 発言内容 int64 create_time = 2; // 発言日時 string author_name = 3; // 投稿者名 }MessageServiceはGetMessageStreamとPostMessageという、ふたつのメソッドをもつ。PostMessageはフロントエンドからバックエンドへのメッセージ送信に用いる。バックエンドはMessageを受け取り、PostMessageResponse (受信に成功したらOK、失敗したらError、等) を返す。このように、1回のリクエストに1回のレスポンスを返すようなRPCは、Unary RPCと呼ばれる。
一方、GetMessageStreamはフロントエンドがバックエンドからメッセージを受信するのに使う。バックエンドはgoogle.protobuf.Empty (void型のようなもの) を受け取り、Messageのstreamを返す。ようは、フロントエンドは初期化時に一回だけGetMessageStreamRequestをバックエンドに送り、その後はMessageがどこかでPostされるたびに、それをstreamで随時受け取ることができる。このように、1回のリクエストに対し複数個のレスポンスを含むストリームを返却するRPCは、Server streaming RPCと呼ばれる。詳細は、公式ガイドを参照のこと。
バックエンド (gRPCサーバ) の開発
パッケージのインストール
バックエンドの開発に入る。必要なパッケージをインストールする。まず、TypeScriptとNode.js。
% yarn add typescript ts-node @types/node続いて、gRPCを利用するためのパッケージを追加する。
% yarn add grpc google-protobuf @types/google-protobuf最後に、TypeScript用protobufコンパイラを導入する。これらは、protoファイルからTypeScript+Node.jsで使える型定義ファイルを生成するために用いる。
% yarn add grpc-tools grpc_tools_node_protoc_ts --dev
そして、以下のスクリプトを書く。
backend/server/protoc.sh#!/usr/bin/env bash set -eu export PATH="$PATH:$(yarn bin)" # protoファイルがあるディレクトリへの相対パス PROTO_SRC=../../proto # 生成したjs、tsファイルを格納したいディレクトリへの相対パス PROTO_DEST=./src/proto mkdir -p ${PROTO_DEST} grpc_tools_node_protoc \ --js_out=import_style=commonjs,binary:${PROTO_DEST} \ --grpc_out=${PROTO_DEST} \ --plugin=protoc-gen-grpc=$(which grpc_tools_node_protoc_plugin) \ -I ${PROTO_SRC} \ ${PROTO_SRC}/* grpc_tools_node_protoc \ --plugin=protoc-gen-ts=$(yarn bin)/protoc-gen-ts \ --ts_out=${PROTO_DEST} \ -I ${PROTO_SRC} \ ${PROTO_SRC}/*これで、
bash protoc.sh
を実行することで、protoファイルをjs/tsコードにコンパイルすることが可能になる。gRPCサーバの実装
生成されたファイル、サーバーサイドのinterface (IMessageServiceServer) が自動生成されるので、このクラスを実装する。
backend/server/src/MessageService.tsimport { EventEmitter } from 'events'; import { Empty } from 'google-protobuf/google/protobuf/empty_pb'; import * as grpc from 'grpc'; import { IMessageServiceServer } from './proto/MessageService_grpc_pb'; import { Message, PostMessageResponse } from './proto/MessageService_pb'; class MessageService implements IMessageServiceServer { // PostMessageにより投稿されたメッセージをGetMessageStreamで返却するstreamに流すための中継器 private readonly messageEventEmitter = new EventEmitter(); // 過去ログを保存する配列 private readonly pastMessageList: Message[] = []; public getMessageStream(call: grpc.ServerWriteableStream<Empty>) { // 過去ログをstreamに流し込む this.pastMessageList.forEach(message => call.write(message)); // PostMessageが実行されるたびに、そのメッセージをstreamに流し込む const handler = (message: Message) => call.write(message); this.messageEventEmitter.on('post', handler); // streamが切断された時、上記Listenerを消去する call.on('close', () => { this.messageEventEmitter.removeListener('post', handler); }); } public postMessage(call: grpc.ServerUnaryCall<Message>, callback: grpc.sendUnaryData<PostMessageResponse>) { // 受け取ったメッセージを過去ログに保存する const message = call.request; this.pastMessageList.push(message); // messageEventEmitter経由で、getMessageStreamで返却するstreamにメッセージを送る this.messageEventEmitter.emit('post', message); // レスポンスを返す const response = new PostMessageResponse(); response.setStatus('ok'); callback(null, response); } } export default MessageService;一旦ローカルで動かすために、インメモリ上にメッセージを貯めることとする。
gRPCサービスの実装ができたので、これをサーバ上で動かす。backend/server/src/index.tsimport * as grpc from 'grpc'; import MessageService from './MessageService'; import { MessageServiceService } from './proto/MessageService_grpc_pb'; (() => { const server = new grpc.Server(); server.bind(`0.0.0.0:9090`, grpc.ServerCredentials.createInsecure()); server.addService(MessageServiceService, new MessageService()); server.start(); })();デバッグ
以下のコマンドにより、ローカル (0.0.0.0:9090) でサーバを起動できる。
% yarn ts-node src/index.ts今回は動作確認のため、grpcurlを使う。
% brew install grpcurl
以下コマンドでGetMessageStreamを呼び出すと、待機状態に入る。
% grpcurl -plaintext -import-path proto/ -proto MessageService.proto 0.0.0.0:9090 MessageService/GetMessageStreamこの状態でターミナルを別に立ち上げ、Messageを投稿してみる。成功すればレスポンスが返ってくる。
% grpcurl -d "{\"text\":\"hello\",\"create_time\":$(node -e 'console.log(Date.now())'),\"author_name\":\"aanrii\"}" -import-path proto/ -proto MessageService.proto -plaintext -v 0.0.0.0:9090 MessageService/PostMessage Resolved method descriptor: rpc PostMessage ( .Message ) returns ( .PostMessageResponse ); Request metadata to send: (empty) Response headers received: accept-encoding: identity,gzip content-type: application/grpc grpc-accept-encoding: identity,deflate,gzip Response contents: { "status": "ok" } Response trailers received: (empty) Sent 1 request and received 1 responseGetMessageStreamを実行したウィンドウに戻ると、受信したMessageが表示されている。
{ "text": "hello", "createTime": "1570468135968", "authorName": "aanrii" }proxyの準備・実行
現状のgRPC-Webの仕様だと、ブラウザから直接gRPCサーバに接続することはできず、プロキシを挟む必要がある (詳細) 。
ここでは、公式の例に倣って、envoyを利用する。まず、envoy.yamlに設定を記述する。backend/proxy/envoy.yamladmin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: message_service max_grpc_timeout: 0s cors: allow_origin: - "*" allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout max_age: "1728000" expose_headers: custom-header-1,grpc-status,grpc-message http_filters: - name: envoy.grpc_web - name: envoy.cors - name: envoy.router clusters: - name: message_service connect_timeout: 0.25s type: logical_dns http2_protocol_options: {} lb_policy: round_robin hosts: [{ socket_address: { address: host.docker.internal, port_value: 9090 }}](Docker Desktop for Macで動かすためには、公式のenvoy.yamlのL45を
hosts: [{ socket_address: { address: host.docker.internal, port_value: 9090 }}]
と書き換える必要がある)そして、envoyを実行するためのDockerfileを記述する。
backend/proxy/DockerfileFROM envoyproxy/envoy:latest COPY ./envoy.yaml /etc/envoy/envoy.yaml CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml EXPOSE 8080そして、以下のコマンドでDockerイメージのビルド、起動を行う。
% docker build -t waiwai-chat-grpc/envoy -f ./Dockerfile . % docker run -d -p 8080:8080 -p 9901:9901 waiwai-chat-grpc/envoy:latest前述のgrpcurlコマンドを、9090から8080にポートを変更して実行してみると、プロキシが機能していることがわかる。
% grpcurl -plaintext -import-path proto/ -proto MessageService.proto 0.0.0.0:8080 MessageService/GetMessageStream { "text": "hello", "createTime": "1570468135968", "authorName": "aanrii" }これで、最低限それらしい挙動をするgRPCサーバが完成した。
フロントエンドの開発
パッケージのインストール
今回はフロントエンドも、バックエンドと同じくTypeScriptで記述していく。
create-react-appを用いて、React + TypeScriptのボイラープレートから開発を始める。% yarn create react-app frontend --typescript
続いて、gRPCを利用するためのパッケージを追加する。
% yarn add @improbable-eng/grpc-web ts-protoc-genフロントエンドではts-protoc-genを用いて.protoファイルをjsファイルに変換する。ここでも、バックエンド同様protoc.shを用意する。
frontend/protoc.sh% #!/usr/bin/env bash set -eu # protoファイルがあるディレクトリへの相対パス PROTO_SRC=../proto # 生成したjs、tsファイルを格納したいディレクトリへの相対パス PROTO_DEST=./src/proto mkdir -p ${PROTO_DEST} # protoc-gen-tsへのパス PROTOC_GEN_TS_PATH="$(yarn bin)/protoc-gen-ts" protoc \ --plugin="protoc-gen-ts=${PROTOC_GEN_TS_PATH}" \ --js_out="import_style=commonjs,binary:${PROTO_DEST}" \ --ts_out="service=true:${PROTO_DEST}" \ -I ${PROTO_SRC} $(find ${PROTO_SRC} -name "*.proto")このスクリプトを動かすには別途protocのインストールが必要となる。
% brew install protoc
実装
まず、gRPCクライアントを生成し、あらゆるコンポーネントから利用できるようにするためのHOCを作る。
frontend/src/attachMessageServiceClient.tsximport React from 'react'; import { MessageServiceClient } from '../proto/MessageService_pb_service'; export type MessageServiceClientAttached = { client: MessageServiceClient; }; const client = new MessageServiceClient(`http://0.0.0.0:8080`); const attachMessageServiceClient = <P extends {}>(WrappedComponent: React.ComponentType<P & MessageServiceClientAttached>) => class MessageServiceAttached extends React.Component<P> { render() { return <WrappedComponent {...this.props} client={client} />; } }; export default attachMessageServiceClient;投稿フォームは次のようにする。フォームに入力された文字列をもとにMessageを生成し、postMessageを実行する。
frontend/src/components/PostForm.tsximport React, { useState, FormEvent } from 'react'; import { Message as ProtoMessage } from '../proto/MessageService_pb'; import attatchMessageServiceClient, { MessageServiceClientAttached } from './attatchMessageServiceClient'; const PostForm: React.FC<{ initialInputText?: string } & MessageServiceClientAttached> = ({ initialInputText = '', client, }) => { const [inputText, setInputText] = useState(initialInputText); const handleSubmit = (e: FormEvent<HTMLFormElement>) => { e.preventDefault(); const currentDate = Date.now(); const message = new ProtoMessage(); message.setAuthorName('hoge'); // 一旦適当に埋める message.setCreateTime(currentDate); message.setText(inputText); client.postMessage(message, (error, response) => console.log(error == null ? error : response)); setInputText(''); }; return ( <div> <form onSubmit={handleSubmit}> <input type="text" name="inputText" value={inputText} onChange={e => setInputText(e.target.value)} /> <input type="submit" value="Submit" /> </form> </div> ); }; export default attatchMessageServiceClient(PostForm);ログの表示に再しては、まずgetMessageStreamによってstreamを得て、Messageが得られるたび随時更新するよう、ハンドラを登録しておく。
frontend/src/components/MessageList.tsximport React from 'react'; import Message from './Message'; import { Message as ProtoMessage } from '../proto/MessageService_pb'; import attatchMessageServiceClient, { MessageServiceClientAttached } from './attatchMessageServiceClient'; import { Empty } from 'google-protobuf/google/protobuf/empty_pb'; interface MessageListState { protoMessageList: ProtoMessage.AsObject[]; } class MessageList extends React.Component<void & MessageServiceClientAttached, MessageListState> { constructor(props: {} & MessageServiceClientAttached) { super(props); this.state = { protoMessageList: [] }; // message streamの取得 const messageStream = props.client.getMessageStream(new Empty()); // streamからmessageを受け取るたび、それをprotoMessageListに格納するハンドラを登録する messageStream.on('data', message => { const newProtoMessageList = [message.toObject()].concat(this.state.protoMessageList); this.setState({ protoMessageList: newProtoMessageList }); }); } render() { return ( <div> {this.state.protoMessageList.map(protoMessage => ( <Message {...protoMessage} key={protoMessage.createTime} /> ))} </div> ); } } export default attatchMessageServiceClient(MessageList);MessageのためのPresentational Componentも適当に作っておく。
frontend/src/components/Message.tsximport React from 'react'; import { Message as ProtoMessage } from '../proto/MessageService_pb'; const Message: React.SFC<ProtoMessage.AsObject> = protoMessage => ( <div> {protoMessage.text} ({new Date(protoMessage.createTime).toString()}) </div> ); export default Message;App,tsxも書き換えよう。
frontend/src/App.tsximport React from 'react'; import PostForm from './components/PostForm'; import MessageList from './components/MessageList'; const App: React.FC = () => { return ( <div> <PostForm /> <MessageList /> </div> ); }; export default App;ここで、
yarn start
を起動してみよう。ブラウザを確認すると、大量にエラーが出ているのがわかる。./src/proto/MessageService_pb.js Line 27: 'proto' is not defined no-undef Line 30: 'proto' is not defined no-undef Line 31: 'COMPILED' is not defined no-undef Line 36: 'proto' is not defined no-undefこれについては、実際のところ、protocで生成されたjsファイルをeslintのチェックから除外する方法が有効だ (参考)。ちょっとダサいが。
/* eslint-disable */再び
yarn start
を実行すると、問題なくフロントが表示されることがわかる。まとめ
ここまでで、gRPC-Web + React + Node.js + TypeScriptを用いて、少なくともローカルで動くチャットアプリケーションを作成した。続編 (今後書く予定) では、GCPへデプロイを行い、Kubernetes (GKE) 上でgRPCサーバを動かし、またその冗長化、負荷分散、SSL対応のための設定について紹介する。
参考文献
- 投稿日:2019-10-09T20:26:32+09:00
理想的なStorybookのワークフローとは
こちらの記事は、Dominic Nguyen 氏により2018年 5月に公開された『 The Delightful Storybook Workflow 』の和訳です。
本記事は原著者から許可を得た上で記事を公開しています。あなたのチームのアドオン、コンフィギュレーション、APIといった一連のニーズに合わせてカスタマイズできる事は、Storybookの大きな利点です。しかし、Storybookは指先一つで様々なオプションが操れるが故に、全体像ーStorybookのワークフローを見失いがちです。
今回、私は4つのプロフェッショナルチームとStorybookのメンテナー達にインタビューを行い、彼らの「生産性、セットアップ、メンバーの満足度」のバランスを取るためのコアワークフローを要約しました。この記事では、StorybookでUIコンポーネントを生産するための効率的な反復プロセスについて説明します。
Storybookの優れた点とは?
まず、Storybookの優れた点について確認してみましょう。コンポーネントエクスプローラー、スタイルガイドジェネレーター、ドキュメントサイト、プレイグラウンド、サンドボックス、UIライブラリなどです。それぞれに有効なユースケースはたくさんありますが、インタビューした全てのチームは、ある一つの重要な機能に依存していました。
元々React Storybookと呼ばれていたStorybookは、「アプリの外側にある孤立した環境でUIコンポーネントを開発・設計する」というただ一つのユースケースから生まれました。UIコンポーネントを単独で開発する事で、めったに発生しない状態やエッジケースの状態の開発が、劇的に簡単になりました。こうして作られたUIコンポーネントは高い堅牢性を持ちます。Storybookの使い道は他にもありますが、ここで説明するワークフローはこの一つに焦点を当てています。
Storybookを使用すると、コンポーネントを分離して構築できるところが気に入っています。さまざまな経験を持つ開発者達それぞれが対処する課題の数の調整に役立っています。
– Kurtis Kemple、テクニカルリード、Major League Soccerプロフェッショナルチームが教えるベストプラクティス
会社はそれぞれ異なるために、ワークフローも異なります。優れたプロセスはあくまでもチームの要件から生まれるものです。そのためアプローチもチームの数だけあると考えていました。しかし、インタビューからは決まって幾つかのパターンがあることがわかり、私は非常に驚きました。以下がその内容になります。
あいまいさを避ける
UI開発において、あいまいな入力や状態というのは混乱をもたらしやすいです。こういった問題への対処としてチームは、常にコンポーネントが特定の状況下でどのように応答すべきかを的確に文章化した「明確な」Storyを作成していました。StoryにはUIの特定の状態を達成するのに必要な値、UIデータを必ず含めましょう。
各コンポーネントの状態を指定する
各コンポーネントの状態を明確にし、Storybookに含めましょう。これにより、コンポーネントが対応できるすべてのエッジケースをカバーできます。Apollo GraphQLのTimは「優れたStoryはそのコンポーネントが実際にどのような状態になりうるかを視覚化することができます」と述べています。
固定された入力
レンダリングされるたびに、プロパティ値の一貫性が保たれていることを確認しましょう。randomized(
math.random
)やrelative(current date)の代わりに静的な入力を使用しましょう。これにより見る人にとってわかりやすくなり、スナップショットや視覚的な回帰テストなどのStoryの出力を検査するツールとの整合性が向上します。Storyを書けば、コンポーネントのプロパティドキュメントとその使用例が無料で手に入るのです!
– Justin Bennett, フロントエンドアーキテクト, Discovery継続的なビジュアルレビュー
コンポーネントの状態をStoryにマッピングしたならば、それらのStoryをQAに活用していきましょう。ビジュアルレビューは、UIへの影響をStoryでチェックするプロセスです。このプロセスは開発中とQA段階の両方で行います。
開発中にあらかじめ確認する
コンポーネントの変更には多くの場合、予期しないなバグを伴います。例えば、ある状態を表現するためのスタイルが、他の状態の見た目を崩してしまう場合などがあります。しかし、開発中にStoryを定期的に切り替えて確認することで、このような意図しないUIバグを防ぐことができます。
QA(自動または手動)中に再確認する
コンポーネントの開発終了後は、Storybookをチームで共有しQAの段階に入ります。
多くのチームがStorybookでのビジュアルレビューを、ビジュアル回帰テストと共に自動化しています。このテスト方法では、それぞれの入力ごとに各コンポーネントのスクリーンショットを取得し、それらを比較する事で変更を検出します。
Squarespaceには複数のStorybookがあります。UIのバグを自動的に検出するのに、それぞれのStorybookのビジュアル回帰テストが役立っています。
–Daniel Duan, ソフトウェアエンジニア, Squarespaceコラボレーションを前提に
ビジュアルレビューの数にかかわらず、欠陥はソフトウェア開発において避けられないものです。ほとんどの欠陥は技術的なものではなくミスコミュニケーションによるもののため、Storybookのワークフローにはフィードバックと問題解決のためのリリースバージョンを含めることが必要不可欠です。
リファレンスとしてStorybookを使う
コラボレーションを機能させるためには、チーム全員が同じUIを見ている必要があります。オンラインでStorybookをデプロイし、ディスカッションのための共通のリファレンスとしてStorybookを使いましょう。
タスクトラッカーとチャットを使ったディスカッション
もし、全員が同じことについて話し合っているのなら、その議論のためのスペースを作りましょう。Storybookへのリンクを使ってタスクトラッカーでタスクを作成し、フィードバックを積極的に求めましょう。
各PRにStorybookをデプロイしておくことでビジュアルレビューが容易になり、プロダクトオーナーがコンポーネントを検討しやすくなります。
– Norbert de Langen, Storybookメンテナー
StorybookでUIコンポーネントとStoryを構築する
コンポーネントがサポートするべき各要素の順列をStorybook内で表現しましょう(ただし必ずしも全てのプロパティの組み合わせを表現する必要はありません)。コーディング中にUIへの影響をStoryで視覚的に確認します。コンポーネントに特殊な機能がある場合はユニットテストを作りましょう。他の開発者とコードレビューをする
PRをレビューするのと同じように、コンポーネントの各Storyが仕様を満たし、エラーがないことを確認しましょう。GithubまたはJiraでチケットを作成し、チームメイトを割り当てます。変更がリクエストされているか、レビューに合格しているかなど、そのリクエストの状態をラベルを使って管理しましょう。より大きなチームでビジュアルレビューをする
Storybookをデプロイし、オンライン上のStorybookへのリンクを使ってタスク管理ツールでIssueを作成します。アプリチーム内で、もしタスク管理ツールを同じもの(Jiraなど)に統一している場合はステップ2のチケットで一緒に行いましょう。この機会を利用して、他の分野の関係者から承認を得ます。アドバイス: ビジュアル回帰テスト
ビジュアル回帰テストツールを使用してビジュアルレビューを自動化することで、テストにかける時間を節約できます。Storybookのメンテナーが開発したクラウドテストツールChromaticもチェックしてみてください。コードベースにマージする
全てのCIチェックに合格したならば、コンポーネントをコードベースにマージ(またはNPMにデプロイ)しましょう。これで作業は完了です! ?まとめ
明示的なStory、継続的なビジュアルレビュー、コラボレーションの組み込みにより、UIコンポーネントの構築が効率化されることは明らかです。この「理想的なStorybookワークフロー」は、これらのベストプラクティスを統合したものになります。初期の開発から始まり、完成したUIコンポーネントをコードベースにマージするまでのエンドツーエンドのプロセスを提供します。
是非このワークフローを実際に試し、さらなる改良を重ねてみてください。どのように進化していくのか楽しみです!
Special thanks
今回の理想的なStorybookのワークフローはTom Coleman、Michael Shilman、Norbert de Langen、Daniel Duan、kurtiskemple、Tim Hingston、 Justin Bennettとのインタビューに基づいて作成されました。
翻訳協力
Original Author: Dominic Nguyen
Thank you for letting us share your knowledge!この記事は以下の方々のご協力により公開する事が出来ました。
改めて感謝致します。
選定担当: yumika tomita
翻訳担当: siho1
監査担当: @nyorochan
Markdown化: Asuma Yamada私たちと一緒に記事を作りませんか?
私たちは、海外の良質な記事を複数の優秀なエンジニアの方の協力を経て、日本語に翻訳し記事を公開しています。
活動に共感していただける方、良質な記事を多くの方に広めることに興味のある方は、ぜひご連絡ください。
MailもしくはTwitterでメッセージを頂ければ、選考のちお手伝いして頂ける部分についてご紹介させていただく事が可能です。
※ 頂いたメッセージには必ずご返信させて頂きます。
- 投稿日:2019-10-09T17:56:55+09:00
create-react-app --typescript + ESLint + Prettier のセットアップ方法
1. 必要なパッケージの導入
ESLint
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-pluginPrettier
npm install -D prettier eslint-config-prettier eslint-plugin-prettier2. ESLint の設定を追加
.eslintrc.json
に設定を追加{ "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:prettier/recommended", "prettier/@typescript-eslint" ], "plugins": ["@typescript-eslint"], "parser": "@typescript-eslint/parser", "env": { "browser": true, "node": true, "es6": true }, "parserOptions": { "sourceType": "module" }, "rules": { // 個別にルールを設定できる } }3. Husky, lint-staged を導入
パッケージをインストール
npm install -D husky lint-staged
package.json
に設定を追加"husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{ts,tsx}": [ "eslint --fix", "git add" ] }4. VSCode の設定を追加する
.vscode/settings.json
{ "eslint.autoFixOnSave": true, "eslint.validate": [ "javascript", "javascriptreact", { "language": "typescript", "autoFix": true }, { "language": "typescriptreact", "autoFix": true } ] }参考
- 投稿日:2019-10-09T13:21:49+09:00
[チュートリアル] ReactNative 初期構築〜HelloWorld表示〜コンポーネント受け渡し
準備
XCode
AppStoreからダウンロード
これがないと始まらない
MacのOSバージョンによってインストールできない可能性あり。
常に最新レベルじゃないとすぐにインストールできなくなるので注意。Node/NPM
Node・・・Node.jsのこと。サーバーサイドで動くjavascriptです。
NPM・・・パッケージ管理ツール。ReactやReactNative、Babelなどのjsの拡張機能を管理するツールNodeのインストール
brew install nodeNPMのインストール
watchman
brew install watchman
ファイルの変更を監視してくれるツールRN CLI(React Native Command Line Interface)
npm i -g react-native-cli※参考
https://rara-world.com/react_native-build/
https://rara-world.com/react-native-tutorial/Hello Worldを表示
ターミナルreact-native init reactHello
※途中「info Installing required CocoaPods dependencies」で止まっているかのように遅い
ターミナルcd reactHello npm i react-native run-ios
※シュミレーター起動(初回は遅い)
Welcome to Reactというタイトルの画面が出るはずです。
今後バージョンによって変わる可能性ありディレクトリやファイル
├── App.js
├── tests
├── android
├── app.json
├── index.js
├── ios
├── node_modules
├── package-lock.json
├── package.json
└── yarn.lock+追加
└── src この中に子コンポーネント入れていく親Componentを作る
App.jsを書き換え
※参考記事でindex.jsになっていますが、記事ではおそらく前のバージョンでのことだと思いますApp.jsimport React from 'react'; import { Text, AppRegistry } from 'react-native'; const App = () => ( <Text>Hello World!</Text> ); export default App; // 記事の「AppRegistry.registerComponent('test', () => App);」だとエラーになったこれで「Hello World!」が表示されました.
子Componentを作る
src/Header.jsimport React from 'react'; import { Text, View } from 'react-native'; const Header = () => { const { headerText, header } = styles; return ( <View style={header}> <Text style={headerText}>ヘッダー</Text> </View> ); }; const styles = { header: { backgroundColor: '#F8F8F8', justifyContent: 'center', alignItems: 'center', height: 90, paddingTop: 25, elevation: 2, position: 'relative' }, headerText: { fontSize: 20, fontWeight: '600' } }; export default Header;App.js修正
App.js// 追加 import Header from './src/Header'; // 修正 const App = () => ( <View> <Header /> </View> );親から子へパラメータを渡す
App.js修正
App.js修正.... // 修正 const App = () => ( <View> <Header showText={'Hello'} /> </View> ); ....src/Header.js修正
src/Header.js.... const Header = (props) => { const { headerText, header } = styles; return ( <View style={header}> <Text style={headerText}>{props.showText}</Text> </View> ); }; .....
- 投稿日:2019-10-09T12:46:06+09:00
React Reduxに相当するものをhooksでやってみる
React Reduxに相当するものをhooksでやってみる
Reduxを使わない場合にそれに相当するやつをHooksで実現したいなーって時に考えたやつ
とりあえず完成したやつ
// TODOの型 interface ITodo { id: number body: string isDone: boolean } // TODO検索用の型 interface ISearchTodoParams { body?: string } // TODO検索のAPIの代わり const mockApi = (params?: ISearchTodoParams): Promise<{ todos: ITodo[] }> => { return new Promise(resolve => { setTimeout(() => { resolve({ todos: [ { id: 1, body: '卵買う', isDone: true }, { id: 2, body: '塩買う', isDone: false }, { id: 3, body: '家買う', isDone: false }, ], }) }, 1000) }) } // メインディッシュ export const useTodos = () => { const [todos, setTodos] = useState<ITodo[]>([]) const [isLoading, setLoading] = useState(false) const [isError, setError] = useState(false) const fetchTodos = (params?: ISearchTodoParams) => { setLoading(true) // apiの代わり mockApi(params) .then(response => { setTodos(response.todos) }) .catch(() => { setError(true) }) .finally(() => { setLoading(false) }) } return { todos, fetchTodos, isLoading, isError } } // 使うとき export const Todos: FC = () => { const { todos, fetchTodos } = useTodos() // 初回取得 useEffect(() => { fetchTodos() }, []) return ( <ul> <input type="text" onChange={e => { const val = e.target.value if (val.length === 0) { return } fetchTodos({ body: val }) }} /> {todos.map(todo => ( <li>{todo.body}</li> ))} </ul> ) }解説
使うときはこの辺の引数を受け取って使える感じ
/** * todos: TODOの配列が入ったやつ * fetchTodos: todoの一覧を取得する関数 * isError: エラーが起きたらtrueになるやつ * isLoading: 通信中にtrueになるやつ */ const { todos, fetchTodos, isError, isLoading } = useTodos()reducers や actionsとかを書くのと比べると記述量が少なくなってる
Componentが離れたところで参照させたいとかがなければ、これくらいでいいのかなと
reducersやactionを以上!