20191009のReactに関する記事は5件です。

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.proto
syntax = "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.ts
import { 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.ts
import * 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 response

GetMessageStreamを実行したウィンドウに戻ると、受信したMessageが表示されている。


{
  "text": "hello",
  "createTime": "1570468135968",
  "authorName": "aanrii"
}

proxyの準備・実行

現状のgRPC-Webの仕様だと、ブラウザから直接gRPCサーバに接続することはできず、プロキシを挟む必要がある (詳細) 。
ここでは、公式の例に倣って、envoyを利用する。まず、envoy.yamlに設定を記述する。

backend/proxy/envoy.yaml
admin:
  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/Dockerfile
FROM 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.tsx
import 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.tsx
import 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.tsx
import 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.tsx
import 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.tsx
import 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対応のための設定について紹介する。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

理想的な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 GraphQLTimは「優れた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メンテナー

  1. StorybookでUIコンポーネントとStoryを構築する

    コンポーネントがサポートするべき各要素の順列をStorybook内で表現しましょう(ただし必ずしも全てのプロパティの組み合わせを表現する必要はありません)。コーディング中にUIへの影響をStoryで視覚的に確認します。コンポーネントに特殊な機能がある場合はユニットテストを作りましょう。

  2. 他の開発者とコードレビューをする
    PRをレビューするのと同じように、コンポーネントの各Storyが仕様を満たし、エラーがないことを確認しましょう。GithubまたはJiraでチケットを作成し、チームメイトを割り当てます。変更がリクエストされているか、レビューに合格しているかなど、そのリクエストの状態をラベルを使って管理しましょう。

  3. より大きなチームでビジュアルレビューをする

    Storybookをデプロイし、オンライン上のStorybookへのリンクを使ってタスク管理ツールでIssueを作成します。アプリチーム内で、もしタスク管理ツールを同じもの(Jiraなど)に統一している場合はステップ2のチケットで一緒に行いましょう。この機会を利用して、他の分野の関係者から承認を得ます。

    アドバイス: ビジュアル回帰テスト

    ビジュアル回帰テストツールを使用してビジュアルレビューを自動化することで、テストにかける時間を節約できます。Storybookのメンテナーが開発したクラウドテストツールChromaticもチェックしてみてください。

  4. コードベースにマージする

    全てのCIチェックに合格したならば、コンポーネントをコードベースにマージ(またはNPMにデプロイ)しましょう。これで作業は完了です! ?

まとめ

明示的なStory、継続的なビジュアルレビュー、コラボレーションの組み込みにより、UIコンポーネントの構築が効率化されることは明らかです。この「理想的なStorybookワークフロー」は、これらのベストプラクティスを統合したものになります。初期の開発から始まり、完成したUIコンポーネントをコードベースにマージするまでのエンドツーエンドのプロセスを提供します。

是非このワークフローを実際に試し、さらなる改良を重ねてみてください。どのように進化していくのか楽しみです!

Special thanks

今回の理想的なStorybookのワークフローはTom ColemanMichael ShilmanNorbert de LangenDaniel DuankurtiskempleTim HingstonJustin Bennettとのインタビューに基づいて作成されました。

翻訳協力

 
Original Author: Dominic Nguyen
Thank you for letting us share your knowledge!

この記事は以下の方々のご協力により公開する事が出来ました。
改めて感謝致します。
選定担当: yumika tomita
翻訳担当: siho1
監査担当: @nyorochan
Markdown化: Asuma Yamada

私たちと一緒に記事を作りませんか?

私たちは、海外の良質な記事を複数の優秀なエンジニアの方の協力を経て、日本語に翻訳し記事を公開しています。
活動に共感していただける方、良質な記事を多くの方に広めることに興味のある方は、ぜひご連絡ください。
MailもしくはTwitterでメッセージを頂ければ、選考のちお手伝いして頂ける部分についてご紹介させていただく事が可能です。
※ 頂いたメッセージには必ずご返信させて頂きます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

create-react-app --typescript + ESLint + Prettier のセットアップ方法

1. 必要なパッケージの導入

ESLint

npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

Prettier

npm install -D prettier eslint-config-prettier eslint-plugin-prettier

2. 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 }
  ]
}

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[チュートリアル] ReactNative 初期構築〜HelloWorld表示〜コンポーネント受け渡し

準備

XCode
AppStoreからダウンロード
これがないと始まらない
MacのOSバージョンによってインストールできない可能性あり。
常に最新レベルじゃないとすぐにインストールできなくなるので注意。

Node/NPM
Node・・・Node.jsのこと。サーバーサイドで動くjavascriptです。
NPM・・・パッケージ管理ツール。ReactやReactNative、Babelなどのjsの拡張機能を管理するツール

Nodeのインストール
brew install node

NPMのインストール

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というタイトルの画面が出るはずです。
今後バージョンによって変わる可能性あり

初期起動画面.png

ディレクトリやファイル

├── 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.js
import 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.js
import 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-09 13.21.18.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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を

以上!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む