20191124のReactに関する記事は6件です。

React + TypeScript + Webpackで開発環境セットアップ

はじめに

React + TypeScript + Webpackの構成でプロジェクト構築する時の手順になります。

インストール

まずは、必要なものを順次インストール。

$ yarn add react react-dom

Babel 7からTypeScriptのサポートが導入されていますが、このあたりの情報を読む限り、まだ積極的にBabelによるトランスパイルに移行しなくても良いかなと、個人的には思っています。ちなみに仕事の方でもts-loaderを使っています。

$ yarn add -D @types/react @types/react-dom
$ yarn add -D typescript ts-loader
$ yarn add -D webpack webpack-cli webpack-dev-server

設定

TypeScriptのトランスパイルにts-loaderを指定します。

webpack.config.js
module.exports = {
  mode: "development",
  entry: "./src/index.tsx",
  output: {
    filename: "bundle.js"
  },
  devServer: {
    contentBase: "./public",
    compress: true,
    hot: true,
    host: "localhost",
    port: 3000,
    publicPath: "/"
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js"]
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [{ loader: "ts-loader" }]
      }
    ]
  }
};

TypeScriptのコンパイラオプションは、こんな感じで。
jsx:reactの指定を忘れずに。

tsconfig.json
{
  "compilerOptions": {
    // 厳格なNullチェックを行う
    "strictNullChecks": true,
    // 未使用のローカル変数を許可しない
    "noUnusedLocals": true,
    // 暗黙的なany型のthis使用を許可しない
    "noImplicitThis": true,
    // strictモードでパースし、出力ファイルに"use strict"を付与
    "alwaysStrict": true,
    // 対応する.mapファイルを生成
    "sourceMap": true,
    // 暗黙的なany型を許可しない
    "noImplicitAny": true,
    // コンパイルに含めるライブラリを列挙
    "lib": ["dom", "es2017"],
    // 生成されるモジュール形式
    "module": "es2015",
    // トランスパイル後の対象ECMAScriptバージョン
    "target": "es5",
    // .tsxファイルのJSXをサポートする際の方式
    "jsx": "react",
    // default export無しでのdefault importを許可
    "allowSyntheticDefaultImports": true
  }
}

コード準備

JSを読み込んで表示するhtmlを用意。

public/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>React Demo</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="bundle.js"></script>
  </body>
</html>

とりあえずdivエレメントを表示。

src/index.tsx
import React from "react";
import ReactDOM from "react-dom";

const App = () => <div>app</div>;

ReactDOM.render(App(), document.getElementById("app"));

起動

yarn startでアプリが立ち上がる事を確認。

package.json
{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "build": "webpack",
    "start": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^16.11.0",
    "react-dom": "^16.11.0"
  },
  "devDependencies": {
    "@types/react": "^16.9.11",
    "@types/react-dom": "^16.9.4",
    "ts-loader": "^6.2.1",
    "typescript": "^3.7.2",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0"
  }
}

まとめ

以上、React + TypeScript + Webpackの構成で、プロジェクト開始する際の流れでした。??‍♂️

参考

React & Webpack

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

React関係まとめ

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

WEB初心のプログラマーがReact+Firebase+Algoliaでクイズアプリを作成するまで

自己紹介

今回がQiita初投稿となります。
普段はメーカーでC++を使った画像処理系のアプリ開発に携わってます。
もともとWEBには一切触れた事がなかったのですが、
世の中でReactやVueといったフレームワークが話題になった時から
徐々にWEB業界に興味を惹かれてきました。
そして本格的に勉強してみようと思い立ち、今回のアプリ制作に至りました。

サービス紹介

Mushiquiという虫食いクイズアプリを作成しました。

きっかけは社内の暗記試験で、解答を含む全文から手っ取り早く虫食い文章を作成するツールが欲しいなと思った経験からでした。
特定のタグで文字を囲うことで、手軽に虫食いクイズが作成できるようになります。
ブラウザ/モバイルから利用が可能で、PWA対応しているため、ホームに追加して利用が可能です。

制作期間

技術の理解を含めて一年半程になります。
WEBの仕組みからの理解で、一進一退の日々でした。
途中段階の出来を見てショックを受け、実際何度か投げだしましたが(汗)、
それでも地道に実装を続け、なんとか当初想定していた機能が一区切りついたため記事にする事に踏み切りました。

代表的な利用技術

フロントエンド

  • react
  • redux
  • redux-first-router
  • algoliasearch
  • draft-js
  • styled-components
  • storybook
  • react-responsive

redux-first-routerはルーティングに使用。
draft-jsはテキストエディタに使用。

バックエンド

  • Firebase
    • Firestore
    • Cloud Function
    • Authentication
    • Hosting
    • Storage
  • Algolia

システム構成

システム構成は以下のようになっています。

今回、バックエンドにはFirebaseを用いており、データベースにはCloud Firestore、認証にはAuthenticationを使っています。
またクイズの全文検索を実現するために、Algoliaを用いており、AlgoliaのインデックスとFirestoreのデータはCloud Functionを使って同期しています。

苦労した点

学ぶ量の多さ

WEBページを作ること自体が初めての自分にとって、
いきなりReactで物を作るというのは情報量との戦いでした。
QiitaやMediumなどを見れば、Reactの使い方はなんとなくわかるのですが、
自分で考えて使えるようになるまでは、
htmlやcssをはじめjsxなどの根底知識が必要で相当の時間を要しました。

そんな中で最後まで心の頼りだったのが、公式ドキュメントでした。
量がそれなりにあって、全部読むには相当時間がかかりますが、
最近では日本語ページも充実しています。
初期のチュートリアルはもちろんなのですが、アプリを作り始めてから暫くして構造が複雑になってきて悩み始めた時にも、render propsHOCなどのテクニックが記載されているADVANCED GUIDESは個人的にとても助かりました。

コンポーネントの責務分割

サンプルを見よう見まねで作ったアプリの状態から色々な機能が盛り込まれていくと、
似たようなデザインのコンポーネントが散在し、ボタンの修正ひとつで複数のソースを修正しなくてはいけない非効率な状況に陥りました。

そうしたときに、デザインの変更を局所的に留める仕組みを考えておくことは重要で、自分の場合はAtomic Designを用いることでこの手間を大幅に改善することができました。

Atomic Designについて:
http://atomicdesign.bradfrost.com/chapter-2/

Mushiquiでは以下のようにcomponents下のフォルダ構成と責務を決めています。

components
|-atoms
|-moleculeus
|-organisms
|-templates
|-pages

スクリーンショット 2019-11-22 23.21.39.png

全体のレイアウトや色合いを変えたいときはtemplates、
ボタンの振る舞いやデザインを変えたいときはatomsといったように
変更する目的に応じて、変更する場所を決めておけば
仮に第三者から見てもわかりやすいのではないかと思います。

コンポーネントの責務分割(2)

上記の責務の分割をしたうえでも、
WEBとモバイルは同じコンポーネントでもレイアウトが大きく変化する場合があるため、必要に応じて表示部分のみ責務を分割する必要がありました。
(特にtemplatesやorganisms層で発生)
今回、react-responsiveを用いてモバイル用PresenterとWEB用Presenterに振り分けています。

Component
 |-Container.js
 |-WebPresenter.js
 |-MobilePresenter.js
 |-index.js
 |-index.stories.js
 |-style.js

  • Container.js・・・このコンポーネントにおけるStateを管理するClass Component
  • WebPresenter.js・・・WEB向けの表示を行うFunctional Component
  • MobilePresenter.js・・・Mobile向けの表示を行うFunctional Component
  • index.js・・・ContinerとPresenterをrender propを使い接続。ウィンドウサイズに応じた振り分けも実施。
index.js
import React from 'react';
import Responsive from 'react-responsive';
import WebPresenter from './WebPresenter';
import MobilePresenter from './MobilePresenter';
import Container from './Container';

export const Mobile = props => <Responsive {...props} maxWidth={767} />;
export const Default = props => <Responsive {...props} minWidth={768} />;

const FavoriteList = (props) => {
    return <Container 
        {...props}
        render={(containerProps)=><React.Fragment>
            <Default>
                <WebPresenter {...containerProps} />
            </Default>
            <Mobile>
                <MobilePresenter {...containerProps} />
            </Mobile>
        </React.Fragment>}
    />;
};
export default FavoriteList;

Algoliaとの連携を考慮したFirestoreのデータ設計

クイズを全文検索可能にするために、
Firestoreのデータの一部をAlgoliaのインデックスに登録しています。
Firestoreのクイズ更新をCloud Functionでトリガし
更新の反映をAlgoliaのインデックスに対しても行います。
こうしたときに複雑になるのが、Firestoreのデータの持ち方でした。

検索にはクイズ名称やタグ、作成者などのメタ情報をインプットするために
これらの情報は、すべてAlgoliaのIndex内に持つ必要がありました。
一方で検索にはクイズ本体の情報は不要のため、
クイズ本体の変更はトリガしないようなFirestoreのデータ設計が必要となりました。

結果として、検索時に不要なクイズ本体はサブコレクションとして、他のメタ情報と分離することで上記を実現することができました。

スクリーンショット 2019-11-24 0.14.42.png

(図中のrecipesはクイズ本体(quizzes)とメタ情報を集約する概念)

最後に

長くなりましたが、ここまで読んでいただきありがとうございます。
これから始めるという方や、Reactのアプリ制作で悩まれている方の少しでも参考になれば幸いです。
今後はHooksやContextなど今回のアプリでは取り入れられなかった要素を学習しつつ、随時アプリの更新や、今回書ききれなかった内容についても書いていきたいと思います。

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

【Gatsby.js 】チュートリアルでError: Cannot find module 'react'

:imp: 公式チュートリアルなのに〜

gatsby-starter-hello-worldのスターターを使用してGatsby.jsを動かしていくのだけども、そのなかのtypographyプラグインを使う箇所で不具合が発生してうまく進められなくなる。

:rotating_light: Error: Cannot find module 'react' 発生

チュートリアルのchapter3でプラグインをnpm installしたあとに gatsby developすると発生する不具合。

:nerd: 解決法

学習が進められるようになるよ :nerd: :nerd:

:one: npmを使わない、yarnを使う。

チュートリアルに合わせていうならば
yarn add gatsby-plugin-typography react-typography typography typography-theme-fairy-gatesをつかう。

原因

npm :package: 側の問題。
結構前からこのissueで論議されているけれどまだnpm側が治ってないみたいです。解決策のみ抜粋しましたので、詳しくは前記issueをご覧ください。

なのでGatsbyでのパッケージインストールはyarnを使用するのが良さそうです :nerd:

以上です:nerd: ありがとうございます.

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

React.jsでGoogleBooksAPIを取り込んでローカルストレージにお気に入りを登録できるやつ作った

自己紹介

G's Academy東京 LAB8期に通っています。
6ヶ月の通いのうち、現在1ヶ月半が経過しました。
今回作ったものは3週目くらいに作成したものです。

2週目に爆死したアプリの記事はこっちに書いてます。
https://poppotennis.com/gs_week2/

完成品はこちら

スクリーンショット 2019-11-24 2.35.29.png

文字を入力して検索するを押すとGoogleBooksAPIから本の情報の一部を取得し、
お気に入りを押すと、ローカルストレージに本のIDを保存しておいて、それを元にお気に入りを表示するという感じです。

クソコードでも動けば誰かの参考になるので恥晒し覚悟で投稿したいと思います。

使ったファイル

index4.html、index4.js、index4.css
CDNでやっていきます。なぜ4なのかというと、4ページ目だからです。

index4.html

Reactなので必要なCDNを読み込んで、idを指定したdivタグがあれば大丈夫ですね。
axiosとReactの公式ページからCDNをコピーして持ってくるのが正解だと思います。
注意点は、

index4.jsを読み込むときに

<script type="text/babel" src="js/index4.js"></script>

としないと読みこまないこと。type指定をしていなくて無駄に時間を過ごしました。

index4.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
    <title>my Memo's Pad</title>
    <script src="js/jquery-2.1.3.min.js"></script>
    <link rel="stylesheet" href="css/reset.css" />
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700"
      rel="stylesheet"
    />
    <link
      href="https://fonts.googleapis.com/icon?family=Material+Icons"
      rel="stylesheet"
    />
    <script
      src="https://kit.fontawesome.com/02cb9ed103.js"
      crossorigin="anonymous"
    ></script>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.min.js"></script>
    <link rel="stylesheet" href="css/index4.css" />
  </head>

  <body>
    <!-- ここからコンテンツ -->
    <div class="container">
      <main>
        <div id="index4"></div>
      </main>
    </div>

    <!-- コンテンツここまで -->
    <div id="content"></div>
    <script
      src="https://unpkg.com/react@16/umd/react.development.js"
      crossorigin
    ></script>
    <script
      src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"
      crossorigin
    ></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
    <script type="text/babel" src="js/index4.js"></script>
  </body>
</html>

https://ja.reactjs.org/docs/cdn-links.html

楽しいReact.js

index4.js
('use strict');

const e = React.createElement;
let keyBook = Number(localStorage.getItem('keyBook'));

今回、ローカルストレージを利用するのでkeyにナンバリングしたいのでグローバル変数で現在の何番が保存されているのかを取得しておきました。

親コンポーネント作成

index4.js
class LikeButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      liked: false,
      data: [],
      inputValue: '',
      errorMessage: ''
    };
  }

//検索で発動する関数
  getProfile = async () => {
    let datas = [];
    let text = this.state.inputValue;
    try {
      const result = await axios.get(
        'https://www.googleapis.com/books/v1/volumes?q=' + text
      );
      let items = result.data.items;
      for (let i = 0; i < items.length; i++) {
        let item = items[i];
        let authors = item.volumeInfo.authors[0];
        let title = item.volumeInfo.title;
        let image = item.volumeInfo.imageLinks.thumbnail;
        let id = item.id;
        let test = { authors, title, image, id };
        console.log('test:' + JSON.stringify(test));
        datas.push(test);
      }
      this.setState({
        errorMessage: ''
      });
    } catch (error) {
      //ここでリクエストに失敗した時の処理、メッセージを記述します。
      this.setState({
        errorMessage: 'エラー:該当するものがありませんでした。'
      });
    }
    console.log('data:' + JSON.stringify(datas));
    this.setState({
      data: datas
    });
  };

//インプットないの文字を渡しながら着火
  onClickSearch = () => {
    this.getProfile(this.state.inputValue);
  };

//文字を入力されるたびに更新
  handleChange = e => {
    this.setState({
      inputValue: e.target.value
    });
  };

  render() {
    return (
      <div className='App'>
        <div className='index4-left'>
          <div className='input-group mb-3'>
            <input
              type='text'
              className='form-control'
              placeholder='好きなキーワード'
              aria-describedby='basic-addon1'
              onChange={this.handleChange}
              onKeyPress={event => {
                if (event.key === 'Enter') {
                  this.getProfile();
                }
              }}
            />
            <div className='input-group-prepend'>
              <button onClick={this.getProfile} className='btn btn-primary'>
                検索する
              </button>
            </div>
          </div>

          <p>{this.state.errorMessage}</p>

          <div className='search'>
            {this.state.data.map(data => {
              return (
                <div className='card search_image'>
                  <img
                    src={data.image}
                    className='card-img-top'
                    alt='...'
                  ></img>
                  <div className='card-body'>
                    <h5 className='card-title'>{data.title}</h5>
                    <p className='card-text'>{data.authors}</p>
                    <AddButton id={data.id} />
                  </div>
                </div>
              );
            })}
          </div>
        </div>
        <div className='index4-right'>
          <FavoliteButton />
        </div>
      </div>
    );
  }
}

const domContainer = document.querySelector('#index4');
ReactDOM.render(e(LikeButton), domContainer);

getProfileとonClickSearchでAPIでの取得はできます。
他は備忘録。

お気に入り追加ボタン

親から渡ってきたAPIで取得した本のidをローカルストレージに保存します。
ローカルストレージのkeyは1個保存されるごとに+1されるようにする記述をここでしています。
ローカルストレージにkeyBookというkeyを用意してMySQLでいうAutoIncrementの役割をしてみました。

index4.js
class AddButton extends React.Component {
  constructor(props) {
    super(props);
  }

  clickAddBook = e => {
    const addBookKey = keyBook + 1;
    localStorage.setItem('keyBook' + addBookKey, e);
    localStorage.setItem('keyBook', addBookKey);
    keyBook++;
  };

  clickAddBook2 = () => {
    this.clickAddBook(this.props.id);
  };

  render() {
    return (
      <button className='btn btn-success' onClick={this.clickAddBook2}>
        お気に入りに保存
      </button>
    );
  }
}

お気に入りで登録したものを表示させるコンポーネント

ローカルストレージに保存されているIDを取得して、GoogleBooksAPIからそのIDの本のデータをもらって、Stateに入れるという流れです。

index4.js
class FavoliteButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      errorMessage: '',
      datas: []
    };
  }

  getFavorite = async () => {
    let items = [];
    for (let i = 0; i <= keyBook; i++) {
      const item = localStorage.getItem('keyBook' + i);
      if (item) {
        console.log(item);
        //データ取得
        try {
          //ここでGETメソッドを使用してgithubのプロフィールを取得します。
          const result = await axios.get(
            'https://www.googleapis.com/books/v1/volumes/' + item
          );
          let obj = result.data.volumeInfo;
          let authors = obj.authors[0];
          let title = obj.title;
          let image = obj.imageLinks.thumbnail;
          let id = obj.id;
          let test = { authors, title, image, id };
          items.push(test);

          this.setState({
            errorMessage: ''
          });
        } catch (error) {
          //ここでリクエストに失敗した時の処理、メッセージを記述します。
          this.setState({
            errorMessage: 'エラー'
          });
        }
        //データ取得ここまで
      }
    }
    this.setState({
      datas: items
    });
  };

  render() {
    return (
      <div className='favorite'>
        <button onClick={this.getFavorite} className='btn btn-primary'>
          お気に入りを表示
        </button>
        <div>
          {this.state.datas.map(data => {
            return (
              <div className='media border favorite_content'>
                <img src={data.image} className='mr-3'></img>
                <div className='media-body'>
                  <h5>タイトル:{data.title}</h5>
                  著者:{data.authors}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    );
  }
}

あとは適当にスタイルをつけて

index4.css
html,
body {
  font-family: 'ヒラギノ明朝 ProN W6', 'HiraMinProN-W6', 'HG明朝E',
    'MS P明朝', 'MS PMincho', 'MS 明朝', serif;
  height: 100%;
  margin: 0;
  padding: 0;
}

main {
  margin-top: 100px;
  margin-bottom: 100px;
}

#nav-drawer {
  /* position: relative; */
}

/* アイコンのスペース */
#nav-open {
  position: relative;
  display: inline-block;
  width: 48px;
  height: 48px;
  vertical-align: middle;
  margin-right: 0;
  background-color: orange;
}

/* ミッキーの真ん中 */
#nav-open span {
  position: absolute;
  left: 4px;
  bottom: 0;
  width: 40px;
  height: 40px;
  background-color: white;
  border-radius: 50%;
  display: block;
  content: '';
  cursor: pointer;
}

/* ミッキーの耳2つの形 */
#nav-open span:before,
#nav-open span:after {
  position: absolute;
  width: 24px;
  height: 24px;
  background-color: white;
  border-radius: 50%;
  display: block;
  content: '';
  cursor: pointer;
}

/* ミッキーの左耳の位置 */
.App {
  display: flex;
  justify-content: space-between;
}

.index4-left {
  width: 40%;
}

.index4-right {
  width: 40%;
}

.search {
  text-align: center;
}

.search_image {
  /* width: 100%; */
  height: 500px;
  margin-top: 30px;
}

.favorite {
  text-align: center;
}

.favorite_content {
  margin-top: 20px;
}

完成です!

苦労した点

axiosに初挑戦した点。
この書き方・・・正しくないだろうなぁ・・・と思いながらとりあえず動かさなければ前に進めない感じ。
徐々に慣れていきたいなぁと思っています。
jsはどのライブラリもフレームワークも面白いです。

参考文献

https://ja.reactjs.org/docs/cdn-links.html
https://developers.google.com/books

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

Gatsby.jsを試してみた

Gatsby.jsとは?

Gatsbyは、Reactに基づく無料のオープンソースフレームワークであり、
開発者が非常に高速なWebサイトやアプリを構築するのに役立ちます

Docsより

:computer:環境構築


まずはCLIが用意されているので、Gatsby CLIをインストールします。

$ npm install -g gatsby-cli

まずは試しにサイトを生成してみます。
※途中でpackage managerの yarnnpm どちらを使うか聞かれます。
今回は yarn を選択しました。

$ gatsby new my-site
? Which package manager would you like to use ? › - Use arrow-keys. Return to submit.
❯   yarn
    npm

生成後のディレクトリ構成は以下の様になりました。

.
├── LICENSE
├── README.md
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── gatsby-ssr.js
├── node_modules
├── package.json
├── public
├── src
└── yarn.lock

develop用のサーバーを起動し、ブラウザで http://localhost:8000 を確認してみます。

$ cd my-site
$ gatsby develop

↓の様に表示されとけば成功です :sparkles:

ちなみに、production用にbuildする場合は以下コマンドで行います。

$ gatsby build

また、production buildした内容を手元の環境で確認する場合は以下コマンドで行います。

$ gatsby serve

:pencil: ブログを作成する


Gatsby.jsの雰囲気が掴めた所で、次はブログを作成してみます。
https://github.com/gatsbyjs/gatsby-starter-blog を使用する事で簡単にブログを作成できます。

$ gatsby new my-blog https://github.com/gatsbyjs/gatsby-starter-blog

生成後のディレクトリ構成は以下の通りで、先ほどよりBlog用にディレクトリやファイルが増えています。

.
├── LICENSE
├── README.md
├── content
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── node_modules
├── package.json
├── public
├── src
├── static
└── yarn.lock

早速ローカルで確認してみます。

$ cd my-blog
$ gatsby develop

ブラウザで http://localhost:8000/ を確認すると以下の様なページが表示されます。

また、http://localhost:8000/___graphql を確認すると、ローカル環境のデータをQueryできるGraphiQLが表示されます。
色々揃っていて素敵です :sparkles:


記事を追加する

記事追加も簡単で、content/blog/ に追加するだけで自動で更新されます。
試しに content/blog/sample/index.md を以下の内容で作成すると

---
title: これはサンプルです
---

## 追加された記事

直ぐに反映されます ↓

追加したページ。URLは http://localhost:8000/sample/ になります。

:rocket: デプロイ


今回はまだ試せてないですが、GitHub, GitLab, Bitbucket いずれかにリポジトリを作成し、
Netlify を使用する事で簡単に公開できるとの事でした。 :sparkles:

:link: 参考になったURL


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