20201218のJavaScriptに関する記事は30件です。

VS Code でデバッグ時の「Could not read source map for...」について

こやつに苦しめられた話。

Could not read source map for file:///d:/hoge/node_modules/fecha/lib/fecha.umd.js:
ENOENT: no such file or directory, open 'd:\hoge\node_modules\fecha\lib\fecha.umd.js.map'


下記のように.vscode/launch.json内の"type""node"から"pwa-node"へ変更したら出なくなりました。詳細分かる人いましたらぜひ共有いただきたいです!!!

launch.json(変更前)
{
    "version": "0.3.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "MAIN",
            "program": "${workspaceFolder}\\index.js",
            "outputCapture": "std"
        }
    ]
}
launch.json(変更後)
{
    "version": "0.3.0",
    "configurations": [
        {
            "type": "pwa-node",
            "request": "launch",
            "name": "MAIN",
            "program": "${workspaceFolder}\\index.js",
            "outputCapture": "std"
        }
    ]
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsで`window is not defined` を解決する(依存ライブラリ対応)

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

【JavaScript】クラス構文について 2 〜継承〜

※当方駆け出しエンジニアのため、間違っていることも多々あると思いますので、ご了承ください。また、間違いに気付いた方はご一報いただけると幸いです。

こちらは、
【JavaScript】クラス構文について 1
の記事の続きとなります。

継承の構文

class 子クラス extends 親クラス {

}

継承することにより、親クラスの構造や機能を引き継ぐことができる。

子クラスから親クラスを呼び出す。

class Parent {
    constructor(name) {
        console.log(name);
    }
}
// Parentを継承したChildクラスの定義
class Child extends Parent {
    constructor(name) {
        super(name); //superメソッドで親クラスのコンストラクタを呼び出す。
        console.log(name);
    }
}
const child = new Child("taro");

//taro  親コンストラクターが実行された結果
//taro  子コンストラクターが実行された結果

子クラスでコンストラクター関数を定義した場合、親コンストラクタ関数を上書き定義している。
子クラスでコンストラクターを定義しなかった場合は、当然親のメソッドを引き継いでいるので親のコンストラクタを子クラスは保持している。

class Parent {
    constructor(name) {
        console.log(name);
    }
}
// Parentを継承したChildクラスの定義
class Child extends Parent {
     //constructor(name) {
        //console.log(name);
     // }   親のコンストラクターを引き継いでいる。
}
const child = new Child("taro");

//taro  親コンストラクターが実行された結果

プロトタイプ継承

親クラスが保持するメソッドは子クラスに引き継がれる。

class Parent {
    say() {
        console.log("oya");
    }
}

class Child extends Parent {
  //子クラスではsayメソッドを定義していない。
}

const instance = new Child();
instance.say(); 

//oya

これは、プロトタイプチェーンの仕組みにより、子クラスから親クラスへ引き継がれることになります。
プロトタイプチェーンについての詳細についてはこちらの記事をご覧ください。

【JavaScript】クラス構文について 1

一方クラスから、インスタンス生成時にもプロトタイプチェーンの仕組みを使ってインスタンスへメソッドが引き継がれます。(参照情報が渡される。)

乱暴な言い方をすると、javascriptでは、メソッドの引継ぎに関して、親クラスから子クラスへと、クラスからインスタンスへとでは同じ方法でメソッドを引き継いでるイメージとなります。

superプロパティ

上記の通り、親クラスのメソッドは子クラスが引き継いでいるので、呼び出すことができます。しかし、同名のメソッドを子クラスで定義した場合、メソッドチェーンの仕組みから子クラスで定義したメソッドが呼び出されることになります。

class Parent {
  say() {
    console.log("oya");
  }
}

class Child extends Parent {
  say() { //親クラスと同名のメソッドを定義
    console.log("ko");
  }
}

const instance = new Child();
instance.say();

//ko

この時誤解してはいけないのは、sayメソッドは上書きされた訳では無いということです。
子クラスが親クラスを継承した時(メソッド定義前の段階)

class Parent {
  prototype : {
    say(){console.log("oya");};
  }
}

//子クラスが親クラスを参照

class Child {
  prototype : { //子クラスのプロトタイプオブジェクト
   prototype : {  //親クラスのプロトタイプオブジェクト(注:実体ではなく親クラスのプロトタイプオブジェクトのメモリ上の位置情報を格納)
     say(){console.log("oya");};
   }
  }
}

この様に、子クラスのプロトタイプオブジェクトに、親クラスへのプロトタイプオブジェクトの参照情報が格納されます。
ここで、子クラスに同名のメソッドを定義した場合

class Child {
  prototype : { 
    say(){console.log("ko");};//子クラスでsayメソッドを定義
     prototype : { 
     say(){console.log("oya"); };//親クラスのsayメソッド
   }
  }
}

当然、スコープチェーンより、子クラスから生成したインスタンスからsayメソッドを呼び出すと、子クラスで定義したsayメソッドが呼び出されます。

const instance = new Child();
instance.say();

//ko

この場合、親クラスのsayメソッドをsuper.メソッド名で呼び出すことができます。
親クラスのコンストラクタを呼び出すには、上記の通り super(); メソッドを使いますが、親クラスのメソッドもsuper.メソッド名で呼び出すことができるのです。

class Child extends Parent {
  say() {
    console.log("ko");
  }
  parentSay() {
    super.say(); //親クラスのsayメソッドを呼び出している。
  }
}
const instance = new Child();
instance.parentSay();

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

javascript基本情報

はじめに

ここではJavaScriptの基本情報を備忘録として記載していく。

要素を取得して操作する基本操作

①取得方法

a)
document.getElementById('')←idで取得
例)let ta1 = document.getElementById('ta1');

b)
document.getElementsByTagName('')←html要素で取得
例)let ta1 = document.getElementsByTagName('h1');

c)
document.getElementsByName('')←nameで取得
例)
<div name="ta1">BOX1</div>
↓
let ta1 = document.getElementsByName('ta1');

d)
document.getElementsByClassName('')←クラスで取得
例)
<div class="ta1">BOX1</div>
↓
let ta1 = document.getElementsByClassName('ta1');

要素の追加

①addEventListener

<body>
    <input type="button" value="クリック" id="ta1">
    <ul id=li_add>
        <li>a</li>
        <li>a</li>
    </ul>
    <script src="*****"></script>
</body>

let ta1 = document.getElementById(ta1)
ta1.addEventListener('click', function() {
    let ta2 = document.createElement('li');
    ta2.textContent = 'a';

    let ul = document.getElementById('li_add');
    ul.appendChild(ta2);
});

① まずgetElementByIdで対象のinput要素を取得
② addEventListener でクリックイベントを追加
③ createElement を使って追加したい要素を入れる。今回は li
④ 作成した li をta2という変数に入れているが、この時点ではまだ中身は空
⑤ textContent で 「a」 という文字列を入れる
⑥ ul タグの子要素である li に「ta2」を追加するという意味で、 ul.appendChild(ta2) と記述する

②クリックイベント

<body>
    <input type="button" value="クリック" onclick="createBtn()">
    <ul id=li_add>
        <li>item</li>
        <li>item</li>
    </ul>
    <script src="*****"></script>
</body>

function createBtn() {
    let ta1 = document.createElement('li');
    ta1.textContent = 'a';
    let ul = document.getElementById('li_add');
    ul.appendChild(ta1);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Hooksについてと、便利な独自フックのご紹介

はじめに

この記事では、React Hooksについての簡単なご紹介と、React Hooksを使った便利な独自フックのご紹介をしたいと思います。

React Hooksとは

フック (hook) は React 16.8 で追加された新機能です。
state などの React の機能を、クラスを書かずに使えるようになります。(参考:フックの導入 - React

ボタンをクリックするとカウントが増えていくコードを、フックを使った場合と使っていない場合で比べてみましょう。

■ フックを使わない場合

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

■ フックを使った場合

import React, { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );

このように、シンプルな記述で実現できます。

フックが登場するまでは、クラスコンポーネントで状態管理(state)を行うのが一般的でしたが、フックの登場により関数コンポーネントでも状態を扱いやすくなりました。

フック API リファレンスをみると、Reactでは以下のフックが用意されています。

  • 基本のフック
    • useState
    • useEffect
    • useContext
  • 追加のフック
    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue

基本的なフックの一つである、useStateの使い方をご紹介します。

useStateの使い方

状態(state)の管理にはステートフックを使います。
ステートフックはuseStateを呼び出すことで使用できます。

引数に初期値を指定し、戻り値としてstateとそれを更新するための関数をペアで返します。
例えば、0から始まるカウントをstateとし、ボタンをクリックするごとにstateを更新するような場合、次のように使用します。

// count: stateの現在の値
// setCount: stateを更新するための関数
// 0: 初期値
const [count, setCount] = useState(0);

return (
  <div>
    <label>{count}</label>
    <button onClick={() => setCount(count + 1)}>カウントアップ</button>
  </div>
);

この他にも、useEffectuseContextなど便利なフックはありますが、この記事では割愛させていただきます。

独自フック

自分独自のフックを作成することで、コンポーネントからロジックを抽出して再利用可能な関数を作ることが可能です。

独自で作成したフックの中で、使えそうなフックを紹介したいと思います。

独自で作成したフック

SPA + REST API構成のサービス開発リファレンスで紹介しているコード例(example-chat)をもとにします。
このコード例では、hooks/index.ts に独自フックをまとめて宣言しています。
※ 言語としてTypeScriptを使用しています

入力コンポーネント用独自フック

フォームの作成についてはReactから以下のようにガイドされております。

HTML では <input><textarea>、そして <select> のようなフォーム要素は通常、自身で状態を保持しており、ユーザの入力に基づいてそれを更新します。
React では、変更されうる状態は通常はコンポーネントの state プロパティに保持され、setState() 関数でのみ更新されます。

そのため、テキストボックスなどの入力値については、useStateを使用して保持します。
入力コンポーネントの実装では、input要素に渡す属性や関数等、同様の実装をすることが多くなります。
そこで、ステートフックとステート更新をラッピングした独自フックを作成し、各入力コンポーネントの実装コストを下げることが目的となっています。

useInput

input要素のステートフックとステート更新をラッピングした独自フック。
onChange属性で、値が変わるたびにstateを更新するようになっています。

export const useInput = (initialState: string = '') : [string, React.InputHTMLAttributes<HTMLInputElement>, React.Dispatch<React.SetStateAction<string>>] => {
  const [value, setValue] = useState<string>(initialState);

  const onChange = (event: React.FormEvent<HTMLInputElement>) => {
    setValue(event.currentTarget.value);
  };

  return [
    value,
    {
      value,
      onChange
    },
    setValue
  ];
};

使い方

引数:
- 初期値
戻り値:
- stateの現在の値
- inputに渡すためのプロパティが設定されたオブジェクト
- stateの更新関数

// text: stateの現在の値
// textAttributes: inputに渡すためのプロパティが設定されたオブジェクト
// setText: stateの更新関数
const [text, textAttributes, setText] = useInput('');

return (
  // textAttributesには `value属性`と`onChange属性`が入っている
  // スプレッド構文で展開して属性を一括設定する
  <input type='text' {...textAttributes}/>
);

型定義が異なるだけで、ほとんど同じコードとしてtextarea要素用の 「useTextarea」があります。
詳しくは、ソースコードを参照してください。

useCheckbox

input[type=checkbox]要素のステートフックとステート更新をラッピングした独自フック。
※単一のチェックボックスの場合に使用

export const useCheckbox = (value: string, initialChecked: boolean = false) : [string, React.InputHTMLAttributes<HTMLInputElement>] => {
  const [checked, setChecked] = useState<boolean>(initialChecked);
  const [checkedValue, setCheckedValue] = useState<string>(initialChecked ? value : '');

  const onChange = (event: React.FormEvent<HTMLInputElement>) => {
    setChecked(event.currentTarget.checked);
    if (event.currentTarget.checked) {
      setCheckedValue(value);
    } else {
      setCheckedValue('');
    }
  };

  return [
    checkedValue,
    {
      value,
      checked,
      onChange
    }
  ];
};

使い方

引数:
- チェックボックスのvalue属性
- 初期状態でチェックをつけるかどうか {true/false}
戻り値:
- チェックしている値
- チェックボックス要素の属性が設定されたオブジェクト

// checkedValue: チェックしている値
// checkboxAttributes: チェックボックス要素の属性が設定されたオブジェクト
const [checkedValue, checkboxAttributes] = useCheckbox('check', false);

return (
  // checkboxAttributesには `value属性`と`onChange属性`と`checked属性`が入っている
  // スプレッド構文で展開して属性を一括設定する
  <input type='checkbox' {...checkboxAttributes}/>
);

useCheckboxes

input[type=checkbox]要素のステートフックとステート更新をラッピングした独自フック。
※複数のチェックボックスがある場合に使用

export const useCheckboxes = (choices: string[], initialChecked: string[] = []) : [string[], string[], (value: string) => React.InputHTMLAttributes<HTMLInputElement>] => {
  const [checkedValues, setCheckedValues] = useState<string[]>(initialChecked.filter(v => choices.includes(v)));
  initialChecked.forEach(value => {
    if(!choices.includes(value)){
      Logger.debug('checkbox initialChecked(' + value + ') is not includes choices.');
    }
  });

  const onChange = (event: React.FormEvent<HTMLInputElement>) => {
    const currentTarget = event.currentTarget;
    if (currentTarget.checked) {
      if (!checkedValues.includes(currentTarget.value)) {
        setCheckedValues([...checkedValues, currentTarget.value]);
      }
    } else {
      setCheckedValues(checkedValues.filter(v => v !== currentTarget.value));
    }
  };
  const attributes = (value: string) => {
    const checked = checkedValues.includes(value);
    return {value, onChange, checked};
  };

  return [
    choices,
    checkedValues,
    attributes,
  ];
};

使い方

引数:
- チェックボックスの選択肢
- 初期状態でチェックをつける選択肢
戻り値:
- チェックボックスの選択肢
- チェックしている値
- チェックボックスの属性を返す関数(選択肢の値が引数)

const [choices, checkedValues, attributes] = useCheckboxes(['a', 'b', 'c'], ['a']);

return (
  {choices.map((choice, index) => (
    <label key={index}>
      <input type="checkbox" {...attributes(choice)}/>
      <span>{choice}</span>
    </label>
  ))}
);

useRadio

input[type=radio]要素のステートフックとステート更新をラッピングした独自フック。

export const useRadio = (choices: string[], initialChecked: string = '') : [string[], string, (value: string) => React.InputHTMLAttributes<HTMLInputElement> ] => {
  const [checkedValue, setCheckedValue] = useState<string>(choices.includes(initialChecked) ? initialChecked : '');
  if(initialChecked && !choices.includes(initialChecked)){
    Logger.debug('radio initialChecked(' + initialChecked + ') is not includes choices.');
  }

  const onChange = (event: React.FormEvent<HTMLInputElement>) => {
    setCheckedValue(event.currentTarget.value);
  };
  // ランダムなname属性を生成する
  const [name] = useState(() => 'radio_' + new Date().getTime().toString(16) + Math.floor(10000 * Math.random()).toString(16));
  const attributes = (value: string) => {
    const checked = value === checkedValue;
    return {name, value, onChange, checked};
  };

  return [
    choices,
    checkedValue,
    attributes,
  ];
};

使い方

引数:
- ラジオボタンの選択肢の値
- 初期状態でチェックをつける値
戻り値:
- ラジオボタンの選択肢
- チェックしている値
- ラジオボタンの属性を返す関数(選択肢の値が引数)

const [choices, checkedValue, attributes] = useRadio(['a', 'b'], 'a');

return (
  {choices.map((choice, index) => (
    <label key={index}>
      <input type="radio" {...attributes(choice)}/>
      <span>{choice}</span>
    </label>
  ))}
);

useSelect

select要素のステートフックとステート更新をラッピングした独自フック。

export const useSelect = (initialState: string = '') : [string, React.SelectHTMLAttributes<HTMLSelectElement> ] => {
  const [value, setValue] = useState<string>(initialState);

  const onChange = (event: React.FormEvent<HTMLSelectElement>) => {
    setValue(event.currentTarget.value);
  };

  return [
    value,
    {
      value,
      onChange
    }
  ];
};

使い方

引数:
- 初期値
戻り値:
- state
- selectに渡すためのプロパティが設定されたオブジェクト

const [select, selectAttributes] = useSelect('');

return (
  <select name="hoge" {...selectAttributes}>
    <option value=''/>
    <option value='1'>1</option>
    <option value='2'>2</option>
    <option value='3'>3</option>
  </select>
);

useSelectMultiple

select(multiple)要素のステートフックとステート更新をラッピングした独自フック。
※複数選択可能なselect

export const useSelectMultiple = (initialState: string[] = []) : [string[], React.SelectHTMLAttributes<HTMLSelectElement> ] => {
  const [value, setValue] = useState<string[]>(initialState);

  const onChange = (event: React.FormEvent<HTMLSelectElement>) => {
    const options = event.currentTarget.options;

    const selectedValues = [];
    for (let i = 0; i < options.length; i++) {
      if (options[i].selected) {
        selectedValues.push(options[i].value);
      }
    }
    setValue(selectedValues);
  };

  return [
    value,
    {
      value,
      onChange,
      'multiple': true
    }
  ];
};

使い方

引数:
- 初期値
戻り値:
- state
- select(multiple)に渡すためのプロパティが設定されたオブジェクト

const [select, selectAttributes] = useSelectMultiple([]);

return (
  <select name="hoge" {...selectAttributes}>
    <option value=''/>
    <option value='1'>1</option>
    <option value='2'>2</option>
    <option value='3'>3</option>
  </select>
);

その他のフック

usePageTitle

SPAではページごとのtitle要素が変わらないため、そのtitle要素を設定するフック。

export function usePageTitle(title?: string): void {
  useEffect(() => {
    if (title) {
      const previousTitle = document.title;
      document.title = title;
      return () => {
        document.title = previousTitle;
      };
    }
  }, [title]);
}

使い方

title要素を変更したい画面で呼び出してください。

usePageTitle('ページタイトル');

useDownloader

次のような手順でファイルのダウンロードを行う用のフック。

  1. レスポンスボディをBlobオブジェクトへ変換する
  2. URL.createObjectURL(blob)でURLを生成する
  3. a要素を動的に生成しhref属性に生成したURL、download属性にファイル名を設定する
  4. JavaScriptでa要素のclick()を実行する
  5. 生成したa要素とURLを破棄する(メモリリークの回避)
export function useDownloader(): (blob: Blob, filename: string) => void {
  const download = (blob: Blob, filename: string) => {
    const url = URL.createObjectURL(blob);
    const anchor = document.createElement('a');
    anchor.href = url;
    anchor.download = filename;
    document.body.appendChild(anchor);
    anchor.click();
    URL.revokeObjectURL(url);
    document.body.removeChild(anchor);
  };
  return download;
}

使い方

Fetch APIでファイルデータを取得し、ResponseのblobメソッドでBlobオブジェクトを得ます。
そのBlobオブジェクトとファイル名をuseDownloaderから返却された関数に渡してダウンロードを行います。

const download = useDownloader();
const blob = (await fetch('/api/download')).blob();
const filename = 'file-name.csv';
download(blob, filename);

まとめ

以上、React Hooksのご紹介と、独自で作成したフックの紹介でした。
使えそうなフックがありましたら、是非使ってみてください。

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

【React】react-scrollでスクロールボタンを実装

この記事はReact Advent Calendar 2020 19日目の記事です。

はじめに

react-scrollを使って、簡単にスクロールボタンを実装していきます。
実装自体はとても簡単なので、是非参考にどうぞ〜。

目標物

Image from Gyazo

react-scrollをインストール

まずは、react-scrollをインストール

npm install react-scroll

実装部分

  1. react-scrollをimport
  2. scrollToTopを実装(ページのトップまでスクロールしてくれる機能)
  3. ボタンのonClickイベントで呼び出す(ボタンが押下されたときにscrollToTopを呼ぶ)
ScrollButton.jsx
import React from 'react';
import './ScrollButton.css';
import { animateScroll as scroll } from 'react-scroll';  //import

class ScrollButton extends React.Component {

  // scrollToTopの実装
  scrollToTop = () => {
    scroll.scrollToTop();
  };

  render() {
    return(
      <button onClick={this.scrollToTop}>Click</button>
    );
  }
}
export default ScrollButton;

スタイルを付ける

ScrollButton.css
 body {
   margin: 0 auto;
   padding: 0;
   max-width: 800px;
   min-height: 100vh;
   text-align: center;
   margin-bottom: 15%;
 }
 .section {
   margin-bottom: 15%;
 }
 button {
  display: inline-block;
  font-size: 32px;
  width: 200px;
  height: 48px;
  border-radius: 4px;
  margin-right: 24px;
  margin-left: 24px;
  color: #fff;
  background-color: #66ccff;
  border: none;
  outline: none;
  box-shadow: 4px 4px #d8d8d8;
  cursor: pointer;
  appearance: none;
  transition: .5s;
 }
 button:active {
  position: relative;
  top: 4px;
  left: 4px;
  box-shadow: none;
 }

全体

最終的な成果物になります。
コピペで直ぐに動きますので、是非ご自身のローカル環境で動かしてみてください。

ScrollButton.jsx
import React from 'react';
import './ScrollButton.css';
import { animateScroll as scroll } from 'react-scroll';

class ScrollButton extends React.Component {

  // scrollToTopの実装
  scrollToTop = () => {
    scroll.scrollToTop();
  };

  render() {
    return(
      <React.Fragment>
        <div className="text">
          <h1>Hello React</h1>
          <div className="section">
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsum doloribus laborum hic dicta odit, ullam quo aperiam tempora, culpa, ipsam consectetur eligendi delectus blanditiis perferendis dolor voluptatum unde molestias numquam!
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aspernatur architecto vero perferendis vel quos incidunt aut numquam recusandae assumenda harum hic iure ipsum consequuntur quasi explicabo, aperiam ea quas eligendi.
            Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ratione ipsam corrupti culpa repellendus incidunt facere amet minus reprehenderit optio sint? Fuga ad at magnam odit temporibus dolore quod, dignissimos quisquam.
          </div>

          <h1>【Section1】What's React</h1>
          <div className="section">
          Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsum doloribus laborum hic dicta odit, ullam quo aperiam tempora, culpa, ipsam consectetur eligendi delectus blanditiis perferendis dolor voluptatum unde molestias numquam!
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aspernatur architecto vero perferendis vel quos incidunt aut numquam recusandae assumenda harum hic iure ipsum consequuntur quasi explicabo, aperiam ea quas eligendi.
            Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ratione ipsam corrupti culpa repellendus incidunt facere amet minus reprehenderit optio sint? Fuga ad at magnam odit temporibus dolore quod, dignissimos quisquam.
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsum doloribus laborum hic dicta odit, ullam quo aperiam tempora, culpa, ipsam consectetur eligendi delectus blanditiis perferendis dolor voluptatum unde molestias numquam!
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aspernatur architecto vero perferendis vel quos incidunt aut numquam recusandae assumenda harum hic iure ipsum consequuntur quasi explicabo, aperiam ea quas eligendi.
            Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ratione ipsam corrupti culpa repellendus incidunt facere amet minus reprehenderit optio sint? Fuga ad at magnam odit temporibus dolore quod, dignissimos quisquam.
          </div>

          <h1>【Section2】react is a JavaScript library created by facebook</h1>
          <div className="section">
          Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsum doloribus laborum hic dicta odit, ullam quo aperiam tempora, culpa, ipsam consectetur eligendi delectus blanditiis perferendis dolor voluptatum unde molestias numquam!
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aspernatur architecto vero perferendis vel quos incidunt aut numquam recusandae assumenda harum hic iure ipsum consequuntur quasi explicabo, aperiam ea quas eligendi.
            Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ratione ipsam corrupti culpa repellendus incidunt facere amet minus reprehenderit optio sint? Fuga ad at magnam odit temporibus dolore quod, dignissimos quisquam.
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsum doloribus laborum hic dicta odit, ullam quo aperiam tempora, culpa, ipsam consectetur eligendi delectus blanditiis perferendis dolor voluptatum unde molestias numquam!
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aspernatur architecto vero perferendis vel quos incidunt aut numquam recusandae assumenda harum hic iure ipsum consequuntur quasi explicabo, aperiam ea quas eligendi.
            Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ratione ipsam corrupti culpa repellendus incidunt facere amet minus reprehenderit optio sint? Fuga ad at magnam odit temporibus dolore quod, dignissimos quisquam.
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsum doloribus laborum hic dicta odit, ullam quo aperiam tempora, culpa, ipsam consectetur eligendi delectus blanditiis perferendis dolor voluptatum unde molestias numquam!
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aspernatur architecto vero perferendis vel quos incidunt aut numquam recusandae assumenda harum hic iure ipsum consequuntur quasi explicabo, aperiam ea quas eligendi.
            Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ratione ipsam corrupti culpa repellendus incidunt facere amet minus reprehenderit optio sint? Fuga ad at magnam odit temporibus dolore quod, dignissimos quisquam.
          </div>

          <h1>【Section3】Build complex UI with react</h1>
          <div className="section">
          Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsum doloribus laborum hic dicta odit, ullam quo aperiam tempora, culpa, ipsam consectetur eligendi delectus blanditiis perferendis dolor voluptatum unde molestias numquam!
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aspernatur architecto vero perferendis vel quos incidunt aut numquam recusandae assumenda harum hic iure ipsum consequuntur quasi explicabo, aperiam ea quas eligendi.
            Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ratione ipsam corrupti culpa repellendus incidunt facere amet minus reprehenderit optio sint? Fuga ad at magnam odit temporibus dolore quod, dignissimos quisquam.
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsum doloribus laborum hic dicta odit, ullam quo aperiam tempora, culpa, ipsam consectetur eligendi delectus blanditiis perferendis dolor voluptatum unde molestias numquam!
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aspernatur architecto vero perferendis vel quos incidunt aut numquam recusandae assumenda harum hic iure ipsum consequuntur quasi explicabo, aperiam ea quas eligendi.
            Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ratione ipsam corrupti culpa repellendus incidunt facere amet minus reprehenderit optio sint? Fuga ad at magnam odit temporibus dolore quod, dignissimos quisquam.
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsum doloribus laborum hic dicta odit, ullam quo aperiam tempora, culpa, ipsam consectetur eligendi delectus blanditiis perferendis dolor voluptatum unde molestias numquam!
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aspernatur architecto vero perferendis vel quos incidunt aut numquam recusandae assumenda harum hic iure ipsum consequuntur quasi explicabo, aperiam ea quas eligendi.
            Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ratione ipsam corrupti culpa repellendus incidunt facere amet minus reprehenderit optio sint? Fuga ad at magnam odit temporibus dolore quod, dignissimos quisquam.
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsum doloribus laborum hic dicta odit, ullam quo aperiam tempora, culpa, ipsam consectetur eligendi delectus blanditiis perferendis dolor voluptatum unde molestias numquam!
            Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aspernatur architecto vero perferendis vel quos incidunt aut numquam recusandae assumenda harum hic iure ipsum consequuntur quasi explicabo, aperiam ea quas eligendi.
            Lorem ipsum dolor, sit amet consectetur adipisicing elit. Ratione ipsam corrupti culpa repellendus incidunt facere amet minus reprehenderit optio sint? Fuga ad at magnam odit temporibus dolore quod, dignissimos quisquam.
          </div>
        </div>
            <button onClick={this.scrollToTop}>Click</button>
      </React.Fragment>
    );
  }
}
export default ScrollButton;
ScrollButton.css
 body {
   margin: 0 auto;
   padding: 0;
   max-width: 800px;
   min-height: 100vh;
   text-align: center;
   margin-bottom: 15%;
 }
 .section {
   margin-bottom: 15%;
 }
 button {
  display: inline-block;
  font-size: 32px;
  width: 200px;
  height: 48px;
  border-radius: 4px;
  margin-right: 24px;
  margin-left: 24px;
  color: #fff;
  background-color: #66ccff;
  border: none;
  outline: none;
  box-shadow: 4px 4px #d8d8d8;
  cursor: pointer;
  appearance: none;
  transition: .5s;
 }
 button:active {
  position: relative;
  top: 4px;
  left: 4px;
  box-shadow: none;
 }

終わり。

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

Next.jsの静的ジェネレーションでデータをfetchできなかった話

はじめに

Next.jsの静的ジェネレーション getStaticProps内でAPIからデータをfetchしようとしたところエラーが出て調べたけど記事として上がってなさそうだったので自分が解決した方法を載せます。
同じ問題に遭遇した人がこの記事を見て解決に役立てればと思います。

事象

ローカル開発環境で、APIのサーバとして json-serverを使いました。
json-serverを立ち上げたときのログです。

log
backend_1   | 
backend_1   |   \{^_^}/ hi!
backend_1   | 
backend_1   |   Loading db.json
backend_1   |   Done
backend_1   | 
backend_1   |   Resources
backend_1   |   http://0.0.0.0:80/users
backend_1   | 
backend_1   |   Home
backend_1   |   http://0.0.0.0:80
backend_1   | 
backend_1   |   Type s + enter at any time to create a snapshot of the database
backend_1   |   Watching...
backend_1   | 
backend_1   | GET /users 200 48.743 ms - 355
backend_1   | GET /users 304 6.090 ms - -

http://0.0.0.0/users にアクセスするとちゃんと画面が表示されます。

画面の表示内容
[
  {
    "name": "aaaaa",
    "password": "password",
    "id": 1
  },
  {
    "name": "aaaaa",
    "password": "password",
    "id": 2
  },
  {
    "name": "daodao",
    "password": "password",
    "id": 3
  },
  {
    "name": "daodao",
    "password": "password",
    "id": 4
  },
  {
    "name": "daodao",
    "password": "password",
    "id": 5
  }
]

Next.jsのソースはgetStaticProps内でfetchしています。

export async function getStaticProps() {
  const res = await axios.get('http://0.0.0.0/users')
  console.log(res.data)
  const users = res.data

  return {
    props: {
      users
    }
  }
}

実行するとconnect ECONNREFUSEDというエラーが表示されました。
スクリーンショット 2020-12-18 22.17.10.png
調べてみると、フロントがサーバとの接続を切断したという意味です。
つまり、リクエストが中断されたということです。

仮説

原因を調べたのですが、良い解決策が見つかりませんでした。
なので、自分で仮説を立てて検証することにしました。

getStaticPropsはビルド時に必要なデータをfetchして事前にHTMLを生成し、ユーザのリクエストがあった時に返します。
ローカル開発環境で立ち上げたサーバは自分のコンピュータのみでしかアクセスできないので、Next.jsをビルドするときにグローバルにアクセスできるサーバでないとデータを取得できないのでこのようなエラーが発生するのかなと仮定しました。

検証

仮説を検証するために、AWSのEC2インスタンスを立ててその中にjson-serverを配置しました。
その際、json-serverで使用するポートを0.0.0.0/0でどこからでもアクセスできるようにしました。

export async function getStaticProps() {
  const res = await axios.get('http://52.196.252.109/users')
  console.log(res.data)
  const users = res.data

  return {
    props: {
      users
    }
  }
}

ブラウザで確認すると、console.logで仕込んだログにちゃんとユーザデータが表示されていました。
また、コンポーネントにusersのpropsが渡ってきているのを確認しました。

まとめ

Next.jsの静的ジェネレーションはサーバがグローバルな環境に置かれていないと、正常にビルドできないと判断しconnect ECONNREFUSEDというエラーを吐き出すようです。
なので、検証するときはサーバをlocalhostではなく、グローバルにアクセスできる環境に置かないと使えないようです。
静的ジェネレーションの仕組みを理解していれば、当たり前といえば当たり前だったかもです。

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

Google検索結果をURLでフィルタするツール作った

Google検索結果をURLでフィルタするユーティリティツールをJavaScriptで実装しました。

Github: yuis-ice/Google-Search-URL-Filter: Google Search URL Filter

https://yuis.xsrv.jp/images/ss/ShareX_ScreenShot_3966d810-f30f-48d1-aaf2-fec5f2b132b8.png

tampermonkeyユーザースクリプトとして使う場合以下からダウンロードしてください。

Google Search URL Filter

ソースコードです。

(async function(){

  console.log("load, ");
  await import("https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js")

    $(document).ready(function(){
        console.log("load,,");

        (async function(){
            console.time('Google Search URL Filter');
            const sleep = m => new Promise(r => setTimeout(r, m))

            elem_searchbar = await document.querySelector("#tsf > div:nth-child(2) > div > div:nth-child(2)") ;
            clone = await elem_searchbar.cloneNode(true) ; // maybe need await for cloneNode
            clone02 = await elem_searchbar.cloneNode(true) ;
            elem_resultarea = await document.querySelector("div#res") ;
            await elem_resultarea.before(clone) ;
            await elem_resultarea.before(clone02) ;
            clone.querySelector("input").value = "" ;
            clone02.querySelector("input").value = "" ;
            await clone.querySelector('.clear-button').remove() ;
            await clone02.querySelector('.clear-button').remove() ;
            await clone.querySelector('div[aria-label="Search by voice"]').remove() ;
            await clone02.querySelector('div[aria-label="Search by voice"]').remove() ;
            await clone.querySelector('button[aria-label="Google Search"]').remove() ;
            await clone02.querySelector('button[aria-label="Google Search"]').remove() ;
            clone.querySelector("input").placeholder = "Type URL match text here" ;
            clone02.querySelector("input").placeholder = "Tags here e.g. not; tld; " ;

            [clone, clone02].forEach(item => {
                item.querySelector("input").addEventListener('input', async function(e){
                    filter = clone.querySelector("input").value ;
                    tag = clone02.querySelector("input").value ;
                    console.log( this.value, filter, tag, new RegExp(filter,"g")) ;

                    if ( tag == "not" ){
                        console.log("not") ;
                        [].slice.call( document.querySelectorAll("div#search div.g > div") )
                        .forEach(a => a.hidden = false ) ;
                        [].slice.call( document.querySelectorAll("div#search div.g > div") )
                        .filter(a => ! ( a.querySelector("div#search div div.yuRUbf > a") && ! a.querySelector("div#search div div.yuRUbf > a").href.match(new RegExp(filter,"g")) ) )
                        .forEach(a => console.log( a.hidden = true ) ) ;
                    } else if ( tag == "tld" ){
                        console.log("tld") ;
                        [].slice.call( document.querySelectorAll("div#search div.g > div") )
                        .forEach(a => a.hidden = false ) ;
                        [].slice.call( document.querySelectorAll("div#search div.g > div") )
                        .filter(a => ! ( a.querySelector("div#search div div.yuRUbf > a") && ! a.querySelector("div#search div div.yuRUbf > a").href.match(/https?:\/\/(.*?)(\/)?$/)[1].includes("/") ) )
                        .forEach(a => console.log( a.hidden = true ) ) ;
                    } else {
                        [].slice.call( document.querySelectorAll("div#search div.g > div") )
                        .forEach(a => a.hidden = false ) ;
                        [].slice.call( document.querySelectorAll("div#search div.g > div") )
                        .filter(a => ! ( a.querySelector("div#search div div.yuRUbf > a") && a.querySelector("div#search div div.yuRUbf > a").href.match(new RegExp(filter,"g")) ) )
                        .forEach(a => console.log( a.hidden = true ) ) ;
                    }
                });
            })

            console.timeEnd('Google Search URL Filter');
        })();

    });

})();

複数のエレメントへのイベントリスナー定義が[clone, clone02].forEach()みたいにできるというのは興味深いと思いました。これは今後も積極的に使っていきたい。

少し解決に時間を取られたのは、clone = await elem_searchbar.cloneNode(true) ;の部分です。cloneNodeは時間がかかるのは、awaitが必要みたいで、awaitつけないでやると[clone, clone02].forEach()らへんでclone02 undefinedになります。

こういうのはclonenodeよりReactとかで実装したほうがいいのかなと思ったりしました。プログラミングしないから完全に取り残されている。

Redditにポストしたら反応が良かったので久々にQiitaです。なんか規約とか変わってて変なことしてたら教えて下さい。

Filter Google Search result by URLs with regular expression : javascript

https://yuis.xsrv.jp/images/ss/ShareX_ScreenShot_35338a76-3373-4891-b966-8f1647d27e94.png

コードでわからないところがあればコメントしてください。そのために僕がいます。コーディングアドバイスもあればぜひお願いします。

Githubスター/フォークもぜひ。

Github: yuis-ice/Google-Search-URL-Filter: Google Search URL Filter

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

【メモ】HTMLとJSでジャンケンする

はじめに

知り合いからプログラミングの課題の協力をして欲しいと言われたので自分なりに書いたコードの復習と説明の代わりになればと思い書きます。

用件①画面リロードするとランダムで自分の手とPCの手を決定し、どちらが勝ったのかを表示する

用件②ユーザーに自分の手を選ばせ、PCの手を決定し、どちらが勝ったのかを表示する

用件①

まず簡単にindex.htmlを用意します。

index.html
<html>
  <body>
    <img id="you" src="" width="10%" />
    <p id="you_text"></p>
    <img id="pc" src="" width="10%" />
    <p id="pc_text"></p>
    <p id="result"></p>
  </body>
</html>

▼ 表示してみる

markdown-mark.png

もちろんなにも表示されませんね。ここにscriptを書いていきます。

index.html
<html>
  <body>
    <img id="you_img" src="" width="10%" />
    <p id="you_text"></p>
    <img id="pc_img" src="" width="10%" />
    <p id="pc_text"></p>

    <p id="result"></p>

    <script>
      //ぐー
      const rock = {
        img:
          "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/8b692708-7744-36b8-ed83-1ee22f4ce7f3.jpeg",
        text: "ぐー",
      };

      //ちょき
      const scissors = {
        img:
          "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/698c6320-ef35-48c7-1f3e-3166ddbdff8a.jpeg",
        text: "ちょき",
      };

      //ぱー
      const paper = {
        img:
          "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/675e2d7d-f599-d21d-e39b-417748fc0fb7.jpeg",
        text: "ぱー",
      };

      //それぞれの手を一個の配列にする
      const Item = [rock, scissors, paper];

      //それぞれのidと連携する
      const youImg = document.getElementById("you_img");
      const youText = document.getElementById("you_text");
      const pcImg = document.getElementById("pc_img");
      const pcText = document.getElementById("pc_text");
      const result = document.getElementById("result");

      //画面がリロードされるとRockPaperScissors関数を叩く
      window.onload = RockPaperScissors;

      // ジャンケンをする関数
      function RockPaperScissors() {
        // youの出すものを決める
        const youChose = Item[Math.floor(Math.random() * Item.length)];

        //手の画像とテキストを設定する
        youImg.src = youChose.img;
        youText.textContent = "あなたが出したのは" + youChose.text + "です";

        //pcの出すもの決める
        const pcChose = Item[Math.floor(Math.random() * Item.length)];

        //手の画像とテキストを設定する
        pcImg.src = pcChose.img;
        pcText.textContent = "パソコンが出したのは" + pcChose.text + "です。";

        let judge = "";

        //ジャンケンは全てで9通り
        //そのうち引き分けになるのは3通り
        if (youChose.text === pcChose.text) {
          judge = "あいこです";
        }

        //勝つ3通りを出す
          //ぐーで勝つ
        else if (youChose.text === "ぐー" && pcChose.text === "ちょき") {
          judge = "あなたの勝ちです";
        } else if (youChose.text === "ちょき" && pcChose.text === "ぱー") {
          //ちょきで勝つ
          judge = "あなたの勝ちです";
        } else if (youChose.text === "ぱー" && pcChose.text === "ぐー") {
          //ぱーで勝つ
          judge = "あなたの勝ちです";
        } else {
          //あいこでも勝ちでもない場合
          judge = "あなたのまけです";
        }

        //結果を反映する
        result.textContent = judge;
      }
    </script>
  </body>
</html>

▼ 表示結果

markdown-mark.png

See the Pen zYKwRjv by ようかん / Yosuke Inoue (@inoue2002) on CodePen.

無事用件通り表示出来ましたね。何度かリロードして、それぞれの出す手や判定が変わることを確認してみましょう。

用件②

先ほどと大まかには似ている実装になりますが、異なるところはyouが出す部分を実際に決められるようにするという点です。
画面がリロードされたら、まずユーザに選択させるダイアログを表示させます。
ダイアログにはwindow.promptを使います。

index.html
<html>
  <head> </head>

  <body>
    <img id="you_img" src="" width="10%" />
    <p id="you_text"></p>
    <img id="pc_img" src="" width="10%" />
    <p id="pc_text"></p>

    <p id="result"></p>

    <script>
      const rock = {
        img:
          "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/8b692708-7744-36b8-ed83-1ee22f4ce7f3.jpeg",
        text: "ぐー",
      };

      const scissors = {
        img:
          "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/698c6320-ef35-48c7-1f3e-3166ddbdff8a.jpeg",
        text: "ちょき",
      };

      const paper = {
        img:
          "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/675e2d7d-f599-d21d-e39b-417748fc0fb7.jpeg",
        text: "ぱー",
      };

      const Item = [rock, scissors, paper];

      const youImg = document.getElementById("you_img");
      const youText = document.getElementById("you_text");
      const pcImg = document.getElementById("pc_img");
      const pcText = document.getElementById("pc_text");
      const result = document.getElementById("result");

      //画面がリロードされたら
      window.onload = promptYou;

      function promptYou() {
        // 入力ダイアログを表示 + 入力内容を user に代入
        user = window.prompt(
          "あなたの出す手を選んでください。 ぐー:1,ちょき:2,パー:3",
          ""
        );

        let youChose = "";
        //youchoseがぐー
        if (user == "1") {
          youChose = Item[0];
        }
        //youchoseがちょき
        else if (user == "2") {
          youChose = Item[1];
        }
        //youchoseがぱー
        else if (user == "3") {
          youChose = Item[2];
        }

        youImg.src = youChose.img;
        youText.textContent = "あなたが出したのは" + youChose.text + "です";

        //pcの手を決める
        RockPaperScissors(youChose.text);
      }

      function RockPaperScissors(youChoseText) {
        //pcの出すもの決める
        const pcChose = Item[Math.floor(Math.random() * Item.length)];
        pcImg.src = pcChose.img;
        pcText.textContent = "パソコンが出したのは" + pcChose.text + "です。";

        let judge = "";

        //ジャンケンは全てで9通り
        //そのうち引き分けになるのは3通り
        if (youChoseText === pcChose.text) {
          judge = "あいこです";
        }

        //勝つ3通りを出す
        //ぐーで勝つ
        else if (youChoseText === "ぐー" && pcChose.text === "ちょき") {
          judge = "あなたの勝ちです";
        } else if (youChoseText === "ちょき" && pcChose.text === "ぱー") {
          //ちょきで勝つ
          judge = "あなたの勝ちです";
        } else if (youChoseText === "ぱー" && pcChose.text === "ぐー") {
          //ぱーで勝つ
          judge = "あなたの勝ちです";
        } else {
          //あいこでも勝ちでもない場合
          judge = "あなたのまけです";
        }
        //結果を反映する
        result.textContent = judge;
      }
    </script>
  </body>
</html>

markdown-mark.png

無事自分の手は選んで、PCもランダムに手を出して結果を表示するところまで完成しました。

See the Pen qBamygy by ようかん / Yosuke Inoue (@inoue2002) on CodePen.

最後に

時間もなかったのでさくっと作りましたが、もっと可愛いモーションとか
リトライボランとか追加して盛り上げてみたいですね。何かの参考になれば幸いです。
CodePenの方で実際に試すこともできるので是非試してみてください。

▼今回使った手の写真


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

TypeScriptにModuleから入門してみた話[TS×Express]

はじめに

普段サーバーサイドはJacaScript×Expressで書いている私ですが、
ロジックが複雑になってくると、さすがに型なしでは辛いなーと感じてきました。
また、補完などの面からもTypeScriptを導入してみたいとは常々思っていました。

ただ、Expressを使ったアプリを全部TSに移行するとなると、
express-tsとか諸々を入れることになって、
正直めんどくさいし、トラブりそうだなーと思ってました。

そこで、複雑な場所だけをModuleとして切り出して、
そこだけをTSで書いて、それ以外の部分はJSで書いていけば、
気軽にTSを導入しつつ、トラブルも減らせそうだなーと考えました。

それで、部分的にTSを導入する方法を調べたら
意外と簡単に出来そうな雰囲気だったので、
実際にTSを使ってHelloWorldをしてみました!

まずはいつも通りJSで作成

最初にターミナルで諸々を作成

Terminal
# プロジェクトフォルダを作成
$ mkdir express-ts
$ cd express-ts

# プロジェクトの作成
$ yarn init
> (いろいろ入力)

# Expressをローカルインストール
$ yarn add express

# 必要なファイルを作成
$ touch app.js
$ touch utils/utils-js.js

次にapp.jsにHelloWorldを記述

app.js
const express = require('express')
const app = express()
const port = 8000

// 自作モジュールの読み込み
const utilsJs = require('./utils-js')

app.get('/', (req, res) => {
  res.send(utilsJs.joinText('HelloWorld!', 'From Module!'))
})

app.listen(port, () => {
  console.log("App listening at http://localhost:" + port)
})

あと、使用するモジュールも記述

utils-js.js
// 文字を足し算する関数
const joinText = (text1, text2) => text1 + " " + text2

// 関数をモジュールとして出力
module.exports = { joinText }

最後に、起動用のコマンドをpackage.jsonに追記して

package.json
{
  "scripts": {
    "start": "node app.js"
  }
}

これでyarn startでアプリを起動し、
localhost:8000にアクセスすると、HelloWorld FromModule! と問題なく表示されました。

TSを導入

いよいよTSを導入してみます。
どうもtypescriptとts-nodeを入れればいいみたいです。

Terminal
# 必要なものをインストール
$ yarn add -D typescript ts-node

# TSの設定ファイルを作成
$ touch tsconfig.json

# TSでのモジュールを記述するファイルを作成
$ touch utils-ts.ts

そして、いい感じにTSの設定を記述

tsconfig.json
{
  "compilerOptions": {
      "target": "es2018",
      "module": "commonjs",

      // jsファイルもコンパイルが通るように設定
      "allowJs": true,

      // ストリックモードは有効に
      "strict": true,  

      // 入出力先を設定
      "rootDir": ".",
      "outDir": "./dist",

      // デフォルトで書かれているもの
      "esModuleInterop": true,
      "skipLibCheck": true,
      "forceConsistentCasingInFileNames": true
  },
  "include": [
    "app.js"
  ],
}

次に、さっそくTSでモジュールを記述

utils-ts.ts
// 文字を足し算する関数
const joinText = (text1: string, text2: string): string => {
  return text1 + " " + text2
}

// 関数をモジュールとして出力
export { joinText }

そして、TSでのモジュールを組み込むためapp.jsを追加・修正

app.js
// 自作モジュールの読み込み
const utilsJs = require('./utilsJs')
const utilsTs = require('./utilsTs') //ここ追記

app.get('/', (req, res) => {
  res.send(utilsTs.joinText('HelloWorld!', 'From Module!')) //ここ修正
})

最後にpackage.jsonにある起動用コマンドを修正して

package.json
{
  "scripts": {
    "start": "ts-node app.js",
    "build": "tsc"
  }
}

これでyarn startしてみると、内部でTSをコンパイルしつつアプリを起動できました。
いつも使用しているnode app.jsと同じように使えていい感じ!

またyarn buildすると、/dist にJSにコンパイルされたファイルが出力できる模様。
実際に稼働させるときは、このファイルをデプロイすればいいみたい。

おわりに

TypeScriptって敷居が高いイメージだったけど、思ってたより簡単に導入できました。
(特にトラブルもなく、逆にあっけなかった気もします・・w

アプリを全部TSにするのは大変でも、部分的なモジュールから導入して、
TSの恩恵(安全性や補完など)を受けれるのはとてもいい感じ。
しかし、安全性という意味では全部TSのアプリにはとてもかなわないというのも事実。

やっぱり、全部TSでアプリを作るってよりは敷居は圧倒的に低いので、
全部TSではなく、メインはJSで部分的にTSってのも選択肢に入る印象!

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

【ReactNative】実機ビルド、APK生成で起きたトラブルシューティング~Android編~

ReactNativeを頑張っているみなさんこんにちは。ブリューアスのwebフロントエンジニアのsuginokoです。
もう年末ですね。1年というのはあっという間です。

弊社ではじわじわとReactNativeの案件にも関わるようになってきておりワタシも初めての経験でアタフタしましたが、なんとか今年のゴールにたどり着けそうです。

今回は初めてのReactNativeということで沢山のトラブルに出くわしたので、その対応トラブルシューティングを書いていこうと思います。アプリ開発に携わったことのない同じようなフロントエンジニアさんに届きますように~

前置き

今回1からプロジェクトを立ち上げたわけではなく、既に実装済みのReactNativeのプロジェクトを頂きました。
実装内容としましても少し古い書き方になっている箇所も多々ありましたので、そちらをリファクタを行ったり、見た目部分を修正するなど、実装自体はそこまで重くなかった対応になります。
辛かったのは最初(環境構築)と最後(実機ビルド、APK生成)です。

環境

  • react: 16.9.0
  • react-native: 0.61.5
  • react-native-agora: 2.9.1-alpha.2
  • react-native-firebase: 5.6.0

※パッケージはおおまかに使用したものだけ記載
基本的にはwindowsで実装してますが、expoは使用しておらず、ios開発は別途Macを用意して確認しつつの実装を行ってました。
yarnも使ってます。

お客様の環境が開発用と本番用としか分かれてなかったため、buildTypeとしては

  • Debug(開発環境)
  • Staging(開発環境を参照しているデプロイゲート更新用)
  • Release(本番)

このように分けました。
今回の納期までのスケジュールではもろもろのパッケージのバージョンアップは行わない方針でいったので後に書きますトラブルに見舞われた可能性があります。。

トラブルシューティング

※エラーを無くすだけなので、その設定はちょっと・・・というのがあるかもしれませんがご了承ください。

error E:\my-gridsome-site\node_modules\sharp: Command failed.Command: (node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)...

1から実装したものでは無くて、他のプロジェクトを取り込む際に起こるエラーなようです。
nodeのバージョンを適切なバージョンに上げて解決。(多分このエラーの下の方に適切なnodeバージョンが書いてあった気がする)
Macでは
sharp: Command failed · Issue #585 · gridsome/gridsome · GitHub
上記だと解消するらしいです。

nodistを入れていたらnpxが使えなくなった話

Windows限定な話な気がしますが
Nodist を入れたら npx が使えなくなったので手動でインストール / Twin Turbo Computing
こちらで解決。

Task :app:processDebugGoogleServices FAILED

cd `{project_name}`
cd android && ./gradlew clean

たまに、上記の対応で直ることがありますが、
firebaseのgoogle-services.jsonが原因の可能性もあります。
環境にdebugとrelease以外の環境が存在するとこのエラーに巡り合えます。
applicationIdSuffix を指定しているかにもよりますが、指定していない場合は

{projectName}/android/app/google-services.json からコピーして
{projectName}/android/app/src/debug/google-services.json を作成します。
パッケージ名はandroid/app/src/main/AndroidManifest.xmlに書いているpackageNameを見ます。
applicationIdSuffixを設定していなければmainに記載している名称と同じで問題ありませんが、設定している場合は
{packageName}.{applicationIdSuffix} になります。これでエラーが無くなります。
applicationIdSuffixを指定している場合はStaging環境にもgoogle-services.jsonが必要だったと思います。

{
  "project_info": {
    "project_number": "...",
    "firebase_url": "...",
    "project_id": "...",
    "storage_bucket": "..."
  },
  "client": [
    {
      "client_info": {
        "mobilesdk_app_id": "...",
        "android_client_info": {
          "package_name": "←ここを環境ごとに合わせる"
        }
      },
・・・

参照:https://noy.hatenablog.jp/entry/2018/02/15/121431

No matching client found for package name

Task :app:processDebugGoogleServices FAILED と同様の対応でなくなりました。
google-services.jsonが適切な場所にないのと、正しいpackage nameになってないことが問題でした。

What went wrong: Execution failed for task ':app:mergeReleaseResources'.

重複エラーだそうです。今思えば
{project_name}/android/app/src/main/assetsにindex.android.bundleが存在しなかったことが原因でしたが以下の方法で解決。
参照:reactjs - React Native 0.57.1 Android Duplicate Resources - Stack Overflow

  1. {project_name}/android/app/build 全部削除
  2. {project_name}/android/build 全部削除
  3. 実行 rm -rf $HOME/.gradle/caches/
  4. {project_name}/android/app/src/main/assetsのindex.android.bundle を削除(この時点で自分のプロジェクトには存在しなかったのでパス)
  5. 以下を実行
react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res

この時結構不要なファイルも出たから削除した気がする・・・・
jsonファイルやnode_modulesのもろもろも色々出てきてビルドには不要だったんで削除しました。

Task :app:transformClassesAndResourcesWithR8ForRelease FAILED

transformClassesAndResourcesWithR8ForReleaseの実行に失敗している。
調べてみると諸々のパッケージバージョンが合わないことで発生しているケースがあるらしいです。(多分色々古かったパッケージもあった。バージョンアップはリスキーなので断念)
R8の設定をtrueにすることで難読化や最適化を行ってくれるそうですが、ここではfalseにしていきます。

{project_name}/android/gradle.properties
追記

#Disables R8 for Android Library modules only.
android.enableR8.libraries = false
#Disables R8 for all modules.
android.enableR8 = false

参照:https://developer.android.com/studio/releases

Execution failed for task ':app:transformClassesAndResourcesWithProguardForRelease'. java.io.IOException: Please correct the above warnings first.

以下続き

Warning: there were 1649 unresolved references to classes or interfaces.
         You may need to add missing library jars or update their versions.
         If your code works fine without the missing classes, you can suppress
         the warnings with '-dontwarn' options.

Staging用APKを生成するときに出たエラーです。
Proguard 関連の処理が原因らしいです。この辺を-dontwarnを使って制御することができるらしいです。
とはいえ、1649件もなんかバージョンアップしてないとか、クラスに欠陥があるとか、不足ライブラリがあるらしいものを一気になんとかできるのか。。。(調べてみると、件数が少ないと1個1個バージョンアップやらすることで解消することもあるそうです)
実機ビルドでは普通にアプリが動いていることから、-dontwarnなどをproguard-rules.proを解消できそうであると考え、以下の対応を行っています。

{project_name}/android/app/build.gradleのbuildTypeにstagingで起きたエラーなので、
stagingに proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" を追加
これでproguard-rulesを見に行きます。(多分ここまでたどり着くのに2日くらい)

buildTypes {
        debug {
      ・・・
        }
        staging {
             ・・・
       // add
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        }
        release {
            ・・・
        }
    }

次にproguard-rulesの設定。
{project_name}/android/app/proguard-rules.pro
元々の専任の方が書いてあった記述にプラスしてReactNative系の処理とWarningはスルー、もろもろの処理をスルーしますよ的な書き方だったりを追記。

# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# Add any project specific keep options here:
-keep class io.agora.**{*;}

# THIS IS VERY VERY BAD. REMOVE AS SOON AS VERSIONING IS FIXED
-dontwarn **


-dontnote **

-keep class host.exp.exponent.generated.AppConstants { *; }

##### Crashlytics #####
-keepattributes SourceFile,LineNumberTable

##### React Native #####
-keep,allowobfuscation @interface **.facebook.proguard.annotations.KeepGettersAndSetters
-keep,allowobfuscation @interface **.facebook.react.bridge.ReadableType


-keepclassmembers @**.facebook.proguard.annotations.KeepGettersAndSetters class * {
  void set*(***);
  *** get*();
}

-keep class * extends **.facebook.react.bridge.JavaScriptModule { *; }
-keep class * extends **.facebook.react.bridge.NativeModule { *; }
-keepclassmembers class *  { @**.facebook.react.uimanager.UIProp <fields>; }
-keepclassmembers class *  { @**.facebook.react.uimanager.ReactProp <methods>; }
-keepclassmembers class *  { @**.facebook.react.uimanager.ReactPropGroup <methods>; }


##### Versioned React Native #####
-keep class **.facebook.** { *; }
-keep class abi** { *; }
-keep class versioned** { *; }

本当はもっと書いてあったけど、いらなそうなのがあったんでその辺は削除。
多分、先人がReactNativeのバージョンとか上げてしまったり、その他のパッケージとかもものすごく古いパッケージとかもあったし、色々な苦労が見えた結果かなと思う。これが正しかったのかわからないけど、このエラーはこうすることで消えました。(proguardの設定も意味がわからなくて2日くらいかかって合わせて4日くらいかかってしまった。。)

Execution failed for task ':react-native-orientation:verifyReleaseResources

自分でreact-native-orientationを入れた記憶がないので先人のものでしょう。
参考:Execution failed for task ':react-native-orientation:verifyReleaseResources' · Issue #290 · yamill/react-native-orientation · GitHub

{project_name}/android/gradle.bundle に以下を追加

buildscript{ ...}

allprojects { ...}

subprojects {
    afterEvaluate { project ->
        if (project.hasProperty("android")) {
            android {
                compileSdkVersion 28        // version of compile sdk used for project
                buildToolsVersion '28.0.3'    // version of build tool used for project
            }
        }
    }
}

これは同じ{project_name}/android/gradle.bundleに記載しているbuildscriptにある、compileSdkVersionbuildToolsVersionを合わせないといけないです。
ちなみに、こちらのバージョンも28以上でないといけなかったらしいです。こちらのプロジェクトでは偶然28以上を使ってたので、問題ありませんでした。

Task :@react-native-community_async-storage:generateDebugBuildConfig FAILED

cd android && ./gradlew clean で解消
キャッシュが残っていることがあるらしいです。

Execution failed for task ':app:installDebug'. > com.android.builder.testing.api.DeviceException: com.android.ddmlib.InstallException: INSTALL_FAILED_VERSION_DOWNGRADE

cd android && ./gradlew cleanだけでは直らず、普段実機ビルドできているのにどうして?というエラー
PC再起動で直りました。

Task :app:mergeDebugResources FAILED

このエラーの前後にエラーの原因となっているログが書いているはずで、自分の記憶にないエラー内容だと、
cd android
./gradlew clean
で解決することがある

でも大概は「あ、これ自分触ったやつ」っていうエラーもあるので、そちらに問題があることもあったりしました。

Task 〇〇:app:packageDebug FAILED

cd android
./gradlew clean
で解決することがあるが、この場合はAndroidStudioがメモリを食っていて出来ない場合もあったので、AndroidStudioで
1.キャッシュ消しての再起動
2.build>clean projectで解消

以上。

開発期間が短かったので出来る限りのことをやろうと思って必死に調べました。
使っているパッケージやライブラリが古く、もうドキュメントすら残されてないのが多い中での対応だったので、そこが一番しんどかったです。でもこれでAndroid設定周りの対応学べた気がしますね
次はiOS編を書いていきます。(Androidほどは無いかもしれない)

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

【Node.js】TwitterAPIのRateLimitとcursorに気をつけろ

Node.jsアドベントカレンダー18日目の記事です。

APIの勉強で利用が楽、且つわかりやすいものが作れるのはTwitterか小説家になろうだと思っています。
参考:[JavaScript]なろうのランキングをAPIで一括取得する

現在参画中のプロジェクトではAPIもフロントも両方開発していますが、サービスが本格運用前なことに加えてある程度仕様がゆるい状態での開発のため、APIリミットがキツいイメージがあったTwitterを触ってみることにしました。

Node.jsのTwitterAPIを使ってみよう系エントリーについてはAPI利用申請の部分に偏った物が多かった事と、
特徴となるcursorに触れている物が少なかったこともありコード側に寄せて実装時気になった箇所を纏めました。

尚、Twitter API利用開始についてはTwitter APIを使ってみるを参考にしました。

3行で

・非同期処理時の残cursorの扱い(結果参照しPromiseを生成する)
・取得対象に応じて設定値が著しく異なるRateLimitに注意する
・entitiyの罠(screen_nameとUID)

実装

app.js
  /* express関連の処理省略 API内部処理のみ記述 */

  // 検索対象のIDを指定(実際はreqにて取得)
  const userId = "任意のID"

  // 初回カーソルの指定
  let now_cursor = -1

  // API取得情報の保持
  const user = new Array()

  const getList = (nc) => {
    // 引数を受けてPromiseを返すfunctionを生成して返す
    return new Promise(() => {
      client.get("followers/list", { screen_name: userId, cursor: nc })
        .then((res) => {
          user.push(res.users)
          now_cursor = res.next_cursor

          // cursorが0以外であれば次ページを取得、0ならファイル出力
          if (now_cursor > 0) getList(now_cursor)
          else fileOutput(user, 'follow')
        })
        .catch((e) => {
          // レートリミットの場合、15分経過したらリトライする
          if(e.code === 88) setTimeout(getList(now_cursor), 15 * 60* 1000)
        })
    })
  }

  // APIによるフォローリスト取得処理の実行
  getList(now_cursor)

今回はexpressのサーバーをFirebaseにてホスティングしてクライアントサイドから叩く想定のコードのうち、
取得処理のコアになる部分を抜粋しています。requestにてtwitterIdを受け取ったのち、そのidユーザーの
フォローしている人を全て取得する処理となります。

非同期処理時の残cursorの扱い(結果参照しPromiseを生成する)

DM欄・フォロー欄・フォロワー欄・リプライ欄等の頻繁に更新が発生しないが一覧で表示したい項目については、新規ツイート取得と異なり全量取得しようとするとAPIによる取得が複数回発生する可能性があります。TwitterAPIのentityの使用上、連続するデータを取得するためにcursorを用いてページ遷移の如く次ページのデータへ移動し、データを取得していく必要があります。

リセットタイムとRateLimitを考慮してある程度先読みさせておくとAPIサーバーへの頻繁なアクセスを減らす事ができ、負荷分散ができることに加えてRateLimitを無駄にしないメリットがあります。
(5の倍数で処理を実行しデータをストック、cursorを保持して待機させ、細かいAPIアクセスを減らすなど)

上記コードを伝えたい箇所のみを使用して簡略すると以下の内容になります。

  const getList = (nc) => {
    return new Promise(() => { client.get()

        /* client.get()処理が終了するまで待機、成功でthen、失敗でcatchに入る*/

        // 受け取った結果を使用して次のPromiseを生成する ※疑似ループ処理
        .then((res) => if(res.nextcursor !== 0) getList(res.next_cursor) }) 

        // 何かしらのエラーが発生したらエラー出力して処理を終了
        .catch((e) => { console.log(e) return }) // RateLimit:コード88 の可能性が一番高くなる

返却される結果の1要素を用いて次の処理を生成する必要があるため、引数を投げられるPromiseを生成する関数の内部で再帰的に処理させる事によりgetList()を実行するだけでループの上全量取得を行います。(RateLimitは除く)

※なにかしら制御しないとアクセスしまくってすごいことになるので、今回のようなエラーコードが
 返却されるものでなく自作APIを叩く場合は必ず制御を入れるようにしましょう。(エラー返すとか、フラグ制御とか)

少し本論とズレましたが、TwitterAPIのentityでcursorを使って次結果に移動するため保持する必要があることと、APIの制限に柔軟に対応できるよう一定回数の処理を纏めて実行できる処理を作りRateLimitを無駄にしないことを頭に入れておけばTwitterAPIによる開発がしやすくなると思います。

取得対象に応じて設定値が著しく異なるRateLimitに注意する

limit.png

TwitterAPIのRate Limitの欄を見ると新ツイートの取得等と異なり、比較的短時間での変動が少ないList・follower等の取得については15分に15回とかなり強めな値に設定されています。List等であれば1回のリクエストですべて取りきれますが、特にフォロワー取得については今回のコードでフォロワー(1-200程度)を検証中に数回取得した所、リクエスト回数全てを消費してしまいました。

Twitter公式のフォロワーページでスクロールを行うと、一定距離スクロールをした所で追加取得の実行が確認できます。自己紹介文などbioの長さによりますが4-50userで1リクエスト=cursor1回分みたいな扱いになっている印象を受けました。15count*約50user≒750人以上取得する場合、1回(15分)のRateLimit内では取り切れない可能性があります。

とはいえ更新が少ない箇所のため見せ方とか作り次第でどうにかできるものではありますが、RateLimitが極端に少ない箇所もあるよという所は開発する機能によってはクリティカルな部分となるため認識しておく必要があると思います。

entitiyの罠(screen_nameとUID)

ここに関してはentityの構造をちゃんと見ろという話ではありますがUserIdは各ユーザーに振られた固定の数値によるIDでありTwitterIdとして認識しているものはentityではscreen_nameである、だとか内部名称が自分が認識しているものと異なるので、先にある程度Twitter Developer Documentationのentityを見ておくとラグがなく作業ができるかなと思いました。(他の記事だとあまり触れられていなかったので念の為)

entity構造については参考サイトが日本語かつ公式より見やすかったため、必要そうな箇所を一通り読んでおけば比較的スムーズに実装できるかと思います。Twitter DeveloperのRate Limitと合わせて確認することをお勧めします。

参考:Twitter REST APIの使い方

補足

npmのTwitterモジュールは4年前から更新ストップしているため、Twitterの仕様変更によってはモジュールを使用したリクエストができなくなる可能性があります。(2020年12月現在で使えており生存ログの意味でもこの記事を書いています)
といってもWeeklyDL数は圧倒的だったりサクッと使いやすいため、使えなくなるタイミングについては注視しておく必要がありそうです。

まとめ

・TwitterAPIはcursorのクセを理解して使う
・公式ドキュメント(特にentityとRateLimit)はよく読もう
・はじめからRateLimitを無駄にしない作りを意識しておくと後で困らないかも

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

JS周りのコードフォーマットをどうするか?

この記事は、ニフティグループ Advent Calendar 2020の23日目の記事です。

はじめに

今年、弊社ではフロントエンドの刷新業務として、Reactの開発を行う機会があり、そこで私も同じプロジェクトの開発メンバー(2~3人程度)と同時にコード開発を行っていました。

自分自身、本格的なチームでの開発というものが初めてだったため、「Gitのブランチの扱いが良く分からない...」だったり、「プルリク時のコンフリクトが頻繁に起こる...」など、共同開発で迷う場面が多かった気がします。。

そんな中、在宅期間中で「これができたら便利そう...とか、もっと効率良い開発方法はがあるのでは?」など、あれこれ考えることが多く、何かと便利機能を調べてみては試してみる、といったこともそれなりに出来たので学びとしては多かったです。

また、開発の面倒な部分を効率化出来れば、きっとエンジニアのモチベーションも上がるはず(!?)です。

そこで、チーム開発で一部導入してみた機能や、最初からもっとこうしておけばよかったな、といったことを書いていこうと思います。

Git開発

Git開発の流れとしては、以下のような手法で開発していました。

  • 機能ごとにfeatureブランチを切る
  • 各ブランチ毎にプルリクを出す

コンフリクトにどう対処するか問題

基本的に各自で開発した機能は被ることがないので、その場合はプルリクでコンフリクトが起こることはありません。
ただし、ちょっとした機能修正などコードを修正したタイミングでは、コンフリクトが起きることがあります。

コンフリクト修正機能

エディター自体にもコンフリクトを修正するような機能は用意されており、後からコンフリクトを修正することもできるのですが、
そもそもが「インデントが違う」などフォーマットが統一されていないがためにコンフリクトが起こっている場合、それを一々修正するのは面倒だと思います。

こういった問題を解消するために、JS用のコードフォーマッターを導入しました。

コードフォーマッターを入れたい

React(JS)での開発では、フォーマッターを入れると便利という事を知り、以下の2つを導入しました。

導入したもの

  • Prettier(フォーマッター)
    • ソースコードをルールに沿った形に良い感じに整形してくれるツール
  • ESLint(リンター)
    • ソースコードを読み込んで内容を分析し、問題点を指摘してくれる

Prettier

.prettierrcというファイルを用意し以下のようにフォーマットのルールを設定します。セミコロン有無や、インデント幅などを設定可能です。

.prettierrc
{
  "printWidth": 120,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "singleQuote": false,
  "trailingComma": "es5",
  "bracketSpacing": true,
  "jsxBracketSameLine": false
}

例えば、app.jsに対してprettierを実行したい場合、prettier app.js --writeとコマンドを実行すれば、整形されたファイルが出力されます。

ここまででは、一先ずはコードフォーマットは出来るようになりましたが、一々コマンドを打たないとフォーマットができないのはかなり面倒です。

そこで、"ファイル保存をする毎にprettierを実行できるよう"、VSCodeのsetting.jsonに以下の一行を追加します

 "editor.formatOnSave": true

これで、かなりの手間が解消されました。

ESLint

prettierはコードフォーマッタのため、ESLintのような構文のチェックはできません。そのため、ESLintも併用で導入します。

同様に、.eslintrcにも設定内容を記述していきます。
(以下が設定例となります。)

.eslintrc
{
  "env": {
    "es6": true,
    "node": true,
    "browser": true,
    "jquery": true,
    "jest": true
  },
  "parser": "babel-eslint",
  "plugins": ["react", "prettier"],
  "parserOptions": {
    "version": 2018,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:prettier/recommended", "prettier/react"],
  "rules": {
    "prettier/prettier": "error"
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  }
}

ESLint と Prettier を共存させる場合、
"extends": ["plugin:prettier/recommended"]を書くだけで、併用ができます。

"git commit"実行時にlinterテストを走らせる

さらに、linterテストが自動で走るようにしたいので、ESLintのテストが失敗した場合は"git commit"が通らないようにします。

使うものとしては以下です。

  • lint-staged
  • husky
yarn add -D eslint lint-staged husky

追加方法としては、以下をpackage.jsonに追記するだけでできます。

package.json
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    ".{js,jsx}": [
      "eslint --fix",
      "git add"
    ]
  },

実際に動かすと...

image.png

このように、git commit時にeslint --fixが実行され、該当するエラーを解消しないとコミットができなくなりました。

導入した結果

ここまでで以下のような事が出来るようになりました

  • "git commit"実行時に自動でlinterテストが走る
  • ファイル保存時にPrettierが実行される

コードフォーマットを自動化することで、無駄なことに人力で注視しなくなるので楽になりました。

なお、VSCode等でJSやHTMLなど専用のフォーマッターツールを別途入れている場合、Prettier、ESLintの設定を上手く変えてあげないと動作しないことがあるので注意が必要です。
今回は実装を簡単にするため最低限の設定にしてありますが、実際のチーム開発を行う場合には、エディターに入れる拡張機能などの環境を、事前に揃えておくなどの工夫が必要かと思います。

感想

初めてコードフォーマッターを導入してみたのですが、調べてみるとこれ以外にもかなり多くの事ができるようで、良い勉強になりました。

個人的には、プルリクなりコードを書くなりでも、きれいな状態(整理された状態)でいることはコードの品質面でもそうですが、自分が見ていても分かりやすいし実際にチームでも使ってもらえたりと、それだけでモチベが上がる気がします。

VSCodeのエディターの機能や、プルリクの自動化機能(GitHub Actions)など、既存ものを自動化して改善できる部分は意外とあるものの、一度面倒な部分を実感してからじゃないと中々思いつかない部分もあるのかなと感じました。

1から調べて実装して導入までやってみると、かなり知識も身に付くと感じたので、プライベートでもこういった経験を増やしていきたいです。

終わりに

次回は@RPcatさんの記事になります!お楽しみに!

参考

Prettier 入門 ~ESLintとの違いを理解して併用する~
【JavaScript】コミットする前にlint-stagedでeslintのチェックをする
VS Code上でファイル保存時にPrettierを走らせる

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

JavaScript開発にNoSQLデータベースを活用する(CEANスタック紹介) ~ Node.js + Couchbaseアプリ開発 ステップバイステップガイド 2

はじめに

本記事は、Node.js + NoSQL(Couchbase) を使ったアプリ開発をステップバイステップで解説していくシリーズの第二回目になります。

下記の第一回記事も適宜参照していただければ幸いですが、開発の実際に進む前に、まずは表題についての解説から始めます。

Node.js + NoSQL(Couchbase) アプリ開発 ステップバイステップガイド (1)

なぜ、JavaScriptとNoSQLの組み合わせなのか?

NoSQLは、非RDB(RDB以降の技術)であることのみを共通点として、様々な異なる特色を持った技術に対する総称となっていますが、ここで対象とするカテゴリーは、その中でもドキュメント指向データベース(ドキュメントストア)と呼ばれるものです。

ドキュメントストアは、JSONデータを格納することを特徴としており、自ずと、JavaScriptと親和性を持っています(JSON = Java Script Object Notation)。

その一方、RDBから移行するだけの利点があるのか、というのが実際的な関心なのではないかと思います。こちらについては後ほど触れていきます。

CEANスタック紹介

ここで、用いるCEANスタックとは、下記の技術要素からなります。

  • C: Couchbase (NoSQLドキュメント指向データベース)
  • E: Express (Webアプリケーション・フレームワーク)
  • A: Angular (フロントエンド・フレームワーク)
  • N: Node.js (サーバサイドJavaScript実行環境)

類似のものとして、MEANスタック、という言葉を聞いたことがある方もいるのではないかと思います(そうは言っても、LAMP程には、浸透していない気もしますが)。この場合のMは、MongoDBとなり、Couchbase同様、JSONデータを扱うことのできるNoSQLデータベースです。
以下では、NoSQL/ドキュメントストアとしてCouchbaseについてのみ記します(MongoDB/MEANスタックについては、様々存在する別の記事・情報に譲ります)。

NoSQL/Couchbaseを選択する理由

慣れ親しんだRDBに替えて、あえてNoSQL/Couchbaseを選択する理由としては、様々な角度から語ることができますが、ここでは、以下の点にフォーカスしたいと思います。

それは、Couchbaseなら、JSONデータとクエリ言語の両方の利点を活用することができる、ということです。

一つずつ、見ていきたいと思います。

JSONデータの利点

これは、RDBの欠点と見ることもできます。つまり...

  1. アプリケーションが必要とするデータ構造(ドメインオブジェクト)と、RDBが要請する形式(第一正規形テーブル構造)との間には、断絶がある。
  2. アプリケーションの設計、実装、改善、機能追加など、全ての工程において、データベースとの兼ね合いを図る必要がある(密結合)

これに対して、データ層が、JSONを許容した場合...

  1. データ層は、第一正規形を要請しないため、アプリケーションが必要とするデータ構造(ドメインオブジェクト)そのものを格納することができる。
  2. (JSONには、データ構造に関する情報がデータ自体に含まれているため)アプリケーション設計・開発工程において、特にデータ設計の変化に(データベース側の作業を伴うことなく)柔軟に対応できる

クエリ言語の利点

これは、RDBの持つ大きな利点であり、標準化されたクエリ言語(SQL)が様々な異なるデータベースで利用できることは、技術者層の拡大に繋がり、SQLの習得は、(特にオープンシステムのWEBアプリケーション全盛時代には)システム開発者にとって、必須知識といえるものとなっていました。

Couchbaseを選択することで、開発者は、SQLの知識を活用することができます。

サンプル・アプリケーション紹介

CEANスタックで開発したアプリケーションを下記に公開しています。

https://github.com/YoshiyukiKono/couchbase_step-by-step_node_jp

このアプリケーションは、下記の画面を見ていただければ分かるように、最小限の機能からなるシンプルなものとなっています。

image.png

アプリケーション実行方法

上記のリポジトリから、コードを取得します。

$ git clone https://github.com/YoshiyukiKono/couchbase_step-by-step_node_jp.git
$ cd couchbase_step-by-step_node_jp

その中に含まれる、package.jsonには、下記の依存関係が定義されています。

  "dependencies": {
    "couchbase": "^3.1.0",
    "express": "^4.17.1"
  }

第一回の内容も合わせて、参照していただきたいと思いますが、このアプリケーションでは、バケット名としてnode_appを使っています。

アプリケーション実行前に、下記のようにインデックスを作成しておく必要があります。

$ cbq -u Administrator
 Enter Password: 
 Connected to : http://localhost:8091/. Type Ctrl-D or \QUIT to exit.
...
cbq> CREATE PRIMARY INDEX node_app_primary ON node_app;

このアプリケーションでは、下記のように、server.jsを使います。

$ node server.js
Server up: http://localhost:80

http://localhost:80にアクセスします。

プログラム解説

routes.jsを見ると、画面のリスト表示のために、下記のようなクエリが使われているのが分かります。

const qs = `SELECT name, id from node_app WHERE type='user'`; 

一方、プログラム中のデータ(ドメインオブジェクト)の表現と、そのデータをデータベースへの保存する部分は、下記のようなものです。

      const user = {
        type: "user",
        id: req.body.id,
        name: req.body.name,
      };
      upsertDocument(user);
const upsertDocument = async (doc) => {
  try {
    const key = `${doc.type}_${doc.id}`;
    const result = await collection.upsert(key, doc);
  } catch (error) {
    console.error(error);
  }
};

JavaScriptのディクショナリをそのまま格納しているのが分かります。

最後に

今回は以上です。コードができるだけ分かりやすいものとなるよう、処理やデータ構造は最小限のものとしています。
実は私自身、CEANスタックを用いるのは今回初めてだったのですが、いたずらに複雑でないぶん、コード自体を見ていただくことで、全体の関係が掴みやすいものになっているのではないかと思います。

今後について

ディクショナリ/JSONデータをそのまま使うことで、ORマッピングなどを用いる必要がなく、シンプルに開発ができることがわかったかと思います。一方で、単なるマップ型のデータ構造ではなく、ドメインオブジェクトをクラスとして定義したいというニーズもあるかと思います。Couchbaseには、Ottoman.jsというオープンソースのODM(Object Document Mapping)フレームワークがあり、そうしたニーズにも対応することができます。
こちらについても、今後紹介していきたいと考えています。

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

create-react-appで生成したReactのJest実行をVSCodeからブレークが効く状態で実行する

概要

create-react-appで生成したReactのJest実行をVSCodeからブレークが効く状態で実行する。

やり方

公式 に書いてあるのでそれ参照。。

公式acebookのcreate-react-appを使用しているなら、Jestのテストを以下の設定でデバッグできます: のコードを launch.json にコピペしてそれを実行する。

※テストコードの方もブレークできるし、実装側の方もブレークできる。

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

Flash Advent Calendar 18日目 - OSSを諦めて、有償化に踏み切った話 -

有償化に踏み切った話

swf2jsは2013年から7年間OSSとして地道に活動していたのですが
注目度も低く、継続してメンテナンスを行うが苦しい状態だったので
2019年に自分の所属する会社(ソニックムーブ)と共同で開発する事で有償化に踏み切りました。

最後までOSSとして開発を行いたかったのですが
swf2jsは誰からも必要とされないライブラリ。っという状況だったので
会社で認めてもらった時はとても嬉しかったです。

FlashPlayerに対しての風当たりも強かったので余計にそう感じたのかもしれません。。。

個人のOSS品質

「個人のOSSは、信頼性が低い」っと判断されがちですが
作っているエンジニアは実際に現場で活躍しているエンジニアが多く
実務では商品プロダクトを実装している方が大半だとおもいます。
どうか、個人のOSSという事だけで品質の判断をしないでもらえると嬉しいです。

応援の仕方は色々

誰かに必要とされ、何かの役に立っているという事が分かるだけで開発を継続できます。
もし、利用しているOSSがあればGitHubstarを押すだけで、開発意欲を十分に満たされると思います。
(他のOSS活動を行っている方の一助になればと思い記事を書きました。)

次のチャレンジ

個人でOSS活動する為の心得や、継続するための開発手法などなど
今回とても学ぶ事が多かったです。

次に取り掛かりたいOSSがあり、その時には今回のような途中で挫けるような事がないよう
上手に立ち回れればと思います。

FlashLover

2021年以降もSWFを配信できる「FlashLover」っというアーカイブサイトを準備しています。
JswfPlayer × swf2js 「FlashLover」

有償版のswf2jsもこのアーカイブサイトであれば無料で利用可能です。
また、将来的には個人の方のAdsenseも配信できる仕組みを導入する予定です。

是非、ご期待いただけばと思います。

今日はOSSの愚痴みたいになってしまったのですが、明日はOSSで得られた事を書こうと思います。

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

MediaDevicesとWeb Audio API Vue.jsとThree.js で 音声の波形表示

MediaDevicesとWeb Audio API vuejsとthreejs

概要

  • WebRTCの話が盛り上がってたのでjsでカメラとマイクを使う方法をおさらいする
  • デバイスの選択をする仕組みをvueで作ってみる
  • ビデオデバイスの映像をvideoタグで表示してみる
  • オーディオデバイスの音声をAnalyserNodeを使ってビジュアライズしてみる

WebRTCの話が盛り上がってたのでjsでカメラとマイクを使う方法をおさらいする

社内でWebRTCについて話題が上がり、そこで以前webGLでカメラ映像をテクスチャとして取り込んでエフェクトかけて遊べる何やらを試作したのを思い出したので、すっかり忘れたその方法を復習してみました。

まずはカメラとマイクのデバイスを取得してVIDEOタグで再生させてみる。

var constraints = { audio: true, video: true };
navigator.mediaDevices.getUserMedia(constraints).then(function(stream){
  document.getElementById("video").srcObject = stream;
  document.getElementById("video").play();
}).catch(function(err){
  console.log("!!!!",err);
});

これでブラウザでHTMLに事前に貼り付けておいたVIDEOタグ(#video)にカメラの映像が表示される様になしました。

ただこれだとカメラもマイクも自動選択なのでHangoutMeetの開始時の様にデバイス選択できる様にしたい。

デバイスの選択をする仕組みをvueで作ってみる

enumerateDevices()という命令でデバイスリストを取得できる様です。

MediaDevices.enumerateDevices() - Web API | MDN

navigator.mediaDevices.enumerateDevices().then(function(devices){
  console.log(devices);
});

さっそくリスト取得してみましたが思いの外いっぱい出てきます。

取得できるデバイスの単品のデータ構造はこんな感じで

{
"deviceId": "default",
"groupId": "13eaa2c10d3b436a8bbb085b5af46a8a721eea3c271546792e9dfe15a1e6c4c2",
"kind": "audioinput",
"label": "既定 - External Microphone (Built-in)"
}

kind がデバイスの種類を示す様で、MDNによれば
"videoinput" "audioinput" "audiooutput"
の3種類あるそうです。
思いの外多かったのは出力用のデバイス "audiooutput" があったからですね。
入力だけとばかり思ってましたがmediaDevicesには出力も含まれる様です。

このリストから"videoinput" "audioinput"を抽出して選択できる様にします。

jsはこんな感じで

var app = new Vue({
  el:"#app",
  data:{
    videomedias: [],
    audiomedias: []
  }
});
navigator.mediaDevices.enumerateDevices().then(function(devices){
  var videomedias = [];
  var audiomedias = [];
  devices.forEach(device => {
    if( device.kind == "videoinput" ){
      videomedias.push(device);
    }
    if( device.kind == "audioinput" ){
      audiomedias.push(device);
    }
  });
  Vue.set(app,"videomedias",videomedias);
  Vue.set(app,"audiomedias",audiomedias);
});

HTMLはこんな感じ

<div id="app">
  <div>
    <select name="sel_video" id="sel_video">
      <option v-for="(item, index) in videomedias" :value="index" >{{item.label}}</option>
    </select>
    <select name="sel_audio" id="sel_audio">
      <option v-for="(item, index) in audiomedias" :value="index" >{{item.label}}</option>
    </select>
  </div>
</div>

これでリストアップはできたので、フォーム入力バインディングを使って簡単に値を取れる様にしておきます。

var app = new Vue({
  el:"#app",
  data:{
    videomedias: [],
    audiomedias: [],
    selectedvideo: 0,
    selectedaudio: 0
  }
});
<div id="app">
  <div>
    <select name="sel_video" id="sel_video" v-model="selectedvideo">
      <option v-for="(item, index) in videomedias" :value="index" >{{item.label}}</option>
    </select>
    <select name="sel_audio" id="sel_audio" v-model="selectedaudio">
      <option v-for="(item, index) in audiomedias" :value="index" >{{item.label}}</option>
    </select>
  </div>
</div>

あとはボタンを追加して選択したデバイスを使った処理を行います。

<button @click="startvideo()" >開始</button>
methods:{
  startvideo:function(){
   ...
  }
}

ビデオデバイスの映像をvideoタグで表示してみる

実際にカメラデバイス指定をしてvideoタグで表示する処理をボタンを押したら実行する様にします。

methods:{
  startvideo:function(){
    var constraints = {
      video: {
        deviceId: this.selectedvideo != -1 ? this.videomedias[this.selectedvideo].deviceId : null,
        width: 1280,
        height: 720
      }
    };
    navigator.mediaDevices.getUserMedia(constraints).then(function(stream){
      document.getElementById("video").srcObject = stream;
      document.getElementById("video").play();
    }).catch(function(err){
      console.log("!!!!",err);
    });   
  }
}

これでvueで作ったボタンを押せば、指定したカメラの映像がブラウザ上で再生されます。

オーディオデバイスの音声をAnalyserNodeを使ってビジュアライズしてみる

ここまでやってカメラの画像をwebGLでどうこうじゃなくて、音声の波形情報とか表示できないかなと思い調べてみると
Web Audio APIを使って波形や周波数スペクトラムデータが取れる模様

Visualizations with Web Audio API - Web API | MDN

MDNの解説に従いオーディオデバイスからオーディオストリーム取得し、それを元にオーディオソースを取得し、
アナライザーノードに接続します。

var audio_ctx = new AudioContext();
var analyser = audio_ctx.createAnalyser();
var audioinput;
var audio_constraints = {
  audio:{
    deviceId: this.selectedaudio != -1 ? this.audiomedias[this.selectedaudio].deviceId : null,
  }
};
navigator.mediaDevices.getUserMedia(audio_constraints).then(function(stream){

  audioinput = audio_ctx.createMediaStreamSource(stream);

}).catch(function(err){
  console.log("!!!!",err);
});

何も変わらないですがこれで取れているはず・・・。
実際に値を表示してみます。

var dataArray = new Float32Array(analyser.frequencyBinCount);
setInterval(function(){
analyser.getFloatTimeDomainData(dataArray);
console.log(dataArray);
},100);

ss01.png
取れている様です。

これをヴィシュアライズしていきますが、ヴィジュアライズには
Three.jsを使っていきます。

まずは、色々表示のための準備をしていきます。
ラインで波形を表示しますがある程度なめらかが必要ですので、点の数が500個でデータを用意します。

var scene;
var renderer;
var camera;
var points = [];
var line;

var width = 1280;
var height = 300;

scene = new THREE.Scene();
camera = new THREE.OrthographicCamera( width / -2, width / 2, height / 2, height / -2, 1, 1000 );
scene.add(camera);

for(var i = 0 ; i < 500; i++){
  points.push(new THREE.Vector2(i / 500 * width, 0));
}

var geometry = new THREE.BufferGeometry().setFromPoints( points );
var material = new THREE.LineBasicMaterial({
  color: 0xff0000;
});
line = new THREE.Line( geometry, material );
line.position.x = width / -2;
scene.add( line );

renderer = new THREE.WebGLRenderer();
renderer.setSize(width,height);
document.body.appendChild( renderer.domElement );

あとはレンダリングの処理を書きます。

function animate(){
  requestAnimationFrame( animate );
  renderer.render( scene, camera );
}

これで animation関数 を実行すれば
アニメーションのレンダリングが開始されます。

今のところただまっすぐ赤い線を引くだけですので、ここにアナライザーノードからの波形データを入れていきます。

まず頂点データを変更するためにジオメトリから頂点データを引っ張ってきます。

var positions = line.geometry.attributes.position.array;

ここには x y z の順番で頂点データが入っていますので、そのうち y の値を変化させて、波形のデータを反映させます。
波形のデータは -1〜1 の範囲でデータが入ってくるはずですので 表示領域の半分の高さを乗算した値を入れていきます。
ただし、波形データのデータ長と、頂点データのデータ長が一致していないので計算で補正して波形データの値を拾っていっています。

function animate(){
  var height = 300;
  var positions = line.geometry.attributes.position.array;
  for(var i = 0 ; i < positions.length; i+=3){
    positions[i+1] = height/2 * dataArray[Math.floor( i/positions.length * dataArray.length) ];
  }
  requestAnimationFrame( animate );
  renderer.render( scene, camera );
}

さらにラインの頂点データの変更を反映するには

line.geometry.attributes.position.needsUpdate = true;

の様にレンダリング前にneedsUpdateにtrueをセットする必要があるとのこと。

function animate(){
  var height = 300;
  var positions = line.geometry.attributes.position.array;
  for(var i = 0 ; i < positions.length; i+=3){
    positions[i+1] = height/2 * dataArray[Math.floor( i/positions.length * dataArray.length) ];
  }

  line.geometry.attributes.position.needsUpdate = true;

  requestAnimationFrame( animate );
  renderer.render( scene, camera );
}

これでひとまず出来上がりです。

アナライザーノードは周波数スペクトラムも取得できるので時間を見てそちらも挑戦してみます。

ss02.png


:christmas_tree: FORK Advent Calendar 2020
:arrow_left: 19日目 Vue.jsのSSGフレームワークのGridsomeはすごいぞ @Kodak_tmo

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

【なんとなく】GASでリダイレクトする方法【うまくいく】

GAS(Google Apps Script)でWebアプリを作るとき、SUBMITしたらホームページに戻るようにしたい。

ぼくは同じHTMLを、部分的に<DIV>タグを使ってhidden属性を有効化/無効化して、擬似的に複数のページに見せている。
SUBMITを行ったら、URLパラメーターを使ってdoget()関数で取得して操作(スプレッドシートの更新など)を行う。

これまではSUBMITの飛び先を(擬似的な)ホームページにしていたのだが、そうするとアドレスバーにURLパラメーターが丸見えなので、ENTERやF5を押すとまた更新動作が走るという問題があった。

そこで、URLパラメーターをクリアするために、(擬似的な)ホームページにリダイレクトしたいと思った。
いわゆる<META>タグにrefreshを使うと、コンピューターが困った顔みたいなアイコンが出て「Connection Refused」と言われる。
JavaScriptを使ってリダイレクトしても同じ現象が起こる。

いろいろググって試行錯誤の末、このやり方ならうまくいった。

function doGet() {
  return HtmlService.createHtmlOutput(
    "<script>window.top.location.href='https://開発中のWEBアプリ';</script>"
  );
}

原理はよく分からない。
相変わらずひどくてスミマセン。

参考:
https://stackoverflow.com/questions/11315521/automatically-redirecting-to-a-page

(この後おわり)

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

React FC ステートで混乱するの巻

Hooksという魔法のせいでなんだかステート(状態)を関数が持てるような錯覚を起こし、
バグで詰まってしまった愚か者がいるらしいですよ。(私のことです)

下記、検証や考察で仮説を建てたものなので誤り等あるかもしれません。
その際はコメントにて指摘いただけると嬉しいです?‍♂️

React FC事件簿

問題が起きていたコードをものすごく簡略化したのが下記のコードになります。

import React, { useEffect, useState } from "react";
import "./styles.css";

export default function App() {
  const [counter, setCounter] = useState(0);
  const [madeDom, setMadeDom] = useState(null);

  const counterCheck = () => {
    alert(`カウントは${counter}`);
  };

  useEffect(() => {
    setMadeDom(<ChildComp handler={counterCheck} />);
  }, []);

  return (
    <div className="App">
      現在カウンターは {counter}
      <button onClick={() => setCounter(counter + 1)}>カウントUP</button>
      {madeDom}
    </div>
  );
}

function ChildComp({ handler }) {
  return <button onClick={handler}>子供カウントチェック</button>;
}


CodeSandBoxはこちら

親コンポーネントは初回レンダリングの際に useEffectにてmadeDomを作成し、それを
useStateで保持しています。
またmadeDomには、親コンポーネントのメソッド counterCheckを渡しています。
こちらは親のステート counter を表示するメソッドです。

親コンポーネントはビューとして
・自身のカウンター、
・カウントアップ用ボタン
・生成したmadeDom
を表示させています。

スクリーンショット 2020-12-16 20.31.48.png

初回はこんな塩梅ですね。
そしてカウントUPさせたのち、子供カウントチェックボタンを押すと....

スクリーンショット 2020-12-16 20.34.55.png

あれ、、、
同じcounterを保持しているはずなのに親と子で異なる結果となりました。
もっと正確にいうなら、子に渡したメソッドから参照するcounterは増加せず、
親からダイレクトに表示しているcounterのみのカウントが増加しています。
ここで理由が即座に説明できる方には、もしかしたらこの記事を読む必要はないかもしれません。
ですが、私と同じような混乱を感じている方は引き続きお付き合いいただけたらと思います。

Classは状態がありますので

上記と全く同じ実装をClassでやってみましょう。

import React, { Component } from "react";
import "./styles.css";

export default class App extends Component {
  constructor(props) {
    super(props);
    this.countCheckHandler = this.countCheckHandler.bind(this);
    this.state = {
      counter: 0,
      madeDom: null
    };
  }

  countCheckHandler() {
    alert(`カウントは${this.state.counter}`);
  }
  componentDidMount() {
    this.setState({
      madeDom: <ChildComp handler={this.countCheckHandler} />
    });
  }
  render() {
    return (
      <div className="App">
        現在カウンターは {this.state.counter}
        <button
          onClick={() => this.setState({ counter: this.state.counter + 1 })}
        >
          カウントUP
        </button>
        {this.state.madeDom}
      </div>
    );
  }
}

class ChildComp extends Component {
  render() {
    return (
      <button
        onClick={() => {
          this.props.handler();
        }}
      >
        子供カウントチェック
      </button>
    );
  }
}

CodeSandBoxはこちら

スクリーンショット 2020-12-17 18.19.30.png

クラスに変えただけなのですが、子コンポーネント経由で増加分を正しく表示できています。
繰り返しになりますが、クラスは内部情報を持ちますが関数は持ちません。
少し真相に近づいてきました。

classコンポーネントが見ているデータ先

実際にthisで確認してみましょう。 渡しているメソッドにconsoleを付け加えます。

  countCheckHandler() {
+    console.log(this);
    alert(`カウントは${this.state.counter}`);
  }

スクリーンショット 2020-12-17 18.23.27.png
意図していた通り親コンポーネントの Appが参照されています。
なので、ハンドラー内で呼び出している this.state.counter は間違いなく Appのcounterが呼び出されています。

【Classでの内部状態考察】

class.png

では関数コンポーネントではどうでしょうか。
【Funcでの内部状態考察】
func.png

このように、生成時のみCounterをコピーしてくる形になるので、その後Appの情報が増えてもChildCompは知らんべ、ということのようです。
なぜなら、関数は内部状態を持たないので(本日n回目)、Appのcounter状態をみる、という芸当はChildComp側はできないわけです。

じゃあuseStateってなんなんだ! propsってなんなんだ! 状態みたいなの保ててるんだけど!?!?!?
と混乱したところで、そもそもHooksで表現しているStateの仕組みって何よ、ってところをおさらいします。

そもそもHooksってどうやって状態を表現しているんだろう。

https://daveceddia.com/intro-to-hooks/#the-magic-of-hooks
上記をぜひ読んでください。
...だけでは味気ないので、ゆる〜く超意訳してみます。

Reactがfunctionコンポーネントを初めてレンダリングする際、オブジェクトを生成します。
このコンポーネントのオブジェクトはDOMに存在し続ける限りずっと生き続けます。
Reactはこのオブジェクトを使用して、色々なメタデータを扱っているわけですね。

また、コンポーネントは自分でレンダリングするのではなく、Reactが呼び出すことでレンダリングされます。
コンポーネント自体は返すものは、DOMノードに変換可能なオブジェクト構造でしかありません。

このReactが呼び出すための準備の際にstateがセットアップされます。

function AudioPlayer() {
  const [volume, setVolume] = useState(80);
  const [position, setPosition] = useState(0);
  const [isPlaying, setPlaying] = useState(false);

}

(コードは記事からそのまま拝借しました)
このように3回useStateが呼び出された場合、Reactは3つの値を配列に入れていきます。
次にレンダーされる場合、この3つのhooksは常に同じ順番で呼び出されます(呼び出し順は常に同じでなくてはならない、というhooksのルールを思い出してください。)そして、新しい状態を作る代わりに、2回目のレンダーではそのポジションにある値を返します。

これがReactが変数がスコープ外の複数の関数の呼び出しがあってもステートを作成・維持できる方法です。
単にオブジェクトであるというのがミソですね。

hooksで起きた問題箇所を詳しく調べる

少しコードサンプルを変えて色々検証してみます。

import React, { useEffect, useState } from "react";
import "./styles.css";

export default function App() {
  const [count, setCount] = useState(0);
  // 1つ目は変数に格納したものを表示させる
  const myChild = <Child count={count} />;

 //2つ目は初回レンダリング次のみ生成し、それを保持する
  const [myChild2, setMyChild2] = useState(null);
  useEffect(() => {
    setMyChild2(<Child count={count} />);
  }, []);
  return (
    <div className="App">
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        UP
      </button>
      <Child count={count} />
      {myChild}
      {myChild2}
    </div>
  );
}

function Child({ count }) {
  return (
    <>
      <p>カウントは{count}</p>
      <button
        onClick={() => {
          alert(count);
        }}
      >
        カウントチェック
      </button>
    </>
  );
}

CodeSandBoxはこちら

わざわざ子コンポーネントに確認用alertのハンドラーを追加しているのは、
レンダリングはされていないが内部の情報は更新されているかも?という疑いを検証するためです。

スクリーンショット 2020-12-18 15.20.39.png

このようにuseEffectで初回レンダリングで生成しているChildコンポーネントのみ、
親のcountを追えていないことがわかります。
もちろん、アラートでの表示も同様でした。

つまりuseEffectで初回のみmyChild2を再計算させているため、
useStateで生成されたオブジェクトが追えていない、ということのようです。
本来であれば
countが変わる
・App内が再計算される
・Childコンポーネントのprops も再計算される
という流れがうまく働いていなかったことが原因でした。

まとめと書簡

今後useEffect内でコンポーネントを生成する場合、
再計算されないこと/そして関数である故に、propsの値などを直に参照できていると思い込まないことに注意していこうと思います。

また、検証に当たってreact内の実装をガツガツ読めるようになった方がより検証しやすいな〜と思ったので、
もっと実装を直でガツガツ読めるようになりたいです..(途中までコード追ってたのですが挫けました)

改善しました

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  const [counter, setCounter] = useState(0);

  const counterCheck = () => {
    alert(`カウントは${counter}`);
  };

  return (
    <div className="App">
      現在カウンターは {counter}
      <button onClick={() => setCounter(counter + 1)}>カウントUP</button>
      <ChildComp handler={counterCheck} />
    </div>
  );
}

function ChildComp({ handler }) {
  return <button onClick={handler}>子供カウントチェック</button>;
}

codeSandBoxはこちら

実際はもっと複雑だったのですが、上記のような形で
レンダリングにChildCompを書き込むことで、問題なくカウンターを呼び出すことができるようになりました。

または、useEffectの第二引数に依存する変数を指定することでも改善できます。

import React, { useEffect, useState } from "react";
import "./styles.css";

export default function App() {
  const [counter, setCounter] = useState(0);
  const [madeDom, setMadeDom] = useState(null);

  const counterCheck = () => {
    alert(`カウントは${counter}`);
  };

  useEffect(() => {
    setMadeDom(<ChildComp handler={counterCheck} />);
  }, [counter]);

  return (
    <div className="App">
      現在カウンターは {counter}
      <button onClick={() => setCounter(counter + 1)}>カウントUP</button>
      {madeDom}
    </div>
  );
}

function ChildComp({ handler }) {
  return <button onClick={handler}>子供カウントチェック</button>;
}

めでたしめでたし。

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

railsとjsを用いてタグ付け機能を実装してみる

railsでタグ付け機能を実装して、後半ではJavaScriptで発展的なタグ付けをしましょう

スクリーンショット 2020-12-18 14.36.29.png

今回は、このようにタグを入力できる機能と、タグを入力するたびに予測変換が下に表示される機能を実装していきたいと思います!

画像で言うと、tagの入力フォームに「酸」と打ったら、下に「酸味」って予測変換的な物が表示されています
ただ、ブラウザが賢いので、ブラウザも予測変換出しちゃってますが、、、笑

下記コマンドを実行

ターミナル

% cd ~/projects

% rails _6.0.0_ new tagtweet -d mysql

% cd tagtweet
データベース作成

データベースを作成する前に、database.ymlに記載されているencodingの設定を変更しましょう。

config/database.yml

default: &default
  adapter: mysql2
  # encoding: utf8mb4
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  socket: /tmp/mysql.sock

んで、データベース作成

ターミナル

rails db:create

Created database 'tagtweet_development'
Created database 'tagtweet_test'

が作成される

データベース設計

tweet と tagは多対多の関係なので、
中間テーブルの
tweet_tag_relationsテーブルを作成するってのがポイント

モデルを作成

ターミナル

% rails g model tweet
% rails g model tag
% rails g model tweet_tag_relation

マイグレーションを編集

db/migrate/20XXXXXXXXXXXX_create_tweets.rb

class CreateTweets < ActiveRecord::Migration[6.0]
  def change
    create_table :tweets do |t|
      t.string :message, null:false
      # messegeカラムを追加
      t.timestamps
    end
  end
end

db/migrate/20XXXXXXXXXXXX_create_tags.rb

class CreateTags < ActiveRecord::Migration[6.0]
  def change
    create_table :tags do |t|
      t.string :name, null:false, uniqueness: true 
      # nameカラムを追加
      t.timestamps
    end
  end
end

今回は、タグの名前の重複を避けるために「uniqueness: true」という制約を設定します。

db/migrate/20XXXXXXXXXXXX_create_tweet_tag_relations.rb

class CreateTweetTagRelations < ActiveRecord::Migration[6.0]
  def change
    create_table :tweet_tag_relations do |t|
      t.references :tweet, foreign_key: true
      t.references :tag, foreign_key: true
      t.timestamps
    end
  end
end

tweet_tag_relationsテーブルでは、「tweetsテーブル」と「tagsテーブル」の情報を参照するので「foreign_key: true」としています。

ターミナル

rails db:migrate
格モデルのアソシエーションを組む

tweet.rb

class Tweet < ApplicationRecord

  has_many :tweet_tag_relations
  has_many :tags, through: :tweet_tag_relations

end

tag.rb

class Tag < ApplicationRecord

  has_many :tweet_tag_relations
  has_many :tweets, through: :tweet_tag_relations

end

tweet_tag_relation.rb

class TweetTagRelation < ApplicationRecord

  belongs_to :tweet
  belongs_to :tag

end
ルーティングを設定しましょう!

routes.rb

Rails.application.routes.draw do
 root to: 'tweets#index'
 resources :tweets, only: [:new, :create]
end

今回のアプリの仕様

何かつぶやくと,「つぶやき(tweet)」と「タグ(tag)」が同時に保存される仕様を目指します。
このような実装をする時に便利なのがFormオブジェクトというものです。

Formオブジェクト

Formオブジェクトは、1つのフォーム送信で複数のモデルを更新するときに使用するツールです。自分で定義したクラスをモデルのように扱うことができます。
このFormオブジェクトは、「ActiveModel::Model」というモジュールを読み込むことで使うことができます。

ActiveModel::Model

「ActiveModel::Model」とは、Active Recordの場合と同様に「form_for」や「render」などのヘルパーメソッドを使えるようになるツールです。
また、「モデル名の調査」や「バリデーション」の機能も使えるようになります。

Fromオブジェクトを導入

まずはmodelsディレクトリにtweets_tag.rbを作成しましょう

app/models/tweets_tag.rbという配置です。

tweets_tag.rb

class TweetsTag

  include ActiveModel::Model
   # include ActiveModel::Modelを記述することでFromオブジェクトを作る
  attr_accessor :message, :name
# ゲッターとセッターの役割両方できる仮想的な属性を作成

# :nameとかt保存したいカラムを書けば、保存できるって理解でまずはok

  with_options presence: true do
    validates :message
    validates :name
  end

  def save
    tweet = Tweet.create(message: message)
    tag = Tag.create(name: name)

    TweetTagRelation.create(tweet_id: tweet.id, tag_id: tag.id)
  end

# saveメソッド内で、格テーブルに値を保存する処理を記述

end

一意性の制約はモデル単位で設ける必要があるため、tagモデルに記述しましょう。

tag.rb

class Tag < ApplicationRecord

  has_many :tweet_tag_relations
  has_many :tweets, through: :tweet_tag_relations

  validates :name, uniqueness: true
end
コントローラーを作成して編集をしましょう

ターミナル

% rails g controller tweets

tweets_controller.rb

class TweetsController < ApplicationController

  def index
    @tweets = Tweet.all.order(created_at: :desc)
  end

  def new
    @tweet = TweetsTag.new
  end

  def create
    @tweet = TweetsTag.new(tweet_params)
    if @tweet.valid?
      @tweet.save
      return redirect_to root_path
    else
      render "new"
    end
  end

  private

  def tweet_params
    params.require(:tweets_tag).permit(:message, :name)
  end

end

「Formオブジェクト」に対してnewメソッドを使用しています。

Fromオブジェクトで定義したsaveメソッドを使ってる

ビューの作成

tweets/index.html.erb

<div class="header">
  <div class="inner-header">
    <h1 class="title">
     TagTweet
    </h1>
    <li class='new-post'>
      <%= link_to "New Post", new_tweet_path, class:"post-btn"%>
    </li>
  </div>
</div>

<div class="main">
  <div class="message-wrap">
    <% @tweets.each do |tweet|%>
      <div class="message">
        <p class="text">
          <%= tweet.message %>
        </p>
        <ul class="tag">
          <li class="tag-list">
            <%tweet.tags.each do |tag| %>
              #<%=tag.name%>
            <%end%>
          </li>
        </ul>
      </div>
    <%end%>
  </div>
</div>

tweets/new.html.erb

<%= form_with model: @tweet, url: tweets_path, class:'form-wrap', local: true do |f| %>
  <div class='message-form'>
    <div class="message-field">
      <%= f.label :message,  "つぶやき" %>
      <%= f.text_area :message, class:"input-message" %>
    </div>
    <div class="tag-field", id='tag-field'>
      <%= f.label :name, "タグ" %>
      <%= f.text_field :name, class:"input-tag" %>
    </div>
    <div id="search-result">
    </div>
  </div>
  <div class="submit-post">
    <%= f.submit "Send", class: "submit-btn" %>
  </div>
<% end %>

CSSは省略!!!

tweets_tag.rbを編集

tweets_tag.rb

class TweetsTag

  include ActiveModel::Model
  attr_accessor :message, :name

  with_options presence: true do
    validates :message
    validates :name
  end

  def save
    tweet = Tweet.create(message: message)
    tag = Tag.where(name: name).first_or_initialize
    tag.save

    TweetTagRelation.create(tweet_id: tweet.id, tag_id: tag.id)
  end

end
    tag = Tag.where(name: name).first_or_initialize

を解説していきます

first_or_initializeメソッドは、whereメソッドと一緒に使います。
whereメソッドは,
モデル.where(条件)のように、引数部分に条件を指定することで、テーブル内の「条件に一致したレコードのインスタンス」を配列の形で取得できます。
引数の条件には、「検索対象となるカラム」を必ず含めて、条件式を記述します。
whereで検索した条件のレコードがあれば、そのレコードのインスタンスを返し、なければ新しくインスタンスを
作るメソッドです

とりあえずこれでタグ付けツイートの実装が完了しました
すでにデータベースへ保存されてるタグをタグ付けしたい場合、入力の途中で入力文字と一致するタグを候補として画面上に表示できる検索機能があれば、より便利なアプリケーションになりそうです

逐次検索機能を実装

逐次検索機能とは、「rails」というタグがすでにデータベースに存在する場合、「r」の文字が入力されると、「r」の文字と一致する「rails」を候補としてリアルタイムで画面上に表示するっていうよくあるやつ
プログラミングで実装するときは** インクリメンタルサーチ**って言われるらしい

それでは実装していきましょう、と言いたいところですが、

application.js

require("@rails/ujs").start()
// require("turbolinks").start() //この行をコメントアウトする
require("@rails/activestorage").start()
require("channels")

上記の行をコメントアウトしないと、jsで設定したイベントが発火しないケースがあるので、コメントアウトしとくのが無難

インクリメンタルサーチ実装の準備

tweets_controller

class TweetsController < ApplicationController

# 省略

  def search
    return nil if params[:keyword] == ""
    tag = Tag.where(['name LIKE ?', "%#{params[:keyword]}%"] )
    render json:{ keyword: tag }
  end


とサーチアクションを定義

LIKE句は、曖昧な文字列の検索をするときに使用するものでwhereメソッドと一緒に使います

%は空白文字列含む任意の文字列を含む

要するに、params[:keyword]で受け取った値を条件に、nameカラムにその条件が一致するか、tagテーブルで検索した物をtagに代入

それをjson形式で、keywordをキーにして、tagを値にしてjsにその結果を返す。

ルーティングを設定

routes.rb

Rails.application.routes.draw do
  root to: 'tweets#index'
  resources :tweets, only: [:index, :new, :create] do
    collection do
      get 'search'
    end
  end
end

ルーティングをネストする (入れ子にする) ことで、この親子関係をルーティングで表すことができるようになります。

collectionとmember

collectionとmemberは、ルーティングを設定する際に使用できます。
これを使用すると、生成されるルーティングのURLと実行されるコントローラーを任意にカスタムできます。

collectionはルーティングに:idがつかない、memberは:idがつくという違いがあります。

今回の検索機能の場合、詳細ページのような:idを指定して特定のページに行く必要が無いため、collectionを使用してルーティングを設定しましょう

tag.jsを作成しましょう

app/javascript/packsはいかにtag.jsを作成しましょう

application.js

をtag.jsを読み込むために以下のように編集しましょう

require("@rails/ujs").start()
// require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require("./tag")

ここまではしっかりカリキュラムやった皆さんなら普通に理解できるはずです、こっからカリキュラムでは説明されてないとこをガッツリ解説します!

tag.js

if (location.pathname.match("tweets/new")){
  document.addEventListener("DOMContentLoaded", () => {
    console.log("読み込み完了");
  });
};

location.pathnameは現在ページのURLを取得、
.matchは引数に渡された文字列のマッチング結果を返す
つまり現在tweets/newにいるときにイベント発火!

documentはhtml要素全体
addEventListenerは様々なイベント処理を実行

DOMContentLoadedはwebページ読み込み完了したときに

つまり、html要素全体が読み込みされたときに、イベントを実行

コンソールに「読み込み完了」と表示されたらok

タグの検索に必要な情報を取得

tag.js

if (location.pathname.match("tweets/new")){
  document.addEventListener("DOMContentLoaded", () => {
    const inputElement = document.getElementById("tweets_tag_name");
    inputElement.addEventListener("keyup", () => {
      const keyword = document.getElementById("tweets_tag_name").value;
    });
  });
};

tweets_tag_nameというidを持ったhtml要素を取得し、InputElementに代入

** ここで注意!!**

form_withによるidの付与

tweets_tag_nameっていったidを持った要素あったっけ??

tweets/new.html.erb

<%= form_with model: @tweet, url: tweets_path, class:'form-wrap', local: true do |f| %>
  <div class='message-form'>
    <div class="message-field">
      <%= f.label :message,  "つぶやき" %>
      <%= f.text_area :message, class:"input-message" %>
    </div>
    <div class="tag-field", id='tag-field'>
      <%= f.label :name, "タグ" %>
      <%= f.text_field :name, class:"input-tag" %>
    </div>
    <div id="search-result">
    </div>
  </div>
  <div class="submit-post">
    <%= f.submit "Send", class: "submit-btn" %>
  </div>
<% end %>

にも、index.html.erbにもそんなidはありません。。。。。

でもなぜ取得できるか?結論からいうと

form_withが勝手にidを付与してくれるから

詳しくいうと、例えば、

form_with model: @tweet

tweets_controller

def new
 @tweet = TweetsTag.new
end

と定義されてあり、

まず、idがtweet_tagになる

そして、

drinks/new.html.erb

      <%= f.label :name, "タグ" %>
      <%= f.text_field :name, class:"input-tag" %>

:nameが

tweet_tag にくっ付いて,tweet_tag_name

ってidが生成されます!!

「どこの誰がいったことか信じられねーよ!!」って意見ももっともなので
実際に検証ツールで form_withによってidが生成されてるかどうか調べます

スクリーンショット 2020-12-13 6.43.28.png

つぶやきをツイートするmessageの場所には

tweets_tag_messagesというidが生成されて、それが、

<%= f.text_area :message, class:"input-message" %>

に付与されます。

tag付けをする場所は

tweets_tag_nameというidが生成されて、それが

  <%= f.text_field :name, class:"input-tag" %>

に付与されます。

form_withによってidが付与される!!!

ってことを頭に入れておいてください

これで、入力フォームが取得できました

変数keywordの中身を確認

app/javascript/packs/tag.js

if (location.pathname.match("tweets/new")){
  document.addEventListener("DOMContentLoaded", () => {
    const inputElement = document.getElementById("tweets_tag_name");
// form_withで生成されたidをもとに入力フォームそのものを取得
    inputElement.addEventListener("keyup", () => {
// 入力フォームからキーボードのキーが離されたときにイベント発火
      const keyword = document.getElementById("tweets_tag_name").value;
// .valueとすることで、入力フォームに入力された値を取り出すことができる
// 実際に入力された値を取得して、keywordに入力
      console.log(keyword);
    });
  });
};

ここまできたら、フォームに何か入力してみましょう。
入力した文字がコンソールに出力できていればokです。

XMLHttpRequestオブジェクトを生成

packs/tag.js

if (location.pathname.match("tweets/new")){
  document.addEventListener("DOMContentLoaded", () => {
    const inputElement = document.getElementById("tweets_tag_name");
    inputElement.addEventListener("keyup", () => {
      const keyword = document.getElementById("tweets_tag_name").value;
      const XHR = new XMLHttpRequest();
    })
  });
};

const XHR = new XMLHttpRequest();は
XMLHttpRequestオブジェクトを用いてインスタンスを生成し、変数XHRに代入しましょう
非同期通信に必要なXMLHttpRequestオブジェクトを生成しましょう。
XMLHttpRequestオブジェクトを用いることで、任意のHTTPリクエストを送信できます。

openメソッドを用いてリクエストを定義

tag.js

if (location.pathname.match("tweets/new")){
  document.addEventListener("DOMContentLoaded", () => {
    const inputElement = document.getElementById("tweets_tag_name");
    inputElement.addEventListener("keyup", () => {
      const keyword = document.getElementById("tweets_tag_name").value;
      const XHR = new XMLHttpRequest();
      XHR.open("GET", `search/?keyword=${keyword}`, true);
    XHR.responseType = "json";
      XHR.send();
    })
  });
};
  XHR.open("GET", `search/?keyword=${keyword}`, true);

openメソッドの第一引数にHTTPメソッド、第二引数にURL、第三引数には非同期通信であることを示すためにtrueを指定しましょう。

なぜこういうURLの指定になるかと言うと,

このURLはqueryパラメーターといって,http://sample.jp/?name=tanakaのように、
「?」以降に情報を綴るURLパラメーターです。
「?」以降の構造は、?<変数名>=<値>となっています。

今回は:idとかでtweetsを識別する必要がないので、queryパラメーターを指定する

drinks#searchを動かしたいのに、searchがなぜURLで省略されてるのか

指定したパスの一個上のディレクトリを基準に,相対的にパスを指定できるから

例えば、今回指定したパスはsearch/keyword=hogehoge
で、一個上のディレクトリはtweetsなので、
一個上のディレクトリを勝手に補完してくれるらしい。。。。

これで、Drinks#searchを動かせる

と、思ったが、

 XHR.responseType = "json";

を書いて、コントローラーから返却されるデータの形式にjson形式を指定しましょう

そして最後!

XHR.send();

を書いて、リクエストを送信しましょう.

タグの入力フォームに何かしら入力されるたびに、railsのsearch アクションが動くといった形になってます!

サーバーサイドからのレスポンスを受け取りましょう

サーバーサイドの処理が成功したときにレスポンスとして返ってくるデータを受け取りましょう。データの受け取りには、responseプロパティを使用します。

tag.js

if (location.pathname.match("tweets/new")){
  document.addEventListener("DOMContentLoaded", () => {
# 省略
      XHR.send();
      XHR.onload = () => {
        const tagName = XHR.response.keyword;
      };
    });
  });
};
const tagName = XHR.response.keyword;

は、サーバーサイドの処理が成功したときに、レスポンスとして返ってくるデータを受け取って変数tagNameに代入してます
データの受け取りにはresponseプロパティを使用します。

 タグを表示させる処理を記述しましょう

スクリーンショット 2020-12-18 14.36.29.png

このように、下に順に表示させていきましょう

タグを表示させる手順は以下の4つです。

1. タグを表示させる場所を取得する

search-resultと言うid名がついた要素を取得しています

  1. タグ名を格納させる場所を取得する。

createElementメソッドを用いてタグを表示させるための要素を生成しています。
生成した要素に検索結果のタグ名を指定しています。

  1. 2の要素にタグを挿入する

2で用意した要素を1の要素に挿入しています。
それぞれinnerHTMLプロパティとappendChildメソッドを用いています。

  1. 2と3の処理を検索結果があるだけ繰り返す

forEachを使って、繰り返し処理を行っています

tag.js

      XHR.send();
      XHR.onload = () => {
        const tagName = XHR.response.keyword;
        const searchResult = document.getElementById("search-result");
          tagName.forEach((tag) => {
          // forEachを使う理由は、railsのsearchアクション
          // で、検索に引っかかったタグを、複数出していく
          // 場合もあるので
          const childElement = document.createElement("div");
          // 2.タグを表示させるための要素を生成してる
          // 名前の通り,要素を作るメソッド


          childElement.setAttribute("class", "child");
          childElement.setAttribute("id", tag.id);
          // 作ったdivタグにclass,idを付与する
          // forEachで作られたローカル変数のtagをここで使ってる

          childElement.innerHTML = tag.tag_name;
          // <div>tagname</div> って感じ
          // innerHTML を使用すると、
          // 中身を入れ替えたり、書き換えたり、入れたりする
          // 3.サーバーサイドから返ってきたtagのtag_name
          // をchildElementの中に入れてくイメージ
           searchResult.appendChild(childElement);
          // htmlのsearch-resultの子要素に
          // childElementが並んでく

          // ここで初めて表示していく

        });
      };
    });
  });
};

new.html.erb

<%= form_with model: @tweet, url: tweets_path, class:'form-wrap', local: true do |f| %>
  <div class='message-form'>
    <div class="message-field">
      <%= f.label :message,  "つぶやき" %>
      <%= f.text_area :message, class:"input-message" %>
    </div>
    <div class="tag-field", id='tag-field'>
      <%= f.label :name, "タグ" %>
      <%= f.text_field :name, class:"input-tag" %>
    </div>
    <div id="search-result">
    </div>
  </div>
  <div class="submit-post">
    <%= f.submit "Send", class: "submit-btn" %>
  </div>
<% end %>

    <div id="search-result">
    </div>

を、

tag.js

  const searchResult = document.getElementById("search-result");

で取得して、上記のような処理をおこなって、何か入力するたび候補を下に表示します

クリックしたタグ名がフォームに入力されるようにしましょう

タグを表示している要素にクリックイベントを指定します。
クリックされたら、フォームにタグ名を入力して、タグを表示してう要素を削除するようにしましょう

tag.js

      XHR.send();
      XHR.onload = () => {
        const tagName = XHR.response.keyword;
        const searchResult = document.getElementById("search-result");
        tagName.forEach((tag) => {
          const childElement = document.createElement("div");
          childElement.setAttribute("class", "child");
          childElement.setAttribute("id", tag.id);
          childElement.innerHTML = tag.name;
          searchResult.appendChild(childElement);
          const clickElement = document.getElementById(tag.id);
          clickElement.addEventListener("click", () => {
            document.getElementById("tweets_tag_name").value = clickElement.textContent;
            clickElement.remove();
          });
        });
      };
    });
  });
};

全体像こんな感じ

          const clickElement = document.getElementById(tag.id);
// さっき生成したタグ入力フォームの下に順に表示されていく、予測変換の欄の要素を取得
          clickElement.addEventListener("click", () => {
// 取得した要素をクリックすると、イベント発火
            document.getElementById("tweets_tag_name").value = clickElement.textContent;
// tweets_tag_nameはform_withで入力フォームに付与されるid
// 入力フォームを取得

// さらに.valueとすることで、実際に入力された
// 値を取得

// clickElementはタグの名前があるので
// .textContentでタグの名前を取得できる

// これでタグの部分をクリックしたら、タグの名前が
// フォームに入ってく
            clickElement.remove();

// クリックしたタグのみ消える

しかし、このままだと同じタグが何度も表示されたままになってしまいます。
この原因は、インクリメンタルサーチが行われるたびに、前回の検索結果を残したまま最新の検索結果を追加してしまうからです。
インクリメンタルサーチが行われるたびに、直前の検索結果を消すようにしましょう。

直前の結果検索を消すようにしましょう

検索結果を挿入している要素のinnerHTMLプロパティに対して、空の文字列を指定することで、表示されているタグを消します。

tag.js

if (location.pathname.match("tweets/new")){
  document.addEventListener("DOMContentLoaded", () => {
# 省略
      XHR.send();
      XHR.onload = () => {
        const tagName = XHR.response.keyword;
        const searchResult = document.getElementById("search-result");
        searchResult.innerHTML = "";

        // 検索結果を挿入してる要素のinnerHTMLプロパティに
        // 対して、空の文字列を指定することで、表示されてる
        // タグを消します

        // 最初にこの処理が呼び出される時は当然何もないので空文字でいいし
        // 2回目に呼び出された時はsearch-resultが空になる
        tagName.forEach((tag) => {
          const childElement = document.createElement("div");
          childElement.setAttribute("class", "child");
          childElement.setAttribute("id", tag.id);
          childElement.innerHTML = tag.name;
          searchResult.appendChild(childElement);
          const clickElement = document.getElementById(tag.id);
          clickElement.addEventListener("click", () => {
            document.getElementById("tweets_tag_name").value = clickElement.textContent;
            clickElement.remove();
          });
        });
      };
    });
  });
};
フォームに何も入力しなかった場合のエラーを解消する

本来、インクリメンタルサーチはフォームに何か入力された場合に動作する想定です。しかし、今回イベントに指定したkeyupは、バックスペースキーなどの「押しても文字入力されないキー」でも発火してしまいます。

その結果、検索に使用する文字列がないため、レスポンスにデータが存在せず、存在しないものをtagNameに定義しようとしているのでエラーが発生してしまいます。
レスポンスにデータが存在する場合のみ、タグを表示させる処理が行われるようにしましょう。

レスポンスにデータが存在しない場合にもtagNameを定義しようとすると、XHR.responseがnullなのでエラーが発生してしまいます。レスポンスにデータが存在する場合のみ、タグを表示させる処理が行われるように修正しましょう。以下のようにif文を用いて解消します。

tag.js

if (location.pathname.match("tweets/new")){
  document.addEventListener("DOMContentLoaded", () => {
# 省略
      XHR.send();
      XHR.onload = () => {
        const searchResult = document.getElementById("search-result");
        searchResult.innerHTML = "";
        if (XHR.response) {
          // イベントに指定したkeyupは、バックスペースキー
          // などの押しても文字入力されないキーでも発火してしまう

          // 存在しないものをtagNameに定義するとエラーが起こる

          // レスポンスにデータがある場合のみタグを表示させる処理を行おう


          const tagName = XHR.response.keyword;
          tagName.forEach((tag) => {
            const childElement = document.createElement("div");
            childElement.setAttribute("class", "child");
            childElement.setAttribute("id", tag.id);
            childElement.innerHTML = tag.name;
            searchResult.appendChild(childElement);
            const clickElement = document.getElementById(tag.id);
            clickElement.addEventListener("click", () => {
              document.getElementById("tweets_tag_name").value = clickElement.textContent;
              clickElement.remove();
            });
          });
        };
      };
    });
  });
};

これで実装完了です。お疲れ様でした。

tag.jsのコードのまとめ
if (location.pathname.match("drinks/new")){
  // location.pathnameは
  // 現在ページのURLのパスを取得、変更
  // .matchは引数に渡された文字列のマッチング結果を返す

  // 現在drinks/new にいる時にイベント発火
  document.addEventListener("DOMContentLoaded",()=>{
    // addEventListenerは様々なイベント処理を実行
    // することができるメソッド

    // documentはhtml要素全体

    // DOMContentLoaded"は
    // Webページ読み込みが完了した時に発動

    // イベント発火する範囲広くね、、、?
    const inputElement = document.getElementById("tweet_tag_name")

    inputElement.addEventListener("keyup",()=>{
      // フォームに入力して、キーボードが離されたタイミング
      // で順次イベント発火していく

      const keyword = document.getElementById("tweet_tag_name").value;
      // テキストボックスの入力した値を取得
      const XHR = new XMLHttpRequest();
      // XHLHttpRequest とはAjaxを可能にするためのオブジェクトでサーバーに
      // HTTPリクエストを非同期で行うことができます

      // インスタンスを生成して、変数に代入する
      XHR.open("GET",`search/?keyword=${keyword}`,true);
      // openはリクエストの種類を指定する
      // 第一引数 HTTPメソッドの指定
      // 第二引数 パスの指定
      // 第三引数 非同期通信のON/OFF

      // GETリクエストで、
      // ?でパラメーターを渡せる
      // ?keywordはキーで、${keyword}が値

      // queryパラメーターとは、http://sample.jp/?name=tanakaのように、
      // 「?」以降に情報を綴るURLパラメーターです。
      // 「?」以降の構造は、?<変数名>=<値>となっています。
      // ?文字列とかの検索をかけたい時に使う

      // サーチアクションを動かしたい
      // drinksが省略されてる理由は
      // 指定したパスの一個上のディレクトリを基準に
      // 相対的にパスを指定できる



      // とりあえず、drinks#searchにリクエストを送って
      // 予測変換したい

      XHR.responseType = "json";
      // コントローラーから返却されるデータの形式には
      // jsと相性がよく、データとして取り扱いやすい
      // json形式を指定してる
      XHR.send();
      // tag.jsからサーバーサイドに送信したい
      // リクエストを定義できたので、
      // 送信する処理を記述しましょう
      XHR.onload = () => {


        const searchResult = document.getElementById("search-result");
        // 1.タグを表示させる場所である,search-resultを取得
        searchResult.innerHTML = "";
        // 同じタグが何度も表示されたままになってしまう
        // 直前の検索結果を消したい

        // 検索結果を挿入してる要素のinnerHTMLプロパティに
        // 対して、空の文字列を指定することで、表示されてる
        // タグを消します

        // 最初にこの処理が呼び出される時は当然何もないので空文字でいいし
        // 2回目に呼び出された時はsearch-resultが空になる
        if (XHR.response){
          // イベントに指定したkeyupは、バックスペースキー
          // などの押しても文字入力されないキーでも発火してしまう

          // 存在しないものをtagNameに定義するとエラーが起こる

          // レスポンスにデータがある場合のみタグを表示させる処理を行おう

          const tagName = XHR.response.keyword;
          // サーバーサイドの処理が成功した時に
          // レスポンスとして返って来るデータを
          // 受け取って,変数に代入

          // データの受け取りには
          // responseプロパティを使用する

          tagName.forEach((tag) => {
          // forEachを使う理由は、railsのsearchアクション
          // で、検索に引っかかったタグを、複数出していく
          // 場合もあるので
          const childElement = document.createElement("div");
          // 2.タグを表示させるための要素を生成してる
          // 名前の通り,要素を作るメソッド


          childElement.setAttribute("class", "child");
          childElement.setAttribute("id", tag.id);
          // 作ったdivタグにclass,idを付与する
          // forEachで作られたローカル変数のtagをここで使ってる

          childElement.innerHTML = tag.tag_name;
          // <div>tagname</div> って感じ
          // innerHTML を使用すると、
          // 中身を入れ替えたり、書き換えたり、入れたりする
          // 3.サーバーサイドから返ってきたtagのtag_name
          // をchildElementの中に入れてくイメージ
           searchResult.appendChild(childElement);
          // htmlのsearch-resultの子要素に
          // childElementが並んでく

          // ここで初めて表示していく

          const clickElement = document.getElementById(tag.id);
          // クリックしたタグ名がフォームに入力されるようにしたい

          // 入力していったら,id = tag.idのdivのhtml要素
          // ができているはずなので、それを取得
          clickElement.addEventListener("click",()=>{
            // clickElement要素をクリックした時にイベント発火
            document.getElementById("tweet_tag_name").value = clickElement.textContent;
            // form_withで作られたidの要素を取得  
            // さらに.valueとすることで、実際に入力された
            // 値を取得

            // clickElementはタグの名前があるので、
            // .textContentでタグの名前を取得できる

            // これでタグの部分をクリックしたら、タグの名前が
            // フォームに入ってく
            clickElement.remove();
            // クリックしたタグのみ消える
          });
          });
        };
      };
    });
  });
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript,10,switch文,case,default,break,初心者向け

10,switch文
JSでのswitch文の書き方を解説しています。
case(ケース)、default(デフォルト)、break(ブレイク)のそれぞれの意味と実際のコーディング例も紹介しています。
if, else ifなどを多く使う場面においては重宝しますので、是非覚えていきましょう!

const weathers = ["晴れ", "曇り", "雨", "雪"],
  randomNumber = Math.floor(Math.random() * weathers.length),
  randomWeather = weathers[randomNumber];
  WeathersMessageElement = document.getElementById("WeathersMessage");

document.getElementById("weather").textContent = "今日の天気は" + randomWeather + "です";

↓ここから

if (randomWeather === "晴れ") {
WeathersMessageElement.textContent = "日傘を持って行った方がいいかも";
} else if (randomWeather === "曇り") {
  WeathersMessageElement.textContent = "今日は一日曇っているみたいです";
} else if (randomWeather === "雨") {
  WeathersMessageElement.textContent = "傘を持って行きましょう";
}else if (randomWeather === "雪") {
  WeathersMessageElement.textContent = "今日は寒そうなのでコートを着て行きましょう";
}

↑ここまではswitchできる

function insertTextWeatherMessage(text) {
  WeathersMessageElement.textContent =text;
}


switch (randomWeather) {
  case "晴れ":
    insertTextWeatherMessage = ("日傘を持って行った方がいいかも");
    break;
  case "曇り":
    insertTextWeatherMessage = ("今日は一日曇っているみたいです");
    break;
  case "雨":
    insertTextWeatherMessage = ("傘を持って行きましょう");
    break;
  case "雪":
    insertTextWeatherMessage = ("今日は寒そうなのでコートを着て行きましょう");
    break;
    default:
      insertTextWeatherMessage = ("今日はやばい");
}

randomWeather === 下のcaseのどれかになりなら実行される
default:はそれ以外

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

Azure環境プリザンターでDBから日時を取得してみた

はじめに

弊社でも利用させていただいているプリザンターの「Advent Calendar 2020」 が盛り上がっているのを拝見し、Qiita初投稿ながら参加させていただきました。

今回は、Azure環境のプリザンターで拡張SQLを実装したときのことを紹介します。
初めて拡張SQLを使ってみたとき少し躓いたので、これを読んでいる方の参考になれば幸いです。

やりたいこと

Azure SQL Databaseから現在日時を取得し、プリザンター上の項目に出力する。

①拡張SQLを用意

開発者向け機能:拡張機能:拡張SQL

まず、Azure SQL Databaseから日時を取得するための、以下2つのファイル「GetDateDB.json」と「GetDateDB.json.sql」を用意します。

GetDateDB.json
{
    "Name": "GetDateDB",
    "Description": "日時を取得します。",
    "SiteIdList": null,
    "IdList": null,
    "Api": true,
    "OnCreating": false,
    "OnCreated": false,
    "OnUpdating": false,
    "OnUpdated": false,
    "OnDeleting": false,
    "OnDeleted": false,
    "OnBulkDeleting": false,
    "OnBulkDeleted": false,
    "OnImporting": false,
    "OnImported": false,
    "OnSelectingWhere": false,
    "CommandText": ""
}

Azure SQL DatabaseはUTC日時のため、日本時間に合わせるため+9時間してます。

GetDateDB.json.sql
select dateadd(hour, 9, getdate());

②Azure上に拡張SQLファイルをアップロード

「App Service」→「高度なツール」→「移動」で Kudu を開く
高度なツール.png

「Debug console」→「CMD」を開く
CMD.png

以下のファイルパスへ移動
「D:\home\site\wwwroot\App_Data\Parameters\ExtendedSqls>」

①で作成した2つのファイルをアップロード。
GetDateDB.png

これで拡張SQLの設定は整いました。
※一度App Serviceを再起動しないと設定ファイルが反映されないのでお忘れなく

③プリザンター上にスクリプトの登録

開発者向け機能:APIから拡張SQLを実行する

あとはAPI実行で、②の拡張SQLを実行するだけです。

getdbDate.js
var DateData = {
    "ApiVersion": 1.1,
    "ApiKey": ApiKey,       //ApiKeyを設定
    "Name": "GetDateDB"
}

$.ajax({
            type:"post", 
            url:"/api/extended/sql",                    // APIのパス
            data:JSON.stringify(DateData),              // JSONデータ本体
            contentType: 'application/json',            // リクエストの Content-Type
            dataType: "json",                           // レスポンスをJSONとしてパースする
        }).done(function(DateData1) { 
            var DateData2 = JSON.stringify(DateData1);  //JavaScriptオブジェクトをJSONに変換
            var objData = JSON.parse(DateData2);

            if (objData.StatusCode == 200){
                var dbDate = objData.Response.Data.Table[0].Column1; 
                $p.set($('#Results_ClassA'), dbDate);   // 結果を出力
            }
        })

④実行結果

Azure SQL Databaseから取得した日時が、プリザンター上の項目(分類A)へ出力されることを確認!
実行結果.png

おわり

今年はプリザンターに出会い学びの多い年となりました。
他サービスとの連携など色々試して、今後もQiitaに投稿していきたいと思います。

みなさま、良い年末年始をお過ごしください。

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

結局フロントエンドエンジニアってなんなんでしょうかっ!

はじめに

こんにちは。
プラコレでフロントエンド(&たまぁにサーバーとか)を担当しているエンジニアのタクシです。

今回のテーマはそもそも、雪が見たい!と思いThree.jsで雪を降らせてみようとか思っていたのですが、ちょうど一昨日パラパラとですが鎌倉でも雪が降ったので私の雪欲は満たされました。
豪雪で困っている方も多いということでもう雪なんて見たくない!と言われても仕方がないので、テーマを変えることに。。

ということで、来年には駆け出し新卒エンジニア三年目を迎えるタクシ的
「フロントエンドエンジニア」ってなんなんだ?
ということについて考えたいと思います。

フロントエンドとは

フロントエンド(Front-end)とはフロント(Front)が英語で「表」という意味であるように、
ユーザー(今読んでいるあなた)の目にふれ、実際に触れられる表側、つまりは顔の部分のことを一般的には指します。
フロントエンドではその顔を作り、様々な情報を提供する場(WEBサイトなど)を作ったり、
時として、データなどの情報をわかりやすく形を変えてあげなければいけない場合もあります。

例えばこんな感じ(イメージ)
Screen Shot 2020-12-18 at 10.33.09.png
https://pla-cole.co

上の図のデータは適当ですが、
例えば図のように、生のデータの形式(JSON)のままだと非常にわかりづらいです。
なのでそれを作成されたデザインなどに合わせて、形を変え、結果としてWEBサイトとして反映します。
ざっくりとした説明ですが、おおよそこのようなデータの整形や、
そこから派生した関連の物事を全般的に担当します。
その時に主に基本となるのが以下3つです。

HTML
CSS
JavaScript

特にJavaScriptには開発を支援するためのライブラリやフレームワークが多く存在し、
その根本となる深い言語知識はフロントエンドエンジニアとは切っても切り離せない、とても重要なものです。なのでそれを中心にお話ししたいと思います。

JavaScript

私は毎日フロント業務で、JavaScriptを書かない日はありません。
起床から就寝までそれしかしてません(嘘です)。
(くどくなりそうなので以降 JS とします)
ですが、それぐらいフロントエンド開発では中心となる技術なのは確かです。
シンプルなランディングページやHPでもだいたいはJSが使用されています。
使用目的は数多くあります。
・クリックやスクロールに応じたアニメーション
・データのグラフ化
・フォーム送信時のバリデーション
・Ajaxなどを使用した非同期通信
etc...

あくまで一部ですがこのように使用用途は多くあります。
アニメーションやグラフなどの生成はUI・UXの観点でも非常に重要であり、
フロントエンドで完結する場合も多く、実装も比較的しやすいです。

ただ少しトリッキーなのは、Ajaxなどを使用な非同期通信を行う場合などの、サーバーとの関係が出てくる部分です。
サーバーサイドとは言わずとも、通信などに関する基礎知識は最低限は必要なのです。

非同期通信(Ajax)

非同期通信とはものすごーーく簡単に言えば、画面をリロードしなくてもデータの送信や受信を可能とするものです。
主にAPI(Application Programming Interface)を使用し、
サーバー側から必要なデータを取得(GET)したり、送信(POST)したりできます。
APIを使いたい場合、XMLHttpRequestオブジェクトを使い、サーバーとやりとりをするのですが、
jQuery ajaxaxiosSuperAgentなど記述を簡易化できるライブラリもあります。

データを取得するためGETでAPIにリクエストを送信した場合、APIは以下のような形式(JSON形式)でデータを返します。
このような⇩
Screen Shot 2020-12-18 at 10.25.41.png

以下の1行を例として説明します

"title": "冒険社プラコレ"

"title" 
keyと呼ばれるものです。サーバー側で名前を設定できることも多いです。

"冒険社プラコレ" 
valueと呼ばれるものです。上記のkeyが表すもの(値)です。

これらはkey-valueペアと呼ばれます。
既存のサービスで決まった形のAPIしか場合は除きますが、自社開発などの場合、
サーバーサイドと連携し、必要な値を返してもらうように依頼したり、
オブジェクトや配列などの形式の変更などをしてもらうこともあります。
私は理想の形を簡単なサンプルコードを書いて修正依頼などは送るようにしています。

このような一見直接的に見た目に関わらない部分でも、
最終的にWEBアプリケーション(WEBサイトやWEBサービスなど)の開発をする上で必要なデータ等のハンドリングやサーバーサイドとのコミュニケーションもフロントエンドということになります。

最近ではReactVue.js、そしてそれらのフレームワークであるNext.jsNuxtJSなどが登場し、JSをメインとしたフロントエンド開発が発展しています。
私も最近はもっぱらReactやGatsby(Reactフレームワーク)をこねくり回しています。

まとめ

ここまで色々話しましたがもちろん、HTML・CSSのコーディングが中心の方や、APIの設計から自分でやるという方もいると思います。
フロントエンドといってもその内容は関わるアプリケーションや会社などによって内容も異なる場合もあるので一概にこれだ!ということは難しいですね。
それはサーバーサイドも同じで、
どこまでがフロントでどこまでがサーバー
かはそれぞれで違います。
なので今回は私個人の思うフロントエンドエンジニアは何をしているのか
というよりは私が普段「フロント」として何をしているのか
という話でした。

Now hiring!
プラコレでは、自由な未来をつくるために
一緒に冒険したいエンジニア・デザイナーを募集しています!
https://www.wantedly.com/projects/262436
運営サービス
PLACOLE(プラコレウェディング)
DRESSY(ドレシー)byプラコレ
farny(ファーニー)byプラコレ

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

FirebaseのonSnapshotを使ったときに起きた、serverTimestampがnullになる現象について

React+TypeScriptでアプリを開発している時のこと。

「ユーザーがformに入力した内容をリアルタイムに画面にに反映したいな」と思い(チャットの様なものだと思ってください)、調べているとget()ではなく、onSnapshot()を使えばFirestore(db)に保存されたデータを即時画面に反映できると知り、使ってみた結果即時反映させることには成功したものの、なぜかserverTimestamp()がnullになるエラーに直面しました。

その時の対処法の解説になります。

コードの例

以下は上手く行った時のReactのコード例ですが、他のフレームワークやライブラリでも同じ様な感じだと思います。

CommentForm.tsx
  useEffect(() => {
    let comments: any = [];
    const unsubscribe: any = db
      .collection("posts")
      .doc(id)
      .collection("comments")
      .orderBy("createdAt", "desc")
      .onSnapshot((snapshots) => {
        snapshots.docChanges().forEach((change) => {
          const data = change.doc.data({ serverTimestamps: "estimate" });
          const changeType = change.type;
          const date = data.createdAt.toDate();

          switch (changeType) {
            case "added":
              comments.push({ ...data, createdAt: date });
              break;
            case "modified":
              const index = comments.findIndex(
                (comment: any) => comment.id === change.doc.id
              );
              comments[index] = comment;
              break;
            case "removed":
              comments = comments.filter(
                (comment: any) => comment.id !== change.doc.id
              );
              break;
            default:
              break;
          }
        });
        setComment(comments);
        unsubscribe();
      });
  }, []);

解決した方法

解決した方法はかなり簡単でした。。。

doc.data({ serverTimestamps: "estimate" }).createdAt

解説

FirebaseでgetやonSnapshotを使ってデータを呼び出した際に、

snapshots.docs.forEach(doc => {...})

の様な感じで、データを取り出すと思います。

この後に、doc.data()とすると一つ一つのデータのオブジェクトが取得できます。

しかし、

doc.data().createdAt

とすると、最新(追加した瞬間)のデータがnullになってしまうのですが、

doc.data({ serverTimestamps: "estimate" }).createdAt

とすることで、追加した瞬間のnullの状態のtimestampに対して、timestampを推定して、確定するまでとりあえず仮のTimestampを入れておいてくれます。

Firebaseの公式ドキュメントを見ていただけたらわかると思いますが、このdoc.data()data()に関して以下の様な解説がされています。

ドキュメント内のすべてのフィールドをオブジェクトとして取得します。文書が存在しない場合は 'undefined' を返します。デフォルトでは、まだ最終的な値に設定されていない FieldValue.serverTimestamp() の値は null として返されます。これをオーバーライドするには、オプションオブジェクトを渡す必要があります。

今回のエラーも加味して簡単に言い換えると、、、

data()でオプションを何も指定していない場合は、FieldValue.serverTimestamp()最終的な値を決定するまでnullを返します。nullが返されたくないなら、オプションを指定すればなんとかできます。

といったところでしょうか。

ドキュメントを見てみると、data()のオプションは2つある様です。

オプション 説明
estimate 保留中のサーバータイムスタンプはローカルクロックに基づいた推定値を返します。この推定値は最終的な値とは異なり、サーバーの結果が利用可能になると、これらの値が変更されます。
previous 保留中のタイムスタンプは無視され、代わりに以前の値を返します。
none 省略された場合や 'none' に設定された場合は、サーバの値が利用可能になるまでの間、デフォルトで null が返されます。

こんなところに大ヒントが書いてありましたね?

答えは公式ドキュメントに?

Firebaseの公式ドキュメントはやはりかなり充実していて、大抵のfirebase関連の問題はドキュメントに書いてあると思いました?

今後はFirebaseを使うときはわからなかったらドキュメントをしっかり読む、という習慣を身につけようと思いました。

参考

公式ドキュメント

公式ドキュメントのdataのページ

公式ドキュメントのdataのoptionのページ

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

【HTML Form】ポップアップウィンドウへ結果を表示させる場合の button タグ 注意点

発生した事象

 検索結果をポップアップウィンドウに表示する際、Javascriptでポップアップウィンドウを作り、target をそのウィンドウに合わせるという常套手段を書いていたのですが、結果が呼び出し元のウィンドウにも同時に表示されるという状態になってしまった。具体的にはこんな感じです。

image.png
(大きなウィンドウが呼び出し元)

 後ほど紹介する検証コードで、解決はみているのですが、なんでこんなシンプルな話が上手く行かないのかハマってしまいました。

検証環境

xampp (Windows) 7.2.34
Egde 87 , Chrome 87 , Firefox 84

結論

  • button タグを使う場合 formtarget 属性が必要
  • input タグは特に属性を必要とせず想定通りの挙動となる

検証コード

abc.php
<?php
  // フォームからPOSTで受け取った場合は、OK! というテキストだけを表示する。
  if(isset($_POST['popup'])){
    echo 'OK!';
    exit;
  }
?>

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>input VS button</title>
  <script>
    function result_pop() {
      var frmPost = document.getElementById('frmpost');
      var printform = window.open('about:blank','pop','width=300,height=200');
      frmpost.target = 'pop';
      frmpost.submit();
      printform.focus();
      frmpost.target = "";
    }
  </script>
</head>
<body>
  <!-- 以下 formタグ action のURL に #result とあるのは、製品のコードにそう書いてあったため。
       書いてなくても挙動に変化はありませんでしたが、一応記述。 -->
  <form id="frmpost" name="frmpost" action="./abc.php#result" method="post">
    <input type="hidden" id="popup" name="popup" value="dummy">
    <input  onClick="result_pop();" type="button" value=" [A] input tag. "> inputタグの利用<br>
    <button onClick="result_pop();"> [B] button tag only. </button> buttonタグの利用 属性なし<br>
    <button onClick="result_pop();" formtarget="pop"> [C] button tag and formtarget attr. </button> buttonタグ と formtarget 属性の利用.
  </form>
</body>
</html>

 結論にも書いたとおり、input タグ([A]のボタン)は、もともと想定したように、ポップアップウィンドウに結果が表示されます。

 ハマったのは[B]のタイプでボタンを作っていたからでした。この時、冒頭のキャプチャ画面にあるように、同じ結果が呼び出し元画面、ポップアップ画面の2画面とも表示されてしまう。
 buttonタグで実装する場合は[C]ボタンのように、formtarget 属性を付加し、ポップアップするウィンドウに付けたターゲット名を指定することで解決です。
 上記のコードでは、"pop"がそれにあたります。

以上

言い訳と反省

 HTML5以前の古いコードをメンテしていながら、buttonタグを使って簡易検証してたのが良くなかった。検証はHTML5以前を意識しないとダメなのが反省と糧ですね。

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

Javascript(JavascriptでHTMLフォームに入力された文字を取得し表示)

目的

テキストフォームに入力されたデータを取得する。

そして、[フォームに入力された値] を検索中です。 と画面に表示させる。

コード

HTML

form.html
<form action ="#" id ="form">
  <input type ="text" name ="word">
  <input type ="submit" value ="送信">
</form>

<p id="output"></p>

Javascript

app.js
  use 'strict';

  document.getElementById('form').onsubmit = function(event){
  event.preventDefault();
  const input = document.getElementById('form').word.value;
  document.getElementById('output').textContent = `${input}の検索中です。`;
  };

解説

1行目

app.js
  document.getElementById('form').onsubmit = function(event){
  };

formというidを持ったフォームが送信された時に処理を実行させるという意味。

2行目

app.js
  event.preventDefault();

formタグの次のページにいくという基本動作を無効にするためイベントオブジェクトのpreventDefaultメソッドを使用。

3行目

app.js
  const input = document.getElementById('form').word.value;

name属性がwordのインプットフォームに入力された値を取得して、定数 input に格納。

4行目

app.js
  document.getElementById('output').textContent = `${input}の検索中です。`;

idがoutputの要素に フォームに入力された値である定数inputと共に表示。

引用

確かな力がつくJavaScript超入門 狩野 祐東

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

1分で理解!Firestoreのサブコレクション

*この記事ではJavaScriptで書いていきます。

NoSQLについてはこちらの記事へ↓
https://qiita.com/Yu-kiFujiwara/items/c1c52495fd321373c544

そもそもfirestoreってどういう形で保存するん?

firestoreはNoSQLの中でもドキュメント型と言われ、JSON形式のまま保存することができます。
firestoreではコレクション、ドキュメント、フィールド、データという4つの情報、
Key-Valueペアで構成されるデータを持つフィールド、フィールドを複数持つドキュメント、ドキュメントをまとめたコレクションという形でデータが構成されており、スキーマを決める必要がないため、自由にデータを保存することができます。

スクリーンショット 0002-12-18 8.36.52(2).png

//書き込み
firebase.firestore().collection("auther").set({name: "太郎", age: 20})

//読み込み
firebase.firestore().collection("auther").where("name", "===", "太郎").where("age", "===", 20)

//更新
firebase.firestore().collection("auther").doc(1).update({age: 21})

サブコレクションってなんなん?

また、firebaseにはサブコレクションという考え方もあり、
1つのコレクションにリレーションを組むような形でコレクションを持たせることができます。
DB構成は下記の通りです。

スクリーンショット 0002-12-18 9.01.21(2).png

autherコレクションがpostコレクションを保有するという、
より現実に近いような形でDBを構成することができます。

//書き込み
firebase.firestore().collection("auther").doc(1).collection("post").set({text: こんにちは})

//読み取り
firebase.firestore().collection("auther").doc(1).collection("post").where("text", "===", "こんにちは")

//更新
firebase.firestore().collection("auther").doc(1).collection("post").doc(1).update({text: こんばんは})

どう設計すべきか

・リレーションを組んでおらず、joinリクエストができないので一度にデータを取得できるようにデータを持つ
・データを重複して持つことを許容する
・データを更新する場合は重複したデータに対してそれぞれ更新リクエストをかける

RDBではNGとされている、これらのデータの持たせ方、更新の仕方が推奨されています。

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

ExpoとReact Native Webを使ってコードを書かずに作ってみるPWA入門

この記事はReact Native Advent Calendar 2020 の18日目の記事です。

こんにちわ、最近さっぽろでコツコツ生きてるたかぎと申します。
人生初のアドベントカレンダーだったにもかかわらず当日まで何を書こうかまったく考えてなかったのは僕のせいじゃないと思います。

はてさて、今回は Expo と React Native Web という技術を使ってこれからのウェッブの時代の理想形である PWA(Progressive Web Application)をさくっと作ってみようという入門記事です。ほとんどコードは書きません。コードを書かなくても一応 PWA が作れるというめちゃくちゃお得な構成になっています。読んで損はないはずですです。

言い訳ですが、間違っている個所がたぶんにあると思いますがご了承ください。あと、内容があっさりしすぎているのは決して忙しいことを理由にまったく記事に手を付けていなかったからでもないです。たぶん

TL;DR

コード自体を知りたい方はこちらのリポジトリを参照してください。

準備

まず Expo CLI をインストールします。

Expo CLI は Expo SDK を使って React Native アプリケーションの開発を行うのに必要なツールです。今回は PWA のみにターゲット絞って説明をしていくため本来のネイティブアプリの開発に必要な設定だったりの詳しい説明は省きます。

Expo SDK と JavaScript や TypeScript を組み合わせて iOS や Android 向けにアプリを開発し、Expo Client App を使って iPhone や Android スマートフォンの上で、開発したアプリを実行するためにはこの Expo CLI を使ってプロジェクトを外部に公開する必要がありますってことだけ覚えていてもらえれば良いです。あとは一番大事かもしれませんが、Expo CLI を使えば iOS と Android 向けのビルドが簡単にできるってこともテストに出ると思います。

$ npm install --global expo-cli

CLI のインストールが完了したら、プロジェクトを作成します。
おそらく土のテンプレートを使って開発を進めるか聞いてくると思います。

このとき上にアプリの名前やアプリ上での現在滞在しているスクリーンのタイトルを表示するヘッダーとか、ホームや設定といったアイコンを表示するボトムタブナビゲーションを最初から導入して開発したい場合はtabs (TypeScript)を選択してください。見た目も THE アプリという感じになってくれるので今回はこちらを選択します。

ちなみに、expo はなにも指定しなければ yarn を使って開発していくことになりますので、npm を使いたい場合は --npm オプションを渡してコマンドを実行してください。今回は yarn を使ってそのまま開発を進めます。

# yarn
$ expo init pwa-sample

# npm
$ expo init pwa-sample --npm

expo_init.png

expo_init_done.png

プロジェクトの作成が完了したら Your project is ready!と表示されますので、プロジェクトに移動します。

$ cd pwa-sample

Hello World を表示する

コードはほとんど書かないと言ったものの、なんかプログラミングぽいことしたいと思いました。なので最初に画面に表示される文言をすこし変えてみましょう。

screens/TabOneScreen.tsx を開きます。このファイルから export されているコンポーネントである TabOneScreen は navigations/BottomTabNavigator.tsx ファイルの BottomTabNavigator に最初の Screen として登録されている TabOneNavigator の最初のスクリーンとして登録されているので、最初にアクセスした際に一番初めに表示される画面の役割を担います。

Text コンポーネントに挟まれている Tab One という文字列を、Hello World に置き換えます。

export default function TabOneScreen() {
    return (
        <View style={styles.container}>
            <Text style={styles.title}>Hello, World</Text>
            <View
                style={styles.separator}
                lightColor="#eee"
                darkColor="rgba(255,255,255,0.1)"
            />
            <EditScreenInfo path="/screens/TabOneScreen.tsx" />
        </View>
    );
}

実際に表示されるか確認してみます。

$ yarn expo run start:web

Hello, World と表示されたらうまくいった証です。

success.png

オフラインで動くようにする

ここからが本番です。アプリをインストール可能にするためにはインターネットにつながっていなくてもアプリがきちんと動けるようにしなくてはいけません。せっかくアプリをインストールしたのに真っ暗な画面しか表示されなかったりしたら悲しいですもんね。

Expo が自動的に作る Webpack の config には PWA の開発に必要なプラグインなどの記述がすでに設定されています。ですが、デフォルトではオフラインでのサポートのみオフ(false)になっています。
なので、これをオン(true)にしてあげることによってインストール可能な状態にしてあげます。

はじめに Webpack のコンフィグファイルをイジェクトします。

イジェクトとは

イジェクトを説明する前に、ひとつだけ。
先ほどインストールした expo-cli、モバイルデバイスにインストールされる Expo Client App、そしてpush notificationsbuild serviceOTA(over-the-air)を通して React Native のアプリを開発することは managed workflow と呼ばれています。なぜ Managed Workflow かというと、iOS や Android アプリを開発しようとするとどうしても関わってくる複雑な設定や手続きを Expo が代わりに補ってくれるから Managed Workflow という名前が付けられています。
このおかげで Xcode や Android Studio を使わなくても JavaScript でコードを書いてとアプリのアイコンとスプラッシュ画像(起動時に表示される画像)さえ用意すればいとも簡単に iOS と Android 向けにアプリを開発することができるというわけです。

ただ、Managed Workflow では完全なデバイスやサービスに対するコントロールを得られることができず、制約の中で開発を行っていかないといけません。これによって Firebase が提供してくれているサービスをフルに使うことができなかったりします。

ただ、現在では Firebase のほとんどの機能を Managed Workiflow でもサポートされてきていますので、その点についてはあまり心配しなくてもいいように思います。

なのでその制限をとっぱらい、Expo の管理からの脱却を完全な制御を得る行為を Ejecting to Bare Workflow とといます。完全な制御を得た状態のことを、Managed Workflow の対極として Bare Workflow と名付けられています。

そして開発者が意識なくてもいいように Expo が自動的に設定してくれるコンフィグファイルの中には Webpack も存在するのですが、今回オフラインでのサポートを可能にするために手動で Webpack を設定しないといけません。そのために、webpack.config.js ファイルのみの eject を行いたいと思います。

WebpackのWorkboxプラグインがPWAに必要なServiceWorkerをビルド時に自動で作成してくれます。

下記のコマンドを実行します。実行すると、スペースバーで選択可能なラジオボタンの一覧がリストで表示されます。その中からwebpack.config.jsを選択してください。

$ expo customize:web

プロジェクトのルートフォルダにイジェクトされた webpack.config.js ファイルの修正を行い、offline: truecreateExpoWebpackConfigAsyncメソッドにオプションとして与えます。スペースバーで選択したらエンターキーを押下します。

// /webpack.config.js

const createExpoWebpackConfigAsync = require("@expo/webpack-config");

module.exports = async function (env, argv) {
    const config = await createExpoWebpackConfigAsync(
        {
            ...env,
            offline: true,
        },
        argv
    );
    // Customize the config before returning it.
    return config;
};

manifest.json ファイルと app.json ファイル

PWA としてインストール可能なアプリを開発するには manifest.json ファイルの存在が欠かせません。ただ、managed workflow を使って開発を行う場合は manifest.json ファイルを自分で作らなくても app.json ファイルに記述した内容がビルドしたときに manifest.json ファイルの内容として登録しらもらうことができます。

キー 説明
favicon ファビコン
backgroundColor スタイルシートが読み込まれる前に表示する背景色。つまり、アプリの起動時にスプラッシュ画像の背景として表示される色
description アプリの説明
dir 書字方向
display アプリの表示モード
lang アプリの主言語
name アプリの名前
orientation 画面の向き
scope PWAとして認識されるナビゲーションスコープ
shortName アプリの名前の略称
startUrl アプリの起動時に表示されるURL
themeColor アプリのテーマカラー
preferRelatedApplications 今回はPWAを前提に進めているのでfalseで大丈夫

上記の内容は expo.web の中に記述します。

{
    "expo": {
        "name": "PWA サンプル",
        "slug": "pwa-sample",
        "version": "1.0.0",
        "orientation": "portrait",
        "icon": "./assets/images/icon.png",
        "scheme": "pwa-sample",
        "userInterfaceStyle": "automatic",
        "splash": {
            "image": "./assets/images/splash.png",
            "resizeMode": "contain",
            "backgroundColor": "#ffffff"
        },
        "updates": {
            "fallbackToCacheTimeout": 0
        },
        "assetBundlePatterns": ["**/*"],
        "ios": {
            "supportsTablet": true
        },
        "android": {
            "adaptiveIcon": {
                "foregroundImage": "./assets/images/adaptive-icon.png",
                "backgroundColor": "#FFFFFF"
            }
        },
        "web": {
            "favicon": "./assets/images/favicon.png",
            "backgroundColor": "#ffffff",
            "description": "PWAのサンプル",
            "dir": "auto",
            "display": "standalone",
            "lang": "ja",
            "name": "PWA サンプル",
            "orientation": "portrait",
            "scope": "/",
            "shortName": "サンプル",
            "startUrl": "/",
            "themeColor": "#ffffff",
            "preferRelatedApplications": false
        }
    }
}

PWA としてアプリを起動する

これでインストール可能な PWA の準備が終わりました。それでは早速ビルドしてみましょう。

expo build:web

ビルドが成功したら、ためしに以下のコマンドを実行して localhost 上でアプリを起動してみます。

npx serve web-build

実は、オフラインでアプリが動いてくれる最大の要因は裏で動いてくれている Service Worker です。詳しい説明は省きますが、この Service Worker が動いてくれる条件がセキュリティ上の理由で https といったセキュアなオリジンのみとなっています。ただし、localhost からのアクセスについてはこの限りではないため、今回は localhost 経由でアクセスを試みたいと思います。

localhost:5000 にアクセス。インストール可能な場合はプラスアイコンがURLバーに表示されています。

install_1.png

インストールして、起動してみましょう。

install_2.png

app.png

上記の画面が表示されれば成功です!!!

過去に localhost:5000 で何かを実行していて Service Worker 等が動いていた場合はその時のアプリが表示されている可能性があるので、dev tools を開いて、Application タブから Clear Site Storage を選んで Clear Site Data をクリックする。何回か更新をかければ今回作ったアプリが表示されます。

まとめ

いかがだったでしょうか?Expo と React Native Web を使えばほとんどというかまったくコードを書かずに PWA を実装することができるのです。

今回作成したアプリは Firebase Hosting を使ったりしてデプロイすればすぐにでもインターネット上からアクセスしインストール可能な PWA が出来上がります。ぜひ試してみてください。

今回開発したアプリのリポジトリ

PWA SAMPLE

参考記事

Progressive Web Apps
Enabling web service workers
ウェブアプリマニフェスト

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

Reactの状態管理ライブラリまとめ

はじめに

今回はReactのReactの状態管理ライブラリについてまとめていきます。
勉強したアウトプットと備忘録として残していこうと思います。

現在の状態管理ライブラリの状況と概要

  • Redux : デファクトスタンダード?
  • Recoil : Facebook社が開発
  • useContext : React16.8から標準のhooksで使える
  • SWR : Next.jsと同じZeit社が開発
  • Apollo Client : Graph QLとの相性
  • react-query : 個人的に推したい新進気鋭

下の3つは状態管理というよりもキャッシュを管理するライブラリである。

Reduxの特徴

  • Action →  ActionCreator → Reducer → Storeの流れで状態の変更を管理
  • 流れが一方向なので状態管理をしやすい
  • reselectによる値の参照とメモ化が強力
  • フロントエンドにビジネスロジックが集中しても対応可能
  • パフォーマンスチューニングしやすい
  • Fluxフローの概念の理解が必要
  • とにかく手続きが多い

useContextの特徴

  • createContext()で生成したコンテクストオブジェクトで一元管理
  • 配下のコンポーネントでuseContextを使って参照可能
  • Reduxよりもシンプル
  • 更新頻度が低い状態の管理に最適
  • 管理している値が変化すると再レンダリングが走る
  • パフォーマンスチューニングが面倒

react-queryの特徴

  • フェッチしたデータをキャッシュとして利用する
  • キャッシュをどのコンポーネントからでも簡単に参照できる
  • データ再フェッチの間隔や回数を調整
  • フェッチの状態を返してくれる(isLoading, errorなど)
  • フロントエンド先行でバックエンドの設計を必要
  • フェッチしたデータを再加工しにくい

まとめ

Redux useContext React-query
導入の容易さ ⭕️ ?
パフォーマンスチューニング ⭕️ ? ⭕️
値の参照 ⭕️ ? ⭕️
値の管理方法 1つのオブジェクト(store) オブジェクト(複数作成可能) APIレスポンスの戻り値(クエリごとに管理)
向いているアプリ 書き込みが多い 読み取りが多い どちらもOK

参考

【群雄割拠】Reactの状態管理ライブラリ事情【2020年末版】

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

gRPC-WebとGoとVue.jsで簡素なチャット

はじめに

何だか良くわからないけどよく聞くgRPC-Webなるものを触りだけでも理解すべく辛うじてチャット呼べそうなものを作ってみました。

2020-12-18_02:05:20.png

gRPC

https://grpc.io/
Protocol BuffersやHTTP2などを利用した環境に依存せず実行できる高パフォーマンスのRPCフレームワーク。

Protocol Buffers

https://developers.google.com/protocol-buffers
言語やプラットフォームに依存しない構造データを定義できる。
コンパイルして指定の言語のコードを生成できる。

proto

test.proto

service TestService {
  rpc Login(User) returns (User) {}
}

message User {
  string name = 1;
  string token = 2;
}

Go

protoファイルからコンパイルしてGoのコードを生成。
test.pb.go

func (t *testServiceClient) Login(ctx context.Context, in *User, opts ...grpc.CallOption) (*User, error) {
    out := new(User)
    err := t.cc.Invoke(ctx, "/test.TestService/Login", in, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}

type User struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Name  string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"`
}

HTTP2

https://http2.info/
HTTP1からの変更例

  • テキストからバイナリ
  • ステートレスからステートフル
  • 1つのTCPコネクションの中で複数のHTTP Requestと複数のHTTP Response

gRPC-Web

https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
https://grpc.io/docs/platforms/web/basics/
ブラウザの制限によりネイティブのgRCPとは違う実装。

envoy

https://www.envoyproxy.io/docs/envoy/latest/
gRCPとgRCP-Webを接続するためには特別なプロキシが必要でデフォルトがenvoy。

コード

https://github.com/tayusa/grpc-web-simple-chat

protoファイル定義

syntax = "proto3";
package chat;

option go_package = "server/proto";

// よくあるデータ型は定義してあるので読み込む
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";

// やりとりを定義
service ChatService {
  rpc Login(User) returns (User) {}
  rpc Logout(User) returns (google.protobuf.Empty) {}
  rpc SendMessage(Message) returns (Message) {}
  // 複数の場合、stream使う。
  rpc GetMessage(User) returns (stream Message) {}
}

// やりとりするデータを定義
message Message {
  // 番号はただの順番
  string content = 1;
  // 自分で定義した型
  User user = 2;
  google.protobuf.Timestamp created_at = 3;
}

message User {
  string name = 1;
  string token = 2;
}

上記以外にもいろんな書式があって表現力高い。

コンパイルしてコード生成

https://github.com/protocolbuffers/protobuf
からコンパイラをダウンロード。
パッケージマネージャーからインストールもできる。
Arch Linuxなら
$ sudo paman -S protobuf
Goのコードを生成するときは
$ go get -u github.com/golang/protobuf/protoc-gen-go
gRPC-Webのコードを生成するときは
$ npm install -g protoc-gen-grpc-web
言語、出力先をを指定してコンパイル

$ protoc chat.proto \
  --go_out=plugins="grpc:." \
  --js_out=import_style=commonjs:client/src/proto \
  --grpc-web_out=import_style=commonjs,mode=grpcwebtext:client/src/proto

生成したコード
https://github.com/tayusa/grpc-web-simple-chat/blob/master/server/proto/chat.pb.go
https://github.com/tayusa/grpc-web-simple-chat/tree/master/client/src/proto

Docker

Go

FROM golang:latest

WORKDIR /server
COPY . .
RUN go mod download
RUN go build -o app
CMD ./app

JavaScript

FROM  node:lts-slim

WORKDIR /client

COPY . .
RUN npm install

docker-compose.yml

3つのコンテナ動かす。

version: '3'
services:
  envoy:
    image: envoyproxy/envoy:v1.14.1
    command: /usr/local/bin/envoy -c /etc/envoy/envoy.yaml -l debug
    volumes:
      - ./envoy:/etc/envoy
    ports:
      - '10000:10000'
    links:
      - 'server'
    container_name: 'envoy'

  server:
    build:
      context: ./server
      dockerfile: Dockerfile
    command: /server/app
    ports:
      - '50051:50051'
    volumes:
      - ./server:/go/src/server
    container_name: 'server'

  client:
    build:
      context: ./client
      dockerfile: Dockerfile
    command: npm run serve
    ports:
      - '8080:8080'
    volumes:
      - ./client:/client
    links:
      - 'envoy'
    container_name: 'client'

Envoy

$ docker run --rm -it envoyproxy/envoy:v1.14.1 bash
で/etc/envoy/envoy.yamlをコピーして来てポートなどを書き換えて利用します。
.ymlにするとエラーになり時間が消えてなくなります。

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: 10000 }
    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: chat_service
                  max_grpc_timeout: 0s
              cors:
                allow_origin_string_match:
                - prefix: "*"
                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: chat_service
    connect_timeout: 0.25s
    type: logical_dns
    http2_protocol_options: {}
    lb_policy: round_robin
    # win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
    hosts: [{ socket_address: { address: server, port_value: 50051 }}]

Go

https://github.com/tayusa/grpc-web-simple-chat/blob/master/server/main.go
https://github.com/tayusa/grpc-web-simple-chat/blob/master/server/server.go
https://github.com/tayusa/grpc-web-simple-chat/blob/master/server/signal.go

生成されたインターフェイス

type ChatServiceServer interface {
    Login(context.Context, *User) (*User, error)
    Logout(context.Context, *User) (*empty.Empty, error)
    SendMessage(context.Context, *Message) (*Message, error)
  // 複数のレスポンスの場合、戻り値がない。引数にレスポンスのためのコネクション。
    GetMessage(*User, ChatService_GetMessageServer) error
}

protoで生成したGoのインターフェイスに合わせてメソッドを定義していく。

1つのリクエストで1つのレスポンス

func (s *server) Login(ctx context.Context, user *pb.User) (*pb.User, error) {
    log.Println("Try to logged in.")

    clientExists := false
    s.clients.Range(func(_, client interface{}) bool {
        if value, ok := client.(string); ok && value == user.GetName() {
            clientExists = true
            return false
        }
        return true
    })
    if clientExists {
        return &pb.User{}, fmt.Errorf("\"%s\" is already in use.", user.GetName())
    }

    user.Token = genToken()
    s.clients.Store(user.GetToken(), user.GetName())

    log.Printf("%s logged in.\n", user.GetName())
    return user, nil
}

1つのリクエストで複数のレスポンス

func (s *server) GetMessage(user *pb.User, stream pb.ChatService_GetMessageServer) error {
    s.wg.Add(1)
    defer s.wg.Done()
    streamCh := s.createStreamCh(user.GetToken())
    defer s.deleteStreamCh(user.GetToken())

    for {
        select {
        case msg, ok := <-streamCh:
            if !ok {
                return nil
            }
            // ここでレスポンスしてる。メソッドは終了しない。
            if err := stream.Send(msg); err != nil {
                log.Println("Sending error.")
                return err
            }
        case <-s.exitCh:
            log.Printf("%s exit.\n", user.GetName())
            return nil
        }
    }
}

JavaScript

https://github.com/tayusa/grpc-web-simple-chat/blob/master/client/src/api/client.js
https://github.com/tayusa/grpc-web-simple-chat/blob/master/client/src/components/Chat.vue

コンパイルして生成したクライアント

import { ChatServiceClient } from '../proto/chat_grpc_web_pb'
export default new ChatServiceClient('http://localhost:10000', null, null)

Vueのscript

// クライアント読み込む
import client from '../api/client.js'
// コンパイルして生成した型を読み込む
import { Message, User } from '../proto/chat_pb'
// googleが定義してる型を読み込む
// import { Empty } from 'google-protobuf/google/protobuf/empty_pb';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';

export default {
  name: "Chat",
  data: () => ({
    userName: "",
    userToken: "",
    message: "",
    messages: [],
    stream: null,
  }),
  filters: {
    toLocaleString: (value) => {
      return (new Date(value.getSeconds() * 1000)).toLocaleString()
    }
  },
  methods: {
    login: async function(e) {
      e.preventDefault();
      if (!this.userName) {
        return;
      }
      await client
        .login(this.getUser(), {}, (err, user) => {
          if (err != null) {
            console.log(err);
          } else {
            this.userToken = user.getToken();
            this.stream = this.fetchMessageStream()
          }
        })
    },
    sendMessage: async function(e) {
      e.preventDefault();
      if (!this.message) {
        return;
      }
      // 生成した型に入れてく。
      // セッターが生えてるので利用する。
      const message = new Message();
      message.setContent(this.message);
      message.setUser(this.getUser());
      const timestamp = new Timestamp();
      // ここはどこにも書いてなくて、開発者コンソールで中身を全部読んだ。
      timestamp.fromDate(new Date());
      message.setCreatedAt(timestamp);

      await client
        .sendMessage(message, {}, (err, res) => {
          if (err != null) {
            console.log(err);
          }
          this.message = '';
        })
    },
    fetchMessageStream: function() {
      const stream = client.getMessage(this.getUser());
      // メッセージが来たら発火するイベント
      stream.on('data', message => {
        console.log(message);
        this.messages = [...this.messages, message];
      });
      return stream;
    },
    getUser: function() {
      const user = new User();
      user.setName(this.userName);
      user.setToken(this.userToken);
      return user;
    }
  }
};

参考

GoでgRPC使う際のクイックスタート
https://grpc.io/docs/languages/go/quickstart/
protocol bufferが生成するGoのコードの説明
https://developers.google.com/protocol-buffers/docs/reference/go-generated
gRCPのGo実装
https://github.com/grpc/grpc-go
ブラウザためのgRCPのJavaScript実装
https://github.com/grpc/grpc-web
Goのライブラリのドキュメント
https://godoc.org/google.golang.org/grpc
GCPのドキュメントにある構成例
https://cloud.google.com/endpoints/docs/grpc/grpc-service-config?hl=ja

試す

$ git clone https://github.com/tayusa/grpc-web-simple-chat.git
$ cd grpc-web-simple-chat
$ docker-compose up -d --build
$ chromium http://localhost:8080

サーバーだけ試す

curlは使えないのでgrpc-cli

パッケージマネージャーからインストール
$ sudo paman -S grpc-cli

$ grpc_cli ls localhost:50051 chat.ChatService -l
$ grpc_cli call localhost:50051 ChatService.Login 'name: "John"'
$ grpc_cli call localhost:50051 ChatService.SendMessage 'content: "Hey"'

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