7
7

More than 5 years have passed since last update.

React + TypeScript で計算ツールを速攻で作ってタダでデプロイする

Posted at

TRPGをやったりしてる人は計算ツールがほしくなりますよね。

いちいち本を開いて数字を見て……ってやってるとまどろっこしいです。

というわけで、今回は例として、用紙サイズとDPIを入れるとピクセル数を計算してくれるツールをReact + TypeScriptで作ります。jQueryなんて投げ捨てよう!

環境構築

まずは nodenpm を入れておいてください。ターミナルかコマンドプロンプト開いて node -vnpm -v って叩いてバージョンが出ればOKです。 yarn 使うかどうかはお好みで。

次に create-react-app を入れます。

> npm i -g create-react-app

プロジェクトのフォルダを置きたいディレクトリの中に cd で移動します。
例えば、 /home/hibikine/src/calc-tool に作りたければ、 cd /home/hibikine/src と移動してください。

/
> cd /home/hibikine/src

/home/hibikine/src
>

プロジェクトを作成します。今回は calc-tool という名前で、TypeScript使って作ります。1

> create-react-app calc-tool --scripts-version=react-scripts-ts

... いろいろ出る

Initialized a git repository.

Success! Created calc-tool at /mnt/c/Users/goods/src/calc-tool
Inside that directory, you can run several commands:

  yarn start
    Starts the development server.

  yarn build
    Bundles the app into static files for production.

  yarn test
    Starts the test runner.

  yarn eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd calc-tool
  yarn start

Happy hacking!

生成されたディレクトリ内に移動し、 npm run start ( yarn の人は yarn start )します。

/home/hibikine/src
> cd calc-tool

/home/hibikine/src/calc-tool
> yarn start

http://localhost:3000 にアクセスし、次のような画面が出ればOKです。

image.png

実装

生成直後のフォルダ構成を見ると次のようになっています。

フォルダ構成.txt
node_modules/
public/
src/
  App.css
  App.test.tsx
  App.tsx
  index.css
  index.tsx
  logo.svg
  registerServiceWorker.ts
.gitignore
... etc

App.tsx を開き、中身を次のように書き換えて保存します。

App.tsx
import * as React from 'react';
import './App.css';

interface IState {
  paperSize: number;
  dpi: number;
}

class App extends React.Component {
  public state = { paperSize: 0, dpi: 0 };

  public render() {
    const { paperSize, dpi } = this.state;
    return (
      <div className="App">
        <header className="App-header">
          <h1 className="App-title">ピクセル数計算機</h1>
        </header>
        <p className="App-main">
          <table>
            <tbody>
              <tr>
                <td>
                  <label htmlFor="paperSize">A</label>
                </td>
                <td>
                  <input id="paperSize" type="number" value={paperSize} />
                </td>
              </tr>
              <tr>
                <td>
                  <label htmlFor="dpi">DPI</label>
                </td>
                <td>
                  <input id="dpi" type="number" value={dpi} />
                </td>
              </tr>
            </tbody>
          </table>

          <table>
            <tbody>
              <tr>
                <td>
                  <label htmlFor="width-mm">(mm)</label>
                </td>
                <td>
                  <input id="width-mm" readOnly={true} value={5} />
                </td>
              </tr>
              <tr>
                <td>
                  <label htmlFor="height-mm">高さ(mm)</label>
                </td>
                <td>
                  <input id="height-mm" readOnly={true} value={5} />
                </td>
              </tr>
              <tr>
                <td>
                  <label htmlFor="width-px">(px)</label>
                </td>
                <td>
                  <input id="width-px" readOnly={true} value={5} />
                </td>
              </tr>
              <tr>
                <td>
                  <label htmlFor="height-px">高さ(px)</label>
                </td>
                <td>
                  <input id="height-px" readOnly={true} value={5} />
                </td>
              </tr>
            </tbody>
          </table>
        </p>
      </div>
    );
  }
}

export default App;

ついでに App.css も書き換えます。

App.css
.App {
  text-align: center;
}

.App-header {
  background-color: #222;
  height: 80px;
  padding: 20px;
  color: white;
}

.App-title {
  font-size: 1.5em;
}

.App-main {
  display: flex;
  flex-direction: column;
  align-items: center;
}

ファイルを保存すると画面が自動で更新されて次のようになります。

image.png

この段階だと数字が変更できないので修正していきましょう。

数字を変えられるようにする

class App の末尾に onChange メソッドを追加しましょう。

App.tsx
  private onChange = (key: keyof IState) => (
    e: React.FormEvent<HTMLInputElement>
  ) => this.setState({ [key]: parseInt(e.currentTarget.value, 10) } as any);

これをそれぞれの input に設定し、 input の中身が書き換えられたときに App 内の state が書き換わるようにします。
state の書き換えは必ず setState メソッドを通して行うようにします。

App.tsx
              <tr>
                <td>
                  <label htmlFor="paperSize">A</label>
                </td>
                <td>
                  <input
                    id="paperSize"
                    type="number"
                    value={paperSize}
                    onChange={this.onChange('paperSize')}
                  />
                </td>
              </tr>
              <tr>
                <td>
                  <label htmlFor="dpi">DPI</label>
                </td>
                <td>
                  <input
                    id="dpi"
                    type="number"
                    value={dpi}
                    onChange={this.onChange('dpi')}
                  />
                </td>
              </tr>

これで数字が書き換えられるようになります。

image.png

計算

最後に出力が書き換わるようにします。
class App の上に getWidth メソッドと calcPixel メソッドを追加しましょう。A系の紙のサイズは再帰で求められます。

App.tsx
interface IState {
  paperSize: number;
  dpi: number;
}

const getWidth = (n: number): number => {
  if (n <= 0) {
    return 1189;
  } else if (n === 1) {
    return 841;
  }
  return Math.floor(getWidth(n - 2) / 2);
};
const calcPixel = (len: number, dpi: number) => Math.floor((len / 25.4) * dpi);

class App extends React.Component<{}, IState> {

render メソッドを書き換えます。

App.tsx
  public render() {
    const { paperSize, dpi } = this.state;
    const width = getWidth(paperSize);
    const height = getWidth(paperSize + 1);
    const pixelWidth = calcPixel(width, dpi);
    const pixelHeight = calcPixel(height, dpi);

    return (
      <div className="App">
        <header className="App-header">
          <h1 className="App-title">ピクセル数計算機</h1>
        </header>
        <p className="App-main">
          <table>
            <tbody>
              <tr>
                <td>
                  <label htmlFor="paperSize">A</label>
                </td>
                <td>
                  <input
                    id="paperSize"
                    type="number"
                    value={paperSize}
                    onChange={this.onChange('paperSize')}
                  />
                </td>
              </tr>
              <tr>
                <td>
                  <label htmlFor="dpi">DPI</label>
                </td>
                <td>
                  <input
                    id="dpi"
                    type="number"
                    value={dpi}
                    onChange={this.onChange('dpi')}
                  />
                </td>
              </tr>
            </tbody>
          </table>

          <table>
            <tbody>
              <tr>
                <td>
                  <label htmlFor="width-mm">(mm)</label>
                </td>
                <td>
                  <input id="width-mm" readOnly={true} value={width} />
                </td>
              </tr>
              <tr>
                <td>
                  <label htmlFor="height-mm">高さ(mm)</label>
                </td>
                <td>
                  <input id="height-mm" readOnly={true} value={height} />
                </td>
              </tr>
              <tr>
                <td>
                  <label htmlFor="width-px">(px)</label>
                </td>
                <td>
                  <input id="width-px" readOnly={true} value={pixelWidth} />
                </td>
              </tr>
              <tr>
                <td>
                  <label htmlFor="height-px">高さ(px)</label>
                </td>
                <td>
                  <input id="height-px" readOnly={true} value={pixelHeight} />
                </td>
              </tr>
            </tbody>
          </table>
        </p>
      </div>
    );
  }

正しく計算が行われればOKです!

image.png

デプロイする

GitHub Pagesに投げます。 yuitnnn さんの「GitHub PagesにReactアプリをデプロイする方法」を参考にしました!

まず gh-pagesを入れます。npmの人は npm i gh-pages --save-dev で。

> yarn add --dev gh-pages

次に package.jsonhomepagepredeploydeploy を追記します。
homepageの先頭部分は自分のGitHubのIDにしたり、後ろはリポジトリ名にしたりしてください。

package.json
  "homepage": "https://hibikinekage.github.io/calc-tool",
  "scripts": {
    // ...,
    "predeploy": "npm run build",
    "deploy": "gh-pages -d build"
  },

GitHubにリポジトリを作成し、リモートに登録してデプロイします。URLは適宜自分のものに変えてください。

> git init
> git remote add origin https://github.com/HibikineKage/calc-tool
> yarn deploy # npm run deploy

自分のIDの https://hibikinekage.github.io/calc-tool で見れるようになります。これで終了です。

まとめ

create-react-app 使うと簡単に色々作れて楽しいです! ちょっとした小物をポンポン作りたい時もjQueryよりもおすすめします!

ソースコード: GitHub

最後に全体のコードを掲載しておきます。

App.tsx
import * as React from 'react';
import './App.css';

interface IState {
  paperSize: number;
  dpi: number;
}

const getWidth = (n: number): number => {
  if (n <= 0) {
    return 1189;
  } else if (n === 1) {
    return 841;
  }
  return Math.floor(getWidth(n - 2) / 2);
};
const calcPixel = (len: number, dpi: number) => Math.floor((len / 25.4) * dpi);

class App extends React.Component<{}, IState> {
  public state = { paperSize: 0, dpi: 0 };

  public render() {
    const { paperSize, dpi } = this.state;
    const width = getWidth(paperSize);
    const height = getWidth(paperSize + 1);
    const pixelWidth = calcPixel(width, dpi);
    const pixelHeight = calcPixel(height, dpi);

    return (
      <div className="App">
        <header className="App-header">
          <h1 className="App-title">ピクセル数計算機</h1>
        </header>
        <p className="App-main">
          <table>
            <tbody>
              <tr>
                <td>
                  <label htmlFor="paperSize">A</label>
                </td>
                <td>
                  <input
                    id="paperSize"
                    type="number"
                    value={paperSize}
                    onChange={this.onChange('paperSize')}
                  />
                </td>
              </tr>
              <tr>
                <td>
                  <label htmlFor="dpi">DPI</label>
                </td>
                <td>
                  <input
                    id="dpi"
                    type="number"
                    value={dpi}
                    onChange={this.onChange('dpi')}
                  />
                </td>
              </tr>
            </tbody>
          </table>

          <table>
            <tbody>
              <tr>
                <td>
                  <label htmlFor="width-mm">(mm)</label>
                </td>
                <td>
                  <input id="width-mm" readOnly={true} value={width} />
                </td>
              </tr>
              <tr>
                <td>
                  <label htmlFor="height-mm">高さ(mm)</label>
                </td>
                <td>
                  <input id="height-mm" readOnly={true} value={height} />
                </td>
              </tr>
              <tr>
                <td>
                  <label htmlFor="width-px">(px)</label>
                </td>
                <td>
                  <input id="width-px" readOnly={true} value={pixelWidth} />
                </td>
              </tr>
              <tr>
                <td>
                  <label htmlFor="height-px">高さ(px)</label>
                </td>
                <td>
                  <input id="height-px" readOnly={true} value={pixelHeight} />
                </td>
              </tr>
            </tbody>
          </table>
        </p>
      </div>
    );
  }

  private onChange = (key: keyof IState) => (
    e: React.FormEvent<HTMLInputElement>
  ) => this.setState({ [key]: parseInt(e.currentTarget.value, 10) } as any);
}

export default App;

  1. ちなみに今回の作り方は create-react-app v2.0.xまでのやり方で、v2.1からは create-react-app --typescript という形で公式サポートされるようになります。ただし、こちらはts-loaderではなくbabelによるトランスパイルのようなので、多少動作に違いが出るかもしれません。 

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7