20200518のJavaScriptに関する記事は22件です。

【JavaScript】リストのページネーション

JQueryなどライブラリを使わず、素のJSでリストのページネーションを実装してみました。

動作イメージ

「<」や「>」をクリックすると、ページ数とリストが切り替わります。
(「1」「2」などの数字部分は、書いてありますが今回は特に機能入ってないです...)

1ページ目
スクリーンショット 2020-05-18 22.46.18.png

2ページ目
スクリーンショット 2020-05-18 22.49.11.png

プログラム

ディレクトリ構成

/pagination_1
   |-- index.html
   |-- index.js
   |-- style.css

HTML

<!DOCTYPE html>
<head>
    <title>JS Pagination</title>
    <link rel="stylesheet" href="style.css"> 
</head>
<body>
    <h2>Cafe Menu</h2>
    <p class="count"></p>
    <ul class="menu_list"></ul>
    <ul class="pagination">
        <li id="prev"><</li>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li id="next">></li>
    </ul>
    <script tyle="text/javascript" src="index.js" charset="UTF-8"></script>
</body>

JavaScript

// リスト表示用(今回name_jpしか使ってません...)
const menu = [
    {"id": 1, "name_jp": "コーヒー", "name_en": "coffee"},
    {"id": 2, "name_jp": "エスプレッソ", "name_en": "espresso"},
    {"id": 3, "name_jp": "カプチーノ", "name_en": "cappuccino"},
    {"id": 4, "name_jp": "カフェモカ", "name_en": "mocha"},
    {"id": 5, "name_jp": "ティー", "name_en": "tea"},
    {"id": 6, "name_jp": "サンドイッチ", "name_en": "sandwich"},
    {"id": 7, "name_jp": "ホットドック", "name_en": "hotdog"},
    {"id": 8, "name_jp": "オムレツ", "name_en": "omelette"},
    {"id": 9, "name_jp": "サラダ", "name_en": "salad"},
    {"id": 10, "name_jp": "カレー", "name_en": "curry"},
    {"id": 11, "name_jp": "ショートケーキ", "name_en": "shortcake"},
    {"id": 12, "name_jp": "チョコレートケーキ", "name_en": "chocolatecake"},
    {"id": 13, "name_jp": "チーズケーキ", "name_en": "cheesecake"},
    {"id": 14, "name_jp": "アップルパイ", "name_en": "applepie"},
    {"id": 15, "name_jp": "プリン", "name_en": "pudding"},
    {"id": 16, "name_jp": "パフェ", "name_en": "pudding"}
]

// ページング機能
const pagination = () => {
    // 初期値設定
    let page = 1; // 現在のページ(何ページ目か)
    const step = 5; // ステップ数(1ページに表示する項目数)

    // 現在のページ/全ページ を表示
    // <p class="count"></p> の中身を書き換え
    const count = (page, step) => {
        const p = document.querySelector('.count');
        // 全ページ数 menuリストの総数/ステップ数の余りの有無で場合分け
        const total = (menu.length % step == 0) ? (menu.length / step) : (Math.floor(menu.length / step) + 1);
        p.innerText = page + "/" + total + "ページ";
    }

    // ページを表示
    // <ul class="menu_list"></ul> の中身を書き換え
    const show = (page, step) => {
        const ul = document.querySelector('.menu_list');
        // 一度リストを空にする
        while (ul.lastChild) {
            ul.removeChild(ul.lastChild);
        }
        const first = (page - 1) * step + 1;
        const last = page * step;
        menu.forEach((item, i) => {
            if(i < first - 1 || i > last - 1) return;
            let li = document.createElement('li');
            li.innerText = item.name_jp;
            ul.appendChild(li);
        });
        count(page,step);
    }

    // 最初に1ページ目を表示
    show(page, step);

    // 前ページ遷移トリガー
    document.getElementById('prev').addEventListener('click', () => {
        if(page <= 1) return;
        page = page - 1;
        show(page, step);
    });

    // 次ページ遷移トリガー
    document.getElementById('next').addEventListener('click', () => {
        if(page >= menu.length / step) return;
        page = page + 1;
        show(page, step);
    });
}

window.onload = () => {
    pagination();
}

CSS

気持ち調整したくらいです...

.menu_list {
    height: 150px;
}

.pagination {
    list-style: none;
}

.pagination li {
    display: inline-block;
    margin: 0 0.5em;
    cursor: pointer;
}

おわりに

GitHubにも同じプログラムをPushしました。
https://github.com/SunHigh105/js_pagination/tree/master/pagination_1

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

ミルクボーイをjavascriptのclass記述にしてみた。

お題を決めたら、すぐネタが作りやすいミルクボーイの漫才。
自分の知識定着のために、javascriptのclass記述で書いてみました。

コーンフレークと最中の振りの部分だけを記述。
正しいことならtrueと記述しました。

milkboy.html
 <body>
    <div id="container">
      <p class="cornflakes true">
        あのー甘くてカリカリしてて で 牛乳とかかけて食べるやつやって言うねんな
      </p>
      <p class="cornflakes">
        オカンが言うには 死ぬ前の最後のご飯もそれで良いって言うねんな
      </p>
      <p class="cornflakes true">
        なんであんなに栄養バランスの五角形デカイんか分からんらしいねん
      </p>
      <p class="cornflakes">
        オカンが言うには 晩ご飯で出てきても全然良いって言うねんな
      </p>

      <p class="monaka">スーパーで子どもがそれ欲しくて泣いてた言うねんな</p>
      <p class="monaka true">食べたら、皮がぜんぶ上あごにひっつくらしいねん</p>
      <p class="monaka">
        オカンが言うにはな、1個食べ出したら止まらへんって言うねん
      </p>
      <p class="monaka true">皮の模様がなんか怖いらしい</p>
    </div>
    <script src="milkboy.js"></script>
  </body>

cssの取ってくるクラスと日本語名を引数として
forEachでひとつずつに分けて、そのテキスト部分を出力。
そのテキストがtrueのクラスが含むか含まないかで処理を変えます。

milkboy.js
class mother_forget {
  constructor(keyword, kana) {
    this.kana = kana;
    this.furis = document.querySelectorAll(keyword);
  }
//掛け合い処理
  kakeai() {
    this.furis.forEach((furi) => {
      console.log(furi.innerText);
      if (furi.classList.contains("true")) {
        console.log(this.kana + "やないかい");
      } else {
        console.log("ほな" + this.kana + "ちゃうやないかい");
      }
    });
  }
}

const milkboy1 = new mother_forget('.cornflakes', 'コーンフレーク')
milkboy1.kakeai();

const milkboy2 = new mother_forget('.monaka', '最中')
milkboy2.kakeai();

出力結果
確かにできてる。

kakeai.jpg

FUJIWARAのフジモンの
「顔でかいからや」関数ならもっと簡潔にできる。全く意味がない。。

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

さっとQiitaを見たいときに使える。全然スマートじゃない”SMART_PREVIEW”Chrome拡張機能を作った話

Qiitaを拡張させたい

最近Qiitaでの調べ物の頻度が増えてきた私は、ふとこんなことを思った。
なんで検索画面にプレビュー機能ないんだろう
くだらないと思った方、正解です。
でも深夜テンションって怖いですね。
こんなことを思いついた結果、
拡張機能作ればいいじゃんww
となったのです。

それではどんな感じになったのかご覧頂きましょう。

こんな感じなイメージ

Movie
https://youtu.be/8_Tt3_7xvEE
Image
SnapCrab_NoName_2020-5-18_22-34-15_No-00.png
SnapCrab_NoName_2020-5-18_22-34-45_No-00.png
SnapCrab_NoName_2020-5-18_22-34-55_No-00.png
SnapCrab_NoName_2020-5-18_22-35-12_No-00.png

How it works

今回もContents_scriptを利用して作成しましたが、
今回はBootstrapのみならず、Jqueryと
MarkdownをHtmlにしてくれる
Markedを導入した上で作成しました。
動きとしては
JqueryとMarkedが読み込まれる
メインスクリプトがページ生成後に実行
・このタイミングでボタン生成
ボタンを押すとイベント発火
・ここでQiitaAPIを用いて記事情報取得
=>Markedでなんとなく整形
(コードなどの一部の特殊なのは読み込めなかったっぽい?)
(URLは長いやつだと画面外にでてうざかったので、
replaceで置き換えといた。)
最後に適当にAppendしておく

チェックポイント1
今回のコード作成においてQiitaで本来使われている、
BootstrapのFlexに邪魔されてるので、
CSSを次のようにして無効(&見やすさアップしました。)

.tr-Item{
display:block !important;
}
.p-home_container {
    max-width: 3000px !important;
}

(この変更で、画面幅が一定以下で一部要素が下に行ったりします)

チェックポイント2
はじめは全く動作せず、こころが折れそうだったので、
はじめ考えていたtooltip形式をやめた途端、
うまくいきました。(なぜでしょう)

コード軍

$(document).ready(function () {
  $(".tr-Item_meta,.tf-ItemContent_meta,.ms-ItemContent_meta,.searchResult_sub").append("<button id='make_pre' class='btn btn-primary'>Preview_show</button>")
});
$(document).on('click', '#make_pre', function () {
  console.log($(this).parent().attr("class"))
  if ($(this).parent().attr("class")==="searchResult_sub"){

    if ($(this).parent().next("#pre").length) {
      $(this).parent().next("#pre").remove()
    } else {
      const url = $(this).parent().prev().find(".searchResult_itemTitle").find("a").attr("href");
      console.log(url)
      const pos = this
      const req = url.replace(/\/.*\/items\//g, "/api/v2/items/")
      $.getJSON("https://qiita.com" + req, function (data) {
        console.log(data.body)
        const content = "<div id='pre' class='card'><h1>Preview</h1><br>" + marked(data.body.replace(/\(http.*\)/g, "[URL]")) + "</div>";
        $(pos).parent().parent().append(content);
      })
    }
  }else{
  if ($(this).parent().next("#pre").length) {
    $(this).parent().next("#pre").remove()
  } else {
    const url = $(this).parent().prev().attr("href");
    console.log(url)
    const pos = this
    const req = url.replace(/\/.*\/items\//g, "/api/v2/items/")
    $.getJSON("https://qiita.com" + req, function (data) {
      console.log(data.body)
      const content = "<div id='pre' class='card'><h1>Preview</h1><br>" + marked(data.body.replace(/\(http.*\)/g, "[URL]")) + "</div>";
      $(pos).parent().parent().append(content);
    })
  }
}
});

メインコード
無駄しかない。

{
  "name": "qiita_smart_preview",
  "version": "1",
  "description": "qiita。\nUsing by jquery and bootstarp4",
  "content_scripts": [{
    "js": ["jquery-3.5.1.min.js","marked.min.js","background.js"],
    "css":["bootstrap.min.css","css.css"],
    "matches": ["https://qiita.com/*"],
    "run_at": "document_end",
    "all_frames": true
  }],
  "icons": {
    "48": "images/icon.png"
  },

  "manifest_version": 2
}

マニフェスト
前回とあんまり変わんない。

まとめ

Jquery便利だけどThisの継承忘れに注意しようと思った。
(中のFanctionでThisなんでむりなーんっと愚痴っておりました。)

const pos = this

おしまい
(ほしいとコメント頂いた暁には、
GithubにコードをUploadしようと思います。
=>コード管理一切していなかっただけ)

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

TypeScriptで学ぶデザインパターン〜State編〜

対象読者

  • デザインパターンを学習あるいは復習したい方
  • TypeScriptが既に読めるあるいは気合いで読める方
    • いずれかのオブジェクト指向言語を知っている方は気合いで読めると思います
  • UMLが既に読めるあるいは気合いで読める方

環境

  • OS: macOS Mojave
  • Node.js: v12.7.0
  • npm: 6.14.3
  • TypeScript: Version 3.8.3

本シリーズ記事一覧(随時更新)

Stateパターンとは

状態をクラスとして表現するためのパターンです。

サンプルコード

Stateパターンで作られたクラス群がどんなものになるのか確認していきましょう。

今回は、題材として"時刻によって挙動が変わる時計"を想定します。GitHubにも公開しています。

今回の題材について補足します。この時計には色々な機能があります。0分になるとそれを伝えるためにピヨピヨ音声が流れます。ただし夜は鳴らないようになっています。また、夜はバックライトを点灯します。バックライトは日中はOFFになっています。このように"日中"や"夜間"という状態によって処理が分岐します。

modules/State.ts

状態を表現するインターフェースです。

State.ts
import ClockContext from "./ClockContext";

export default interface State {
  setTime(clockContext: ClockContext, hour: number): void;
  alert(): string;
  backlight(): string;
}

各メソッドの役割は実装クラスで解説します。

modules/DayState.ts

日中という状態を表現するクラスです。

DayState.ts
import State from "./State";
import ClockContext from "./ClockContext";
import NightState from "./NightState";

export default class DayState implements State {
  private static singleton: DayState = new DayState;

  private constructor() {}

  static getInstance(): DayState {
    return DayState.singleton;
  }

  setTime(clockContext: ClockContext, hour: number): void {
    if ((0 <= hour && hour < 7) || (19 <= hour && hour <= 23)) {
      clockContext.setState(NightState.getInstance());
    }
  }

  alert(): string {
    return 'ピヨピヨ';
  }

  backlight(): string {
    return 'OFF';
  }
}

getInstance周りはSingletonパターンを使っています。
setTimeで時刻を設定し、もし0時〜7時あるいは19時〜23時なら状態を夜間に変更します。状態を変更する処理はClockContextで解説します。
alterでは鳥の鳴き声を(擬似的に)鳴らせます。
backlightではバックライトを(擬似的に)オフにします。

modules/NightState.ts

夜間という状態を表現するクラスです。

NightState.ts
import State from "./State";
import ClockContext from "./ClockContext";
import DayState from "./DayState";

export default class NightState implements State {
  private static singleton: NightState = new NightState;

  private constructor() {}

  static getInstance(): NightState {
    return NightState.singleton;
  }

  setTime(clockContext: ClockContext, hour: number): void {
    if (7 <= hour && hour < 19) {
      clockContext.setState(DayState.getInstance());
    }
  }

  alert(): string {
    return '(シーン・・・)';
  }

  backlight(): string {
    return 'ON';
  }
}

DayStateと同様の処理なので解説は割愛します。

modules/ClockContext.ts

状態を制御するためのクラスです。

ClockContext.ts
import State from "./State";
import NightState from "./NightState";

export default class ClockContext {
  private state: State;

  constructor() {
    this.state = NightState.getInstance();
  }

  setState(state: State): void {
    this.state = state;
  }

  print(hour: number): void {
    this.state.setTime(this, hour);
    console.log(hour + '');
    console.log('アラート: ' + this.state.alert());
    console.log('バックライト: ' + this.state.backlight());
  }
}

setStateでは状態を設定します。日中状態か夜間状態かのどちらかの状態クラスのインスタンスが保持されます。
printでは時刻を引数にして、アラートとバックライト機能を処理します。日中状態か夜間状態かのどちらになるかをsetTimeで制御した上でアラートとバックライト機能を処理します。

Main.ts

本デザインパターンで作成されたクラス群を実際に使う処理です。

Main.ts
import ClockContext from "./modules/ClockContext";

const clockContext: ClockContext = new ClockContext;

for (let i = 0; i < 24; i++) {
  clockContext.print(i);
}

0時から23時を繰り返し処理を使って走査しています。日中時刻なら日中のalertbacklightが呼ばれ、夜間時刻なら夜間のalertbacklightが呼ばれます。

クラス図

ここまでStateパターンで作られたクラス群を1つずつ確認してきました。次にクラス図を示します。Stateパターンの全体像を整理するのにお役立てください。

State.png

  • State: サンプルコードではStateインターフェースが対応
  • ConcreteState: サンプルコードではDayStateクラスNightStateクラスが対応
  • Context: サンプルコードではClockContextが対応

LucidChartを使用して作成

解説

最後に、このデザインパターンの存在意義を考えます。

状態がロジックに影響を与える場合ソースコードがシンプルでかつ拡張性の高い実装にできます。たとえばサンプルコードにおいてDayStateとNightStateにわけない場合alertbacklightに条件分岐が入ってしまいます。今回はシンプルな例でしたが、状態がもっと多い場合は条件分岐のコードだらけになってしまいます。これを防ぐことができます。
さらに、DayStateやNightState以外に状態を追加したい場合他の状態(DayStateやNightState)を修正する必要はありません。

補足

サンプルコードの実行方法はこちらと同様です。

参考

あとがたり

状態というモノとは言い難いものをクラスにすることはけっこうあるので、そのあたりの温度感になれると設計しやすくなりそう。

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

Instagram APIを使ってみた

Instagramの投稿一覧を表示してみたくなったのでInstagram Graph APIを使って表示させてみた。

準備

Instagramのアカウントをビジネスアカウントに切り替える。(アプリでしか出来ない?)
[設定]-[アカウント]-[プロアカウントに切り替える]

facebookでページの作成をする。
[設定とプライバシー]-[設定]-[ページを作成](フッターにある)

Instagramアカウントをfacebookにリンク

facebookのページの設定でInstagramアカウントをリンクする。
[ページ設定]-[Instagram]-[アカウントをリンク]
スクリーンショット 2020-05-17 0.04.44.png

facebook for developersでアプリを作成

アカウントを作成してアプリを作成する。
facebook for developers

アプリのプロダクトにInstagramを追加

[プロダクト]-[Instagram]
スクリーンショット 2020-05-15 21.05.52.png

アクセストークンの作成

グラフAPIエクスプローラを表示する。
スクリーンショット 2020-05-15 21.03.18.png

Facebookアプリ、ユーザまたはページ、アクセス許可を設定する。
スクリーンショット 2020-05-15 21.08.41.png

Facebookアプリには作成したアプリを設定
ユーザまたはページにはユーザートークンを設定
アクセス許可には
pages_show_list
instagram_basic
を設定

(画像にはあるけど)以下は無くても大丈夫。
business_management
instagram_manage_comments
instagram_manage_insights

[Generate Access Token]ボタンをクリックするとアクセストークンが作成される。

Instagram User IDを調べる

グラフAPIエクスプローラでme/accounts?fields=idを入力して送信ボタンをクリックする。
スクリーンショット 2020-05-17 11.28.04.png

続けて取得したidを使って000000000000000?fields=instagram_business_accountを入力して送信ボタンをクリックする。
スクリーンショット 2020-05-17 11.28.04.png

Instagram User IDが表示される。

実装

ページ表示時に自分の投稿一覧をタイル状に表示させてみる。
スクリーンショット 2020-05-18 21.44.49.png

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Instagram</title>
</head>

<body>

  <div class="container"></div>

  <script src="./app.js"></script>
</body>

</html>
app.js
import style from './style.scss';

const apiUrl = 'https://graph.facebook.com/v7.0/';
const igUserId = '10000000000000000';
const accessToken = 'トークンを入力';

document.addEventListener('DOMContentLoaded', () => {
  _getMyMeida();
});

function _getMyMeida(url = '') {
  // Get the Instagram Business Account's Media Objects
  const api = url || `${apiUrl}${igUserId}/media?fields=caption,children,comments_count,id,like_count,media_type,media_url,thumbnail_url&access_token=${accessToken}`;

  fetch(api)
    .then(response => {
      return response.json();
    })
    .then(data => {
      if (data.data !== undefined) {
        _createTile(data.data, (url === '') ? true : false);

        if (data.paging !== undefined && data.paging.next !== undefined) {
          _getMyMeida(data.paging.next);
        }
      }
    })
    .catch(error => {
      console.log(error);
    });
}

function _createTile(media, insert) {
  const container = document.querySelector('.container');
  const fragment = document.createDocumentFragment();

  media.forEach((item, i) => {
    const tile = document.createElement('div')
    tile.classList.add('tile');

    const figure = document.createElement('figure');
    const img = document.createElement('img');
    img.src = item.thumbnail_url || item.media_url;
    figure.appendChild(img);

    const caption = document.createElement('figcaption');
    caption.textContent = item.caption;
    figure.appendChild(caption);
    tile.appendChild(figure);

    fragment.appendChild(tile);
  });

  if (insert) {
    container.insertBefore(fragment, null);
  } else {
    container.appendChild(fragment);
  }
}
style.scss
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  height: 100vh;
  background-image: linear-gradient(to left bottom, #fa709a 0%, #fee140 100%);
}

.container {
  display: flex;
  flex-wrap: wrap;

  .tile {
    flex: 0 0 calc(100% / 5);
    position: relative;
    transition: transform 0.2s linear;
    &:hover {
      transform: scale(1.5);
      z-index: 1;
    }

    &::before {
      display: block;
      content: "";
      padding-top: 100%;
    }

    figure {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
    img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
    figcaption {
      position: absolute;
      left: 2%;
      bottom: 2%;
      width: 96%;
      height: 3em;
      padding: 0 5px;
      background: rgba(80, 80, 80, 0.7);
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 2;
      overflow: hidden;
      text-overflow: ellipsis;
      color: #f0f0f0;
      font-size: 12px;
      line-height: 1.5;
    }
  }
}

github
h23k/instagram-embed

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

node-fetchをwebpackすると実行できない・・・

やりたかったこと

シンプルにとあるWebサイトをスクレイピングして、情報を取得したかった。
過去にもスクレイパーを作成したことはあるのだが、その時リクエストを送信するのにrequestモジュールを使用していたが、どうやらパッケージが2020年2月に廃止されたらしい…
そこで見つけたのが「node-fetch」。
試しにこれを使って指定したページのHTML要素を取得したい。

使ってみる

TypeScriptをインストール

yarn add typescript

詳しい環境構築はこの記事の本筋からズレるため省略させていただきます。

node-fetchをインストール

yarn add node-fetch

ここでは詳しい使い方などは省略させていただきます。
詳しくはこちらを

サンプルコードを作成

index.ts
import fetch from 'node-fetch';

const url: string = "https://qiita.com/";

fetch(url)
  .then(res => res.text())
  .then(body => console.log(body));

これでHTMLが取得できるはず。

補足

JavaScriptで動的に生成されている要素を取得したい場合、ヘッドレスブラウザが必要になるようです。
この記事の本筋と異なるため、こちらも省略させていただきます。
(今回スクレイピングするサイトには必要なかったので詳しく調べてないです…ごめんなさい)

実行してみる

ts-nodeを使用して実行してみる。

package.json
"scripts": {
  "start": "ts-node ./index.ts",
  "build": "webpack"
}

実行

$ npm run start

<!DOCTYPE html><html><head><meta charset="utf-8" />
<title>Qiita</title><meta content="Qiitaは、プログラマのための技術情報共有サービスです。 プログラミングに関するTips、ノウハウ、メモを簡単に記録 &amp;amp; 公開することができます。" name="description" />
<meta content="width=device-width,initial-scale=1,shrink-to-fit=no" name="viewport" />
<meta content="#55c500" name="theme-color" />
<meta content="XWpkTG32-_C4joZoJ_UsmDUi-zaH-hcrjF6ZC_FoFbk" name="google-site-verification" />
<link href="/manifest.json" rel="manifest" />
<link href="/opensearch.xml" rel="search" title="Qiita" type="application/opensearchdescription+xml" />
<meta name="csrf-param" content="authenticity_token" />

....省略

うん。取れた。

webpack

たったこれだけなのでJavaScriptで書いても良かったのですが、今回作るプロダクトではTypeScriptを使いたかった。
というわけでwebpackでトランスパイルします。

まずはwebpackのツールをインストール

yarn add -D webpack webpack-cli ts-loader

webpack.config.jsを作成します。

webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './index.ts',
  output: {
      path: path.join(__dirname, "dist"),
      filename: "index.js"
  },
  module: {
    rules: [{
      test: /\.ts$/,
      use: [
        {loader: 'ts-loader'}
      ]
    }]
  },
  resolve: {
      modules: [
      "node_modules",
      ],
      extensions: [ '.ts', '.js', 'json' ]
  }
};

webpackして実行

$ node ./dist/index.js

webpack:///./node_modules/node-fetch/browser.js?:11
        throw new Error('unable to locate global object');
        ^

Error: unable to locate global object
    at getGlobal (webpack:///./node_modules/node-fetch/browser.js?:11:8)
    at eval (webpack:///./node_modules/node-fetch/browser.js?:14:14)
    at Object../node_modules/node-fetch/browser.js (/Users/taisei/Documents/project/MagicReview/scraping/dist/index.js:97:1)
    at __webpack_require__ (/Users/taisei/Documents/project/MagicReview/scraping/dist/index.js:20:30)
    at eval (webpack:///./src/index.ts?:26:36)
    at Object../src/index.ts (/Users/taisei/Documents/project/MagicReview/scraping/dist/index.js:109:1)
    at __webpack_require__ (/Users/taisei/Documents/project/MagicReview/scraping/dist/index.js:20:30)
    at /Users/taisei/Documents/project/MagicReview/scraping/dist/index.js:84:18
    at Object.<anonymous> (/Users/taisei/Documents/project/MagicReview/scraping/dist/index.js:87:10)
    at Module._compile (internal/modules/cjs/loader.js:1158:30)

あれ?トランスパイルしたらエラーでた…

解決方法

調査した結果、以下の方法で解決しました。

webpack.config.js
// 省略

target: "node",   //この行を追加
module: {
    rules: [{
      test: /\.ts$/,
      use: [
        {loader: 'ts-loader'}
      ]
    }]
  },

....

どうやら、webpackでバンドルするときにtargetをnodeに指定しないとbrowserオブジェクトが取得できないようです。
省略しますが、webpack.config.jsを上記のように修正後、webpack→実行すると意図した結果が得られました。

targetをnodeに指定することで、Node.js環境で実行できるようにコンパイルしてくれるようです。
参考→webpackドキュメント

追記

ちなみに、axiosでやってみても同じことが起こりました。
フロントでaxiosを使用していたときには、targetを指定しなくても意図した挙動をしてくれていたのですが、サーバーサイド(Node.js環境)で使用するには必要なようです。
いい勉強になりました。

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

華麗なるGatsby.jsの実践(型を変更してみよう)

GraphQLスキーマをカスタマイズする

先日、markdownのimgをgraphQLで管理しようとしたところ、型の類推エラーによってgraphQLのスキーマをいじらなければならなくなりました。なのでこのガイドを学習します!
ハイパー意訳がほぼほぼですのでご了承ください?‍♂️

graphQLのスキーマ言語をおおよそ知る必要がある

その前に、そもそも、graphQLに関する記述法を知らなかったので、そこをざっくり学習します。
https://employment.en-japan.com/engineerhub/entry/2018/12/26/103000
私は上記で学習させていただきました!

特に必要となる知識は、スキーマ言語(Web APIの仕様を定義する方)の
fieldの定義方法と,Interfaceについてです。

type Query {
 currentUser: User!
}
interface Node {
 id: ID!
}

Interfaceは必要となるfieldのセットを束ねた抽象型です。

公式ガイド

では公式ガイドを見てみます。

このガイドは

  • プラグインの作成者

  • 自動型類推によって作成されたスキーマを修正したい方(私)

  • ビルドの最適化を測る方

  • スキーマに興味のある方

を対象としているようです。こちらかなり長いのですが、ここでは前編部分のみを扱います。
気になる方は続きを読むと良さそうです!

graphQLの強みは何と言っても多くの情報源を一括で扱えること!
そのためにはGraphQLスキーマを生成しなければなりません。

例 マークダウンとJSON

Markdownファイルからブログ記事を生成する。記事内容に加えて筆者情報をJSON形式で提供する。
時々ある寄稿者の情報は別のJSONとして保存する。

.md
---
title: Sample Post
publishedAt: 2019-04-01
author: jane@example.com
tags:
  - wow
---
# Heading
Text
author.json
[
  {
    "name": "Doe",
    "firstName": "Jane",
    "email": "jane@example.com",
    "joinedAt": "2018-01-01"
  }
]
contributor.json
[
  {
    "name": "Doe",
    "firstName": "Zoe",
    "email": "zoe@example.com",
    "receivedSwag": true
  }
]

これらをGraphQLでクエリするためにはgatsby-source-filesystem,gatsby-transformer-remark,gatsby-transformer-jsonを使用しますが、これらのプラグイン内部で何が行われているかというと、
markdownファイルをユニークなidとMarkdownRemarkタイプを持つnodeオブジェクトに変換しています。

同様に著者データはAuthorJsonタイプのnodeオブジェクトに変換され、
寄稿者データはContributorJson型のノードオブジェクトに変換されています。

スクリーンショット 2020-05-18 18.57.49.png

Node interface

Source pluginやtransformer pluginによって作られるGatsbyのGraphQLのスキーマです。
Id, parent,children,またinternal型などといったfieldがセットされています。

interface Node {
  id: ID!
  parent: Node!
  children: [Node!]!
  internal: Internal!
}

type Internal {
  type: String!
}

** TODO: internalって何でしょう..イマイチ調べてもわからずでした。

例 autor.json

例)Gatsby-transformer-jsonで作成するautor.json用のnode型は、以下のようになっています。

type AuthorJson implements Node {
  id: ID!
  parent: Node!
  children: [Node!]!
  internal: Internal!
  name: String
  firstName: String
  email: String
  joinedAt: Date
}

実際にgatsbyが作成したスキーマを確認するには、GraphQL Playgroundがオススメです。

プロジェクト上で

$ GATSBY_GRAPHQL_IDE=playground gatsby develop

そしてhttp://localhost:8000/___graphqlを開いて右画面側にある Schemeタブを開きます。
スクリーンショット 2020-05-18 18.03.52.png

思った以上にぎっしりしてます。

自動型推論

先ほどのautor.jsonを見てみましょう。

[
  {
    "name": "Doe",
    "firstName": "Jane",
    "email": "jane@example.com",
    "joinedAt": "2018-01-01"
  }
]

どこにも型定義などされていないですが、GraphQLではうまいこと動かせています。
これは型の類推を行なってくれているからですね。
1つ1つのfield内容を確認し、型をチェックすることで実現しています。

しかしこの型類推には2つの問題点があります。

  • 時間がかかり、負担がかかる

  • 型とデータが異なっていた場合、型の類推が失敗する

2つ目についてですが、例をあげます。

.md
  +  {
  +    "name": "Doe",
  +    "firstName": "John",
  +    "email": "john@example.com",
  +    "joinedAt": "201-04-02"
  +  }
  ]

joinedAt部分がスペルミスによって、Dateと解釈できなくなっています。

これらを解決するためには、型を明示的に示すことです。

## 型定義

createType actionを使って型を明示的に定義します。

gatsby-node.js
  exports.createSchemaCustomization = ({ actions }) => {
    const { createTypes } = actions
    const typeDefs = `
      type AuthorJson implements Node {
        joinedAt: Date
      }
    `
    createTypes(typeDefs)
  }

全ての型を定義する必要がない点に注意してください。(name,firstName等)

そもそも型推論をやらない!というストイックな方もいるでしょう。
これを行うことでパフォーマンスの向上が見込めます。先ほども開設したように、型の類推は負担が大きいため、大規模なプロジェクトほどパフォーマンス低下が著しくなります。
@dontInferディレクティブを用いて型推論をオプトアウトできます。

ディレクティブってなんだ?という方は先ほどのGraphQLスキーマについての解説をどうぞ!https://employment.en-japan.com/engineerhub/entry/2018/12/26/103000)

gatsby-node.js
exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
    type AuthorJson implements Node @dontInfer {
      name: String!
      firstName: String!
      email: String!
      joinedAt: Date
    }
  `
  createTypes(typeDefs)
}

これがストイックなバージョンになります。
Node interfaceが提供するフィールドについては、Gatsby側で追加してくれるので明示しなくて大丈夫です。(id,parentなど)

ネストしている型

今まではString型やDate型などのスカラー型のみを扱ってきました。GraphQLでは他にも

  • ID型

  • Int型

  • Float型

  • Boolean型

  • JSON型

なども扱うことが可能です。また、複雑なオブジェクト値だって扱えます。

markdown-remarkを例にとってみます。

以下の記述によってfrontmatter.tagsは必ず文字の配列となります。

gatsby-node.js
  exports.createSchemaCustomization = ({ actions }) => {
    const { createTypes } = actions
    const typeDefs = `
      type MarkdownRemark implements Node {
        frontmatter: Frontmatter
      }
      type Frontmatter {
        tags: [String!]!
      }
    `
    createTypes(typeDefs)
  }

こちらの記述のように、直接Frontmatter型を指定すると失敗してしまいます。

gatsby-node.js
exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
    # これは失敗!!!
    type Frontmatter {
      tags: [String]!
    }
  `
  createTypes(typeDefs)
}

なぜなら、Frontmatter型がソースプラグインやトランフォーマープラグインによって作成されないため、Nodeインターフェースが実装されくなってしまうからです。

そのためにこのような処理となります。

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

Canvas API の基本的な使い方

はじめに

技術系同人誌を書くつもりでエディタを開いたのですが、何となくエタりそうな気がしたので、とりあえずこちらにまとまっている部分だけでもアップしようと思った次第です。note にも同じ内容をアップしています。

JavaScript で図を描き時の Canvas API の基本的な扱い方、注意点について述べています。なお、登場するメソッドの引数等の説明は行いません。メソッドについて詳しく知りたい方は MDN などで調べて下さい。

それでは、雛形となる html を用意しましょう。

<!DOCTYPE html>
<html lang="ja">
 <head>
   <meta charset="utf-8"/>
 </head>
 <body>
   <canvas id="cvs">
   </canvas>
   <script>
   </script>
 </body>
</html>

script タグを body の中に書いています。これから記述する JavaScript は、すべて script タグの中に書いて下さい。

直線を描く

canvas 要素は 2D レンダリングコンテキストというインスタンスを持ち、このオブジェクトが持つメソッドを使って図形、文字、画像といったものを描画します。手始めに 2D レンダリングコンテキストを取得し、直線を描いてみましょう。

const canvas = document.getElementById('cvs');
const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(50, 10);
ctx.stroke();

canvas ではまずパスを設定し stroke メソッドを使って線を描画します。まず beginPath でパスの開始を宣言します。それから moveTo で始点を指定し lineTo で中間点を指定します。そして stroke を実行すれば直線が描かれます。

四角形を描く

点を結べば四角形を描くことができます。

ctx.beginPath();
ctx.moveTo(60, 10);
ctx.lineTo(100, 10);
ctx.lineTo(100, 50);
ctx.lineTo(60, 50);
ctx.closePath();
ctx.stroke();

closePath は最後に指定した中間点と moveTo で指定した始点とを結ぶメソッドです。lineTo で始点を指定しても挙動は同じになります。

四角形は始点と縦横の幅が分かればいいので moveTo から closePath までをまとめた rect というメソッドがあります。そして塗りつぶしを行う時は fill メソッドを使います。

ctx.beginPath();
ctx.rect(110, 10, 40, 40);
ctx.fill();

注意してもらいたいのは beginPath から fill/stroke まではひとまとまりのコマンドだということです。例えば以下のようなコードを組んだ場合、最初の四角形は枠線のみで次の四角形は塗りつぶしの描画を期待すると思います。

ctx.beginPath();
ctx.rect(10, 60, 40, 40);
ctx.stroke();
ctx.rect(60, 60, 40, 40);
ctx.fill();

しかし、実行してみてもらえば分かる通り、両方とも塗りつぶしの四角形になります。ポイントは beginPath で、このメソッドがパスから描画までの一連の流れをリセットする命令になっています。なので stroke の後に beginPath を入れると想定通りの描画になります。

なお、四角形には beginPath から fill/stroke までをひとまとめにしためドッドも存在します。

ctx.fillRect(160, 10, 40, 40);
ctx.strokeRect(160, 60, 40, 40);

円を描く

今度は円を描いてみましょう。

ctx.beginPath();
ctx.arc(230, 30, 20, 0, 2 * Math.PI);
ctx.stroke();

arc は円弧のパスを設定するメソッドです。このメソッドで気をつけたいのは、弧の始点座標です。

ctx.beginPath();
ctx.arc(230, 80, 20, 0, Math.PI / 2);
ctx.stroke();

中心座標から右に半径分ずらした座標が始点になります。そして時計回りにパスが設定されます。角度の単位もラジアンになりますので、そこも注意が必要です。

早足でしたが、以上が Canvas API の基本的な使い方、注意点になります。次回はこれらを念頭に置いた、トランプ画像の描き方について述べるつもりです。

それでは。

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

スクロールで動く仕組み検証 【Javascript】【脱jQuery】

はじめに

画面をスクロールで、何かの要素が浮かび上がったり、反応する仕組みを、JQuery無しで作るのに、
具体的に要素がどのように判定されているのかを検証するものです。

また、動きの要素にコーディング終了後にクラスを付け加えることで、動きを一括して実装できるので
製作スピードが向上します。

動作デモ

See the Pen スクロール判定のデモ by masato yamada (@marchin133) on CodePen.

スクロールの計測

スクロールは window.pageYOffset で取得。

判定したい要素の計測

getBoundingClientRect().topで、判定したい要素の上部の、HTMLの一番上からの高さがわかる。

判定ロジック

スクリーンショット 2020-05-18 17.20.13.png

javascript

document.addEventListener("DOMContentLoaded", function () {

    let windowHeight = document.getElementById('windowHeight');
    let scrollVolume = document.getElementById('scrollVolume');
    let markerLine = document.getElementById('markerLine');
    let markerLinetext = document.getElementById('markerLinetext');

    //動かす要素を探す
    let fadeConts = document.querySelectorAll('.anime');
    //動かす要素の配列を作る
    let moveConts = [];


    //windowの高さ & リサイズされたら再取得
    let windowH = window.innerHeight;
    window.addEventListener('resize', function () {
        windowH = window.innerHeight;
        windowHeight.textContent = windowH;
    }, false);

    windowHeight.textContent = windowH;

    //動かす要素の位置情報を取得しておく
    for (let i = 0; i < fadeConts.length; i++) {
        let rect = fadeConts[i].getBoundingClientRect().top;
        moveConts.push(rect);
        fadeConts[i].textContent = rect;
    }

    // marker
    // 画面の下からXpxのライン

    let markerLineVolume = windowH - (windowH / 4);
    markerLinetext.textContent = markerLineVolume;

    markerLine.style.top = markerLineVolume + 'px';

    //画面スクロールしたときに呼び出される
    window.addEventListener('scroll', function () {

        //スクロール位置
        let scrollDepth = window.pageYOffset;
        scrollVolume.textContent = scrollDepth;

        //判定位置は、スクロール量に伴ってそのままずれる
        markerLine.style.top = markerLineVolume + scrollDepth + 'px';

        //判定位置
        let markerLineNum = markerLineVolume + scrollDepth;

        for (let i = 0; i < moveConts.length; i++) {
            if (markerLineNum > moveConts[i]) {
                fadeConts[i].classList.add("show");
                console.log(i + "が超えた");
                //console.log(moveConts[0]);
                //console.log(windowHeight);
                //console.log(moveConts[i]);
            } else {
                //fadeConts[i].classList.remove("show");
            }
        }


    }, { passive: true });



}, false);

スクロール量の考え方

当たり前のようで、意外に私はハマってしまいました。
スクロール量と判定要素の関係から、画面に対して下にスクロール量が発生するような感覚だったのですが、
画面のウィンドウの上部にスクロール量が発生するイメージで考えるとわかりやすいです。

判定する要素が、ブラウザ画面の下端(実際には画面の比率などで判定位置は変わることが多いと思いますがそれも例には含めてあります)
に達しているということは、スクロール量だけでは足りず、スクロール量に、画面の高さを加えたものが、判定位置になります。

例:
判定位置の要素が、2000pxの位置に絶対位置で配置されているとする
画面の高さは、1200px(ブラウザの表示領域の高さ) の場合、800pxスクロールすると、判定位置の2000pxに達する。

スクリーンショット 2020-05-18 17.56.58.png

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

ZONeチャレンジ第一弾をJavaScriptでハックした話

要約

キャンペーンサイトへPCからスマホサイズでアクセスし、WebブラウザのコンソールからJavaScriptでフリック入力を再現した。

ソースコード: https://github.com/r-yanyo/flick-challenge/blob/master/newfuga.js

キャンペーンサイト: https://cp.zone-energy.jp/challenge/kuma/

キャンペーンサイトのconsoleにソースコードを貼り付けると(途中まで)フリック入力が自動的に入力されます(途中までの理由は最後の方に書いてます)。

PCでスマートフォン専用サイトにアクセスする

PCでアクセスした図

PCでキャンペーンページを普通に開くと、スマートフォン専用サイトである旨が表示されます。
このままではPCでフリック入力ゲームで遊ぶことが出来ません。

同じWebサイトでも、PCでアクセスした時とスマートフォンでアクセスした時にWebページのデザインが違う経験があるのではないでしょうか?これは、デバイスに依存してWebサイトの表示を変更する場合に、一般的にメディアクエリ が基準として使用されるからです。

メディアクエリはユーザが使用しているデバイスのViewport(ウィンドウのサイズ)を基準として、Webサイトの表示を変更する方法です。つまり、実際にPCやスマートフォンのどちらを使用しているかは検証しておらず、ウィンドウのサイズだけで判断しています。よって、何らかの方法でウィンドウサイズを変更すれば、PCでもスマートフォン専用ページにアクセスすることが出来ます。

Google Chromeでは、デベロッパーツールを開き、Device Toolbarを使用することでウィンドウのサイズをスマートフォンサイズに変更できます。

フリック入力をプログラミングで再現する

大きく分けて工程は3つあります。
1. ひらがなボタンのElementを取得
2. 取得したElementに対してフリック入力を再現
3. 答えを機械が認識できる形で入力&変換

1. ひらがなボタンのElementを取得

ボタンをインスペクト

デベロッパーツールで見てみると、どうやら.css-xx1yz5-buttonStyleというクラスが各「あかさたなはまやらわ」ボタンに付与されているようです。
さらに、そのボタンをタップすると出現するフリック入力部分("あ"をタップした場合は「あいうえお」)には.css-2rzvda-wingStyleというクラスが付与されています。

このことから、例えばキーボードの「さ」を取得したいときは、
[0]: "あ", [1]: "か", [2]: "さ"
なので

document.getElementsByClassName("css-xx1yz5-buttonStyle")[2]

フリック入力の「す」を取得したいときは、「す」は上から13番目なので

document.getElementsByClassName("css-2rzvda-wingStyle")[12]

とします。

ひらがな以外の文字に関しては、デベロッパーツールでその要素が上から何個目にあるかを見れば分かります。

2. 取得したElementに対してフリック入力を再現

初めはClickイベントでフリック入力が再現されると思ったのですが、
document.getElementsByClassName("css-xx1yz5-buttonStyle")[2].click()
のようにしても何も起こりませんでした。

そこで次に、TouchEvent を試したところ、キーボードのボタンがタッチされるイベントが発生しました。よって、このTouchEventを使うことにしました。

touchEvent.ts
//elmはフリックの起点となるelement。「す」を入力するときは「さ」
//moveElmはフリック先のelement。「す」を入力するときは「す」

touchMove = function (elm, moveElm) {
  if (!moveElm) return;
  rect = moveElm.getBoundingClientRect();
  X = rect.x + rect.width / 2;
  Y = rect.y + rect.height / 2;
  let touch = new Touch({
    identifier: Date.now(),
    target: elm,
    clientX: X,
    clientY: Y,
    force: 1,
    pageX: X,
    pageY: Y,
    radiusX: 41.66666793823242,
    radiusY: 41.66666793823242,
    rotationAngle: 0,
  });
  touchMoveEvent = new TouchEvent("touchmove", {
    bubbles: true,
    touches: [touch],
  });
  elm.dispatchEvent(touchMoveEvent);
};

TouchEventに関してはあまり詳しくなかったため、実際にイベントを発生させて必要な部分だけを変更する戦略を取りました。
結果的には clientX,clietnY,pageX,pageY の数値をフリック先のelementの座標に変えることで、フリック入力を再現することが出来ました。

フリック入力をするときにTouchEventで発生するイベントは、touchstart -> touchmove -> touchend の順で発生します。また、それぞれのイベントの発生元は全てフリックの起点となるelementで発火します。1

//elmはフリックの起点となるelement。「す」を入力するときは「さ」
//moveElmはフリック先のelement。「す」を入力するときは「す」
function touchEmulate(startElement, moveElm) {
  touchStart(elm);
  touchMove(elm, moveElm); //イベント発火元はelm
  touchEnd(elm);
}

3. 答えを機械が認識できる形で入力&変換

「ひらがな」から数字への変換は愚直に一つ一つ入力しました。

"あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもや(ゆ)よらりるれろわをんー 、。?!".split("")

例えば「す」をフリック入力したい場合は、

flick.ts
//「さ」ボタン
const button = document.getElementsByClassName("css-xx1yz5-buttonStyle")[2];
//「す」フリック
const flick = document.getElementsByClassName("css-xx1yz5-buttonStyle")[12];

このように「あ」〜「ん」や「!?」の記号などは入力できるようになりました。
ここで問題となるのが、濁音や半濁音、促音(「が」、「ぱ」、「っ」など)の入力です。これらはフリック入力を2回行う必要があります。例えば、「が」であれば、「か」をフリックした後に「゛」をフリックする必要があります。
これを解決する方法は色々あるのですが、今回は全部条件分岐で書きました(具体的にはソースコード参照)。2

「これで晴れてフリック入力がエミュレート出来た・・・」と思ったのですが、もう1つ問題があります。
フリック入力では同じボタンを2回押すと次の文字が表れるという特性上、連続で同じ文字を入力しようとすると正しい文字が入力されません。
例えば、「けたたましく」を入力しようとすると「けちましく」になってしまいます。
この問題はキーボードにある「→」ボタンを入力すれば解決できますが、当時の私は気付かなかったため手動で入力を分けて解決しました(ダサい・・・)。

おわりに

以上でフリック入力をエミュレートすることが出来ました。便宜上「ハックした」と書いていますが、JavaScriptでフリック入力をエミュレートしただけです。
キャンペーンは終了してしまいましたが、まだゲームで遊ぶことはできるようなので、是非実際に試してみてください。


  1. フリック入力周りの動作は実際のイベントを複製して改変したため、あまり理解していない部分が多々あります。 

  2. 「が」を「か」と「゛」に分割する方法は、もっと良い方法があると思います。ひらがなの部分も含め、文字コードを上手く使えばもっと綺麗に書けるかも? 

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

Electron で地図を表示する無料で最短の道

概要

Electron で地図を表示するアプリを作ってみます。
ライブラリとして地図ライブラリはleafletを、マーカークラスタのためにleaflet.markerclusterを使います。

それぞれのバージョンは以下のとおりです。

ライブラリ バージョン
electron 1.12.2
leaflet 1.6.0
leaflet.markercluster 1.4.1

準備

node と npm

 ここからダウンロード 

最短の道

ディレクトリを作成

ディレクトリを名前はなんでもいいので作って、そこに以下の内容の package.json をおきます。ディレクトリの名前はとりあえず le にしておきます。

package.json
{
  "name": "le",
  "version": "1.0.0",
  "description": "Sample map application.",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "author": ""
}

Electron をインストール

npm i electron --save

アプリの作成

le の下に以下の3ファイルを作成します。

index.html

index.html
<!DOCTYPE html>
<html lang=zxx>
<link
    rel="stylesheet"
    href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
>
<script
    src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"
></script>

<style>
html, body, #mapid { height: 100% }
body { margin: 0 }
</style>

<title>Electron-Leaflet</title>
<div id="mapid"></div>

main.js

main.js
const { app, BrowserWindow } = require('electron')

const
createWindow = () => {
    const mainWindow = new BrowserWindow(
        {   width           : 1000
        ,   height          : 1000
        ,   webPreferences  : {
                preload         : __dirname + '/preload.js'
            }
        }
    )

    mainWindow.loadFile( 'index.html' )
}

app.whenReady().then(
    () => {
        createWindow()
        app.on(
            'activate'
        ,   () => !BrowserWindow.getAllWindows().length && createWindow()
        )
    }
)

app.on(
    'window-all-closed'
,   () => process.platform !== 'darwin' && app.quit()
)

preload.js

preload.js
window.onload = () => {
    const
    map = L.map( 'mapid' ).setView( [ 35.6825, 139.752778 ], 15 )
    L.tileLayer(
        'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
    ).addTo( map )
}

デバッグ

npm run start

image.png

勘所

html, body, #mapid { height: 100% }

で地図を囲む全ての要素の height を 100% にしないと地図が表示されません。

body { margin: 0 }

がないと、地図の左上にちょっと余白ができて、右下が切れてしまいます。

L.map( 'mapid' ).setView( [ 35.6825, 139.752778 ], 15 )

で id が mapid の div に地図をつけています。
center と zoom の両方を指定しないと、地図が表示されません。

発展

デフォルトのマーカー

デフォルトのマーカーを皇居に置いてみます。

  • preload.js の onload の中に以下の内容を加えます。
    L.marker( [ 35.6825, 139.752778 ] ).addTo( map ).bindPopup( 'Imperial Palace.' ).openPopup()

image.png

divIcon を使ったマーカー

divIcon を使うと、マーカーのアイコンとして div を使えます。div なんでやりたい放題できます。とりあえず赤いぽっちにしてみます。

  • index.html の <style>に以下の内容を加えます。
.marker {
    text-align      : center
;   color           : white
;   font-size       : 16
;   border-radius   : 8px
;   box-shadow      : 8px 8px 8px rgba( 0, 0, 0, 0.4 )
}
.red {
    background      : red
}
  • preload.js の onload の中に以下の内容を加えます。
    const   redMarker = { icon: L.divIcon( { className: 'red marker', iconSize: [ 16, 16 ] } ) }
    L.marker( [ 35.6825, 139.752778 ], redMarker ).addTo( map ).bindPopup( 'Imperial Palace.' ).openPopup()

image.png

マーカークラスターグループ

leaflet.markercluster をインストールすると、地図にたくさんマーカーを置くとき、近接するマーカーをまとめて表示するような機能を実現できます。
とりあえず5個のマーカーのクラスターグループを北の丸公園においてみます。

  • index.html に以下の内容を加えます。
<link
    rel="stylesheet"
    href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css"
>
<link
    rel="stylesheet"
    href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css"
>
<script
    src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"
></script>
  • preload.js の unload の中に以下の内容を加えます。
    map.addLayer(
        L.markerClusterGroup().addLayer(
            L.marker( [ 35.691, 139.752 ], redMarker )
        ).addLayer(
            L.marker( [ 35.692, 139.752 ], redMarker )
        ).addLayer(
            L.marker( [ 35.690, 139.752 ], redMarker )
        ).addLayer(
            L.marker( [ 35.691, 139.753 ], redMarker )
        ).addLayer(
            L.marker( [ 35.691, 139.751 ], redMarker )
        )
    )

5個まとめて表示されています。

image.png

5 と書いてあるクラスターにズームしてみます。

image.png

余談

z-index について

この記事では地図の上に他の HTML エレメントがのっていませんが、たとえばメニューみたいなものを上にのせたいというような場合が考えられます。この時 #mapid の z-index を 0 にするか、上にのせたいものの z-index を 400 以上にする必要があります。

#mapid {
    z-index         : 0
}

警告について

View メニューから Toggle Developer Tools を選ぶと、HTML のデバッガが出てきて、console をみると以下のような警告が表示されています。

security-warnings.ts:179 Electron Security Warning (Insecure Content-Security-Policy) This renderer process has either no Content Security
    Policy set or a policy with "unsafe-eval" enabled. This exposes users of
    this app to unnecessary security risks.

For more information and help, consult
https://electronjs.org/docs/tutorial/security.
This warning will not show up
once the app is packaged.

これが気になる方は preload.js の頭で

process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = '1'

としてください。

もう一つの方法としてスタンドアローンアプリであれば、index.html に以下のように書けばいいのですが、Leaflet を使う場合はこれを書くと地図データをダウンロードできなくなります。

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">

Leaflet 1.6.0

前回の記事(Vue で地図を表示する無料で最短の道)を書いた時に比べ Leaflet は少し書きやすくなりました。

また前回は npm でインストールしたのですが、いずれにしろネットワークが必ず必要なので今回は CDN にしてみました。

最後に

leaflet は機能豊富なライブラリなので、いくらでもネタはあるんですが、キリがないのでこのへんにしておきます。最後までおつきあいいただいてありがとうございました。

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

Cypress:セッションを保持した状態でテストする

はじめに

新天地で、社内システムの自動テスト化について取り組ませていただくことになりました。
テストは、独自ランナーが使いやすい、Cypressです。
英語がまったく読めないので、苦戦・・・
Chromeの翻訳機能に助けられながら、やったことをメモしておきます。
OSはMacです。

自動テストの概念

自動テストについて調べた結果、概念はこんな感じです。

  • できるだけ簡単なコードで作成する
  • 機能別・テスト名・コード単位で検索しやすい
  • 簡単に更新できる
  • ひとつのものをみんなで共有出来る
  • 誰かに依存しない

セッションを保持したまま、複数のテストを実施する

まずは、ログインIDとパスワードを入れ、セッションを保持したまま別のテストを行う。
という入り口的な部分。
試行錯誤をしましたが、以下の2点を活用しました。

1.Cookieをファイル経由で保存する

参照サイト:Cypressで送る快適E2Eライフ

上記サイト様を参考にして、Cookie情報を別のファイルに出力し、それを読み込むという手法。
ログイン処理をカスタムコマンドにし、ログイン〜Cookieの出力まで行います。

カスタムコマンドについてはこちら ▶ 公式ドキュメント

cypress/support/commands.js
Cypress.Commands.add('login, () => {

  cy.url().then(url => {
    if(url === 'http://...'){

        console.log('relogin')

        cy.clearCookie('session_id')

        const login_id = Cypress.env('login_id') 
        const password = Cypress.env('password')

        cy.get('[name=login_id]').type(login_id).should('have.value', login_id)
        cy.get('[name=password]'). type(password,{ log:false }).should(el$ => {
          if(el$.val() !== password){
            throw new Error('Different value of typed password')
          }
        })
        cy.get('.submit').click()

        //セッションを保持
        cy.getCookie('session_id').should('exist').then((cookie) => {
        cy.writeFile("cache/cookie/session_id.json", {
          value: cookie.value
        })

      })
    }
  })

});


cypress/support/commands.js
内容としては、もし、アクセスしたURLがログインフォームと同じだったらば、clearCookieして再ログインしてね。
セッションがあればスルーされる部分です。

また、ログインIDとパスワードは、cypress.jsonファイルに、環境変数としてセット。
真ん中の色々書いてある部分は、ログイン情報はセキュリティ上表示させないようにしてね、という処理。
ですので、ランナーのコマンドログにパスワードは表示されません。

 

cypress/integration/login_spec.js

describe('Login Action',() => {
    /**
     * セッション設定
     */
    before('Session Setting', () => {
        cy.readFile("cache/cookie/session_id.json").then(cs => {
            cy.setCookie("session_id", cs.value)
        });
    });

    /**
     * ログインチェック
     */
    beforeEach ('Login Check',() => {
          cy.login()
          Cypress.Cookies.preserveOnce('session_id')
    })

    /**
     * テスト処理
     */
    it ('Test Case1', () => {
        cy.visit('/URL')
        // something assertion
        })
})

cypress/integration/login_spec.js
内容としては、セッションがあればテスト開始!
セッションが無ければ、再ログイン(カスタムコマンド)処理をしてからテスト開始!
beforeに、ファイルに出力されたCookie情報をreadさせて、beforeEachで再ログインの有無をチェック。
因みにbeforeEachはすべてのテスト(it)に当たるので、毎回再ログインチェックしている。
別に要らないんですけどね。。。:joy:
余計な処理も、Cypress速いからいいじゃない。っていうことで。
 

2.Cypress.Cookies.preserveOnce('session_id') を使う

公式ドキュメント

cypress/integration/login_spec.js
ファイルに出力されたCookie情報を、保持しておくことが出来るコマンドです。

Cypress.Cookies.preserveOnce('session_id')
おわりに

他にも、セッションについては永続的に保持しておけるlocalStorageの利用もひとつの手段かと思いましたが、なかなか実装が出来ず。
Cypressは便利な分、不安定な要素や、実装できない要素等まだまだあるようです。
npmモジュールを入れると、ランナーが起動しなくなるし、正しく実行できるテストも、5回に1回はなんとかエラーが出ます。(ホットリロードすると正常)

まだまだ勉強することはたくさんありますが、セッションの保持が一個クリア出来たので、記録します。
シンプルな方法ですが、初心者なのでこれからこれから。

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

【JavaScript】 yyyy-mm-dd 表記を yyyy年mm月dd日に変更する(文字列 to 文字列)

正規表現を使って変更

下記のように書くことで 2020-01-012020年1月1日 に変更することが可能です。
(もちろん、2020-12-252020年12月25日 になります)

date.js
let beforeDate = '2020-01-01';
let afterDate  = beforeDate.replace(/(\d{4})-0?(\d{1,2})-0?(\d{1,2})/, '$1年$2月$3日');

console.log(afterDate);
// 出力:2020年1月1日
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

3の倍数、5の倍数のとき

学習過程の覚書です。

 <script>
    var i=1; 
    while (i <= 100) {
     if (i%3 == 0 && i%5 == 0) {
       document.write("<p>A</p>")
     } else if ( i%3 == 0) {
         document.write("<p>B</p>")
     } else if (i%5 == 0) {
         document.write("<p>AB</p>")
     } else {
         document.write("<p>"+i+"<br></p>") //その他の数字
     }
     i++;
    }
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

もう非同期処理に悩まない!非同期処理をシンプルに書けるフレームワークを作りました?

この記事は何ですか?

この記事は、筆者が作ったフレームワークの紹介記事で、フレームワークの使い方や有用性について解説しています。

タイトルに「 非同期処理をシンプルに書ける」と書いてありますが、それはあくまで筆者の主観ですので、本当にシンプルかはご自身の感覚で判断してください。また、「もう非同期処理に悩まない!」なんて書いてありますが、どのみち悩むのは言うまでもないでしょう。

※紹介するフレームワークはまだ開発中なので、バグが多く含まれている可能性があります。( 一応、一通り動くことは確認しましたが。 )

作ったモノ


trela link card image

非同期処理をシンプルに記述できる、Store管理フレームワークです。現在は、React.jsのみに対応しています。

インストール

npmでダウンロードできます。

npmでインストール
$> npm install trela

or

yarnでインストール
$> yarn add trela

使い方

基本的な使い方を3つほど紹介します。

シンプルな使い方

fetchUserという非同期関数を Trela を使って、実行して、その結果をコンポーネントに反映するサンプルコードです。

シンプルな例
import React from "react";
import { render } from "react-dom";
import { useTrela, TrelaProvider, createContextValue } from "trela";

const contextValue = createContextValue({
  initState: {
    user: {
      name: "まだ情報がありません",
      age: NaN,
    }
  },

  reducer: (state, action) => {
    switch(action.type) {

      // fetchUserの通信結果をStateに反映
      case "fetchUser":
        return { ...state, user: action.payload };

      default:
        return state; 
    }
  },

  apis: {
    fetchUser: async (user_id) => {
      const ref = await fetch("__YOUR_API_URL__?user_id=" + user_id);
      const user = await ref.json();

      return user; // actionのpayloadとしてreducerに渡される値
    }
  } 
});

const SampleComponent = () => {
  const { apis } = useTrela();
  const { fetchUser } = apis;

  // 非同期処理を実行するためのobjectを取得する
  const ref = fetchUser("ユーザーID");

  // fetchUserを実行する。通信が完了した時に、描画更新が入る
  const [user, isLoading] = ref.start(state => state.user);

  return (
    <>
      {isLoading ? <p>Now Loading ...</p> : null}      

      <h1>User</h1>
      <p>name : {user.name}</p>
      <p>age : {user.age || "まだ情報がありません"}</p>       

      {isLoading ? (
        <button onClick={() => ref.cancel()}>
          通信をキャンセル
        </button>
      ) : (
        <button onClick={() => ref.forceStart()}>
          ユーザー情報をリロード
        </button>
      )}
    </>
  );
}

const Root = () => (
  <TrelaProvider value={contextValue}>
    <SampleComponent  />
  </TrelaProvider>
);

render(<Root />, document.getElementById("root"));

色々と書いてありますが、やっていることは定義したfetchUserを実行して、コンポーネントにそれを反映しています。
本来であれば、useEffectuseStateを用いて非同期処理を書くはずですが、そこら辺の面倒くさい所は、Trelaがやってくれますし、TrelaがグローバルStateを持っているので、他のコンポーネントとの連携もできます。

直列処理

今度は、checkLoginfetchUserの二つの非同期関数を直列に実行するサンプルコードです。
checkLoginが終わった後にfetchUserを実行する形ですね。

直列処理の例
/* -- import部分はシンプルな例と同じため省略 -- */

const contextValue = createContextValue({
  initState: {
    isLogin: false,
    user: {
      name: "まだ情報がありません",
      age: NaN,
    }
  },

  reducer: (state, action) => {
    switch(action.type) {
      // checkLoginの通信結果をStateに反映
      case "checkLogin":
        return { ...state, isLogin: Boolean(action.payload) };

      // fetchUserの通信結果をStateに反映
      case "fetchUser":
        return { ...state, user: action.payload };

      default:
        return state; 
    }
  },

  apis: {
    checkLogin: async () => {
      const ref = await fetch("__YOUR_API_URL__");
      const { isLogin } = await ref.json();

      return isLogin; // actionのpayloadとしてreducerに渡される値
    },

    fetchUser: async (user_id) => {
      const ref = await fetch("__YOUR_API_URL__?user_id=" + user_id);
      const user = await ref.json();

      return user; // actionのpayloadとしてreducerに渡される値
    }
  } 
});

const SampleComponent = () => {
  const { steps, apis } = useTrela();
  const { checkLogin, fetchUser } = apis;

  // 直列実行する関数を定義。今回の例では、checkLogin -> fetchUser の順番で実行される
  const ref = steps([ checkLogin(), fetchUser("ユーザー") ]);

  // 配列内の非同期関数を直列に実行する
  // 全ての非同期処理が完了した時に、描画更新される
  const [state, isLoading] = ref.start(state => state);

  return (
    <>
      {isLoading ? <p>Now Loading ...</p> : null}      

      <p>{state.isLogin ? "ログインしています" : "ログインしていません"}</p>

      <h1>User</h1>
      <p>name : {state.user.name}</p>
      <p>age : {state.user.age || "まだ情報がありません"}</p>       

      {isLoading ? (
        <button onClick={() => ref.cancel()}>
          直列処理をキャンセル
        </button>
      ) : (
        <button onClick={() => ref.forceStart()}>
          直列処理をリロード
        </button>
      )}
    </>
  );
}

/* -- シンプルな例と同じため省略 -- */

上記の注意点としては、steps関数の配列内には非同期関数のref( 関数の返り値 )を渡す必要があります。

サンプルで使った、steps関数は配列内の非同期処理を直列で実行しますが、配列内には他のsteps関数のrefも入れることができます。
以下に例を示します。

ちょっと複雑なsteps関数の使用例
/* -- 省略 -- */

const SampleComponent = () => {
  const { steps, apis } = useTrela(); 
  const { a, b, c, d } = apis;

  // a -> b の直列処理
  const ab_ref = steps([ a(), b() ]); 

  // a -> b -> c -> d の順番で直列実行されます
  const all_ref = steps([ ab_ref, c(), d() ]); 

  /* -- 省略 -- */
};

/* -- 省略 -- */

上記はちょっと複雑ですが、一応上記のような事もできます。これは、並列処理のrefも入れることができるので、直列と並列の組み合わせなんかもできます。

並列処理

非同期処理の中でも結構めんどくさい並列処理ですが、Trelaだと簡単に実装できます。
基本的な使い方は、直列の時と同じです。

並列処理の例
/* -- 直列のコードと同じため省略 -- */

const SampleComponent = () => {
  const { all, apis } = useTrela();
  const { checkLogin, fetchUser } = apis;

  // 並列実行する関数を定義。checkLogin と fetchUser が同時に実行される
  const ref = all([ checkLogin(), fetchUser("ユーザー") ]);

  // 配列内の非同期関数を並列に実行する
  // 全ての非同期処理が完了した時に、描画更新される
  const [state, isLoading] = ref.start(state => state);

  return (
    <>
      {isLoading ? <p>Now Loading ...</p> : null}      

      <p>{state.isLogin ? "ログインしています" : "ログインしていません"}</p>

      <h1>User</h1>
      <p>name : {state.user.name}</p>
      <p>age : {state.user.age || "まだ情報がありません"}</p>       

      {isLoading ? (
        <button onClick={() => ref.cancel()}>
          処理をキャンセル
        </button>
      ) : (
        <button onClick={() => ref.forceStart()}>
          直列処理をリロード
        </button>
      )}
    </>
  );
}

/* -- 直列のコードと同じため省略 -- */

steps関数とほとんど同じなので、簡単ですね。

all関数に渡す配列内には、steps関数のrefも入れることが可能です。なので、直列と並列を組み合わせた複雑なフローも構築可能です。

直列と並列を組み合わせた例
/* -- 省略 -- */

const SampleComponent = () => {
  const { all, steps, apis } = useTrela();
  const { a, b, c, d } = apis;

  const ab_ref = steps([ a(), b(), ]); // 直列処理
  const cd_ref = all([ c(), d() ]); // 並列処理

  // 直列処理と並列処理をまとめる
  const all_ref = steps([ ab_ref, cd_ref ]);

  // a -> b -> [ c, d ] の順番で実行される 
  const [state, isLoading] = all_ref.start(state => state);

  /* -- 省略 -- */
}

/* -- 省略 -- */

このような直列と並列を組み合わせた処理は、async/awaitでやっても結構めんどくさい所なので、そこを簡単に出来るように工夫しています。

Trelaが解決する事

使い方が分かったところで、Trelaが何を解決するかを紹介したいと思います。

非同期処理をシンプルに書ける

Trelaは、非同期処理をシンプルに書けるようにするために作りましたので、他のライブラリーよりかは書きやすくなっていると思います。どのくらいシンプルかは、以下にTrelaを使わないで非同期処理を書いた場合と比較すれば明らかだと思います。

Trelaを使わないで非同期処理を書いた場合
import React, { useState, useEffect } from "react";
import { render } from "react-dom";

const checkLogin = async () => {
  const ref = await fetch("__YOUR_API_URL__");
  const { isLogin } = await ref.json();

  return isLogin;
} 

const fetchUser = async (user_id) => {
  const ref = await fetch("__YOUR_API_URL?user_id=" + user_id);
  const user = await ref.json();

  return user;
}

const SampleComponent = () => {
  const [state, setState] = useState({ 
    isLogin: false,
    user: { 
      name: "", 
      age: NaN 
    } 
  });

  useEffect(() => {
    (async () => {
      const isLogin = await checkLogin();
      const user = await fetchUser("ユーザーID");

      setState({ isLogin, user });
    })()
  }, []);


  /* -- 省略 -- */
}


render(<SampleComponent />, document.getElementById("root"));

上記のソースコードだと、処理をキャンセルしたり、処理結果を子コンポーネント以外のコンポーネントに伝える事が出来ていないことに注意しましょう。また、これで直列や並列の組み合わせをやるのは骨が折れると思います。

早期Fetch & キャッシュ

useEffectを使ってないので、コンポーネントのマウントを待たずに非同期処理を実行します。また、Trela内部で非同期処理をキャッシュしているので、まったく同じ処理が再度実行された時は、実行を破棄します。これにより、無駄な通信や処理を省き、描画更新も最小限に保ちます。

forceStart関数は、キャシュ関係なしに非同期処理を実行するので、注意してください。

const SampleComponent = () => {
  const { apis } = useTrela();
  const { fetchUser } = apis;

  fetchUser("1").start(state => state); // 非同期処理を開始
  fetchUser("1").start(state => state); // 既に実行されているので、処理を破棄
  fetchUser("2").start(state => state); // 引数の値が違うので、非同期処理を開始

  // fetchUser("1").forceStart(); <- この関数をコンポーネントのフィールドで実行すると無限ループします
}

依存関係に考慮した描画更新

Reduxなどを使ってグローバルStateを更新する時、描画更新が無駄に実行されることがあります。
これは、Reactなどの差分描画するライブラリを使っている場合は、特に気にする必要がない場合が多いですが、
無駄に実行されているのはちょっと良くないと思いますし、結構な頻度で重くなる時があるので、Trelaではなるべく最小限に描画更新するようにしています。

Trelaは、コンポーネントの構造を記憶しているので、どのコンポーネントを更新すべきかを判断できます。なので、ユーザーはそこまで気にしなくても勝手に最適化してくれます。が、今はまだ開発中なので、ちゃんと最適化出来てない所がある可能性あります。

以下に申し訳程度の画像載せるので、なんとなく伝わっていれば幸いです。

trela-update-flow.001.jpeg

これから来るConcurrent Modeに柔軟に備える事ができる( 予定 )

React.jsでは、Concurrent Modeという非同期処理をより扱いやすくするためのAPIが発表されました。
2020/05 時点では、まだ実験的な機能となっていますが、後々はリリースされることでしょう。
Trelaでは、来たるConcurrent Modeに備えるためにそれらに対応する予定です。※ 2020/05 時点ではまだ対応してません。

Concurrent Modeに備えると言っても、ほとんど実装は変わらないのでConcurrent Modeにすぐ切り替えたり、すぐ戻したりできます。

※ まだReact.js側で仕様が決定していない所は、未対応になります。

ConcurrentModeに対応した場合
// Suspenseを使うときでも、同じソースコード
const SampleComponent = () => {
  const { apis } = useTrela();
  const { fetchUser } = apis;
  const [state] = fetchUser("user_id").start(state => state);

  /* -- 省略 -- */
}

// Suspenseを使いたいときは専用のコンポーネントで、useTrelaを使った既存のコンポーネントをラップするだけ
const Root = () => (
  <TrelaSuspense fallback={<Spinner />}>
    <SampleComponent />
  </TrelaSuspense >
);

const Spinner = () => <div>Now Loading ...</div>;

今後実装予定のモノ

今開発中の機能を紹介します。

テストツールを完備する

jestmochaで、e2eテストをすると思いますが、それをやりやすくするためのコンポーネントや関数を開発中です。
これにより、テスタビリティを損なわずに処理が書けると思います。

sample.spec.jsx
// 開発中のモノなので、ここに書いてあることは変更される可能性があります。

import { render, wait } from "your-testing-library";
import { TrelaProvider, createMockContext } from "trela";
import { SampleComponent } from "./src/SampleComponent";

describe("サンプルテスト", () => {
  test("fetchUserが呼ばれたかテストする", async () => {
    const mockValue = createMockContext({ /* ... */ });

    render(
      <TrelaProvider value={mockValue}>
        <SampleComponent />
      </TrelaProvider>
    );

    await wait(() => expect(mockValue.apis.fetchUser.complite).toBeTruthy());
  }); 
});

後書き&雑多な感想

ここまで読んでくれてありがとうございます!まだまだ未完成ですが、一応紹介してみました。
使い方はシンプルにしたつもりですが、まだまだ煮詰まってない感が否めない感じがしています。もっと改良せねば!
また、ReactのConcurrent Modeは思想はいいのですが、現状だとそこまで使うメリットが無いように感じます。使いこなすのが難しいですし、親コンポーネントに責任を丸投げしているのが、私はちょっと好きじゃないですね。だからと言って、無視するわけにもいかないので、Trelaで間を取っていけたらと思っています。

また、今回のフレームワークは個人開発をしているときに非同期処理がとても面倒くさく感じたので作ってみましたが、
おかげで、作っているサービスの進行が3か月も止まってしまい、挙句の果てには、仕事が一切無くなってしまったのですが、Trelaがある程度まで作れたので、良しとしておきましょう。

また何かのフレームワークなりライブラリなりを作ると思うので、その時にまた記事を書こうと思います。

それでは?

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

Could not find a JavaScript runtimeが出て、サーバーを起動できなくなった時の話(ターミナル)

サーバーを起動できない!

ある朝、サーバーを起動してさあ作業に取り掛かろう!ということで$rails sをしてサーバーを起動しようとすると、以下のエラーが発生しサーバーを起動することができない。

Gem Load Error is: Could not find a JavaScript runtime. 
See https://github.com/rails/execjs for a list of available runtimes.

この時に解決するのがなかなか大変だったので、以後同じエラーが起きた時のための備忘録として書かせて頂きます。。

2つの解決法

どうやら調べてみるとエラー内容は文字通り、JavaScriptのruntimeがないよということらしい。

過去にも同様の問題に遭遇した人の記事がたくさんあり、調べてみると大まかに2つの解決法があるらしい。

Node.jsをインストールする

1つがこの方法。実際自分はこの方法で解決しました。
Node.jsをインストールするには以下の①②③のステップが必要らしい
①Homebrewをインストール
②nodebrewをインストール
③Node.jsをインストール

①Homebrewをインストール
ホームディレクトリで以下を実行

$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Press RETURN to continue or any other key to abort
と出たらEnterキーを押す
Password:
と出たらパスワードを入力しEnterキーを押す(入力中のパスワードは表示されないがしっかりとは入力されている)

②nodebrewをインストール

brew install nodebrew

③Node.jsをインストール

$ brew install nodejs

最後にこれをインストールして、無事Node.jsのインストール完了

この後に再び$ rails sをするとサーバーが起動するように!!

Gemのtherubyracerを追加する

Node.jsのインストール以外に多くヒットしたもう1つがこの方法。

Gemfileに⬆️を追加して、コマンドで$bundle installするだけなので、こちらの方が簡単そうに見えるが、こちらはこちらでまたバージョン関係の別のエラーが発生してしまうことがあるらしい。(ここで発生するエラー内容については曖昧です)

最後に

今回自分がサーバーを起動できない時の対処法を自分なりに噛み砕いてメモさせて頂きました。

もし間違ってる箇所や他にも対処法が存在する場合は、ご指摘してくださると幸いです?‍♂️

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

【個人用】

随時更新

変数(const,let,var)

再代入 再定義 備考
const × ×
let ×
ver 古い。ブロックスコープの無視。変数の巻き上げがおこりバグの原因になる可能性がある。

const

script.js
//再代入できない
const constNumber = 100;
constNumber = 200;
>> "TypeError: Assignment to constant variable."

//再定義できない
const constNumber = 100;
const constNumber = 200;
"SyntaxError: Identifier 'constNumber' has already been declared"

//constが厳密に定数ではない理由
const obj = {
  foo: 100
};
console.log(obj.foo);
>> 100
obj.foo = 200;
console.log(obj.foo);
>> 200 //変数objのfooの値が更新できるため。

let

script.js
//再代入できる
let letNumber = 100;
console.log(letNumber);
>> 100
letNumber = 200;
console.log(letNumber);
>> 200 //再代入により値が更新された

//再定義できない
let letNumber = 100;
let letNumber = 200;
>> "SyntaxError: Identifier 'letNumber' has already been declared"

ver

script.js
//再代入できる
//再定義できる
var varNumber = 100;
var varNumber = 200;
console.log(varNumber);
>> 200

条件式(if文)

配列

script.js
let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS'];
console.log(list);
>> ["Ruby", "Ruby on Rails", "JavaScript", "HTML", "CSS"]

配列の要素を取得

script.js
let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS'];
console.log(list[2]);
>> "JavaScript"

配列の要素数を取得

script.js
let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS'];
console.log(list.length);
>> 5

lengthメソッド...配列の要素の数を設定または取得する
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/length

配列の要素を追加

script.js
let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS'];
list.push('GitHub');
console.log(list);
>> ["Ruby", "Ruby on Rails", "JavaScript", "HTML", "CSS", "GitHub"]

pushメソッド...配列の末尾に要素を追加する
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/push

配列の要素を削除

script.js
let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS'];
list.pop();
console.log(list);
>> ["Ruby", "Ruby on Rails", "JavaScript", "HTML"]

let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS'];
let poped = list.pop();
console.log(poped);
>> "CSS"

popメソッド配列の最後の要素を取り除き、呼び出し元にその値を返す。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/pop

script.js
let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS'];
list.shift();
console.log(list);
>> ['Ruby on Rails', 'JavaScript', 'HTML', 'CSS']

shiftメソッド...配列から最初の要素を取り除き、その要素を返す。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/shift

オブジェクトを扱う

オブジェクトを定義

script.js
let obj = { name: 'yamada', age: 25, address: 'tokyo' };
console.log(obj);
>>
[object Object] {
  address: "tokyo",
  age: 25,
  name: "yamada"
}

オブジェクトはデータを名前と値をセットで管理。このセットをプロパティと言う。
addressはプロパティ名、'yamada'は対応する値となる。

プロパティの値を取得

script.js
let obj = { name: 'yamada', age: 25, address: 'tokyo' };
console.log(obj.name);
>> "yamada"

プロパティの値を変更

script.js
let obj = { name: 'yamada', age: 25, address: 'tokyo' };
obj.name = 'tanaka';
console.log(obj);
>>
[object Object] {
  address: "tokyo",
  age: 25,
  name: "tanaka"
}

繰り返し

for 文

script.js
num = 1;
for (let i = 0; i < 10; i += 1) {
  console.log(num + '回目の出力')
  num +=  1
}

"1回目の出力"
"2回目の出力"
.
.
"10回目の出力"

関数( 関数宣言と関数式(無名関数) )

JavaScriptにおける関数の定義には2種類の方法がある

script.js
// 関数宣言
function hello(){
  console.log('hello');
}

// 関数式(無名関数)
let hello = function 関数名(){
    console.log('hello');
}

関数式(無名関数)...関数を変数の中に値として入れたもの。また、関数名が無くても動かすことができる。

関数を定義

script.js
function 関数名(引数) {
  // 処理の内容
}

function sayHello(){
  console.log('hello');
}
function sayName(name){
  console.log(name);
}

let myName = 'yamada';
sayHello();
>>hello
sayName(myName);
>>yamada

オブジェクトに対してプロパティ名を続けて記載することで値を取り出すことができる。

return

script.js
function calc(num1,num2){
  return num1*num2;
}

let num1 = 3;
let num2 = 4;
console.log(calc(num1,num2));

高階関数(関数の中で関数の処理を行っている関数)

script.js
function 高階関数(コールバック関数) {
  // 色々処理を入れたり...
  コールバック関数();
}

コールバック関数(引数として渡されている関数のこと)

script.js
script.js

HTM要素の取得

test.js
$("セレクタ")

//クラス属性の取得
$(".classSelector")
//id属性の取得
$("#idSelector")
//要素の取得
$("h1")
//属性の取得
$("input[ type='radio' ]");

thisでイベントの発生元となった要素を取得する

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

[JavaScript] RxJSを使わずにRxJSを理解する

はじめに

この記事は、RxJSの使い方を解説する記事です。
けど、RxJSは出てきません。
(えっ

よくわからないままにRxJSを使い始めて悩んでいるひと(←自分)のための再入門記事です。

2つのテーマについて書いています。

  • subscribeしないと動かないのはなぜか
  • 何度もsubscribeすると、何度も動くのはなぜか

この記事の登場人物

プロデューサー

tv_producer.png

プロデューサーは、依頼を受けて何か仕事をしてくれます。
仕事がいつできあがるかはわかりません。
できあがり次第教えてくれることになっています。

オブザーバー

shinbun_woman.png

オブザーバーは、仕事の結果を受け取ります。

場合によって、3つの役目を受け持ちます。
正常結果を受け取る役目、失敗結果(エラー)を受け取る役目、仕事が全て終わったことを受け取る役目です。

オブザーバブル

job_shinbunhaitatsu.png

オブザーバブルは、プロデューサーとオブザーバーの仲介役です。
オブザーバーから注文を受けると、プロデューサーを働かせて仕事結果をオブザーバーへ渡させます。

subscribeメソッドを持ちます。
今回のキモです。

実装

RxJSを理解するために、それぞれを簡単に実装してみます。

まずキモであるオブザーバブルです。

オブザーバブルの実装

オブザーバブルは、オブザーバーからの注文をsubscribeというメソッドで受け付けます。

仕事の内容は、コンストラクタ引数で受け取ったプロデューサー任せです。

subscribeメソッドの中身はまた後で実装します。

class オブザーバブル {
    constructor(プロデューサー) {
        this._プロデューサー = プロデューサー;
    }

    subscribe(オブザーバー) {
      // 後で
    }
}

オブザーバーの実装

オブザーバーはnext error completeの3つのメソッドを持つオブジェクトです。
それぞれ、正常結果、失敗結果(エラー)、仕事の完了を受け取るためのものです。

const オブザーバー = {
    next: function(結果) {},
    error: function(エラー) {},
    complete: function() {},
};

プロデューサーの実装

プロデューサーはただの関数にしました。
オブザーバーを引数に取り、自分の仕事結果を渡します。
仕事がすべて正常に終わればcompleteを呼び出し、途中で何か問題があればerrorを呼び出します。

実際の開発現場では、フレームワーク等があらかじめ用意しているプロデューサーを使うことになると思います。

function プロデューサー(オブザーバー) {
    try {
        オブザーバー.next(仕事1());
        オブザーバー.next(仕事2());
        オブザーバー.next(仕事3());
        オブザーバー.complete();
    } catch (e) {
        オブザーバー.error(e);
    }
}

使い方

上記で実装したプログラムを使うシーンはこうなります。

オブザーバブルオブジェクトを作って、subscribeメソッドにオブザーバーを渡します。

const observable = new オブザーバブル(プロデューサー);

observable.subscribe(オブザーバー);

subscribeメソッドの実装

後回しにしたオブザーバブルのsubuscibeメソッドの実装です。
ここが今回のテーマのキモです。
キモですが、内容はごく単純です。プロデューサーを呼んでいるだけです。

class オブザーバブル {
    constructor(プロデューサー) {
        this._プロデューサー = プロデューサー;
    }

    subscribe(オブザーバー) {
        this._プロデューサー(オブザーバー);
    }
}

まとめ

subscribeしないと、プロデューサーは呼ばれないことがわかりました。

subscribeする度に、プロデューサーが呼ばれることがわかりました。

次は……!?

  • subscribeしなくても動くプロデューサーがあるんだってよ
  • プロデューサー一回の働きを、複数箇所でsubscribeするやりかたがあるんだってよ

参考リンク

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

js.erbで簡単に非同期通信画像削除

はじめ

FontAweSomeで削除アイコンを導入

qiita.rb
<head>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
xxx.html.erb
  <i class="fa fa-trash-o fa-lg" aria-hidden="true"></i>

image.png

Destroy一例

photos_controller.rb
  def destroy
    @photo = Photo.find(params[:id])
    room = @photo.room

    @photo.destroy
    @photos = Photo.where(room_id: room.id)

    respond_to :js

respond_to :jsのファイル

photos/destroy.js.erb
$('#photos').html("<%= j render 'photos_list' %>")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Firefox】でローカルファイル(file:///)を読み込んだ時にCORSエラーを出ないようにする

TL;DR;

以下の手順で、開いたHTMLの親ディレクトリ配下のファイルはCORS制限を受けなくなります。

  1. about:config を開く
  2. privacy.file_unique_origin をfalseに設定

CORSについて

あるページで実行されているJavaScriptが、別ドメインのファイルにアクセスしようとしたときにそれを制限する「ブラウザの機能」です。
制限なので、アクセスされる側のサーバーに設定をするなどすることで許可することもできます。また、ブラウザの機能なので、起動オプションや一部のアドオンによっても無効化できるケースがあるようです。

元々は、エンドユーザーのセキュリティというよりは、他のサイトのスクリプトから勝手にコンテンツを盗用されないためのいわば著作権保護的な側面が強かったと聞いたことがあります。

http//example.com/index.html
<!-- 一般的なCORS、回避するにはサーバーの設定が必要 -->
<script>
var xhr = new XMLHttpRequest();
xhr.open( 'GET', 'http://hoge.com/data.hteml' );
xhr.send(); // CORSエラー!
</script>
file///c/Users/Admin/Downloads/index.hteml
<!-- 上記設定で回避可能な例 -->
<script>
var xhr = new XMLHttpRequest();
xhr.open( 'GET', 'file:///c/Users/Admin/Downloads/data.hteml' );
xhr.send(); // CORSエラー!同じ「file:///c」だけどダメ。
</script>

補足と弊害

元々はデフォルトで許可されていたらしいのですが、Firefox68からデフォで制限されるようになったらしいです。
(執筆時点(2020/05/18)では最新が 76)
理由としては「CVE-2019-11730 の対策」とのこと。
この「CVE-2019-11730」ですが、悪意のあるhtmlをダウンロードしてしまいそれをうっかり開いてしまったときに、ダウンロードフォルダの中身が自由にみられてしまうというものです。なので、上記オプションによりこのような攻撃に対して脆弱になっている状態だということは頭に入れておいてください。
(うっかり開いたのがhtmlではなくexeだったらなんでもありですがw)

参考

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

Javascriptでtweenerを自作する

たまに作るような内容ですがコピペで使えるように、メモ的に書いています。

setIntervalで回数制限
https://ndruger.hatenadiary.org/entry/20100928/1285679176
イージングの使い方の確認
https://gist.github.com/gre/1650294
経過時間の参照
https://webbibouroku.com/Blog/Article/js-time

上記リンクを参考

EasingFunctionsはそのまま利用する(jsに直書きでもOK)

qiita.js
function tweener(_time,_func,_ease){
   var start = new Date().getTime();
    var _si = setInterval(function(){
        var _progress = new Date().getTime() - start;
        if ( _progress > _time) {
          //1
          _func(1);
          clearInterval(_si);
        }else{
          //
          _func(_ease(_progress/_time));
        }
    }, 33);
}

これで使い方は

qiita.js
tweener(3000,function(progress){
  console.log(progress)
},EasingFunctions.easeOutQuint);

こんな感じでprogressに0-1までの値で3秒間でeaseOutQuintで値が出てくるので変数に当てはめればいい。

でこれで終わりでもいいのですが、途中で止めるっていうのも欲しい。やめ方も
skipで終了値にする、cancelで初期値にする、killで止めるっていうのができたらいいので追加すると

qiita.js
var TWEENER = {
  tween:function(_time,_func,_ease){
    var start = new Date().getTime();
    var _this = this;
    _this.functionHolder = _func;
    _this.setIntervalObj = setInterval(function(){
        var _progress = new Date().getTime() - start;
        if ( _progress > _time) {
          //1
          _func(1);
          _this.functionHolder = null;
          clearInterval(_this.setIntervalObj);
        }else{
          //
          _func(_ease(_progress/_time));
        }
    }, 33);
  },
  cancel:function(){
    if(this.functionHolder != null){
      this.functionHolder(1)
      clearInterval(this.setIntervalObj);
    }
  },
  skip:function(){
    if(this.functionHolder != null){
      this.functionHolder(1)
      clearInterval(this.setIntervalObj);
    }
  },
  kill:function(){
    clearInterval(this.setIntervalObj);
  },
  functionHolder:null,
  setIntervalObj:null
}

で使い方は

qiita.js
TWEENER.tween(3000,function(progress){
  console.log(progress)
},EasingFunctions.easeOutQuint);

setTimeout(function(){
  TWEENER.cancel();
},1000);

※無理やり1秒後にcancelをしています
な感じで自前でtweenerが完成。

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

Incoming Webhookからランダムに掃除の通知をしよう!

何をしたいのか?

Bot(Incoming Webhook)に掃除する日を決めてもらい、指定する時間に通知をしてほしい。

きっかけ

  • 複数人で利用している事務所がある
  • 定期的に掃除する日がない
  • 掃除のために定期的に集まるのは難しい

→ Botから通知で居合わせた人たちで掃除しよう
と考えた

使うアプリケーション

  • Slack
    業務でよく使うため

  • Incoming Webhook
    常時稼働が不要とサーバーを立てる必要がない

  • Google Apps Script(GASと略す)
    定期的にコードを実行する設定がしやすい

具体的な進め方

  1. SlackのチャンネルとIncoming Webhookを連携させる
  2. Incoming Webhookから1/30の確率でSlackへ通知するように、GASにコードを書く
  3. GASのnotifyトリガーで決まった時間帯にコードを実行するように設定する

1. SlackのチャンネルとIncoming Webhookを連携させる

参考 Slack Botの種類と大まかな作り方

https://slack.com/apps/A0F7XDUAZ--incoming-webhook-?next_id=0 にアクセスして、Slackに追加を押す

チャンネルへの投稿で導入したいチャンネルを選択する

※ Webhook URL はあとで使うため、コピーする
スクリーンショット 2020-05-17 23.29.27.png

2. Incoming Webhookから1/30の確率でSlackへ通知するように、GASにコードを書く

https://drive.google.com/drive/u/0/my-drive にアクセスし、左上から新規→その他→Google Apps Scriptを押す

Screenshot from Gyazo

  コードは以下のように書いた
  Incoming Webhookからのメッセージは自由に変えられる

参考 Block Kit Builder


function main(){
  var result;

// 1/30の確率で通知させたい
//Math.random()は0~1未満までをランダムに生成
//Math.floor()は小数点以下を切り捨て整数を返す
//0~29のうち29以上であればIncomigWebhookから通知が来る
  result = Math.floor(Math.random()*30); 
  if(result >= 29){
    Do_notify();
  }
}

function Do_notify() {
  var options =
  { // 以下のテキストはJSONの様式にそって書くと変更できる
    "method" : "post",
    "contentType" : "application/json",
    "payload" : JSON.stringify(
      {
        "attachments":[
      {
         "fallback":"今日は掃除の日です",
         "pretext":"<!channel> 今日は掃除の日です",
         "color":"#D00000",
         "fields":[
            {
               "title":"掃除内容は以下の通りです ",
               "value":"*_掃除機かける_* \n *_デスクのアルコール除菌_* \n *_ホワイトボード消し_*",
               "short":false,
              "mrkdwn_in": ["value"]
            }
         ]
        }
       ]
      }
    )
  };

  UrlFetchApp.fetch("https://hooks.slack.com/services/XXXXXXX/XXXXXX/XXXXXXXXXXXXXX", options);
}

3. GASのnotifyトリガーで決まった時間帯にコードを実行するように設定する

以下のボタンを押してトリガーを設定する

スクリーンショット 2020-05-17 23.48.25.png

トリガーを追加を押すと分ベースや時間ベースで定期的に実行できるように設定できる

1日毎の17時から18時の間で実行するようにセットし、保存を押せば完成

スクリーンショット 2020-05-17 23.56.27.png

参考にした記事

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