20190430のReactに関する記事は7件です。

react-qr-reader を利用したQRコードリーダーの作成

はじめに

ReactでQRコードリーダを作ります。
create-react-app と react-qr-reader を使って簡単に作ります。

QRコードリーダの作成

最初にcreate-react-appを使って、React環境を作成します。
今回は、qrcode-readerというディレクトリに作成します。

npx create-react-app qrcode-reader

次に、react-qr-readerをインストールします。
react-qr-readerのページに書かれているインストール方法で、
以下のコマンドを実行します。

cd qrcode-reader
npm install --save react-qr-reader

最後に、react-qr-readerのサンプルコードを参考にsrc/App.jsを書き換えます。

import React, { Component } from 'react'
import QrReader from 'react-qr-reader'

class App extends Component {
  state = {
    result: 'No result'
  }

  handleScan = data => {
    if (data) {
      this.setState({
        result: data
      })
    }
  }
  handleError = err => {
    console.error(err)
  }
  render() {
    return (
      <div>
        <QrReader
          delay={300}
          onError={this.handleError}
          onScan={this.handleScan}
          style={{ width: '100%' }}
        />
        <p>{this.state.result}</p>
      </div>
    )
  }
}
export default App;

QRコードリーダの確認

作成したコードが動作するか確認します。
以下のコマンドでローカルサーバを起動します。

npm start

この後ブラウザで、http://localhost:3000/ を表示します。
カメラでQRコードを読むと、画面の下にQRコードの文字列を表示します。

スマホから確認

QRコードリーダがスマホでも動くか確認します。
ローカルサーバを起動した開発マシンと同一ネットワークに
存在するスマホのブラウザで確認します。
npm startした時に画面上に以下のような文字が出ていると思います。
IPアドレスは、環境により異なるので、画面上に出ているURLを
ブラウザに入力します。これで作成したコードがスマホで実行できるか確認できます。

  On Your Network:  http://192.168.0.1:3000/

上記の方法では、iPhone(iOS12.2)では、うまく表示できませんでした。
最初は、開発コードのバグかと思いましたが、iOS12.2のsafariでは、
httpsではないとカメラにアクセスできないようです。

HTTPSでローカルサーバを起動

ローカルサーバにhttpsでアクセスできるように、以下のコマンドでローカルサーバを
起動します。このコマンドは、Linuxとmac用です。他のOSの場合は、参考文献に書いてある、create-react-app公式ページ(Using HTTPS in Development)を確認ください。

HTTPS=true npm start

HTTPSでローカルサーバを起動

以下のコマンドで公開用ビルドを作成します。

npm run build

ビルド結果は、buildディレクトリに生成されます。
このディレクトリをWEBサーバに設置することで公開できます。

環境

  • react-qr-reader 2.2.1
  • create-react-app 3.0.0

参考文献

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

typelessでReduxチュートリアルのTodoListを作ってみた

↓の記事でtypelessというツールキットを知ったので、typelessを使って、ReduxチュートリアルのTodoListを作ってみました。

ソースコード

メモ

redux-devtools-extension

// src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { RootEpic, RootReducer, TypelessProvider } from 'typeless';
import { createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { App } from './components/App';

const rootEpic = new RootEpic();
const rootReducer = new RootReducer();
const store = (() => {
  if (process.env.NODE_ENV === 'production') {
    return createStore(rootReducer.getReducer());
  }
  return createStore(rootReducer.getReducer(), composeWithDevTools());
})();

ReactDOM.render(
  <TypelessProvider rootEpic={rootEpic} rootReducer={rootReducer} store={store}>
    <App />
  </TypelessProvider>,
  document.getElementById('app')
);

redux-devtools-extensionを使えるようにします。

stateが初期化されるタイミング

素のReduxだと@@INITのタイミングで初期状態がstateの中に保存されています↓

スクリーンショット 2019-04-30 17.24.50.png

typelessではuseModuleが呼ばれたタイミングで@@typeless/addedがdispatchされて初期状態が保存されます↓

スクリーンショット 2019-04-30 17.31.10.png

VisibleTodoListの中ではtodosvisibilityFilterの両方の状態を使うので、
typelessのクイックスタートにあるようにuseModuleしてコンポーネントを返す、というのができませんでした。なのでuseModuleとコンポーネントを別々に呼び出しています。(ここうまく書く方法がわからない…)

// components/App.tsx

import React from 'react';
import {
  useTodoListModule,
  AddTodoComponent,
  VisibleTodoListComponent
} from '../features/todoList/module';
import { useFooterModule, FooterComponent } from '../features/footer/module';

export function App() {
  useTodoListModule();  // <- useModuleのみ
  useFooterModule();    // <- useModuleのみ

  return (
    <>
      <AddTodoComponent />
      <VisibleTodoListComponent />
      <FooterComponent />
    </>
  );
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScript x create-react-app x re-ducks雑感

概要

create-react-app/TypeScript/re-ducksを用いてアプリケーションを作成した雑感
※ 主にre-ducksのことについて書く

コードはこれ(未完)
https://github.com/MasahikoJinno/re-ducks-study

技術スタック

  • TypeScript
    • JavaScriptに型のパワーをもたらすやつ(スーパーセット)
    • コードが大規模になったときに堅牢かつスピーディーに開発できると思う
    • vscodeを使えばめっちゃ補完が効くようになる
  • React
  • Redux
  • create-react-app
    • Reactのビルド環境セットアップツール
    • webpackとかbabelの設定をいい感じにしてくれるやつ
    • SSRは非対応(だったはず)
      • ReactでSSRしたかったらNext.jsがいいと思う
    • 公式ドキュメント
  • re-ducks

TypeScriptについて

乗るしかねぇッッ!!
このビッグウェーブにッッ!!

色々めんどくさいところもあるけど、VSCodeによる補完は素晴らしい。
基本プロとして複数人でコード書くならTypeScriptにしたほうがいいと思う。

使い捨てだったり、(未来永劫)自分一人で書くコードだったり、短命(なのが確実)なアプリだったらJavaScriptでもいいかな・・・

create-react-appについて

webpackとかbabelとかあんまり考えずにReactのアプリ作れるのでとても便利だと思う。
そのかわりwebpackの設定とか書き換えられないけどそのへんはお察しください。
というかSSRをしないという前提なら、create-react-appのwebpack書き換えたいって思った時点でちょっとアーキテクチャが正しいのか考え直したほうがいい。基本的にセオリーからズレると思うので。トリッキーは辛いぞ。

クライアントサイドレンダリングのだけのSPAを作るならcreate-react-app一択かなぁと思ってます。
思考停止してモダンな環境作れるので。

re-ducksについて

今まで、以下のようなディレクトリ構成でReduxを用いていた。
https://github.com/MasahikoJinno/study/tree/master/js/react_todoapp

- components: Functional Componentを配置
- containers: Container Componentを配置
- actions: ReduxのAction Creatorsを配置
- reducers: ReduxのReducerを配置

同じ名前のファイルが大量にできるし、Stateの世界とViewの世界がごっちゃになってる感があってあまりしっくり来ていなかった。

re-ducksというものを知り、そのデザインパターンがどのようなものなのか知るためにサンプルコードを書いてみた。

俺の思うre-ducksでの責務分離

re-ducksは以下のようなディレクトリ構成になる

+ ducks
└+ todos
 └ actions.ts
 └ index.ts
 └ operations.ts
 └ reducers.ts
 └ selectors.ts
 └ types.ts

各ファイルの責務分離は以下のようになっている。(と思う)
※ index.tsはただのモジュールのエントリポイントだから無視。
※ 詳しくは冒頭のリポジトリを見てほしい。

actions.ts

  • Action Creatorを配置する
  • Action Creatorは単純なオブジェクト(ActionTypeとPayload)を返すだけにする
    • Dispatchとかしない
    • 値の計算もしない
    • async/awaitとかも書かない

operations.ts

  • Actionを発火するためのラッパー関数を配置する
    • Actionを複数かい同時に叩きたい場合やThunkを使う場合はここに書く
    • Actionにわたす引数の作成等もこのoperationsで行う
  • Actionを発火させたい場合は、必ずこのoperations経由で行う
    • Actionをカプセル化する
    • ViewはどのActionを発火するとか知らないほうがいい

reducers.ts

  • ここは他のパターンでもあんまり変わらないと思う
  • Reduxのreducerを配置する
  • Stateの定義とActionTypeに応じたState変更処理を記述する
  • 書いてて思ったけどファイル名複数形じゃないほうがいいね

selectors.ts

  • Viewが使いたい値はStateの値とイコールとは限らない
  • Viewが使いたい形にStateを加工する関数を配置する
  • 単純なデータ構造であればStateをそのまま利用しても良いが、ViewがStateを知りすぎるのは良くないのでViewが使いやすい形に加工して渡してやるのがよい
  • 配列のフィルタリングとかはここでやる(Viewにロジックを書かない。ただ渡されたものを表示するだけにする。)

types.ts

  • ActionTypesの定義とかはここでしている
  • actions.tsで定義すればよくね?って意見もあるけどそれでもいいと思う(好みの問題)
  • TSじゃなかったら不要かなぁ

Reduxの作用はactions.tsreducers.tsに記述して、副作用をoperations.tsselectors.tsに記述するイメージだと思う。

operations.tsselectors.tsの違いで最初混乱したけど今は以下のように理解している。

  • operations.ts
    • State変更時の(ビジネス)ロジックを記載
  • selectors.ts
    • State取得時の(ビジネス)ロジックを記載

全体的な所感

アプリケーション全体では大まかに以下のようなディレクトリ構成にしている

+ state
└+ ducks
+ views
└+ components
└+ containers
└+ pages
└ App.tsx
index.tsx

大まかにstateviewsで状態管理の世界と表示の世界で分離。
stateには前述のducksディレクトリが入る。

viewsは純粋なComponent(ほぼFunctional Componentsになる)のcomponentsと状態管理の世界と表示の世界を繋ぐcontainersといわゆるContainer Componentsを格納するpagesに分離した。
componentspagesは正直適当。Atomicデザインとかに習えばatom, molecules, organisms, templates, pagesとかになるのかな・・・
ただ、containersとその他は分けたほうがいいと思う。(containersにJSX書かないほうがいい)

んで、何が嬉しいの?

究極は「責務が明確になる」であると思う。
re-ducksのファイル分割では、各々の責務が明確になっており、actionsやreducerは非常に単純なコード(ピュアな状態)を維持できる。
ユニットテストも容易になると思われる。

その分operationsとselectorsに副作用が追いやられているが、これはこれでどこのテストをしっかり書かなきゃだめかわかりやすくなると思う。

re-ducksとは直接関係ないけど、今回コードを書いていてViewとStateの依存をなくすことの重要さを再認識した。
Stateのデータ構造がViewのに依存する形で作られていたり、ViewがStateの構造を完全に理解して実装されていたりっていうのはあるあるだと思うがこの状態は良くない。

ViewはViewとして必要なものだけを、StateはStateとして必要なものだけを実装すべきだと思うし、そうでなければ作業分担当もできない。

ViewとStateの互いの依存をなくすために、containerの実装が重要になってくる。
containerがViewとState両方に依存して、複雑さを一手に引き受けることでViewとStateの依存関係が初めて解消される」ということにようやく気づくことができた。

selectorsoperationsはcontainerが肥大化するのを避けるために生まれた概念なのかなってちょっと思った。

まとめ

  • ViewとStateはお互い依存しないようにしよう
  • 責務分離をしっかりと行おう
  • そのための手段がre-ducks

以上、最後までご覧いただきありがとうございました。

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

自分の環境設定を書いておく (webpack.config.js)

JavaScript(ES6)

  • @babel/register.babelrc
webpack.config.babel.js
import path from 'path';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import TerserWebpackPlugin from 'terser-webpack-plugin';
import MiniCSSExtractPlugin from 'mini-css-extract-plugin';
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin';

const isDev = process.env.NODE_ENV === 'development';

export default {
  mode: isDev ? 'development' : 'production',
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCSSExtractPlugin.loader,
            options: {
              hmr: isDev,
              reloadAll: true,
            },
          },
          {
            loader: 'css-loader',
            options: {
              sourceMap: isDev,
            },
          },
        ],
      },
      {
        test: /\.(tiff|gif|jpe?g|png|svg|ttf|eot|wof|woff2?)$/,
        loader: 'file-loader',
        options: {
          name: 'images/[name].[ext]',
        },
      },
    ],
  },
  resolve: {
    extensions: ['.js'],
  },
  optimization: isDev
    ? { minimize: false }
    : {
        minimizer: [new TerserWebpackPlugin(), new OptimizeCSSAssetsPlugin({})],
      },
  plugins: [
    new MiniCSSExtractPlugin({}),
    new HtmlWebpackPlugin({
      template: './src/index.html',
      favicon: './src/favicon.ico',
    }),
  ],
  devtool: isDev ? 'source-map' : false,
  devServer: {
    contentBase: path.resolve(__dirname, 'build'),
    port: 8000,
  },
};

JavaScript + React

webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
const MiniCSSExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

const isDev = process.env.NODE_ENV === 'development';

module.exports = {
  mode: isDev ? 'development' : 'production',
  entry: './src/index.jsx',
  resolve: {
    extensions: ['.jsx', '.js'],
  },
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env', '@babel/preset-react'],
          plugins: ['@babel/plugin-proposal-class-properties'],
        },
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCSSExtractPlugin.loader,
            options: {
              hmr: isDev,
              reloadAll: true,
            },
          },
          {
            loader: 'css-loader',
            options: {
              sourceMap: isDev,
            },
          },
        ],
      },
      {
        test: /\.(tiff|gif|jpe?g|png|svg|eot|wof|woff2?|ttf)$/,
        loader: 'file-loader',
        options: {
          name: 'images/[name].[ext]',
        },
      },
    ],
  },
  plugins: [
    new MiniCSSExtractPlugin({}),
    new HtmlWebpackPlugin({
      template: './src/index.html',
      favicon: './src/favicon.ico',
    }),
  ],
  optimization: isDev
    ? { minimize: false }
    : {
        minimizer: [new TerserWebpackPlugin(), new OptimizeCSSAssetsPlugin({})],
      },
  devtool: isDev ? 'source-map' : false,
  devServer: {
    contentBase: path.resolve(__dirname, 'build'),
    port: 3030,
  },
};

JavaScript + Electron

webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
const MiniCSSExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

const isDev = process.env.NODE_ENV === 'development';

const main = {
  mode: isDev ? 'development' : 'production',
  target: 'electron-main',
  entry: './src/main.js',
  resolve: {
    extensions: ['.js', '.json'],
  },
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'main.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env'],
        },
      },
    ],
  },
};

const app = {
  mode: isDev ? 'development' : 'production',
  target: 'electron-renderer',
  entry: './src/app.js',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'app.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env'],
        },
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCSSExtractPlugin.loader,
            options: {
              hmr: isDev,
              reloadAll: true,
            },
          },
          {
            loader: 'css-loader',
            options: {
              sourceMap: isDev,
            },
          },
        ],
      },
      {
        test: /\.(tiff|gif|jpe?g|png|svg|eot|wof|woff2?|ttf)$/,
        loader: 'file-loader',
        options: {
          name: 'images/[name].[ext]',
        },
      },
    ],
  },
  plugins: [
    new MiniCSSExtractPlugin({}),
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
  ],
  optimization: isDev
    ? { minimize: false }
    : {
        minimizer: [new TerserWebpackPlugin(), new OptimizeCSSAssetsPlugin({})],
      },
  devtool: isDev ? 'source-map' : false,
};

module.exports = [main, app];

TypeScript

webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

const isDev = process.env.NODE_ENV === 'development';

module.exports = {
  mode: isDev ? 'development' : 'production',
  entry: './src/index.tsx',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'bundle.js',
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx'],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        loader: 'ts-loader',
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              hmr: isDev,
              reloadAll: true,
            },
          },
          'css-loader',
        ],
      },
      {
        test: /\.(tiff|gif|jpe?g|png|svg|ttf|eot|wof|woff|woff2)$/,
        loader: 'file-loader',
        options: {
          name: 'images/[name].[ext]',
        },
      },
    ],
  },
  optimization: isDev
    ? { minimize: false }
    : {
        minimizer: [new TerserWebpackPlugin(), new OptimizeCSSAssetsPlugin({})],
      },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      favicon: './src/favicon.ico',
    }),
    new MiniCssExtractPlugin({}),
  ],
  devtool: isDev ? 'source-map' : false,
  devServer: {
    contentBase: path.resolve(__dirname, 'build'),
    port: 3000,
  },
  stats: 'minimal',
  performance: {
    hints: false,
  },
};

公式ドキュメント

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

Chrome拡張機能で、GitHub上でSwaggerをプレビューできるツールを作った

概要

GitHub 上で、Swagger の yaml | yml | json を Swagger-UI に変換できる Chrome 拡張機能を作った。
その紹介。

「Swagger とは?」という話は、下記等を参照のこと。

Demo

クリックするだけ。簡単に変換できる。
README-Demo_1.gif

全展開・全折り畳みができて便利。
README-Demo_2.gif

インストール

Chrome Web Store からインストールできる。
https://chrome.google.com/webstore/detail/swagger-viewer/nfmkaonpdmaglhjjlggfhlndofdldfag

特徴

簡単

  • 1クリックで変換できる

依存なし

  • この拡張機能のみで動作する
  • Web の Swagger Editor を開いたり、ドキュメント生成サーバーを起動する必要なし

セキュア(たぶん)

  • 外部ネットワークへの送受信はないため、業務でも使用できる
    • ※自己責任でお願いします
  • たぶん
    • Chrome の Network タブを眺めたが、通信する様子はなかった
    • Swagger の描画のために使用しているライブラリ「swagger-ui」のコードも軽く読んだが、ネットワーク送受信に関するコードはなかった(自信なし)
      • やべーやつだったらご連絡いただけると幸いです

開発裏話(?)

動けばいいんだよ2割、美しく書きたい8割、くらいの気持ちで作った
趣味のコードは盆栽
名言だと思った

src

https://github.com/arx-8/swagger-viewer

技術要素

似た技術スタックで Chrome 拡張機能を作りたい人の一助になれば(と盆栽自慢に)少し書く

  • WebExtension Toolbox (Chrome 拡張機能ジェネレーター)
  • TypeScript
  • Jest
  • jsdom
  • React
  • ESLint
  • Clean Architecture

以下盆栽ポイント

  • 最新フロントエンド技術 (TypeScript, typescript-eslint, prettier, etc.) を導入した
  • 壊れやすいDOM操作に対してUnitTestを書いた
    • 生DOMは、Jest + jsdom
    • Reactは、Jest Snapshot test + react-test-renderer
  • WebExtension Toolbox で作った雛形には、TypeScriptはもちろんESLintもテストも入ってないので、自分で追加が必要
  • Clean Architecture
    • (ゆるく)パッケージ構成をこれでやるとだいたいうまく整理できる気がする
    • Clean Architecture * React のベストプラクティスがほしい・・・

TypeScript導入

下記issueを参考に、
https://github.com/webextension-toolbox/generator-web-extension/issues/11
ts-loaderで導入
https://github.com/arx-8/swagger-viewer/commit/fcf385363729f6fb0a7e39018a5fff2ec5881145

後に、Jest導入時にうまく動作しなかったため、babelに変更
https://github.com/arx-8/swagger-viewer/commit/a5dbbe19054189cb03cc88d825ee9c21d2671987

幸いwebextension-toolboxがbuildに使ってるbabelが7系だっため、TypeScriptのトランスパイルができた
https://github.com/arx-8/swagger-viewer/blob/master/swagger-viewer/package-lock.json#L9871

Jest導入

前述の通り、ts-loaderではうまく動かず、babelに変えた
https://github.com/arx-8/swagger-viewer/pull/9/files#diff-47c7df0a0b4c5f24af1531f28d7a4d57

Reactでの css import がエラーになったため、これをやった
https://jestjs.io/docs/ja/webpack#静的アセットの管理

ESLint (+ Prettier)

https://github.com/arx-8/swagger-viewer/pull/11/files#diff-514b7220e7bfcb11a68634cba4db8961

  • .eslintrcのextendsが激盛り・・・
  • 個人的には、ガチガチのairbnbをプロジェクトに応じて緩めるのが好き

no-restricted-globals, no-restricted-properties で禁止機能を定義している

ESLint + TypeScript + VSCode

VSCode拡張機能のESLintはちゃんとtsファイルにも対応していた
しばらく気付かず悶々としていたが、やはりMSは神

https://github.com/Microsoft/vscode-eslint/issues/609#issuecomment-460554105

  • ただデフォルト有効にはならなそうな雰囲気なので、自分で設定が必要

実際の設定はこんな感じ

今のところ特にバグにもハマってないので、typescript-eslint は十分プロダクトに投入できるレベルなのでは

Jest + jsdom

だいたいこんな手順で、documentオブジェクトに任意の値を展開してテストしている

テスト結果を手で定義するのが面倒なものは、toMatchSnapshotで雑にassert

no-restricted-globals で document -> getDocument() としてる理由はこれ

  • JestでMockできるのは、外部ファイルからimportしたリソースのみらしい
  • なので、documentオブジェクトにアクセスする場合は、必ずgetDocument(モック化できるfunction)を経由させている

Jest + react-test-renderer

enzyme より react-test-renderer の方がいいぞ、という記事を読んだのでそうした

コード例

DOM丸ごと生成したりライブラリのrender結果を丸ごとSnapshot取ったり力技なUTだが、「規模が小さいし実行してほっときゃ終わるだろ」ということで富豪チックに書いた

未解決の課題

ReactTestUtils.Simulate.click ができない

やり方を知ってる方いたら、ご教授願いたい・・・
公式には記述があるのだが、ver違いなのかReactTestUtils.Simulateがundefinedだった
https://reactjs.org/docs/test-utils.html#simulate

「Reactコンポーネントが、ある箇所をクリックされた結果どうなってるか」のSnapshot testができていないのが心残り

まとめ

  • Chrome拡張機能、型・テストともガッチガチに作れて楽しかったのでオススメ!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitHub上でSwaggerをプレビューできるChrome拡張機能を作った

概要

GitHub 上で、Swagger の yaml | yml | json を Swagger-UI に変換できる Chrome 拡張機能を作った。
その紹介。

「Swagger とは?」という話は、下記等を参照のこと。

Demo

クリックするだけ。簡単に変換できる。
README-Demo_1.gif

全展開・全折り畳みができて便利。
README-Demo_2.gif

インストール

Chrome Web Store からインストールできる。
swagger-viewer - Chrome ウェブストア

特徴

簡単

  • 1クリックで変換できる

依存なし

  • この拡張機能のみで動作する
  • Web の Swagger Editor を開いたり、ドキュメント生成サーバーを起動する必要なし

セキュア(たぶん)

  • 外部ネットワークへの送受信はないため、業務でも使用できる
    • ※自己責任でお願いします
  • たぶん
    • Chrome の Network タブを眺めたが、通信する様子はなかった
    • Swagger の描画のために使用しているライブラリ「swagger-ui」のコードも軽く読んだが、ネットワーク送受信に関するコードはなかった(自信なし)
      • やべーやつだったらご連絡いただけると幸いです

開発裏話(?)

動けばいいんだよ2割、美しく書きたい8割、くらいの気持ちで作った
趣味のコードは盆栽
名言だと思った

src

https://github.com/arx-8/swagger-viewer

技術要素

似た技術スタックで Chrome 拡張機能を作りたい人の一助になれば(と盆栽自慢に)少し書く

  • WebExtension Toolbox (Chrome 拡張機能ジェネレーター)
  • TypeScript
  • Jest
  • jsdom
  • React
  • ESLint
  • Clean Architecture

以下盆栽ポイント

  • 最新フロントエンド技術 (TypeScript, typescript-eslint, prettier, etc.) を導入した
  • 壊れやすいDOM操作に対してUnitTestを書いた
    • 生DOMは、Jest + jsdom
    • Reactは、Jest Snapshot test + react-test-renderer
  • WebExtension Toolbox で作った雛形には、TypeScriptはもちろんESLintもテストも入ってないので、自分で追加が必要
  • Clean Architecture
    • (ゆるく)パッケージ構成をこれでやるとだいたいうまく整理できる気がする
    • Clean Architecture * React のベストプラクティスがほしい・・・

TypeScript導入

下記issueを参考に、
https://github.com/webextension-toolbox/generator-web-extension/issues/11
ts-loaderで導入
https://github.com/arx-8/swagger-viewer/commit/fcf385363729f6fb0a7e39018a5fff2ec5881145

後に、Jest導入時にうまく動作しなかったため、babelに変更
https://github.com/arx-8/swagger-viewer/commit/a5dbbe19054189cb03cc88d825ee9c21d2671987

幸いwebextension-toolboxがbuildに使ってるbabelが7系だっため、TypeScriptのトランスパイルができた
https://github.com/arx-8/swagger-viewer/blob/master/swagger-viewer/package-lock.json#L9871

Jest導入

前述の通り、ts-loaderではうまく動かず、babelに変えた
https://github.com/arx-8/swagger-viewer/pull/9/files#diff-47c7df0a0b4c5f24af1531f28d7a4d57

Reactでの css import がエラーになったため、これをやった
https://jestjs.io/docs/ja/webpack#静的アセットの管理

ESLint (+ Prettier)

https://github.com/arx-8/swagger-viewer/pull/11/files#diff-514b7220e7bfcb11a68634cba4db8961

  • .eslintrcのextendsが激盛り・・・
  • 個人的には、ガチガチのairbnbをプロジェクトに応じて緩めるのが好き

no-restricted-globals, no-restricted-properties で禁止機能を定義している

ESLint + TypeScript + VSCode

VSCode拡張機能のESLintはちゃんとtsファイルにも対応していた
しばらく気付かず悶々としていたが、やはりMSは神

https://github.com/Microsoft/vscode-eslint/issues/609#issuecomment-460554105

  • ただデフォルト有効にはならなそうな雰囲気なので、自分で設定が必要

実際の設定はこんな感じ

今のところ特にバグにもハマってないので、typescript-eslint は十分プロダクトに投入できるレベルなのでは

Jest + jsdom

だいたいこんな手順で、documentオブジェクトに任意の値を展開してテストしている

テスト結果を手で定義するのが面倒なものは、toMatchSnapshotで雑にassert

no-restricted-globals で document -> getDocument() としてる理由はこれ

  • JestでMockできるのは、外部ファイルからimportしたリソースのみらしい
  • なので、documentオブジェクトにアクセスする場合は、必ずgetDocument(モック化できるfunction)を経由させている

Jest + react-test-renderer

enzyme より react-test-renderer の方がいいぞ、という記事を読んだのでそうした

コード例

DOM丸ごと生成したりライブラリのrender結果を丸ごとSnapshot取ったり力技なUTだが、「規模が小さいし実行してほっときゃ終わるだろ」ということで富豪チックに書いた
これでGitHubのhtmlが変更されたとしても、UT回して検知・修正ができるであろうと期待

未解決の課題

ReactTestUtils.Simulate.click ができない

やり方を知ってる方いたら、ご教授願いたい・・・
公式には記述があるのだが、ver違いなのかReactTestUtils.Simulateがundefinedだった
https://reactjs.org/docs/test-utils.html#simulate

「Reactコンポーネントが、ある箇所をクリックされた結果どうなってるか」のSnapshot testができていないのが心残り

まとめ

  • Chrome拡張機能、型・テストともガッチガチに作れて楽しかったのでオススメ!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

create-react-appを利用してReactアプリをGitHub Pagesで公開

はじめに

Facebookが作成しているReactを試してみたかったので、GitHub PagesでReactアプリを公開する方法です。現時点のReactの公式ページとGitHubの公式ページに記載されている方法で動いたので、この方法を書いていきます。

Reactアプリの環境構築

Reactアプリの環境構築は、コマンド1つで作成できるcreate-react-appを使います。このツールは、Reactと同様にFacebookが公開しています。使い方は、簡単。インストールに必要な環境は、npxのバージョンが5.2以上。この環境があれば、公式ページに書かれている手順でReactが使えるようになります。
Reactアプリの環境を作るのは、以下のコマンドを実行します。

npx create-react-app my-app

my-appは、ディレクトリ名なので、自分の好きな名前に変更できます。この記事では、my-appとします。Reactのページをブラウザで表示するには、以下のコマンドを実行して、ブラウザで http://localhost:3000/ を開きます。my-app/src/App.js を編集すれば、表示内容を修正することができるので、試してみてください。

cd my-app
npm start

Reactアプリのデプロイ

GitHub Pagesに設置するURLをpackage.json ファイルのhomepageの項目に設定します。初期のpackage.jsonファイルには、homepageの項目はないので、例えば、以下のように設定を追加します。

  "homepage" : "http://myname.github.io/myapp",

設定後、以下のコマンドでbuildを行います。
ビルド結果は、my-app/build ディレクトリに生成されます。
このディレクトリをGitHubに設置すれば公開することができます。

npm run build

GitHub Pagesで公開

  1. GitHubでusername.github.ioのレポジトリを作成
    usernameの部分は、自分のGitHubユーザ名に変更します。このレポジトリは、GitHub Pagesとしてwebサーバとして公開されます。
  2. GitHubのレポジトリ(username.github.io)にbuildディレクトリを設置
    ディレクトリ名は、myappにします。git pushしてGitHubに反映させます。
  3. http://myname.github.io/myapp にアクセスして確認

環境

  • create-react-app 3.0.0
  • npx 6.7.0
  • node 11.10.1

参考文献

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