20200209のJavaScriptに関する記事は25件です。

【動画付き】 draw.io 使い方まとめ 〜エンジニアでなくても使えるTips集〜

title

draw.io はブラウザを使用してフローチャート、プロセス図、組織図、UML 図、ER モデル、ネットワーク図などを作成できる優れたツールです。作成した図は xml ファイルとして保存でき、GitHub との連携もシームレスに行われます。3 年ほど愛用しているツールですが、隠された使い方がたくさんあります。すぐに忘れてしまうので取りまとめておきます。

「こんな使い方あるよ!オススメだよ!!」という方はぜひ編集リクエストをいただければ追記していく予定です ?

※ 主に参照している文献は以下、公式ブログは非常に分かりやすいのでオススメです。

ショートカット

ショートカット集です。机の上に置いて覚えましょう。

shortcut

Line / 線

まずは最も頻繁に使う Line(線)の使い方からご紹介します。

矢印をまっすぐに揃える

ちまちまと矢印の線をドラッグして微調整していませんか? 右メニューから簡単に直線に揃えることができますよ。

adjastarrow.gif

線にラベルを挿入する

線上のどこでもいいのでダブルクリックをするとラベルを挿入できます。テキストボックスを作成してから矢印を繋げるよりもずっと早いですね。

arrowlabel.gif

Shape をコピーしつつ、矢印もつける(Alt+Shift+十字キー)

Shape をコピーしてから、矢印を引っ張って繋げていませんか?  Alt+Shift を押しながら十字キーを押してみましょう。これなら 10 倍は効率がいいですね。

copyobjecttoline.gif

Shape をコピーしつつ、矢印もつける(ctrl+drag)

Ctrl キーを押しながら、Shape から引っ張れる青い線を引いてみましょう。あら不思議、Shape が複製されました。Ctrl キーを押しながら青い線をダブルクリックすると線は引かれずに Shape だけが複製されます。

ctrlclick.gif

Shape の接続

Shape 間から伸ばす線が勝手に他の Shape にくっついてしまって困ることはありませんか? そんな時は Ctrl+Shift を押しながら線を引っ張ってみましょう。ちょっとだけ幸せになれますね。その他、Shift キーだけを押しながら線を引っ張ると接続点を無視することもできます。

connectpoint.gif

曲線

デフォルト設定だと直線ですね。曲線ももちろん使えますよ。

curve.gif

Shape / 図形

この章では Shape に関連した Tips をご紹介します。Shape とは1つの図だと思ってください。リソースやオブジェクトなどと呼ばれたりしますが、公式の呼び方は Shape のようです。

画像の切り抜き

画像を丸や四角い形に切り抜きたい場合があります。画像のワンちゃんのかわいさが際立ちますね ?

ssr.gif

要素の置き換え Shift+click

Shape を一度置いてみたものはいいものの、もっと適切な形があるので変えたいという時に便利です。Shift を押しながら左メニューの中から Shape をクリックしてみましょう。簡単に置き換えられます。

shiftclick.gif

その他の図形

もっと他の図形も見てみたい、使いたいという方はこちらから。AWS リソースのアイコンなんかもありますよ。

moreobject.gif

系統図

Shape 間の関係を維持したまま、系統図を自動整形できます。とりあえず線を繋げまくったけど、関係性がわからなくなってきた、ドラッグで1つずつ移動させるのはしんどいなぁという時に便利です。

organic.gif

プレースホルダ(文字埋め込み)

ほとんど知られていませんが、プレースホルダが使えます。特定の条件にあわせて文字列を動的に組み換えることができます。IP アドレスが振られた構成図や、小さな Shape を寄せ集めた図を書く時に重宝しそうですね。公式ブログ ~How to work with placeholders?~ にて詳しく解説されています。

placeholder

placeholder.gif

比率を維持したままリサイズ Cmd(Mac) または Ctrl(Windows)

右メニューから比率を固定することもできますが、Cmd キーを押しながらドラッグするだけでリサイズできるショートカットは便利です。

resize.gif

スクラッチパッドにグループを追加する

左メニューのスクラッチバッドに図を追加できます。

scratchboard.gif

Shape をコミカルにする

図の形を漫画風にできます。

importfromspreadsheet.gif

フローチャート

様々なフローチャートの書き方を覚えておくと、適切なフォーマットで情報を伝える力が身につきます。以下のブログを読んでおけばよいでしょう。テンプレートも用意されているので、すぐにそれなりのものが出来上がります。

公式ブログ ~Creating different types of flowcharts with draw.io
~

flowachart

Settings / その他操作、設定など

意外と知られていない設定や操作が存在します。この章ではそんな隠れた Tips をご紹介します。

ズームイン/アウト(Alt or Option) + Mouse

Alt または Option を押しながらカーソルを動かしてみましょう。拡大縮小ができます。ちなみに Ctrl+プラスまたはマイナスでサイズを変更している方は Ctrl+0 を押してみましょう。きっと幸せになれるはずです。

zoom.gif

ダークモード

最近流行りのダークモード、もちろんできます。Extras > Theme から設定しましょう。

darkmord.gif

その他のフォントを使用する

標準のフォント以外にもシステムフォント、Google フォント、Web フォントが使用できます。

font.gif

設定の共有

draw.io の設定を Json 形式で保存して共有できます。公式ブログ ~How to configure draw.io?~を参照しましょう。

config.gif

PlantUML

PlantUML をインポートして描画できます。PlantUML の書き方はこちらの記事によくまとまっていました。

plantuml.gif

ルーラー

細かい微調整にはルーラーが欠かせません。

ruler.gif

リンクを作成

作成した図は公開できます。リンクを作成すると画像ファイルの形式で閲覧できます。

link.gif

埋め込み HTML の作成

作成した図をブログなどに挿入する場合は埋め込み HTML がオススメです。

https://about.draw.io/publish-link-and-embed-html/

CSV から読み込む

Google のスプレッドシートなどで CSV ファイルを管理しておいて、draw.io に読み込ませることができます。
こちらのツールを使用してスプレッドシートと draw.io を連携しましょう。スプレッドシートを更新すると draw.io の図も更新されます。公式ブログ ~Automatically create draw.io diagrams from CSV files~に詳しい説明があります。

importfromspreadsheet.gif

プラグイン / 拡張

draw.io には様々なプラグインが用意されています。この章では、その一部をご紹介します。

現在使用できるプラグインの一覧

※ ちなみにプラグインは JavaScript で書かれていました。

Line をアニメーション化

Flow プラグインを使用することで、Line にアニメーションを付与できます。詳細は公式ブログ ~Connector styles and animations in draw.io~を参照してください。

anime.gif

SQL を読み込んで ERD を作成する

SQL プラグインを使用することで CREATE 文から ERD を生成します。

sql.gif

さいごに

ざっと draw.io さん公式から情報を集めてみました。
他にも良い使い方を知っている方は、ぜひ教えてください ?

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

【React】 郵便番号から住所を自動入力

こんにちは。
今回はajaxzip3を使って、Reactでも簡単に住所の自動入力を実装する方法を紹介します。

住所自動入力

オンラインショッピングを利用するユーザーの約3分の2が購入を完了する前にカートを離脱していることが明らかとなっています。ECサイトのカート破棄率は平均約70%とされていて、カートの離脱を防ぎ、購入率を上げるためにもEFO(入力フォーム最適化)は重要です。

使い方

はじめにhtmlファイルのbody内にajaxzip3を読み込みます。

index.html
<script src="https://ajaxzip3.github.io/ajaxzip3.js" charset="UTF-8"></script>

関数

AjaxZip3.zip2addr()の引数にinputタグのnameを指定し、郵便番号を入力すると、指定したフォームに対して都道府県、市町村、番地が自動で入力されます。
自動入力ではonChangeが発火しないため、stateが更新されず、フォームからapiにデータを渡す時に空判定されてしまう可能性があります。
そのため、指定したidのフォーム内に入力されている住所をdocument.getElementById('id').valueで取得してsetStateで更新してあげます。

Form.js
class Form extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      user: {}
    };
  }

  handleChange = e => {
    const params = this.state.user;
    params[e.target.name] = e.target.value;
    this.setState({ user: params });
  };

  complementAddress = () => {
    const { AjaxZip3 } = window;
    AjaxZip3.zip2addr(
      'postCodeH',
      'postCodeF',
      'address1',
      'address2',
      'address3'
    );
  };

  onBlurZipcode = () => {
    this.setState({
      user: {
        ...this.state.user,
        address1: document.getElementById('address1').value,
        address2: document.getElementById('address2').value,
        address3: document.getElementById('address3').value
      }
    });
  };

  render() {
    return(
      <div>下記フォーム</div> 
    )  
  }
}
export default Form;

郵便番号を入力するフォーム

 <input
    name="postCodeH"
    size="3"
    maxLength="3"
    onChange={e => this.handleChange(e)}
/>
-
<input
    name="postCodeF"
    size="4"
    maxLength="4"
    onChange={e => this.handleChange(e)}
    onKeyUp={this.complementAddress}
    onBlur={this.onBlurZipcode}
/>

住所を入力するフォーム

<input
    name="address1"
    id="address1"
    onChange={e => this.handleChange(e)}
/>
<input
    name="address2"
    id="address2"
    onChange={e => this.handleChange(e)}
/>
<input
    name="address3"
    id="address3"
    onChange={e => this.handleChange(e)}
/>

動画をアップロードできないので

スクリーンショット 2020-02-09 23.05.36.png
郵便番号を入力すると、、、、、
スクリーンショット 2020-02-09 23.06.00.png

とても便利。
ありがとうございました。

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

個人的にオススメしたいTech系YouTuberたち

僕は普段から『Qiita』以外にもプログラミングの情報収集を
『YouTube』で行っているので今回は
個人的にオススメしたいTech系YouTuberの方々と彼らの動画ベスト③を
紹介していきたいと思います!(独断と偏見が入っています笑)

①KENTA / 雑食系エンジニアTV

https://www.youtube.com/channel/UC_HLK-ksslL-Z_2wiIZDlMg

  • もう既にご存知の方も多いと思いますが、KENTAさんはプログラミングのみならずIT業界(自社開発、受託、SES等)に関しても深い知見があり今まで見てきたTech系YouTuberの中でもダントツに質の高い情報を提供されている方です。彼の動画はどの動画も有益だと思いますが中には忙しい方もいると思います。そこで、個人的に特にオススメしたい彼の動画ベスト③を発表します!

【第一位】モダンなIT企業を見極める7つの技術的チェックポイント

https://www.youtube.com/watch?v=Aw8w9qSdZtY

  • 就職される方や転職される方がモダンなIT企業を見極める際に重要となるチェックポイントを提示してくださっています!普通に有料級の情報だと思うのですがそれがYouTubeでタダで見れてしまうので恐ろしいですね(笑)

【第二位】良い質問をする技術 〜質問テンプレートのススメ〜

https://www.youtube.com/watch?v=P6yRjOyM4jU

  • 個人的に思う優秀な人の共通点が「質問力」が高いということ。「質問力」に自信のない方は見ておいて損はないかと思います!

【第三位】Web系自社開発企業さんへの転職に必要なポートフォリオのレベルとは

https://www.youtube.com/watch?v=N0yetny4Zco

  • Web系自社開発企業さんへの転職が難しくなってきている現実を知るのに有益かと思います!

ちなみにですが、僕は彼の雑食系エンジニアサロンに今月入会しました!
月額980円で参加できるサロンにしては
かなり質の高いコミュニティなのでは?と思います!

具体的に《サロンのどういう所が良いと思ったのか》と言うと、

①サロン内で技術に関して質問できる環境が整っていること
②アウトプット報告でサロン内のメンバー間で互いに高め合う環境があること
③ポートフォリオを見せ合いレビューし合う環境があること

(人によっては技術に関しての質問なんてteratailがあるから
不要と思う方もいるかもしれませんが、個人的な感想を言うとあのサービスのユーザーは
結構質問が無視されているケースも見かけるので何とも言えない感じがします。)

続いて二人目のTech系YouTuberを紹介していきます!

②【人生逆転エンジニア】アップスターツ

https://www.youtube.com/channel/UCczh4w4k5cjuZBKFqlIF3kw

  • コンテンツの内容はどちらかというと初心者向きな感じがします! ポップなBGMを聞きながら楽しい雰囲気で情報をインプットできるかと思います!

【第一位】【年代別】プログラミング学習のゴールを教えます

https://www.youtube.com/watch?v=Zh7njKsR_U0

  • なぜこの動画を第一位に選んだのかというと 学習には「ゴール」を設定するべきだという持論があるからです。 「ゴール」を決めて「逆算」して学習していきましょう!

【第二位】エンジニアなら知っておきたいGoogle検索のコツ

https://www.youtube.com/watch?v=S22Bs-G5bZU

  • エンジニアとして働く以上「情報検索能力」いわゆる、「ググり力」も 大事な基礎スキルになってきます。その「ググり力」を身に着けるのに 良いヒントを提示してくれている動画ですので時間がある時に 是非とも視聴することをお奨めします。

【第三位】エンジニアが良く使う便利ショートカットキーを紹介!【13個】

https://www.youtube.com/watch?v=Yt92zqwCdyc

  • ショートカットキーもエンジニアが効率的にプログラミングするための 重要なスキルの一つなので是非とも習得してしまいましょう!

続いて、三人目のTech系YouTuberを紹介していきます!

③進撃する人【現役エンジニア】

https://www.youtube.com/channel/UC3mMWAA1Lo3rwMorseTcn2g

  • プログラミングスクールの実態や転職活動で経験したこと、SESの闇など 面白くて有益な情報を提供してくださる方です!

【第一位】プログラミング学習でおすすめの教材を紹介しよう

https://www.youtube.com/watch?v=vvAqUvek-60

  • 進撃する人が実際に使ったことのあるプログラミング学習教材 (progate, Udemy, ドットインストール, Techpit)で 各々の特徴とオススメ度を語ってくれています!

【第二位】未経験エンジニアが転職をするならwantedlyが最強な件について

https://www.youtube.com/watch?v=YgHvdxUdI0Q

  • なぜwantedlyがオススメなのかを語ってくれています! 個人的に面白いと思う自社サービスをリリースしている企業さんも多いので 是非とも使ってみてほしいと思います!

【第三位】転職サイトでSESを見分ける方6つの方法とは!?

https://www.youtube.com/watch?v=UM1kajpU4JI

  • SESを見分ける際に使えるかなり有益な情報を発信してくださっています!

最後に四人目のTech系YouTuberを紹介していきます!

④くろかわこうへい【渋谷で働くクラウドエンジニアTV】

https://www.youtube.com/channel/UCX30pfp4p82rIiSJmBygADw

【第一位】未経験ループ脱出4つの基本戦略「未経験からIT業界に飛び込んだとき」を振り返る

https://www.youtube.com/watch?v=_kDIvVYy81U

  • 自分自身も就職活動をしている過程で認識したことですが、 日頃からQiita等の技術ブログでアウトプットしていることを 重視している企業さんは増えてきていると思います!

【第二位】未経験でも渋谷エンジニア業界のベテラン勢を簡単に追い抜ける2つの理由と給料に現れない市場価値

https://www.youtube.com/watch?v=TvHtaealnzg

  • 彼曰く、AWS や Docker や Git できれば kubenertes を習することで ベテランエンジニアとの差別化を図れるとのことです!

【第三位】バックエンドエンジニア5分類と意外と狙い目のポジションである3つの理由

https://www.youtube.com/watch?v=ujUFOi49Rwc&t=3s

  • なぜバックエンドエンジニアがオススメなのかを論理的に 分かりやすく解説して下さっています!

【番外編】

しまぶーのIT大学

https://www.youtube.com/channel/UCti6dG0zSAetLGGYcgNML4Q

  • 個人的に応援しているTeck系YouTuberです! プログラミングだけでなく起業やスタートアップ界隈の情報も提供して下さる方なので 興味のある方はチャンネル登録をしておくことをオススメします!

とだこうき

https://www.youtube.com/channel/UCzZiw3exu_81WvN3DKRNXTA/videos

  • 最近YouTubeの活動をストップされているので番外編に入れさせて頂きましたが 彼の発信しているコンテンツもなかなか有益なのでは?と思います!

マコなり社長

https://www.youtube.com/channel/UC7I3QTra4_kC4TSu8f7rHkA

  • マコなり社長はエンジニア出身で現在株式会社divの代表取締役として 会社を経営されておられる方です! 番外編に入れた理由ですが プログラミングに関する情報発信はほとんどされていないからです。 コンテンツの内容は「自己啓発」が多い印象です。 少しでもより良い自分になりたい方は 彼のYouTubeチャンネルを登録してみるのをオススメします!

迫 佑樹

https://www.youtube.com/channel/UCKxnXboujhwy7osAwu75-2w

  • 大学在学中にプログラミングのインターンに参加したり、 プログラミングのコンテストに参加して受賞したり、起業して自身で Skill HacksやFront Hacksなどのプログラミング学習教材を作ったり、 YouTubeで情報発信したりなど多方面で活躍されておられる凄い方です! 番外編に入れた理由ですが、プログラミングに関する情報発信というより 内容が「ビジネス」寄りだからです。ただ内容自体は 見ていて個人的には勉強になるな~と思いながら勝手に勉強させてもらっています!

やまもとりゅうけん

https://www.youtube.com/channel/UCp60qNFmqRy7Q5ymP20dsGw

  • やまもとりゅうけんさんは数あるTech系YouTuberの方々の中で 「フリーランスエンジニア」になることを推奨されています! 彼も迫 佑樹さんと同様に内容が「プログラミング」というよりは「ビジネス」寄り だったので番外編に入れました。ただ発信されているコンテンツは勉強になる内容が 多いので「プログラミング」だけでなく「ビジネス」も!という方にはオススメです!

以上で「個人的にオススメしたいTech系YouTuberたち」の紹介を終えます!
この記事を読んでくれた読者の方に少しでも参考になれれば嬉しいです(^^)

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

Google Apps Script を V8 ランタイムで実行する方法

概要

先日 (2020/02/05) Google Apps Script が V8 Runtime で実行できるようになりました。 Google Apps Script はこれまで JavaScript 1.6 のバージョンがベースとなっていましたが、アロー関数や const, let が利用できるなど、モダンな書き方ができるようになりました。

詳しくは各種ドキュメントをリンクしておきます。

V8 ランタイムで実行する

公式ドキュメントにも V8 ランタイムでの実行方法 は書かれていますが、手っ取り早く始めたい方のために、ここでまとめておきます。

clasp を使っている方もいると思いますので、スクリプトエディタと clasp 両方での方法を記述しておきます。

スクリプトエディタで始める

まず https://script.google.com/home から 新しいプロジェクト を選択します。既存のプロジェクトを V8 対応したい場合は該当プロジェクトを選択して開きます。

新しいプロジェクトを選択

現状、以下 3 つほど方法がありますので全て載せておきます。

1. 起動時のトーストで "有効にする" を選択

プロジェクトを開いた際に以下のトーストが表示されたら、そちらで 有効にする を選択すれば完了です。

Enable new Apps Script runtime powered by Chrome V8 for this project.

スクリーンショット 2020-02-09 17.55.07.png

2. メニューから有効化

スクリーンショット 2020-02-09 17.55.22.png

スクリプトエディタのメニューから 実行 > Chrome V8 を搭載した新しい Apps Script ランタイムを有効にする を選択すれば OK です。

もとのランタイムに戻したかったら、同様に 実行 > Chrome V8 を搭載した新しい Apps Script ランタイムを無効にする を選択すれば OK です。

3. マニュフェストファイルで設定する

スクリーンショット 2020-02-09 17.56.05.png

スクリプトエディタのメニューから 表示 > マニュフェストファイルを表示 を選択します。すると、左側のファイル一覧に appscript.json というマニュフェストファイルが追加されるので選択して開きます。

runtimeVersion というフィールドに "V8" を指定すれば OK です。

スクリーンショット 2020-02-09 17.56.10.png

もとのランタイムに戻したかったら、フィールドごと項目を削除するか、 "DEPRECATED_ES5" という値を指定すれば良さそうです。(公式ドキュメント参照: Manifest structure

3 つ方法を紹介しましたが、結局全ての方法でマニュフェストファイルが更新されているだけなので、困ったらマニュフェストファイルを確認すれば大丈夫です。

clasp で始める

clasp で始める際もマニュフェストファイルを更新すれば大丈夫です。 clasp create コマンドでプロジェクトを作成すると、 appscript.json が生成されるので、それに runtimeVersion というフィールドに "V8" を指定すれば OK です。

$ clasp create
$ cat appscript.json
{
  "timeZone": "America/New_York",
  "dependencies": {
  },
  "exceptionLogging": "STACKDRIVER"
}

以下のように編集します。

appscript.json
{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

まとめ

以上 V8 ランタイムで Google Apps Script を実行する方法を紹介しました。すごく快適に JavaScript が書けるようになっているので是非試してみてください。

関連記事

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

bullで管理するNodeJSの分散job

bullとは

NodeJSで分散ジョブとメッセージを処理するためのキューパッケージです。redisをベースに動作します。
kueの後継的なライブラリです。

確認環境

express-generator にてプロジェクト作成

ここでは、play_node_bullというプロジェクトで作成します。

npx express-generator play_node_bull
cd play_code_bull
npm install

bullのインストール

npm install bull

app.js の編集

app.jsに以下の行を追記します。

App.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
+ var jobsRouter = require('./routes/jobs');  

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);
+ app.use('/jobs', jobsRouter); 

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

jobs.js の追加

1ブロック目

routesフォルダに、jobs.jsを追加します。
routerとBullを準備します。

routes/jobs.js
const express = require('express');
const router = express.Router();
const Bull = require("bull")

2ブロック目

重い処理のスタブを準備します。

routes/jobs.js
// 重い処理のスタブ
const heavyJob = async () => {
  return new Promise((resolve)=>{
    setInterval(() => {
      resolve();
     }, 10000);
  })  
}

3ブロック目

bullオブジェクトと処理名を定義します。
処理名は、キューの送信と受信を紐づけるキーとなります。

routes/jobs.js
const queue = new Bull('bulltest', { redis: { port: 6379, host: '127.0.0.1' } });

const processor = 'processor_name'; // 処理名

4ブロック目

エンキューするエンドポイントを定義します。
エンキューするqueue.add()には、データオブジェクトを処理名を指定します。

routes/jobs.js
router.post('/', (req, res, next) => {
  const data = { greetingTime: (new Date()).toString() }     // jobのデータ(object)
  queue.add(processor, data);
  res.send('ok');
});

module.exports = router;

5ブロック目

デキューするハンドラを記述します。
queue.process()に処理名と並行処理数、ハンドラ関数を指定します。

routes/jobs.js
const concurrency = 2;  // 並行処理数

queue.process(processor, concurrency, async (job, done) => {
  try {
    await heavyJob();
    done();
  } catch (e) {
    done(e);
  }
});

実行

アプリケーションを実行します。デフォルトでは3000番ポートで待機します。

npm start

APIの呼び出し

curlコマンドで、作成したjobsAPIを呼び出します。

curl -X POST 'http://127.0.0.1:3000/jobs'

jobのオプション

interface JobOpts {
  priority: number, // オプションの優先度。1(最高の優先度)~MAX_INT(最低の優先度)を指定する。パフォーマンスにわずかな影響を与えるため、必要でない限り使用しない。
  delay: number, // このジョブを処理できるようになるまで待機するミリ秒数。正確に遅延するためには、サーバーとクライアントの時刻を同期する必要がある。
  attempts: number, // ジョブが完了するまでのリトライ回数
  repeat: RepeatOpts, // cron仕様に従ってジョブを繰り返す。
  backoff: number | BackoffOpts, // ジョブが失敗した場合の自動再試行設定。遅延時間を設定するか、{type: 'fixed' or 'exponential')を指定。
  lifo: boolean, // 後入れ先出しにする (default false)
  timeout: number, // タイムアウトエラーでジョブが失敗するまでのミリ秒数
  jobId: number | string, // ジョブIDを上書きする。既に存在するIDを持つジョブを追加しようとしても、追加されない。
  removeOnComplete: boolean | number, // trueの場合、正常に完了したときにジョブを削除します。falseの場合は`completed`セットに保持される。
  removeOnFail: boolean | number, // trueの場合、処理に失敗したときにジョブを削除します。falseの場合は`faild`セットに保持される。
  stackTraceLimit: number, // スタックトレースに記録されるスタックトレース行の量を制限。
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

redux-sagaで排他制御をするサンプル

はじめに

redux-saga で排他制御がやりたくて,
await-semaphoreを使ったらうまくいきました.

サンプルは こちら にアップロードしています.
何かの参考になれば幸いです.

やりたいこと

やりたいことは, 複数のsagaがstore上のデータを同時並行に取り合う処理です.
より具体的には, 次のようなことがやりたいです.

  • storeにデータのリストがある ['a', 'b', 'c']
  • 2つのsagaが同時並行で以下をやる
    • storeからリストを取得する
    • リストから最初のデータを選択する
    • 取り出したデータをstoreから削除する

sagaが同じデータを取り出してしまったらNGです.
2個のsagaが順に実行されればOKなのですが,
sagaは並行に実行されるので, NGになるかもしれません.

排他制御が必要になるケース

例えば, 以下のケースでNGになります.

  • saga1: リストを取得 ['a', 'b', 'c']
  • saga2: リストを取得 ['a', 'b', 'c']
  • saga1: 'a'を選択
  • saga1: リストから'a'を削除 ['b', 'c']
  • saga2: 'a'を選択 (saga1によるリストの更新に気が付かず'a'を選択してしまう!)
  • saga2: リストから'a'を削除 ['b', 'c'] ('a'はすでにsaga1により削除されている!)

saga1, saga2どちらも'a'を取得してしまいました...

セマフォを使う

NGの原因は, saga1, saga2 が同時並行で実行されることです.
saga1が実行を終えるまで, saga2を待たせることができれば, うまくいきそうです.
本記事では, await-semaphore の セマフォを使ってこの排他制御を実現します.

排他制御なしでやる

コード

reducer.js
const initialState = {
  // このデータを複数のsagaが取り合う
  items: ["a", "b", "c"],
}

export default function reducer(state = initialState, action) {
  switch (action.type) {
    // 指定されたデータをstoreから削除
    case "REMOVE": {
      const { item } = action
      return {
        items: state.items.filter(x => x !== item),
      }
    }
    default:
      return state
  }
}
saga.js
import { fork, select, put, join } from "redux-saga/effects"

function* popItem() {
  // リストを取得
  const items = yield select(state => state.items)
  if (items.length <= 0) {
    throw new Error("insufficient items")
  }

  // リストから先頭のデータを選択
  const item = items[0]

  // 選択したデータをリストから削除
  yield put({ type: "REMOVE", item })

  // 呼び出し元に選択したデータを返す
  return item
}

export default function* mainSaga() {
  // 3個のsagaを起動
  const tasks = [yield fork(popItem), yield fork(popItem), yield fork(popItem)]

  // sagaの終了を待つ
  yield join(tasks)

  // 各sagaが取得したデータを表示
  console.info(
    "fetched items =",
    tasks.map(x => x.result())
  )
}

実行結果

実行すると, 3個のsagaすべてが 'a' を取得してしまいます...

% yarn start
yarn run v1.21.1
$ babel-node src/main.js
store changed = { items: [ 'b', 'c' ] }
store changed = { items: [ 'b', 'c' ] }
store changed = { items: [ 'b', 'c' ] }
fetched items = [ 'a', 'a', 'a' ] # すべてのsagaが 'a' を取得してしまった!
Done in 0.55s.

排他制御を加える

排他制御なしのコードにセマフォの記述を加えるだけです.

コード

sagaWithLock.js
import { call, fork, select, put, join } from "redux-saga/effects"
import { Semaphore } from "await-semaphore"

// セマフォ作成
// 引数が 1 ならば同時にロックを獲得できる saga は1つ
// 引数 1 を指定するなら, new Mutex() と等価
const sem = new Semaphore(1)

function* popItemWithLock() {
  // sem.acquire を呼び出して, ロックの獲得をする
  // もし, 他の saga がロックを獲得していれば, その saga が release() を呼び出すまで待つ
  // 他の saga がロックを獲得していなければ, すぐにロックを獲得できる
  //
  // call([sem, sem.acquire]) は sem.acquire() の呼び出しを意味する
  // https://redux-saga.js.org/docs/api/#callcontext-fn-args
  //
  // sem.acquire() は Promise を返すため, yield call でロックの獲得を待つ
  // https://www.npmjs.com/package/await-semaphore#semaphoreacquire-promise--void
  const release = yield call([sem, sem.acquire])
  try {
    // このブロックを実行できるsagaは高々1個
    const items = yield select(state => state.items)
    if (items.length <= 0) {
      throw new Error("insufficient items")
    }
    const item = items[0]
    yield put({ type: "REMOVE", item })
    return item
  } finally {
    // 忘れずにロックを解放する
    release()
  }
}

export default function* mainSagaWithLock() {
  const tasks = [
    yield fork(popItemWithLock),
    yield fork(popItemWithLock),
    yield fork(popItemWithLock),
  ]
  yield join(tasks)

  console.info(
    "fetched items =",
    tasks.map(x => x.result())
  )
}

実行結果

3個のsagaがそれぞれ'a', 'b', 'c'を取得できました!

% yarn start --enable-lock # オプションをつけると mainSagaWithLock が起動する
yarn run v1.21.1
$ babel-node src/main.js --enable-lock
store changed = { items: [ 'b', 'c' ] } # 'a' が取り出される
store changed = { items: [ 'c' ] }      # 'b' が取り出される
store changed = { items: [] }           # 'c' が取り出される
fetched items = [ 'a', 'b', 'c' ] # 相異なるデータを取得できた
Done in 0.53s.

さいごに

ありがとうございました.

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

Node.jsでGoogle Slides内のテキストの書き換え

Google Slides APIをNode.jsから触ってみてます。

の記事の続きです。

batchUpdate()で更新

presentations.batchUpdateでどうやら更新ができそうです。

presentations.batchUpdate()の

presentations.batchUpdate()の使い方がいまいち分からなかったけど、stackoverflowの投稿を参考に

presentationId: プレゼンテーションIDを指定, 
resource: {
   requests: {
      //ここにリクエストする内容を入れる
   }
}

みたいな使い方というのが分かりました。

app.js
省略

function listSlides(auth) {
  const slides = google.slides({version: 'v1', auth});

  slides.presentations.batchUpdate({
    presentationId: presentationId,
    resource: {
      requests: {
        replaceAllText: {
          containsText: {
            text: "Node.js" //置き換え前のテキスト
          },
          replaceText: "Hoge" //置き換え後のテキスト
        }
      }
    }
  }, (err, res) => {
      if (err) return console.log('The API returned an error: ' + err);
      console.log(res.data);
  });
}

Node.jsというテキストをHogeに書き換えます。

実行

こんな感じで書き換えができました。

所感

今回はreplaceAllTextというリクエストをしましたが、他のリクエスト方法もあるので試していきたいです。

/v1/presentations/request

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

FirebaseのRealtime Databaseで簡易投票システムを作ってみる

概要

社内の懇親イベントで、Web上での投票ページが必要になりました。
しかし1回きりのイベントなので、そんなに工数をかけたくない。
そうだ、Firebaseを使ってみよう!

Firebaseで何ができる?

Firebase統合バックエンドサービスであり、
本来自力でサーバーサイドで構築するようなDBや認証機能などを提供しています。
またWeb,iOS,Androidなど多くのプラットフォームに対応しています。

従量課金のサービスではありますが、小規模な開発には充分な無料枠が設定されています。
(Sparkプランの場合。大規模向けのBlazeプランは完全従量課金制)

料金の詳細はこちら

コンソール画面から各種サービスの利用を開始することができます。

Firebaseの主な機能

アナリティクス(Analytics)
Googleが提供するGCPのサービスであるため、Googleアナリティクスが標準で利用できます。
Firebaseコンソールの管理画面でアクセス推移の確認が容易にできます。

データベース(Realtime Database)
NoSQL形式のデータベース。
変更が即座に接続されているデバイスに反映されるリアルタイムの同期機能をもちます。

認証(Authentication)
メール・パスワードでのログイン機能のほか、
TwitterやFacebookなどのSNS認証や、電話番号によるSMS認証(有料)も組み込めます。

ストレージ(Cloud Storage)
クラウドストレージが5GBまで無料で利用できます。

サーバーレスコンピューティング(Cloud Functions)
ファンクション単位で実行できるサーバーレス機能。
データベースの更新をトリガーにして起動させるといったことが可能です。

作りたいもの

  • 立候補者が複数名いる、賞争いの投票システム
  • 不特定多数の参加者が、候補者うちいずれかに投票する
  • 参加者1人につき、1回のみ投票できる

今回は投票結果を保持するためにRealtime Databaseを、
投票のタイミングで集計を行うためにCloud Functionsを利用しました。
また、投票回数はクッキーを用いて制限しましたが、Firebaseの機能とは関係がないため割愛します。

Firebaseアプリ開発を始める


コンソールはこんな感じです。
左側のナビゲーションエリアに、Firebaseで利用できる各機能が表示されています。


「プロジェクトを追加」から新規作成します。


プロジェクト名を決めます。
これはFirebaseサービス全体でユニークなものである必要があります。

スクリーンショット 2020-02-02 20.48.09.png
アナリティクスを利用したい場合は、有効にしましょう。
その場合、登録済みのGoogleアナリティクスアカウントと連携させます。


無事プロジェクトが作られました!
今回はWebサイトにFirebaseを導入するので、</>ボタンを押します。


アプリ名は「event」「イベント用」など、自分が識別しやすいものにできます。
プロジェクト名と違い、Firebase内でユニークである必要はありません。

スクリーンショット 2020-02-02 21.04.04.png
スクリプトをコピーし、Webサイトに貼り付けます。これは一番簡単なSDK(Software Development Kit)を読み込んで利用する方法です。
上記に加え、利用したい機能ごとにスクリプトを追加で読み込ませる場合があります。
各SDKについて詳しくはこちらを参照してください。

index.html
<!-- すべてのFirebaseアプリで必要 -->
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-app.js"></script>
<!-- Realtime Databaseを利用するには下のSDKを追加で読み込ませます -->
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-database.js"></script>

<!-- 投票のスクリプト -->
<script src="/path_to/vote.js">

注: セキュリティルールについて


Firebaseでは、データベースへのアクセスルールを設定できます。
セキュリティルールの.readおよび.writetrueに設定すると、任意のスクリプトからの接続が可能となります(外部からの改ざんが可能である旨の警告が出る)。
今回は会員登録も存在しない簡易な投票システムを作るため、不特定多数のアクセスを許可するテストモードで作成しました。
しかし、データの適切な保護をするために、認証機能と合わせるなどセキュリティルールの設定が必要不可欠であるとドキュメントで言及されています。

実際に投票システムを作る

アプリの初期化

vote.js
//Firebaseの接続設定
const firebaseConfig = {
  apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  authDomain: "PROJECTNAME.firebaseapp.com",
  databaseURL: "https://PROJECTNAME.firebaseio.com",
  projectId: "PROJECTNAME",
  storageBucket: "PROJECTNAME.appspot.com",
  messagingSenderId: "00000000000",
  appId: "1:XXXXXXXXXXXX:web:XXXXXXXXXXXXXXXXXXXX",
  measurementId: "XXXXXXXXXXX"
};

//初期化
firebase.initializeApp(firebaseConfig);

投票処理

firebase.database()でRealtime Databaseにアクセスできます。
RealtimeデータベースはNoSQLであり、JSON形式でデータを保持します。
パスを指定してその階層に読み書きを行うようなイメージです。

{
  votes: {
    LwY0fIos2fdKi46zvuw: {
      for: 'Andy',
      created: '2020/02/02 02:02:02',
    },
    LwY0px7Rx7y4qISIiJq: {
      for: 'Bob',
      created: '2020/02/02 02:03:04',
    },
  }
}

今回は一票ごとにユニークなキーを持たせ、
そのメンバーとして投票に関する候補者や時刻などの情報を持たせました。

vote.js
...

//投票する
function vote (candidate) {
 //票ごとにユニークなキーを取得する
  let key = firebase.database().ref('votes').push().key;

  //投票データ
  let values = {
    'for': candidate,
    'created': new Date().toLocaleString(),
  };

  //作成したキーに投票データを代入する
  let updates = {};
  updates['/votes/' + key] = values;

  //データ更新
  firebase.database().ref().update(updates);
}

ここで送信したデータは、コンソール上で確認することができます。

集計処理

投票データはこれでFirebase上に保存できましたが、
結果表示用に、このデータを集計したものが欲しいです。

Cloud Functionの機能を用いると、
Realtime Databaseに更新をトリガーにして処理を実行させることができます。

npm install -g firebase-tools

Firebase CLI用いてローカルで開発し、Firebase上にデプロイします。
https://firebase.google.com/docs/functions/get-started?hl=ja

index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

//votesに書き込みがあったタイミングで実行される
exports.countVotes = functions.database.ref('/votes').onWrite(change => {
  // onWriteに渡されるオブジェクトは、変更前beforeと変更後afterにわかれている
  let changed = change.after.val();
  let countAll = Object.keys(changed).length;

  // 候補者
  const candidates = ['Andy', 'Bob'];

  // 得票数countの初期化
  let count = {
    'all': countAll,
  }
  for (let cndkey in candidates) {
    count[candidates[cndkey]] = 0;
  }
  //{'all': 0, 'andy': 0, 'bob': 0}

  // 集計
  for (let key in changed) {
    for (let cndkey in candidates) {
      if (changed[key]['for'] === candidates[cndkey]) {
        count[candidates[cndkey]] += 1;
      }
    }
  }
  //votesと同じ階層に、集計結果countを保存
  return change.after.ref.parent.child('count').set(count);
});

データベースのvotesに変更があったタイミング(onWriteイベント)で、すべての投票を集計し、
countに保存するトリガー関数を作成します。

作り終わったら

firebase deploy --only functions

デプロイした関数はコンソール上で確認することができます。

スクリーンショット 2020-02-09 17.48.27.png

votesに投票データが挿入されるたびに、countが更新されます。

結果取得処理

Realtime Databaseを用いると、
データに変更があったタイミングでイベント(valueイベント)を発生させることができます。
今回の場合は投票がされたタイミングでリアルタイムに結果に反映されることになります。

result.js
function getCount () {
  let counts = firebase.database().ref('/count');
  //countが更新されるたびに実行
  counts.on('value', snapshot => {
    console.log(snapshot.val());
  //データ表示処理
  });
}

まとめ

結論から言うと、Firebase自体の学習コストは多少あるので、
普通のRDBS使った方が場合によっては早く実装できるかもしれません。

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

FirebaseをWebアプリに導入する準備をした【備忘録】

自分用の備忘録です。
Firebaseを自作Webアプリに導入する準備をしました。
Firebaseの公式サイト

0. 概要

  1. Firebase上で準備をする
  2. 便利なパッケージをインストールする
  3. 設定ファイルを作成する

1. Firebase上で準備をする

1-1. アカウントを作成する

Sign in ボタンをクリックして、アカウントを作成します。
ログインしたら Go to console ボタンをクリックします。

1-2. プロジェクトを作成する

名前を付けて、プロジェクトを作成します。
アナリティクスは入れても入れなくても問題ありません。

1-3. アプリを追加する

iOS Android Web の3つの選択肢があると思います。
Webアプリなら Web をクリックして追加します。

2. 便利なパッケージをインストールする

Firebaseを便利に設定できるパッケージがあるので、それを導入します。

$ npm install --save firebase

3. 設定ファイルを作成する

Firebaseを使うための設定ファイルを作成します。

firebase.js
import firebase from 'firebase/app';

if (!firebase.apps.length) {
 const config = {
   apiKey: "~~~",
   authDomain: "~~~",
   databaseURL: "~~~",
   projectId: "~~~",
   storageBucket: "~~~",
   messagingSenderId: "~~~"
 };
 firebase.initializeApp(config);
}

~~~ と書いてあるところに、必要な情報を入力します。
Firebase上で、自分のWebアプリの設定で Firebase SDK snippet を見ます。
ここに書かれてある情報をコピペして導入完了です。

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

Web Workerを別ファイルにしないでインラインスクリプトで実行する方法

参考:The Basics of Web Workers - HTML5 Rocks

こちらの参考で知ったので、そのまま試してみたという内容になります。よろしくお願いいたします。:bow:

index.html
<!DOCTYPE html>
<meta charset="UTF-8">
<title>Document</title>

<script id="worker1" type="javascript/worker">
self.addEventListener("message", e => {
    self.postMessage(`${e.data} from worker!`);
});
</script>

<script>
const blob = new Blob([document.querySelector('#worker1').textContent]);
const worker = new Worker(window.URL.createObjectURL(blob));

worker.addEventListener("message", e => {
    console.log("Received: " + e.data);
});

worker.postMessage("hello");
</script>

Screen Shot 2020-02-09 at 16.07.39.png

workerファイルを別ファイルにすることなく実行することができました


window.URL.createObjectURL(blob) でBlobオブジェクトに保存されたデータを参照するためのURL文字列を作成できるので、それを別ファイル扱いにして読み込めるらしいです。

const blob = new Blob([document.querySelector('#worker1').textContent]);
console.log(window.URL.createObjectURL(blob)); // "blob:http://127.0.0.1:8000/a9a7fcfe-5952-49f1-a088-1835a6d0e872"

Screen Shot 2020-02-09 at 16.14.33.png

以上です。

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

新型コロナウイルス感染者数マップ

新型コロナウイルス対抗のために、エンジニアにできること

WHOは新型コロナウイルスの流行に対し、根拠のない情報が大量に拡散する「インフォデミック」が起きていると指摘しています。

このインフォデミックに対抗するため、溢れかえる情報の中から信頼できる情報をまとめ、整理し、発信するプロジェクトWUHAN2020が進行しています。
WUHAN2020日本語公式サイト

GitHub上でissue及びプルリクエストを受け付けています。

データサイエンティスト、フロントエンドエンジニアが中心に参加していますが、
翻訳などの比較的簡単なタスクでも人手が不足しています。
OSS開発へ興味のある方はぜひ、参加をおすすめします。

新型コロナウイルス感染者数マップ

百度(baidu)でも利用されている最新データを元に、簡単な感染者数マップを作成しました。
制作物及びソースコードは、observablehqにて、ノートブック形式で公開しています。

新型コロナウイルス 流行マップ

以下は、記事執筆時での感染者数マップです。
最新のものではありません。最新データはノートブックまたは百度の発表を参照してください。

map.png

制作したマップについてなにかございましたら、Qiitaにコメントもしくはobservablehqにてプルリクエストを送っていただけると幸いです。

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

独学エンジニアほどlintを使うべきだ!ESLintとJavaScriptで解説するlintの必要性

YouTubeに公開した動画「独学エンジニアほどlintを使うべきだ!ESLintとJavaScriptで解説するlintの必要性」の台本です。
動画の補足にご活用ください。

https://www.youtube.com/watch?v=Ga_ucygeKHA&feature=youtu.be


今日はlintを説明しよう!

Lintってのは静的解析ツールの一種で、
要は「汚ないコードを自動的に掃除してくれる」
自動お掃除ロボット、
つまりルンバみたいなもんだ。

今日はlintが何をしてくれるものなのか、
そして、どうしてlintを使うといいのか説明しよう。

Lintと一口に言っても、
言語ごとにいろんなlintが存在するから
今日は多分みんなが一番よく
使うであろう言語JavaScriptのlint、
ESLintについて説明しよう。

まず、lintは何をしてくれるのか。
一言で言うと、
エンジニアのミスを掃除してくれる。

例えば、
importしてるのに使っていないモジュールとか、
定義したのに使われていない変数がコードに残ってる。
これは初歩的なミスだ。

使われないコードを残しておくと、他の人がコードを見た時に
「あれ、これは何か意図があって残してるのかな?」
と混乱しちゃうだろ。
それに、ただのメモリの無駄遣いだ。

こういうミスをlintが指摘してくれる。
「これ使われてませんよ」
と教えてくれるから
使われないコードが残ったままにならない。

その他にも
いろんなミスを教えてくれる。
Defaultケースを書き忘れたswitchとか、
書き方を間違えた正規表現とか、
とにかく人がやりがちなミスを
全部勝手に掃除してくれる。

さてここまでの説明を聞いて
「あれ、それだけ?
それだけのためにlint使うの?
俺はルンバなくても困らない」
って顔をしている人がいそうだな。

いいかい、
初歩的なミスを舐める奴はだめだ!
特にチーム開発の現場ではNG、
BIG NOだ。
なぜか説明しよう。

よくあるチーム開発の現場では、
書いたコードを別の誰かがレビューする事が多い。

さて、コードレビューしてくれるありがたーい人を
簡単に怒らせる方法がある。
わかるかな?

そう、
初歩的なミスを何個も仕込む事だ。

使わない変数をたくさん定義して、
意味のないconsole.logを大量に残して、
到達不可能なコードをしこたま書いておけ。

Switchにはデフォルトを書かず、
意味のないcatch文を大量に作り、
非同期処理をしない関数に
意味もなくasyncをつけておくんだ。

いわゆるゴミコードってやつだ。
ゴミコードを大量に詰め込んで、
レビューに出しまくれ。

ただでさえ忙しい相手に、
初歩的なミスを浴びせ続けるんだ。
何度でもイージーなミスを繰り返せ。
1週間もすればどんな人だってブチ切れる。

レビューしてくれる人を怒らせたくない?
じゃあlintを入れるのがオススメだ。
Lintさえ入れておけば、
こういう初歩的なミスを
自動的に指摘して、
物によっては勝手に直してくれる。

機械でもわかるような初歩的なミスを減らせば、
人間様じゃないとわからない
高度なミスを発見する時間に使える。
それだけコードレビューの質も上がるし、
時間効率が高まるよな。

これがlintを使うべき理由、
その1だ。
初歩的なミスを減らし、
人間様の頭を使う時間を生み出す。

これだけじゃないぞ。
Lintは初歩的なミスの他に、
いわゆる良いコードの書き方も教えてくれる。

例えJavascriptだとコールバック関数をよく使うが、
最近のJS環境なら、
コールバックにはアロー関数を使ったほうがいい。

ちょっと難しい話になるが、
簡単に言うとアロー関数は
定義された瞬間のスコープを維持するのに対して、
通常の関数定義は、
実行された瞬間のスコープを使う。

つまり通常の関数は、
呼び出される場所によって振る舞いが変わる可能性がある。
コールバック関数のように、
いつどこで呼ばれるか分からない関数に
それを使っちまったら、
どう動くか読みにくくなるよな。

もちろん意図的に
呼び出し先のスコープを使いたい時もあるけど、
基本的にはコールバックには
アロー関数を使ったほうがいい。

こんなことをESLintは指摘してくれる。

他にもESLintが指摘してくれる、いい書き方の例をあげよう。

例えばjavascriptの等価記号に == と === があるよな?
== は厳密に一致していない値も一致とみなすことがあるから、
基本的には===を使った方がいい。

まだある。
特に意味もなくネストしたif else文があると、
これも「アーリーリターンしたらどうですか?」って勧めてくれる。
ネストは浅い方が読みやすいからな。

「間違っちゃいないけど、もっといい書き方があるコード」
をESLintは指摘してくれる。
自分だけで開発してると「なんか動く」コードならかけるが、
実はもっといい書き方がある事には気づきにくい。

自己流でコードを書いていると、
自分でも気づかないうちに悪い癖がついたり、
理解を間違える事がある

でもlintを使えば、これまで先人たちが積み重ねてきた英知、
つまり良いコードを書く方法を簡単に学べる。

だから、俺は独学の人にもlint導入を勧めるぜ。
周りに良いコードの書き方を教えてもらえない状況なら、なおのことlintを使おう。

これがlintを使うべき理由、その2だ。
良いコードの書き方を身につけられる。
この動画のコメントに色々リンクを貼っておくから、
気になった人は読んでみると良いぞ。

Airbnbってサービス、わかるよな?
あそこの開発チームもESLintを使ってる。
そんで自分たちが使ってる
ESLintの設定を公開してる。
手始めにその設定を、
そのまま使ってみてもいいと思うぜ。

彼らのGithubを見れば
「どうしてそのルールを採用したのか」
「良い書き方の例、悪い書き方の例」も上がってる。
勉強になるぞ。

ちなみに言語によっては
最初っからlintが入ってることもある。
Go言語とかそうだ。
それぐらいlintは大事ってことだ。
ちなみに今のはgopherの真似だ

あとlintとは別に、
コードフォーマッターも使うと良いかもしれない。
JavaScriptだとprettierって奴が有名かな。
プリキュアじゃないぞ、prettierだ。

これはlintみたいに
「間違ったコード」を指摘するわけじゃなくて、
ただコードを綺麗な見た目にしてくれる。

例えば改行した時の空白の入れ方にムラがあると、
コードがデコボコしてくる。
時々スペース2個、時々3個みたいな事してると、
見た目が汚くなるだろ。
そのほかにも、
あまりに横に長いコードを書いてると、
読みにくくなる。

Prettierを入れておけば君の代わりに
コードのスペースを合わせてくれたり、
横に長すぎるコードを改行してくれる。

そこまで必須ではないと思うけど、
これも入れておいて損はない。
「スペースを2個にしてください」
なんてレビューで指摘するのも疲れるからな。

ちなみにprettierとかESLintは、
VSCodeみたいな
コードエディタに拡張機能として追加できる。
ファイルを編集・保存したら
自動的にlintとprettierがかかる設定にできる。

どんなプロジェクトでも良いからlintを使ってみよう。
lintはマジでリスペクトだ。

ってわけで今日のおさらい。
Lintを使う理由
その1。初歩的なミスが減って、人間様の考える時間が増える。
その2。良いコードの書き方がわかる。先人の知恵を借りられる。

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

もう2020年!いい加減、React勉強し始めてみた

始めに

UIを作成する際は、ずっとjQuery(たまーにRiot.js)にお世話になっていましたが、2020年にもなったし、そろそろ重い腰をあげて、本格的にReactを勉強したい、という事で勉強を始めています。(自分はjQueryも好きですし、現在進行形でお世話になってます)

年始(2020年)に掲げたゴールの一つがReactの習得なので、個人的なアウトプットとして、いくつかメモとして、文章を残していこうと思います。それと同時に、自分と同様、現場で導入されそうだし、そろそろReact勉強したい!という人に向けて書いていこうかなと思います。

※随時、セクションを追加更新していく予定です。
本記事はReactを構成する概念や記法について、ざっくり整理しながら触れます。
(詳細はReactのドキュメントを見まっしょ!)

Reactとは

UIを構築するための、宣言型JSライブラリです。

jQueryで、少し複雑なUI(例えば、状態を持つような)を作ろうとすると、コードが冗長になりやすかったり、後で見返したときに、「え...何してるのかよく分からん...」となったりということは、jQueryで頑張ってきたエンジニア、Webデザイナーなら、一度は経験があるはずです。

Reactは状態を持つようなインタラクティブなUIの作成をやりやすくしてくれるライブラリとして、設計されています。

導入すると何が嬉しいかというと、コンポーネント単位での実装が容易なので、再利用性が高く、保守性も担保した運用がしやすくなります。
ここら辺、Atomic Designの考え方とも親和性が高そう。
(後でこちらも記事としてまとめたい)

ちょっと前まで英語のドキュメントを読むしかなかったが、今は日本語のドキュメントもVue.js並に充実してる印象。手始めにチュートリアルを一通りやってみるのが、取っ掛かりとして、一番早いかも。(何事も自分で手を動かす方が理解が深まりますよね?)
Reactの導入

Reactを構成する記法

① JSX

ReactでHTML(DOM)を出力するための独自構文で、JSの機能を備えています。また、埋め込まれた値をレンダリングする前に、エスケープしてくれるので、インジェクション攻撃を防ぐ事ができます。

Reactは、JSXでの記述を強制してる訳ではないですが、JSXで書いた方が圧倒的に楽です。よっぽどこだわりのある偏屈な人じゃなければ、JSX使っていきましょう!最初は違和感あるかもしれないけれど、ここら辺は慣れの問題かなと思います。普段、HTMLやJS書いてるような人ならすぐに慣れると思います。

JSX記法
import React from 'react';

const title = <h1>Study React!!!</h1>
const greeting = <h2 className='greeting'>Hello.</h2>
JSXでない記法を参考までに

JSX = シンタックスシュガーである事が分かりますね。

これは上記JSXと等価です
import React from 'react';

const title = React.createElement('h1', null, 'Study React!!!')
const greeting = React.createElement('h2', {className: 'greeting'}, 'Hello.')
JSXは子要素も定義できます
子要素を定義
const elem = (
    <>
     <section className='head'>
         <h1>Study React!</h1>
        <h2>Hello.</h2>
          </section>
        <section className='body'>
          <h1>
            Roadmap
          </h1>
        </section>
    </>
)
JSXに値を埋め込むことも可能

ただタグを表示するだけでなく、JSの値を埋め込むこともできます。
{ 変数や値 }

JSXの中にJSの値を埋め込む
let title = 'Study React!';
let greeting = 'Hello';
const heading_s = { color: 'red', borderBottom: '1px solid #DDD' }

let elem = (
    <>
        //属性の値にも利用できることに注目
        <h1 style={ heading_s }>{ title }</h1>
        <h2>{ greeting }</h2>
    </>   
)
JSXを書く上での注意点

Reactを勉強したての頃、JSXでエレメントを定義する際、以下のように書いてしまってエラーになるケースがあります。(上記の違いは <></>で囲っていない点)これ、何がいけないかというと、renderメソッドで、描画できるのは、一つのエレメントだけというルールがあるからです。

JSXに子要素をもったDOMを代入する際は、親として、必ず <div>タグ もしくは、<>タグ(=<React.Fragment>タグ)で囲ってあげましょう。

(VSCodeやInteliJとか使ってれば、そもそも記法の間違いは教えてくれますが)

初学者がやりがちなミス
const elem = (
    <section className='head'>
        <h1>Study React!</h1>
        <h2>Hello.</h2>
    </section>
    <section className='body'>
        <h1>
            Roadmap
        </h1>
    </section>
)

コンポーネント

JSXでエレメントを定義し、ReactDOM.renderで描画するだけだと、全く便利さは感じないと思ったはずです(自分もそうでした)。Reactの真価はコンポーネントを組み合わせてUIを構築していくことにあります。

コンポーネントとは、画面に表示する部品で、表示の内容や必要なデータ、処理などを一つのオブジェクトにまとめます。
ボタンや入力フォーム、カードなど、Webアプリは沢山の小さなコンポーネントとその組み合わせで構築されています。

コンポーネントを作成することによって、再利用可能で、堅牢な仕組みが構築しやすくなります。

デザイナーやフロント寄りの開発をされてる方にとっては、デザインシステムという言葉は見聞きした事があると思います。有名どころだとGoogleのMaterial Designや、MailchimpのデザインシステムSalesforceのLightning Design System等がありますが、世の中には沢山のデザインシステムが存在します。
(デザインシステムのギャラリーサイトもあって、眺めてるだけでも面白いですよ)

なんでも良いのですが、試しに上記3リンクを開いてみてください。どのサイトにもComponentが定義されているはずです。各企業は、ブランディングやユーザビリティ、開発効率性の観点から、デザインシステムを設計し、UIをコンポーネントとして定義することによって、一貫性、保守性を保とうと努力しています。

Reactは、そのような要求にも耐えうる開発を進めていくための一つの選択肢とされています。

コンポーネントの書き方にはClass Component(クラスコンポーネント)Functional Component(関数コンポーネント)の2種があります。参考までに書き方の例を示します。

コンポーネントの記法は2種類ある
import React, { Component } from 'react';

//関数コンポーネント
function StudyReact(props) {
    return <h1>During study...</h1>
}

//クラスコンポーネント
class StudyReact extends Component {

    constructor(props) {
        super(props);
        this.state = {
            status: 'During study...'
        }
    }

    render() {
        return <h1>{this.state.status}</h1>
    }
}

React Hooksが導入されて、クラスコンポーネントの力を借りずとも、関数コンポーネントでも状態を管理することが出来たので、両者の使い分けは、現場ごとの選択になるのかなと思ってるのですが、そこら辺はもう少し突っ込んで、追ってアウトプットしたいと思います。

React初学者の所感

  • 静的なページならjQueryでも全然良いと思う。
  • アプリライクな動的ページを作るならReactやReactでUIを作る考え方は、いいかげんキャッチアップしといた方が良さそう。
  • 逆にいうと、LPなどを作るようなWebデザイナーさんは、無理して勉強しなくても良いと思う。JSの流行り廃りは激しいし。興味があれば、仮想DOMの概念やコンポーネント化してUIを設計する手法とかは勉強してみると視野が広がるし、面白いと思うし、今後の仕事に活かせるかも。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ChromeでUserAgentの緩慢な死、もしくはUser Agent Client Hints誕生の軌跡

UserAgentがGoogle Chromeで使えなくなる

「Google Chrome」の開発チームは1月14日、User-Agent文字列を非推奨とし段階的にUserAgent文字列の更新をやめるという計画を発表しました。これにより2020年9月中旬を目処にChromeでは、PCやSPなどのデバイスの種類・OSのバージョンを問わず、同じUserAgent文字列が返却されるようになりそうです。

UserAgentとは

UserAgentはブラウザがWebサーバーにデータを取りに行く際にサーバーに対して自動的に通知している、ブラウザの種類やバージョンやOSの種類やバージョンなどの情報を組み合わせた識別子のことです。
例えば、私が今利用しているGoogleChromeの開発者ツールをしようして navigator.userAgent を実行すると、以下のユーザーエージェントが表示されます。

"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"

Image from Gyazo

Webサービスを提供しているサーバーはこのUserAgentの情報を利用してWebブラウザーの種類やバージョンを判別し、挙動を変えていることがあります。

変更の背景

先に書いたようにUserAgentはサーバーでWebブラウザーの種類やバージョンを判別するために使われることがあるのですが、様々な弊害があることが指摘されていました。

個人の特定のために利用される可能性がある

AppleやGoogleが本格的に3rd Party Cookieの排除に乗り出し始め、Cookieによるユーザーの特定がどんどん難しくなっている中、Cookieを使わないでユーザーを特定する方法を模索する人たちがいます。
その方法の一つとして、「ブラウザーフィンガープリント」というWebブラウザーから得られる断片的な情報をつなぎ合わせて個人を特定する手法があります。

ブラウザーフィンガープリントという言葉は聞き慣れないと思うのですが、ブラウザから取得できる情報のうち、単体では何気ない情報を複数組み合わせることにより個人を特定する手法のことです。例えばあるユーザーが「20代女性である」という情報だけでは個人を特定することができませんが、「〇〇駅の近くに住んでいる」「△△社に勤めている」「エンジニアである」といった情報を組み合わせることで個人が特定できる可能性が高まります。ブラウザーフィンガープリントもこれと同じ話で、「ユーザーエージェント」「使用できるフォントの一覧」「端末のメモリ」「端末のCPUコア数」などブラウザから取得できる情報を組み合わせてCookieに頼らず個人を特定する技術です。

ユーザーエージェント文字列はブラウザーフィンガープリントで個人を特定しようとする際に重要な判断材料になってしまう可能性があります。このため、PCやSPなどのデバイスの種類・OSのバージョンを問わず、同じUserAgent文字列を返すようにすることで個人を特定できる情報をなるべく減らしたいというモチベーションがあります。

一部のブラウザで特定のWebサイトが正常に利用できなくなってしまうことがある

UserAgentはブラウザで特定の機能がサポートされているかどうかを判定するために利用されることがあります。
Chromeではサポートされているけれど、IEでは利用できない機能を使いたい場合、IEであるかどうかを判定し、IEでは特定の機能を利用できないようにする……ということが行われていることがあります。
本来は「その機能が使えるかどうか」を正しく判定して処理を変えるプログラムを書く必要があるのですが、古いサイトなどだとUserAgentを使って「このブラウザが送ってくるUserAgentには○○という文字が含まれていないからChromeではないだろう」という判定をしてしまうことがあるので、本来Chromeとほぼ同等の機能が使えるはずのブラウザで、正常に動作しないといったことが起こってしまっています。

もっとタチが悪いケースでは他社製のWebブラウザーを排斥するために、特定のUserAgentをもつブラウザに対して不便な挙動をするWebサービスがあるという指摘もあります。
その対策として、あえて嘘のUserAgentをWebサーバーに送るようにするなんていう仕様になっているブラウザもでてきています…。

凍結スケジュール

Google Chromeのブラウザエンジンの開発グループでは凍結しやすい部分を凍結し、残りを徐々に統合することを計画しています。

Chromeバージョン(リリース時期) 対応内容
Google Chrome 81(2020年3月中旬) navigator.userAgent によるUAの取得が非推奨となる(navigator.userAgentを実行するとコンソールに警告が出るようになるそうです)
Google Chrome 83(2020年6月上旬) ブラウザバージョンやOSバージョンなど、UserAgent文字列の中で後方互換性を損わずに固定できる部分を凍結したり、統一する
Google Chrome 83(2020年9月中旬) UserAgent文字列を統一。デスクトップブラウザ用のUserAgentかモバイル用UserAgent文字列のいずれかしか取得できなくなる。

代替方法

User-Agent Client Hints

Chromeでは凍結するUserAgent文字列の代用機能として User-Agent Client Hints という機能を提供しようとしています。
まだ仕様は検討中のようですがドラフト仕様を見る限り、基本的にはブラウザのブランド名とメジャーバージョンのみしかデータを送ってくれません。さらに詳細な情報が欲しい場合は、サーバー側でレスポンスに Accept-CHというヘッダを含める必要があります。

Chrome80では既にUser-Agent Client Hintsを試してみることができます。
まずは chrome://flags/ にアクセスし、 Experimental Web Platform features をEnabledにしてChromeを再起動してください。

image.png

再起動した後、どこでもいいのでWebサイトにアクセスすると、Webサーバーに sec-ch-ua: Google Chrome 80 というヘッダを送信するようになっています。

image.png

先ほどの書いた通り、サーバー側で何も対応しないと、プラットフォームなどの情報を取得することはできません。
さらに情報を取得するため、レスポンスにAccept-CHを含めたレスポンスを返却するWebサーバーを構築して確認してみたいと思います。

作ったWebサーバーのソースコードはこちらからダウンロードできるので、手元で動かしたいという人はご利用ください。Dockerとdocker-composeがインストールされているPCであれば、docker-compose upコマンドを実行するだけで localhost:4567にWebサーバーが立ち上がるようになっています。

Webサーバーの中身は単純なsinatraサーバーで、'Accept-CH'ヘッダに'UA, Platform, Arch, Model'という文字列を乗せているだけのものです。

スクリーンショット 2020-02-09 11.59.44.png

この時点では何も追加の情報を得ることはできませんが2回目以降のリクエストでは、 sec-ch-uaだけではなく、sec-ch-ua-archsec-ch-ua-modelsec-ch-ua-platformといった情報送信されるようになっているのが確認できます。

スクリーンショット 2020-02-09 12.13.07.png

JS APIによる取得

navigator.getUserAgent 関数が、Brand、 Platform、 Arch、 Model、 Versionといったデータが格納された Promise<NavigatorUAData> を返却してくれるようになるようです。Promiseなのでawaitか何かして使う必要がありそうですが…。

image.png

競合ブラウザの対応見込み(2020年2月6日現在)

Chromeは上記のようにUserAgent文字列を凍結し、代替機能としてUser-Agent Client Hintsを提供しようとしていますが、他のブラウザでは対応方針に違いがありそうです。このため、ブラウザによってはUserAgent文字列に依存し、他のブラウザに対してはUser-Agent Client Hintsに依存する必要があるという難しい状況が発生する可能性があります。

参考までに、現在の主要ブラウザの対応方針を記載します。

ブラウザ 対応方針
Edge 公式サポートの見込み
Firefox UserAgent文字列の凍結は公式サポート。ただしUser-Agent Client Hintsは実装せずNavigatorUADataインターフェースを介したJS API呼び出しにのみ対応する方針
Safari Safariはすでに部分的にUserAgent文字列を凍結している。かつてはUserAgent文字列を完全に凍結しようとしたが、様々な問題が発生して完遂には至らなかった模様。User-Agent Client Hintsへの対応方針は不明

参考文献

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

ChromeでUserAgentが凍結される日(User Agent Client Hintsの使い方)

UserAgentがGoogle Chromeで使えなくなる

「Google Chrome」の開発チームは1月14日、User-Agent文字列を非推奨とし段階的にUserAgent文字列の更新をやめるという計画を発表しました。これにより2020年9月中旬を目処にChromeでは、PCやSPなどのデバイスの種類・OSのバージョンを問わず、同じUserAgent文字列が返却されるようになりそうです。

UserAgentとは

UserAgentはブラウザがWebサーバーにデータを取りに行く際にサーバーに対して自動的に通知している、ブラウザの種類やバージョンやOSの種類やバージョンなどの情報を組み合わせた識別子のことです。
例えば、私が今利用しているGoogleChromeの開発者ツールをしようして navigator.userAgent を実行すると、以下のユーザーエージェントが表示されます。

"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"

Image from Gyazo

Webサービスを提供しているサーバーはこのUserAgentの情報を利用してWebブラウザーの種類やバージョンを判別し、挙動を変えていることがあります。

変更の背景

先に書いたようにUserAgentはサーバーでWebブラウザーの種類やバージョンを判別するために使われることがあるのですが、様々な弊害があることが指摘されていました。

個人の特定のために利用される可能性がある

AppleやGoogleが本格的に3rd Party Cookieの排除に乗り出し始め、Cookieによるユーザーの特定がどんどん難しくなっている中、Cookieを使わないでユーザーを特定する方法を模索する人たちがいます。
その方法の一つとして、「ブラウザーフィンガープリント」というWebブラウザーから得られる断片的な情報をつなぎ合わせて個人を特定する手法があります。

ブラウザーフィンガープリントという言葉は聞き慣れないと思うのですが、ブラウザから取得できる情報のうち、単体では何気ない情報を複数組み合わせることにより個人を特定する手法のことです。例えばあるユーザーが「20代女性である」という情報だけでは個人を特定することができませんが、「〇〇駅の近くに住んでいる」「△△社に勤めている」「エンジニアである」といった情報を組み合わせることで個人が特定できる可能性が高まります。ブラウザーフィンガープリントもこれと同じ話で、「ユーザーエージェント」「使用できるフォントの一覧」「端末のメモリ」「端末のCPUコア数」などブラウザから取得できる情報を組み合わせてCookieに頼らず個人を特定する技術です。

ユーザーエージェント文字列はブラウザーフィンガープリントで個人を特定しようとする際に重要な判断材料になってしまう可能性があります。このため、PCやSPなどのデバイスの種類・OSのバージョンを問わず、同じUserAgent文字列を返すようにすることで個人を特定できる情報をなるべく減らしたいというモチベーションがあります。

一部のブラウザで特定のWebサイトが正常に利用できなくなってしまうことがある

UserAgentはブラウザで特定の機能がサポートされているかどうかを判定するために利用されることがあります。
Chromeではサポートされているけれど、IEでは利用できない機能を使いたい場合、IEであるかどうかを判定し、IEでは特定の機能を利用できないようにする……ということが行われていることがあります。
本来は「その機能が使えるかどうか」を正しく判定して処理を変えるプログラムを書く必要があるのですが、古いサイトなどだとUserAgentを使って「このブラウザが送ってくるUserAgentには○○という文字が含まれていないからChromeではないだろう」という判定をしてしまうことがあるので、本来Chromeとほぼ同等の機能が使えるはずのブラウザで、正常に動作しないといったことが起こってしまっています。

もっとタチが悪いケースでは他社製のWebブラウザーを排斥するために、特定のUserAgentをもつブラウザに対して不便な挙動をするWebサービスがあるという指摘もあります。
その対策として、あえて嘘のUserAgentをWebサーバーに送るようにするなんていう仕様になっているブラウザもでてきています…。

凍結スケジュール

Google Chromeのブラウザエンジンの開発グループでは凍結しやすい部分を凍結し、残りを徐々に統合することを計画しています。

Chromeバージョン(リリース時期) 対応内容
Google Chrome 81(2020年3月中旬) navigator.userAgent によるUAの取得が非推奨となる(navigator.userAgentを実行するとコンソールに警告が出るようになるそうです)
Google Chrome 83(2020年6月上旬) ブラウザバージョンやOSバージョンなど、UserAgent文字列の中で後方互換性を損わずに固定できる部分を凍結したり、統一する
Google Chrome 83(2020年9月中旬) UserAgent文字列を統一。デスクトップブラウザ用のUserAgentかモバイル用UserAgent文字列のいずれかしか取得できなくなる。

代替方法

User-Agent Client Hints

Chromeでは凍結するUserAgent文字列の代用機能として User-Agent Client Hints という機能を提供しようとしています。
まだ仕様は検討中のようですがドラフト仕様を見る限り、基本的にはブラウザのブランド名とメジャーバージョンのみしかデータを送ってくれません。さらに詳細な情報が欲しい場合は、サーバー側でレスポンスに Accept-CHというヘッダを含める必要があります。

Chrome80では既にUser-Agent Client Hintsを試してみることができます。
まずは chrome://flags/ にアクセスし、 Experimental Web Platform features をEnabledにしてChromeを再起動してください。

image.png

再起動した後、どこでもいいのでWebサイトにアクセスすると、Webサーバーに sec-ch-ua: Google Chrome 80 というヘッダを送信するようになっています。

image.png

先ほどの書いた通り、サーバー側で何も対応しないと、プラットフォームなどの情報を取得することはできません。
さらに情報を取得するため、レスポンスにAccept-CHを含めたレスポンスを返却するWebサーバーを構築して確認してみたいと思います。

作ったWebサーバーのソースコードはこちらからダウンロードできるので、手元で動かしたいという人はご利用ください。Dockerとdocker-composeがインストールされているPCであれば、docker-compose upコマンドを実行するだけで localhost:4567にWebサーバーが立ち上がるようになっています。

Webサーバーの中身は単純なsinatraサーバーで、'Accept-CH'ヘッダに'UA, Platform, Arch, Model'という文字列を乗せているだけのものです。

スクリーンショット 2020-02-09 11.59.44.png

この時点では何も追加の情報を得ることはできませんが2回目以降のリクエストでは、 sec-ch-uaだけではなく、sec-ch-ua-archsec-ch-ua-modelsec-ch-ua-platformといった情報が送信されるようになっているのが確認できます。

スクリーンショット 2020-02-09 12.13.07.png

単純に実装すると初回のリクエストでは詳細情報を取得できなさそうなので、ちょっと使いづらいかもしれません。

JS APIによる取得

navigator.getUserAgent 関数が、Brand、 Platform、 Arch、 Model、 Versionといったデータが格納された Promise<NavigatorUAData> を返却してくれるようになるようです。Promiseなのでawaitか何かして使う必要がありそうですが…。

image.png

競合ブラウザの対応見込み(2020年2月6日現在)

Chromeは上記のようにUserAgent文字列を凍結し、代替機能としてUser-Agent Client Hintsを提供しようとしていますが、他のブラウザでは対応方針に違いがありそうです。このため、ブラウザによってはUserAgent文字列に依存し、他のブラウザに対してはUser-Agent Client Hintsに依存する必要があるという難しい状況が発生する可能性があります。

参考までに、現在の主要ブラウザの対応方針を記載します。今後変更される可能性も高いので、あくまで現時点のものと割り切ってご確認ください。

ブラウザ 対応方針
Edge 公式サポートの見込み
Firefox UserAgent文字列の凍結は公式サポート。ただしUser-Agent Client Hintsは実装せずNavigatorUADataインターフェースを介したJS API呼び出しにのみ対応する方針
Safari Safariはすでに部分的にUserAgent文字列を凍結している。かつてはUserAgent文字列を完全に凍結しようとしたが、様々な問題が発生して完遂には至らなかった模様。User-Agent Client Hintsへの対応方針は不明

参考文献

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

npmモジュールをインポートしてRollupでビルドしたときに○○ is not definedが出たときの対処

svelteを使ってちょっとしたWebアプリを作っているときにハマったのでメモ。

環境

  • macOS Catalina
  • Node v12.15.0
  • npm v6.13.4
  • rollup.js v1.20.0
  • Svelte v3.0.0

事象

svelteではクイックスタート用のテンプレートが用意されており、その中でモジュールバンドラとしてRollupを利用しています(参考)。
このテンプレートを元に開発を進めていたところ、特定のnpmモジュールをインポートして動かした際に下記のエラーに遭遇しました。

Uncaught ReferenceError: stream is not defined

streamというモジュールは自分が書いたプログラムの中では使っていません。

問題点

インポートしたnpmモジュールが内部でNodeのビルトインモジュールを利用しており、ブラウザ環境ではそんなモジュールないよ!となってエラーとなっていたようです。

解決方法

下記のRollupプラグインモジュールを導入する。

インストール

npm install --save-dev rollup-plugin-node-builtins rollup-plugin-node-globals

rollup.config.jsの設定

rollup.config.js
...
+ import builtins from 'rollup-plugin-node-builtins';
+ import globals from 'rollup-plugin-node-globals';

export default {
    ...
    plugins: [
    ...
        resolve({
            browser: true,
            dedupe: ['svelte'],
        }),
        commonjs(),

+       globals(),
+       builtins(),
    ...
    ],
  ...
}

まとめ

プラグインを利用することで、Nodeのビルトインモジュール依存の処理をRollupでビルドすることができます。

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

TypeScriptの型定義からJSON Schemaを生成するオンラインツールを作ってみた

先日、TypeScript + Tynderから始める宣言的検証生活の記事にて
スキーマ検証ライブラリTynderを紹介いたしました。

Tynderとは

Tynderは、TypeScriptのサブセット+独自の拡張文法から成るDSLによって

  1. 型の検査
  2. 単独の項目の必須・値の長さ・範囲や文字列パターンの検証
  3. 複数項目の相関や整合性検証の一部 (Union typeによる)

宣言的に行うことができます。

JSON Schemaを生成するオンラインツール

今回はTynderのスキーマ変換機能を使用して
JSON Schema、GraphQL、Protobuf3 のスキーマを生成するオンラインツールを公開しました。
(GraphQL、Protobuf3については実験的機能です)

tool-scr.png

追記(2020/2/9)

作成したスキーマはオンラインツールのJSON Schema validator等で動作確認できます。

上記のJSON Schema validatorで確認するには、オブジェクト内の最後の行に、definitions内のエントリーに対する$refを記述します。

{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "definitions": {
    ...
    "Entry": { ... }
  },
  "$ref": "#/definitions/Entry" // <- ここです
}

動機

API Blueprint等でのモックサーバー作成が捗らないかな、と思い作成しました。
UIは以前作成したこちらを流用することで、すぐに作成できました。

ぜひ、使ってみてくださいね

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

4つの数字で10を作る「テンパズル」の問題用紙を生成する

といってもやっていることはmake10 問題一覧と解答の問題を拝領しているのですが。

子供の頃に、電車の切符に記載されている4桁の数字を使って10を作るというのをよくやっていたと思います(こういうのをテンパズルと呼ぶのは知りませんでした)。Suicaになって、そういう数字を見ることもなくなってしまい、あっても車のナンバーくらいでしょうか。ドラゴン桜2で、この計算が数字に強くなるのに適していると書いてありました。とはいえ、毎度切符を買うわけにもいかないし、ドライブしている時くらいしかできないのは辛いです。

テンパズル - Wikipedia

アプリでもあるようですが、子供向けには紙でプリントされた状態で、手を動かす方が学習効果は大きいかと思います。ということで、問題用紙を生成するGoogleスプレッドシートを作りました。

https://docs.google.com/spreadsheets/d/1-lxy2oEp6Alq78mYAyFeFsyKoJoZBIJ--_5MQ30Gsb8/edit#gid=0

誰でも編集権限を与えていますが、GoogleドライブにアクセスするのでGoogleログイン必須のようです。また、このシート自体はGoogleの承認を得ていないので、警告メッセージが出ます。

手順

Googleスプレッドシートにアクセスします。問題生成ボタンを押すと、承認が必要といわれます。

Screenshot_ 2020-02-09 10.02.51.png

このアプリは確認されていませんと出ます。

Screenshot_ 2020-02-09 10.02.58.png

詳細のリンクをクリックします。 テンパズル(安全ではないページ)に移動 をクリックします。この時点で私が信頼できない方は止めておいてください(生成に際して、編集権限を付与しているので、悪意を持った人が編集している可能性もあります…)。

Screenshot_ 2020-02-09 10.03.02.png

権限を付与します。

Screenshot_ 2020-02-09 10.03.05.png

問題が生成できるようになります。

Screenshot_ 2020-02-09 10.03.17.png

自分で作る場合

こちらの方がお勧めです。Googleスプレッドシートの 問題用紙問題一覧 の内容をそのままコピーしてください。シート名も同じにしてください。

ツールメニューのスクリプトエディタを開いて、次のコードを貼り付けます。552というのは問題の数で、決め打ちです(問題一覧シート参照)。なお、今回は全問解けるものだけにしています。解けないものを入れることもできますが、0000とか簡単すぎるものも入ってしまうので、今回は除外しています。

function generate() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('問題一覧');
  var s = ss.getSheetByName('問題用紙');
  var data = sheet.getRange(1, 1, 552, 3).getValues();
  for (var i = 0; i < 5; i++) {
    var params = data[Math.floor(Math.random() * data.length)];
    var numbers = zeroPadding(params[0], 4).split('');
    var row = (i + 1) * 4;
    for (var j = 0; j < 4; j++) {
     s.getRange(row, (j + 1) * 2).setValue(numbers[j]); 
    }
  }
}

function zeroPadding(num,length){
  return ('0000000000' + num).slice(-length);
}

やっていることは問題一覧を取得して、その中からランダムに4問取り出して、問題用紙に貼り付けているだけです。

後は問題用紙にある 問題生成 ボタンのトリガーとして generate 関数を実行しています。

まとめ

問題はボタンを押す度に新しいものに変わります。色々な問題を作って、算数脳を鍛えてみてください。なお、小学校5〜6年生レベルだそうですが、中高生で数学が苦手な人にもいいそうです。

謝辞&参考:

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

Choices.js + fetchAPIでフィルタ付き動的セレクトボックス [脱jQuery]

概要

件名のモノが必要になった時
ググって出てきたのはajaxやcoffee script、select2にchosenと
内容が古かったり環境制約で使えないものだったりで苦しめられたので
rails6とpure javascriptで動くサンプルを遺します↓


親カテゴリの選択に応じて動的に子の選択肢をセットする
Screen Shot 2020-02-09 at 2.18.54.png


フィルタ検索機能
Screen Shot 2020-02-09 at 4.01.08.png


Sample app source code

  • Ruby 2.7.0
  • Rails 6.0.2.1
  • Choices.js
  • Fetch API

Choices.js

素のjavascriptで書かれた軽量custom select box
jQueryに依存せずにselect2やchosenのようなフィルタ機能を実現します
option設定やメソッドの詳細はGitHubで確認出来ます
公式動作デモ


Fetch API

Webブラウザの新しめの標準APIで、非同期通信処理をカンタンに書けます
従来のjQuery.ajax、XHR(XMLHttpRequest)を代替するとかしないとか。


How does it work?

大まかな処理の流れは以下になります

  1. ユーザが親select boxからカテゴリを選択する
  2. 親select boxのchangeイベントが発火
  3. javascriptがfetchでserverのtile一覧apiを叩く
  4. serverがtile一覧をjsonで返す
  5. javascriptがjsonを受け取る
  6. javascriptがchoicesメソッドを経由して子select boxにjsonの内容をセットする

以下具体的なコードです


javascript

  • /javascript/tiles.js
document.addEventListener('DOMContentLoaded', function() {
  /* apply choices to select boxes */
  const choicesOptions = {
    shouldSort: false,
    removeItemButton: true,
    searchResultLimit: 9,   // default: 4
    searchFields: ['label'] // default: ['label', 'value']
  };
  const selects = document.querySelectorAll('select');
  selects.forEach(function(select) {
    select.choices = new Choices(select, choicesOptions);
  });

  /* add event listener to first select */
  const firstSelect = document.getElementById('first_select')
  if (firstSelect != null) {
    firstSelect.addEventListener('change', setSecondChoices); // type: 'input' is not effective.
  }

  /* get tiles json by fetch api */
  async function getTiles(tile_category_id) {
    const params = new URLSearchParams();
    params.set('tile_category_id', tile_category_id);
    const apiURL = `/api/tiles?${params}`;
    const response = await fetch(apiURL);
    const json = await response.json();
    return json;
  }

  /* set values to second select */
  function setSecondChoices() {
    const secondSelect = document.getElementById('second_select');
    const choices = secondSelect.choices;
    /* clear current values & selected item */
    choices.clearStore();
    const tile_category_id = firstSelect.value;
    getTiles(tile_category_id).then(json => {
      // setChoices(choices, value, label, replaceChoices);
      choices.setChoices(json, 'index', 'display_name', true);
    });
  }
});

要点を抜粋解説します

序盤の以下の部分で素のselect要素に対してchoicesを適用しています

const selects = document.querySelectorAll('select');
selects.forEach(function(select) {
  select.choices = new Choices(select, choicesOptions);
});

choices適用後はカスタムタグが挿入されて見た目が変わり、フィルタが使えるようになります
select boxに対する項目の追加や削除はchoicesメソッドを通す必要があるので注意して下さい


続くaddEventListenerで親select boxのchangeイベントを拾い、setSecondChoices()を呼びます

firstSelect.addEventListener('change', setSecondChoices);

setSecondChoices()内
choices適用時にselect.choices = new Choices...という風に書いたので
以下のようにselect boxのchoicesにアクセス出来ます

const choices = secondSelect.choices;

親カテゴリが変更された時、変更前に選んだ子が残っていると宜しくないのでクリアします

choices.clearStore();

getTiles()は非同期処理で/api/tiles?を叩きます
async/await+fetchAPIを使用します

/* get tiles json by fetch api */
async function getTiles(tile_category_id) {
  const params = new URLSearchParams();
  params.set('tile_category_id', tile_category_id);
  const apiURL = `/api/tiles?${params}`;
  const response = await fetch(apiURL);
  const json = await response.json();
  return json;
}

ES6縛りでasync/await記法が使えない場合は以下をお使い下さい

/* set values to second select */
function setSecondChoices() {
  const secondSelect = document.getElementById('second_select');
  const choices = secondSelect.choices;
  /* clear current values & selected item */
  choices.clearStore();
  const tile_category_id = firstSelect.value;
  const params = new URLSearchParams();
  params.set('tile_category_id', tile_category_id);
  const url = `/api/tiles?${params}`;
  /* get tiles JSON by fetch api */
  fetch(url).then(function(response) {
    return response.json();
  }).then(function(json) {
    // setChoices(choices, value, label, replaceChoices);
    choices.setChoices(json, 'index', 'display_name', true);
  });
}

apiを叩いて返ってくるjsonの構造は以下のような形式です

[
  {"display_name":"","index":41},
  {"display_name":"","index":42},
  {"display_name":"","index":43}
]

setChoices()の第二, 第三引数で
choicesのvalueとlabelに対応するjson中のhashのkeyを指定します(value, labelの順)

// setChoices(choices, value, label, replaceChoices);
choices.setChoices(json, 'index', 'display_name', true);

これで子select boxの選択肢が書き換わります


  • /javascript/packs/application.js
    webpackerにpackしてもらう為、↑のtiles.jsをimportしてるだけ
import '../tiles.js'

routes.rb

  • select boxesを表示: / (root)
  • カテゴリ毎のtile一覧を取得するapi: /api/tiles?tile_category_id=xxx
Rails.application.routes.draw do
  root controller: :top_page, action: :show
  namespace :api do
    resources :tiles, only: [:index]
  end
end

model

Model定義と一覧取得は今回の本筋ではないのでhashでベタ書きしました (DB不要です)
RailsユーザならActiveRecordに置き換えるのは造作も無い事でしょう
indexは各牌識別用の通し番号です
rubyのsymbolって漢字使えたんですね…:scream_cat:


  • /models/concerns/tiles_identifiable.rb
module TilesIdentifiable
  extend ActiveSupport::Concern

  included do
    def get_tiles(tile_category_id:)
      tile_category = tile_categories.key(tile_category_id)
      case tile_category
      when :萬子 then characters
      when :筒子 then dots
      when :索子 then bamboos
      when :風牌 then winds
      when :三元牌 then dragons
      else all_tiles
      end
    end

    def tile_categories
      {
        萬子: 0,
        筒子: 1,
        索子: 2,
        風牌: 3,
        三元牌: 4,
      }
    end

    def numbers
      1..9
    end

    def chinese_numerals
      ['一', '二', '三', '四', '五', '六', '七', '八', '九']
    end

    def to_chinese_numerals(number)
      chinese_numerals[number - 1]
    end

    # tile: { display_name:, index: }
    def characters(base_index: 10 * 0)
      numbers.map { | number | { display_name: "#{to_chinese_numerals(number)}萬", index: base_index + number } }
    end

    def dots(base_index: 10 * 1)
      numbers.map { | number | { display_name: "#{to_chinese_numerals(number)}筒", index: base_index + number } }
    end

    def bamboos(base_index: 10 * 2)
      numbers.map { | number | { display_name: "#{to_chinese_numerals(number)}索", index: base_index + number } }
    end

    def winds(base_index: 10 * 3)
      ['東', '南', '西', '北'].map.with_index(1) {
        | wind, index | { display_name: wind, index: base_index + index } }
    end

    def dragons(base_index: 10 * 4)
      ['白', '發', '中'].map.with_index(1) {
        | dragon, index | { display_name: dragon, index: base_index + index } }
    end

    def all_tiles
      characters | dots | bamboos | winds | dragons
    end
  end
end

controller

  • controllers/top_page_controller.rb
class TopPageController < ApplicationController
  include TilesIdentifiable
  before_action :set_values

  private
    def set_values
      @first_choices = tile_categories.to_a
    end
end

  • controllers/api/tiles_controller.rb
    指定されたカテゴリに対応する牌一覧をjsonで返すapi
class Api::TilesController < ApplicationController
  include TilesIdentifiable

  def index
    tile_category_id = params[:tile_category_id]
    if tile_category_id.blank?
      render json: all_tiles
    else
      render json: get_tiles(tile_category_id: tile_category_id.to_i)
    end
  end
end

view

素のerbです

  • views/top_page/show.html.erb
<div class='form_wrapper'>
  <%= form_with do | form | %>
    <%= form.label 'tile categories' %>
    <%= form.select :first_select, @first_choices, { include_blank: true }, {} %>
    <%= form.label 'tiles' %>
    <%= form.select :second_select, [], { include_blank: true }, {} %>
  <% end %>
</div>

  • views/layouts/application.html.erb
    choicesのjsとcss、whatwg-fetchのjsをCDNでお手軽導入
<head>
  ...
  <!-- Include Choices CSS -->
  <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css'/>
  <!-- Include Choices JavaScript (latest) -->
  <script src='https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js'></script>
  <!-- Include whatwg-fetch -->
  <script src='https://cdn.jsdelivr.net/npm/whatwg-fetch@3.0.0/dist/fetch.umd.min.js'></script>
</head>

最後に

誰かの何らかの助けになれば幸いです
フィルタの日本語動作の確認で漢字を使う麻雀牌(Tile)にしてみたけど
牌一覧用意するのが無駄に面倒でもうやらないと思います:dizzy_face:

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

core-jsを読み込んだけど、IE11で"Exception thrown and not caught"というエラーが発生する

Babel 7.4.0から、@babel/polyfillが非推奨になったそうです。
ではどうすれば良いのかというと、もともと@babel/polyfillがインクルードしていたcore-jsとregenerator-runtimeを直接インポートしてやれば良いらしい。
babelの公式にも書いてある。

import "core-js/stable";
import "regenerator-runtime/runtime";

設定ファイルにも書く必要がある。
このあたりの説明はこの記事に詳しい。

これでコンパイルはできるのだが、IE11でエラーが出て動かない。

SCRIPT5022: Exception thrown and not caught
internal-state.js

Google先生に問い合わせたところ、日本語の情報は見つからなかったが、同様のエラーで悩んでいる人々がいた。
core-jsの開発者に質問ぶつけている模様。
https://github.com/zloirock/core-js/issues/514
で、質問者は自力で解決したようだ

恥ずかしながら正確なところは理解できていないのだが、
どうもBabelがコンパイルするときに、core-jsのpolyfillを破壊してしまっているのが問題のようだ。
したがって、babel-loaderの除外対象としてcore-jsを指定することで解決するらしい。

私の場合はwebpackを使っているので、こんな感じで書いたところ、IE11でのエラーが出なくなった。
excludeを追加した)

webpack.config.js
module.expots = {
  /* 省略 */
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          loader: "babel-loader",
          options: {
              presets: [
                [
                  "@babel/preset-env",
                  {
                    targets: {
                      ie: 11,
                      esmodules: true
                    },
                    useBuiltIns: "entry",
                    corejs: 3
                  }
                ]
              ]
          }
        ],
        exclude: /node_modules\/(?!(core-module)\/).*/
      }
    ]
  }
}

IE11のためにわざわざpolyfillしているのに、その過程でIE11にエラーが出るなんて。
いつまでたっても拾いきれない例外を投げまくっているのは、お前のことだぞ、IEよ。

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

404を許さないChrome拡張機能

調べもの中にリンク切れページや、削除済みページに出会いすぎてイライラしてたのでchrome拡張機能の作り方の勉強のついでに404のページに対抗する拡張機能を作ってみました。

github: https://github.com/mugi111/page-not-found-detector

作り方

まずはmanifestファイルを作成していきます。
出来上がりは↓のようになります。

manifest.json
{
    "manifest_version": 2,
    "name": "404 DETECTOR",
    "description": "",
    "version": "1.0.0",
    "content_scripts": [{
        "matches": ["http://*/*", "https://*/*" ],
        "js": ["script.js"],
        "css": ["style.css"]
    }],
    "background": {
        "persistent": true,
        "scripts": ["background.js"]
    },
    "browser_action": {
        "default_title": "404 detector",
        "default_popup": "popup.html"
    },
    "permissions": [
        "tabs",
        "webNavigation",
        "background",
        "http://*/*",
        "https://*/*"
    ]
}

下がそれぞれの項目の詳細です。

プロパティ名 概要
manifest_version マニフェストのバージョン
name 拡張機能の名前
description 拡張機能の説明
version 拡張機能のバージョン
content_script matches スクリプトの読み込みを行うページの指定
js 読み込まれるjavascript
css 読み込まれるcss
background persistent 永続化の有無
scripts バックグラウンドで読み込まれるスクリプト
browser_action default_title バーに表示されるときの名前
default_popup バーのアイコンをクリックしたときに開くhtml
permissions 拡張機能に与える権限

当初の予定

最初はgoogleの検索結果一覧のリンクに対して存在しないリンクに対してマークをつけようとしていました。

こんな感じでcontent_scriptで指定したscript.jsから、リンクのリストをchrome.runtime.sendMessageを使用してbackground.jsに送信していました。
background.jsのほうでchrome.runtime.onMessage.addListenerで設定しておくことでchrome.runtime.sendMessageを拾うことができるようになります。

ページの存在の確認については、XMLHttpRequestでレスポンスのステータスコードを見ることで確認しようとしてました。

script.js
var atags = document.getElementsByTagName("a");
var hrefs = [];
for (let index = 0; index < atags.length; index++) {
  href = atags.item(index).href;
  chrome.runtime.sendMessage({href});
  hrefs.push(atags.item(index).href);
};

console.log("hello", hrefs);
background.js
const ping = (url) => {
  var xhr = new XMLHttpRequest();

  xhr.onreadystatechange = () =>{};

  xhr.open("GET", url, true);
  try {
    xhr.send(null);
  } catch (err) {
    console.log(err);
  }
}

chrome.runtime.onMessage.addListener(
  async function(request, sender, callback) {
    console.log(request);
    ping(request.href);
  }
);

結果

ここで何が起こったかというと、、、
見慣れたあいつが出てきました。
google robot.PNG
「お使いのコンピュータ ネットワークから通常と異なるトラフィックが検出されました。このページは、リクエストがロボットではなく実際のユーザーによって送信されたことを確かめるものです。」
だそうです。。

引っかかったところ

最初script.jsからリクエストを送信していたところ、
CORB(Cross Origin Read Block)ではじかれまくりました。
corb.PNG

回避策

background.jsからリクエストを送ることで回避できました。

別の案

ここでそこら辺のリンクに無差別にリクエストを投げるのはやめようと思い、飛ぶ先のリンクだけに絞りました。
chrome.webNavigation.onCommitted.addListenerwebNavigationonCommittedを拾うようにしました。
webNavigation参考ページ

ページの存在の確認は当初の案と同じ方法をとりました。
ステータスコードが404だった場合はtabIdを指定してgoBackメソッドを呼び出すことでブラウザバックさせています。

urlはurlMacthesでhttpかhttps(一般的なページ..?)のみを拾うようにしました。これは、chrome://settingschrome://extensionsのような特に拾わなくていいようなページを除外したかったためです。

script.js
const ping = (url) => {
  var xhr = new XMLHttpRequest();

  xhr.onreadystatechange = () =>{};

  xhr.open("GET", url, false);
  try {
    xhr.onload = function () {}
    xhr.send();
    return xhr.status;
  } catch (err) {
    console.error(err);
  }
}

chrome.webNavigation.onCommitted.addListener((details) => {
  if (details.frameId !== 0){
    return;
  }
  const status = ping(details.url);
  console.log(status);
  if(status === 404) {
    chrome.tabs.goBack(details.tabId);
  }
}, {url: [{urlMatches: "http://*/*"}, {urlMatches: "https://*/*"}]});

notfound.gif
動作はこんな感じです。
存在しないページに飛ぼうとすると強制的にブラウザバックされます。

これでキーボードを壊す心配もありません。
computer_keyboard_yatsuatari_man.png

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

[ニコニコ動画]一般会員でもプレミアム機能を使える理由

はじめに

PC版ニコ動において
実は一般会員でもプレミアム限定の機能を使えてしまうという話です。
もちろん普通に使ってたら無理なのですが、ちょっとした裏技とか強引に使う方法とかがあるということです。

プレミアム会員になると使える機能

  • 好きな位置で再生
  • 反転再生
  • 指定した秒数だけ動画をスキップ
  • 広告なし(Adblock導入すれば一般会員でも消せる)

これらの機能は理論上(?)一般会員でも使えます。

一般会員がプレミアム機能を使える理由

これが可能な理由は「プレミアム会員かどうかの判定とか制限はすべてブラウザ側で行われているから」です。
逆にサーバー側で制限する方法が思いつきません・・・。

動画プレイヤーはブラウザで読み込まれたJavaScriptで動作しているので、プレイヤーにかかる制限なども全部JavaScriptです。

これがわかれば、2通りの制限回避方法が思いつきます。

  • ニコ動のプレイヤーを使わずに直接動画を再生する
  • プレイヤーのJavaScriptを書き換えてプレミアム会員だと認識させる

「ニコニコ動画のプレイヤーが制限かけてるんだから、違うプレイヤーで再生すりゃいいじゃん!」っていうのと「制限されないようにプレイヤーのコード書き換えりゃいいじゃん!」っていうやつですね。

実際やってみた

本当にできるかどうか、両方とも試してみました。

まず1つ目の方法

動画のURLを探して直接アクセスするだけなのでかなり楽な方法。ただ、当然この方法ではニコニココメントは読み込まれないのでただの動画をみるだけになります。
これはChrome開発者ツール使えばすぐにできました→動画URLを探す手順

そして2つ目の方法

これはどうやってやるかというと、ニコ動のJavaScript読み込みをブロックして代わりに書き換えたものを読み込ませます。
Tampermonkeyを使えば実現できます。
(過去に制限回避できるスクリプトを配布して公式から警告受けた人がいるので、今回配布はせずに流れだけ簡単に紹介します)

ニコ動で読み込まれるJavaScriptの1つにwatch_appがあります。
その中の至る所にisPremiumというキーがあるので全部値をtrueにしてやればプレミアム会員と認識されることになります。
書き換えたものを自分のサーバーにアップロードします。
Tampermonkeyを使ってニコ動で読み込まれるスクリプトをブロックし、書き換えたものを代わりに読み込ませればプレミアム機能を使えます。

さいごに

一般会員でもプレミアム機能を使える理由を説明してみました。これを防ぐにはサーバー側で制限する必要がありますが、それは無理なんじゃないかと思っています。
それか、JavaScriptを超絶読みづらくするとか・・・?

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

ブラウザ上のピュア JavaScript で OAuth 認証して Twitter API を使う

Twitter API や OAuth を学ぶために、ブラウザ上のピュア JavaScript で認証して GET してみました。

学習目的でムリやりブラウザ上で動作させるため、実用的なコードではないです。

1. 準備

1.1. Twitter API 申請など

申請に関しては他の記事が分かりやすいので、各自で調べて申請をお願いします。英作文は必要ですが、自分の場合は申請からすぐ API を使える状態になりました。

1.2. Access token & access token secret

ここでは簡単にするために、Twitter Developers のアプリ管理画面から自分用のアクセストークンを作成します。

image.png

2. サンプルソースコード

ここでは話を簡単にするために以下の条件でコードを書きます。

  • リクエストメソッドは GET のみ
    • POST メソッドやストリーミングは扱わない
  • 認証はアクセストークンによる OAuth 認証のみ
    • Bearer トークンは扱わない
  • ブラウザ上で動作
    • Twitter のドメイン上でブックマークレットを実行するか、デベロッパーツールで実行
  • ピュア JavaScript
    • 外部のライブラリは使用しない

モダンな JavaScript で書いているので、古いブラウザ等ではトランスコンパイルしないと動きません。

Chrome 79 ではコードそのままで動作確認しました。

コード全体
(async () => {

    const Twitter = class {

        constructor(apiKey, apiSecretKey, accessToken, accessTokenSecret) {
            this._apiKey       = apiKey;
            this._apiSecretKey = apiSecretKey;
            this._accessToken       = accessToken;
            this._accessTokenSecret = accessTokenSecret;
        }

        async get(url, params) {

            const query = this._percentEncodeParams(params).map(pair => pair.key + '=' + pair.value).join('&');

            const method = 'GET';

            // 認証情報
            const authorizationHeader = await this._getAuthorizationHeader(method, url, params);

            const headers = {'Authorization': authorizationHeader};

            // 通信
            const response = await fetch((! params ? url : url + '?' + query), {method, headers});

            return response.json();

        }

        async _getAuthorizationHeader(method, url, params) {

            // パラメータ準備
            const oauthParams = [
                {key: 'oauth_consumer_key'    , value: this._apiKey        },
                {key: 'oauth_nonce'           , value: this._getNonce()    },
                {key: 'oauth_signature_method', value: 'HMAC-SHA1'         },
                {key: 'oauth_timestamp'       , value: this._getTimestamp()},
                {key: 'oauth_token'           , value: this._accessToken   },
                {key: 'oauth_version'         , value: '1.0'               }
            ];

            const allParams = this._percentEncodeParams([...oauthParams, ...params]);

            this._ksort(allParams);

            // シグネチャ作成
            const signature = await this._getSignature(method, url, allParams);

            // 認証情報
            return 'OAuth ' + this._percentEncodeParams([...oauthParams, {key: 'oauth_signature', value: signature}]).map(pair => pair.key + '="' + pair.value + '"').join(', ');

        }

        async _getSignature(method, url, allParams) {

            const allQuery = allParams.map(pair => pair.key + '=' + pair.value).join('&');

            // シグネチャベース・キー文字列
            const signatureBaseString = [
                method.toUpperCase(),
                this._percentEncode(url),
                this._percentEncode(allQuery)
            ].join('&');

            const signatureKeyString = [
                this._apiSecretKey,
                this._accessTokenSecret
            ].map(secret => this._percentEncode(secret)).join('&');

            // シグネチャベース・キー
            const signatureBase = this._stringToUint8Array(signatureBaseString);
            const signatureKey  = this._stringToUint8Array(signatureKeyString);

            // シグネチャ計算
            const signatureCryptoKey = await window.crypto.subtle.importKey('raw', signatureKey, {name: 'HMAC', hash: {name: 'SHA-1'}}, true, ['sign']);

            const signatureArrayBuffer = await window.crypto.subtle.sign('HMAC', signatureCryptoKey, signatureBase);

            return this._arrayBufferToBase64String(signatureArrayBuffer);

        }

        /**
         * RFC3986 仕様の encodeURIComponent
         */
        _percentEncode(str) {
            return encodeURIComponent(str).replace(/[!'()*]/g, char => '%' + char.charCodeAt().toString(16));
        }

        _percentEncodeParams(params) {

            return params.map(pair => {
                const key   = this._percentEncode(pair.key);
                const value = this._percentEncode(pair.value);
                return {key, value};
            });

        }

        _ksort(params) {

            return params.sort((a, b) => {
                const keyA = a.key.toUpperCase();
                const keyB = b.key.toUpperCase();
                if ( keyA < keyB ) return -1;
                if ( keyA > keyB ) return 1;
                return 0;
            });

        }

        _getNonce() {
            const array = new Uint8Array(32);
            window.crypto.getRandomValues(array);
            // メモ: Uint8Array のままだと String に変換できないので、Array に変換してから map
            return [...array].map(uint => uint.toString(16).padStart(2, '0')).join('');
        }

        _getTimestamp() {
            return Math.floor(Date.now() / 1000);
        }

        _stringToUint8Array(str) {
            return Uint8Array.from(Array.from(str).map(char => char.charCodeAt()));
        }

        _arrayBufferToBase64String(arrayBuffer) {

            const string = new Uint8Array(arrayBuffer).reduce((data, char) => {
                data.push(String.fromCharCode(char));
                return data;
            }, []).join('');

            return btoa(string);

        }

    };

    const apiKey       = '...';
    const apiSecretKey = '...';
    const accessToken       = '...';
    const accessTokenSecret = '...';

    const url = 'https://api.twitter.com/1.1/friends/list.json';
    const params = [
        {key: 'screen_name', value: 'TwitterJP'}
    ];

    const twitter = new Twitter(apiKey, apiSecretKey, accessToken, accessTokenSecret);

    const json = await twitter.get(url, params);

    console.log(json);

})();

3. 解説

3.1. 認証の流れ

通常の HTTP リクエストに Authorization ヘッダーを加えることで認証します。

Authorization ヘッダーの例
Authorization: OAuth oauth_consumer_key="...", 
oauth_nonce="6eb24361e7250e5112288fa4954dd8f634a7320c342c43019510c2cda8c8b3db", 
oauth_signature_method="HMAC-SHA1", 
oauth_timestamp="1581159389", 
oauth_token="...", 
oauth_version="1.0", 
oauth_signature="8TACi1tsshSi9dfiLa8Vm8SasTs%3D"
  • oauth_consumer_key, oauth_token: 手持ちの API キーとアクセストークン
  • oauth_signature_method, oauth_version: 固定値 "HMAC-SHA1", "1.0"
  • oauth_timestamp: リクエスト時の秒単位のタイムスタンプ
  • oauth_nonce: リクエストごとに固有の値。生成方法に特に規定なし
  • oauth_signature: OAuth 1.0a HMAC-SHA1 シグネチャ (※計算方法は後述)

参考「Authorizing a request — Twitter Developers

3.2. シグネチャの計算

材料

  • リクエストメソッド GET/POST
  • リクエスト URL (GET の場合はクエリパラメータを付けていない状態の)
  • GET や POST のパラメータ
  • oauth_* パラメータ (oauth_signature 除く)
    • API キー・アクセストークン
    • タイムスタンプ・NONCE
    • "HMAC-SHA1", "1.0"
  • シークレット
    • API シークレットキー・アクセストークンシークレット

計算の大まかな手順

  1. 「GET や POST のパラメータ」と「oauth_* パラメータ」を合わせてキーの名前順にソートし、クエリ文字列にする。
  2. 「リクエストメソッド」「リクエスト URL」「パラメータを合体したクエリ文字列」をさらに合わせてクエリ文字列にする。これをシグネチャベースとする。
  3. 「API シークレットキー」「アクセストークンシークレット」を合わせてクエリ文字列にする。これをシグネチャキーとする。
  4. シグネチャベースとシグネチャキーから HMAC-SHA1 ハッシュアルゴリズムで計算 (後述) し、バイナリ文字列のシグネチャを得る。
  5. バイナリ文字列のシグネチャを Base64 エンコードし、文字列のシグネチャを得る。

注意点

  • URL エンコードは RFC 3986 に基づく。
  • 当たり前ながら、クエリ文字列を作る際にキーと値は URL エンコードする。
    • JavaScript の encodeURIComponent() は RFC 3986 に基づいていないため、一部文字の置換が必要。
    • ちなみに PHP では http_build_query() を使うときに PHP_QUERY_RFC3986 を指定すると良い。
  • 「パラメータを合体したクエリ文字列」を作る際に、パラメータをソートする前にキーを URL エンコードする。
  • (OAuth の仕様では、キーが重複する際に値でソートするが、Twitter API の場合はキーが重複しない。)
  • (リクエストメソッドは大文字にする。)
  • シグネチャベースを作る際に、「パラメータを合体したクエリ文字列」の中の '&' はエスケープされ、全体として '&' が 2 つのみの状態になる。

参考「Creating a signature — Twitter Developers
参考「encodeURIComponent() - JavaScript | MDN
参考「PHP: http_build_query - Manual

3.3. SubtleCrypto.sign() による HMAC-SHA1 の計算

アルゴリズムを自前で実装することもできますが、Web Crypto API に HMAC-SHA1 を計算できる機能がありますので、これを利用します。

SubtleCrypto.sign() を使うためにはまずシグネチャベースとシグネチャキーが TypedArray などの型になっている必要があるので、文字列から変換します。また、シグネチャキーに関しては SubtleCrypto.importKey() で CryptoKey にする必要があります。

参考「SubtleCrypto.sign() - Web APIs | MDN
参考「SubtleCrypto.importKey() - Web APIs | MDN

3.4. その他

本質とは関係のない部分の説明。

3.4.1. パラメータのデータの扱い方について

Object で記述したほうが簡潔に書けますし、Map で記述したほうが意味的には正しいと思うのですが、キー順にソートするところで Array.prototype.sort() を使いたかったため、Object や Map を使っても途中で Array に変換することになるため、ここでは初めから Array で扱うことにしました。(ライブラリとして外部に公開などをする場合にはもう少し何とかすべきかもしれませんが…。)

3.4.2. RandomSource.getRandomValues() による NONCE 乱数生成

ここでは RandomSource.getRandomValues() で乱数のバイト列を生成しました。

Base64 やハッシュ値で NONCE を作ることもできますが、そのまま 16 進数文字列にするだけで充分と思われます。

参考「RandomSource.getRandomValues() - Web API | MDN

3.4.3. 秒単位のタイムスタンプ取得

JavaScript の Date.now() はミリ秒単位のタイムスタンプなので、1000 で割って切り捨てます。

3.4.4. 型変換いろいろ

Uint8Array から 16 進数文字列
// array: Uint8Array
return [...array].map(uint => uint.toString(16).padStart(2, '0')).join(''):
String から Uint8Array
// str: String
return Uint8Array.from(Array.from(str).map(char => char.charCodeAt()));
ArrayBuffer から Base64 文字列
// arrayBuffer: ArrayBuffer
const string = new Uint8Array(arrayBuffer).reduce((data, char) => {
    data.push(String.fromCharCode(char));
    return data;
}, []).join('');

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

webサービスを運営してみた(2020/2/8)

はじめに

アルバイトの出勤時間を無料で管理できるサービスTimestampを運用しています。
ここでは運用開発に関する記録を残していきます

ページビュー

スクリーンショット 2020-02-08 23.52.55.png
ページビューの記録はほとんどが自分のものですね。
qiita記事から何人か来ていただいたみたいでうれしいです!

雑記

googleアナリティクスを導入しました

googleアナリティクスのページに従うだけで簡単でした。

ランディングページを作成しました

デザイン難しいですね。出来に納得していないのですが先に進むことを優先します。
利用規約とプライバシーポリシーのページを作ったのですが、webサービスの規約をちゃんと読んだのはじめt
あと英訳は断念しました!!!

開発予定

SEO対策や広告導入、機能改善などを予定しています。
サーバサイドでログ出力するとかエラーが発生したときの通知とかもやらなきゃですね。

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

お絵かきできるSNSを作りたい!3

お久しぶりです。

前回canvasを使って線を引くことが出来ました。
今回はマウスかペンを使って線を引けるところまで作ります。

基本的にcanvasの上でマウスまたは指を押した座標と動かしている時の座標と離すまでの座標を1ストロークごと書きこめば完成です。

指の場合はtouchstart→touchmove→touchend、
PCの場合はmousedown→mousemove→onMouseUp
のイベントを追加し、それぞれの関数で処理を書きます。

var can;
can=document.getElementById("canvas");
can.addEventListener("touchstart",onDown,false);
can.addEventListener("touchmove",onMove,false);
can.addEventListener("touchend",onUp,false);
can.addEventListener("mousedown",onMouseDown,false);
can.addEventListener("mousemove",onMouseMove,false);
can.addEventListener("mouseup",onMouseUp,false);

それぞれの関数は以下の通りにしました。

function onDown(event){
    mf=true;
    ox=event.touches[0].pageX-event.target.getBoundingClientRect().left-scx();
    oy=event.touches[0].pageY-event.target.getBoundingClientRect().top -scy();
    event.stopPropagation();
}

function onMouseDown(event){
    mf=true;
    ox=event.clientX-event.target.getBoundingClientRect().left;
    oy=event.clientY-event.target.getBoundingClientRect().top ;
}

function onMove(event){
    if(mf){
        x=event.touches[0].pageX-event.target.getBoundingClientRect().left-scx();
        y=event.touches[0].pageY-event.target.getBoundingClientRect().top -scy();
        drawLine();
        ox=x;
        oy=y;
        event.preventDefault();
        event.stopPropagation();
    }
}

function onMouseMove(event){
    if(mf){
        x=event.clientX-event.target.getBoundingClientRect().left;
        y=event.clientY-event.target.getBoundingClientRect().top;
        drawLine();
        ox=x;
        oy=y;
    }
}

function onUp(event){
    mf=false;
    event.stopPropagation();
}

function onMouseUp(event){
    mf=false;
}

function drawLine(){
    var ct;
    ct=can.getContext("2d");
    ct.strokeStyle="#000000";
    ct.lineWidth=1;
    ct.lineJoin="round";
    ct.lineCap="round";
    ct.beginPath();
    ct.moveTo(ox,oy);
    ct.lineTo(x,y);
    ct.stroke();
}

function scx(){return document.documentElement.scrollLeft || document.body.scrollLeft;}
function scy(){return document.documentElement.scrollTop  || document.body.scrollTop ;}

これできっと動くはず!
マウスと指で線が引けるようにする
WS000000.JPG

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