20200713のJavaScriptに関する記事は27件です。

身近な項目の正規表現を読み解く

この記事について

勉強のために、身近な項目の正規表現をネットで調べて解読しました。
同じように正規表現を勉強されている方の役にたったら嬉しいです。

  • 郵便番号
  • 電話番号
  • 携帯番号
  • IPアドレス
  • URL

正規表現って何?

そもそも正規表現は・・・

正規表現とは指定した条件に一致する複数の文字列を一つのパターンで表現する方法です。指定する条件は例えば「アルファベットと数値からなる 5 文字の文字列」や「末尾が ing で終わる文字列」など目的に合わせて条件を設定します。

基礎的な説明は以下のサイトがわかりやすかったです。
- Let's プログラミング -正規表現とは-
- サルにもわかる正規表現入門

以下のツールは正規表現をわかりやすく可視化してくれます。
- Regulex

よくある正規表現

? 郵便番号

郵便番号(ハイフンなし)

const regex = /^\d{7}$/;
console.log(regex.test("1234567"));
  • /^\d{7}: 0〜9 の数字 7 桁で始まる
  • $: 上記で終わる

郵便番号(ハイフンあり)

const regex = /^[0-9]{3}-[0-9]{4}$/;
console.log(regex.test("111-1111"));
  • ^[0-9]{3} : 0〜9 の数字 3 桁から始まる
  • - : ハイフン
  • [0-9]{4}: 0〜9 の数字 4 桁で終わる

郵便番号(ハイフン任意)

const regex = /^[0-9]{3}-?[0-9]{4}$/;
console.log(regex.test("123-4567"));
console.log(regex.test("1234567"));
  • ^[0-9]{3} : 0〜9 の数字 3 桁から始まる
  • -? : ハイフンあり、またはなし
  • [0-9]{4}: 0〜9 の数字 4 桁で終わる}

郵便番号(郵便マーク付き)

const regex = /^〒\s[0-9]{3}-[0-9]{4}$/;
console.log(regex.test("〒 111-1111"));
  • ^〒: 〒マークではじまる
  • \s: スペース 1 つ
  • ^[0-9]{3} : 0〜9 の数字 3 桁から始まる
  • - : ハイフン
  • [0-9]{4}: 0〜9 の数字 4 桁で終わる

参考:

日本の郵便番号を正規表現で検出する

? 電話番号

電話番号(ハイフンあり)

const regex = /\d{2,4}-\d{2,4}-\d{4}/;
console.log(regex.test("03-1234-5678"));
  • /\d{2,4}: 2-4 桁の数字
  • - : ハイフン
  • /\d{2,4}: 2-4 桁の数字
  • - : ハイフン
  • /\d{2,4}: 2-4 桁の数字

電話番号(ハイフンなし)

const regex = /^(0{1}\d{9,10})$/;
console.log(regex.test("0312345678"));
  • ^(0{1}: 0 から始まる
  • \d{9,10})$: 数字 9 桁で終わる

携帯番号(ハイフンあり)1

const regex = /^0[789]0-[0-9]{4}-[0-9]{4}$/;
console.log(regex.test("080-1234-5678"));
  • ^0: 0 から始まる
  • ^[789]0: 「7、8、9 のいずれか」と 0
  • - : ハイフン
  • [0-9]{4}: 0〜9 の数字 4 桁
  • - : ハイフン
  • [0-9]{4}: 0〜9 の数字 4 桁で終わる

携帯番号(ハイフンあり)2

const regex = /^(070|080|090)-\d{4}-\d{4}$/;
console.log(regex.test("080-1234-5678"));
  • ^(070|080|090): 070 まはた 080 または 090 で始まる
  • - : ハイフン
  • [0-9]{4}: 0〜9 の数字 4 桁
  • - : ハイフン
  • [0-9]{4}: 0〜9 の数字 4 桁で終わる

参考:

Let's プログラミング -携帯電話の電話番号の正規表現-

? IP アドレス

IP アドレス

image

const regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
console.log(regex.test("192.168.0.0")); // true
console.log(regex.test("192.256.0.0")); // false
console.log(regex.test("192.168.0.")); // false
  • 25[0-5] = 25 と 0〜5 のどれか
  • | = または
  • 2[0-4][0-9] = 「2」と「0〜4 のどれか」と「0〜9 のどれか」
  • [01]?[0-9][0-9]? = 「0 か 1 か何も無い」と「0〜1 のどれか」と「0〜9 のどれかか何も無い」

上記を.で繋いで繰り返す。

参考:

IP アドレスをチェックする JavaScript 正規表現

? URL

HTTP/HTTPS ではじまる場合

image

※こちらは隅々まで解読しきれていないため、解説が間違っている可能性があります ?‍♀️

const regex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
console.log(regex.test("https://www.google.com/")); // true
console.log(regex.test("http://example.shinjuku.tokyo/newspage?id=123")); // true
console.log(regex.test("http://github")); // false
console.log(regex.test("github.com")); // false
  • /https? : 「http」または「https」
  • :\/\/: 「://」
  • (www\.)?: 「www.」またはなし
  • [-a-zA-Z0-9@:%._\+~#=]{1,256}:「-、a〜z、A〜Z、0〜9、@、:、%、.、_、+、~、#、=」のどれか 1~256 文字(ドメイン名部分) *1
  • \.: ドット
  • [a-zA-Z0-9()]{1,6}: 「a〜z、A〜Z、0〜9、なにもなし」のどれか(gTDL(「.com」や「.net」)の部分)*2
  • -b: 単語の先頭か末尾に一致
  • \b([-a-zA-Z0-9()@:%_\+.~#?&//=]*):「-、a〜z、A〜Z、0〜9、@、:、%、.、_、+、~、#、=」の文字を 0 回以上繰り返す

*1 ドメイン名の文字数についてはRFC1035で決められているようです。
ドメイン名のしくみ

ドメイン名の仕様と実装を定義した RFC1035 では、 DNS メッセージ中のドメイン名を表すパラメータの長さは 255 オクテット以下とされています。

*2 最長の globalTDL は「museum」なので 6 文字に設定していると思われます。
https://www.nic.ad.jp/ja/dom/types.html

参考:

What is a good regular expression to match a URL?

まとめ

正規表現を学ぶ際、

  • ユースケースから解読すると理解しやすい
  • 同じようなパターンを沢山見てると覚える
  • 一見複雑なパターンでも、部分ごとに区切ってみると理解しやすい

ということがわかりました。
正規表現に関する記事はたくさんあるので、また機会があれば解読に挑戦してみたいと思います。

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

JavaScriptで現在日時を表示させてみた

JavaScriptで現在日時を表示するコードを書いたのでメモ程度に残しておきます。

とりあえず雛形の作成から

ターミナル
$ mkdir clock
$ cd clock
$ touch index.html

作成したhtmlファイルに以下を追記

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>clock</title>
  </head>

  <body>
    <div id="clock"></div>
  </body>
</html>

これで雛形が完成したのでjavascriptを書いていきます。

以下のコードを<div id="clock"></div>の下に記載してください。

index.html
<script>
  timerID = setInterval('clock()', 500);

    function clock() {
      document.getElementById("clock").innerHTML = getNow();
    }

    function getNow() {
      var now = new Date();
      var year = now.getFullYear();
      var mon = now.getMonth() + 1;
      var day = now.getDate();
      var hour = now.getHours();
      var min = now.getMinutes();
      var sec = now.getSeconds();

      var s = year + "" + mon + "" + day + "" + hour + "" + min + "" + sec + "";
      return s;
    }
</script>

これでindex.htmlを実行すると現在日時が表示できます。

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

簡単レシート印刷 receiptline を導入してみた

街のお店で iPad のレジをよく見かけるようになりましたね。
一般的にタブレット POS と呼ばれています。

おかげで?レシートプリンターがネットオークションやフリマアプリに出品されるようになりました。
業務用ってワクワクしませんか?個人でも利用できる業務用スーパーも人気がありますよね。

ネットには Raspberry Pi でレシートプリンターの制御にチャレンジしたブログもあって楽しそうです。
レシートプリンターの落札を試みつつ、レシート印刷にトライしてみようと思います。

レシートプリンターで印刷するには

レシートプリンターで印刷する手段はいろいろあるので、独断で整理してみました。

  • デスクトップアプリ向け
    • Windows プリンタードライバー
    • Linux や Mac の CUPS ドライバー
  • スマートフォン・タブレットアプリ向け
    • iOS 用 SDK ライブラリ
    • Android 用 SDK ライブラリ
  • コンソールとエスケープシーケンスが好きな人向け
    • StarPRNT コマンド
    • ESC/POS コマンド
  • 玄人向け
    • OPOS (Win32, .NET)
    • JavaPOS
  • オープンソース
    • GitHub に多数
    • Python, JavaScript, Java, PHP, Ruby, Go, ...

メーカーさんが提供しているものは、ドキュメントもサポートも充実しています。
ここで記事にする意味があるのは、オープンソース一択と言えると思います。

多種多様なオープンソースの中に、日本発のプロジェクトもありました。
https://github.com/receiptline/receiptline

JavaScript / Node.js の変換ライブラリです。
テキストを入力すると、SVG 画像やレシートプリンターのコマンドを出力します。
アスキーアート的に文字を並べると清書してくれるイメージですね。

おまけにプレビューできる開発ツールが付いています。
プレビューに対応する印刷手段は意外に少ないかもしれません。
これならレシートプリンターを持っていなくても試すことができます。

receiptline をセットアップ

開発する人

npm でインストール・実行して、http://localhost:10080/ を開きます。
https://www.npmjs.com/package/receiptline

$ npm i receiptline
$ cd node_modules/receiptline
$ npm start

一般の人

GitHub からダウンロード・展開して、designer/index.html を開きます。
https://github.com/receiptline/receiptline

hello, world!

開発ツールを使ってみましょう。

01.png

左側の黒い編集エリアに文字を入力してみます。
編集エリアの文字の大きさは Zoom スライダーで適当に調節。

hello, world!

入力すると、右側の白いレシート用紙にプレビューが表示されます。
文字列はレシート用紙の中央に配置されました。

02.png

レシート用紙の幅は Width スライダーで変更することができます。
単位はインチ・ミリではなく文字数 (桁数) となっています。

03.png

財布の中に入っていたレシートの多くは、1 行あたり漢字 16 文字 (32 桁) でした。
ところが、文字数が違うレシートや、幅広のレシートも・・・

混沌のレシート世界

昔の明和電機社長ブログから引用。
“レシートの印刷の大混乱ぶり、どうにかならんものか・・・”
https://www.maywadenki.com/blog/2011/05/17/post-bbf8/

これは今でも変わっていないと思います。
果たして混沌のレシート世界に秩序をもたらすことができるのか?

次回は文字装飾を試してみようと思います。

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

Fiddlerでレスポンスを置き換える

初めてFiddlerを使い、カスタムスクリプトでレスポンスを書き換えようと思ったのですが、うまくいかず苦労したのでメモを残します。
※もっと良い方法があれば教えていただきたいです

やりたかったこと

{
  data: [
    {
      hoge: "abcde",
    },
    {
      geho: "12345",
    }
  ]
}

上記のレスポンスが来た時に、下記のように書き換えるようなことがしたかった

{
  data: [
    {
      hoge: "abcde",
      fuga: [ // ここから
        {
          hogehoge: "fugafuga"
        },
        {
          hogehoge2: "fugafuga2"
        },
      ] // ここまで追加
    },
  ]
}

解決

static function OnBeforeResponse(oSession: Session) {
    var response = Fiddler.WebFormats.JSON.JsonDecode(oSession.GetResponseBodyAsString());
    response.JSONObject["data"][fuga] = Fiddler.WebFormats.JSON.JsonDecode('[{},{}]').JSONObject;
    response.JSONObject["data"][fuga][0]["hogehoge"] = "fugafuga";
    response.JSONObject["data"][fuga][1]["hogehoge2"] = "fugafuga2";

  oSession.utilSetResponseBody(Fiddler.WebFormats.JSON.JsonDecode(response));
}

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

LGTMをくれた人のプロフィールページに通知一覧から飛びたいので4行でChrome拡張作った。

何を作ったの?

通知一覧 - Qiita もしくは、上メニューの通知欄で、通知に表示されている人のアイコンから、その人のユーザーページに飛ぶためのChrome拡張です。

imgタグ以外の場所をクリックすると、従来どおり「LGTM(やストックやコメント)がついた記事」に飛べます。

なぜ作ったの?

自分の記事にLGTMやストックをしてくれた人がどんな記事を書いてるか知りたいのに、通知機能から一発で記事一覧に飛べなくてめちゃくちゃイライラしたからです。私はQiitaにポケモン関係の記事を投稿しているのですが、読者数が他の記事に比べて多くないので、ポケモン記事に何らかのアクションをしてくれた方の記事は特に読みたいです。
今のUIだと、「LGTMがついた記事に飛ぶ」->「LGTM欄をクリック」という動線になってしまうのと、ストックに至ってはユーザー名直打ちしか方法がなさそうに見えます。

どうやって作ったの?

正攻法で、content scriptで通知一覧ページにあるimgのouterHTMLを編集しました。ユーザー名はaltから取得可能です。きれいなHTMLなので編集しやすくて良いですね…。

const targets = document.querySelectorAll(".notification_icon > img");
targets.forEach((target) => {
  target.outerHTML = `<a href="https://qiita.com/${target.alt}" target="_top">${target.outerHTML}</a>`;
});

この方法の良いところは、imgタグ以外の場所をクリックすると、従来どおり「LGTM(やストックやコメント)がついた記事」に飛べる点です。

上部メニューの通知欄については、 https://qiita.com/notifications?iframe=1 をiframeで表示しているため、manifest.jsonに"all_frames": trueが必要です。
manifest.jsonは以下のように書きました。

{
  "name": "Qiitaで自分に通知した人のユーザーページに飛ぶやつ。",
  "description": "Qiitaで自分に通知した人のユーザーページに飛ぶための拡張です。",
  "version": "0.1",
  "content_scripts": [
    {
      "all_frames": true,
      "matches": [
        "https://qiita.com/*"
      ],
      "js": [
        "script.js"
      ],
      "run_at": "document_end"
    }
  ],
  "manifest_version": 2
}

所要時間は15分程度でした。この記事を書くほうが時間がかかっています。
Chrome Web Storeに乗せるまでが筋かなと思ったので、登録して現在審査待ちです。

まとめ

  • Chromeの拡張はすぐ作れる。ドキュメントもわかりやすい。
  • querySelectorAllは便利。
  • Qiita夏祭り2020_Qiitaの記事数6つしかなくてびびった。(2020/07/13 20時現在)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptで+++は許されない+ ++は許される

+++,---とは

これらは一つの演算子ではなく、それぞれ+と++または++と+の組み合わせ、それの-番です。
わかりやすくいえば一つずつ増やして足していくということですね。

var a=0;
var b=13;
console.log(a+ ++b) // 14
a--;
console.log(a++ +b) // 13

これがスペースを開けずに+++にするとエラーが出ます。もちろん理由は解析できないからです。
ちなみに++-や--+は使えます。

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

イミュータブルとミュータブル

プログラミングの基礎についての他の記事はコチラ

イミュータブルとミュータブルについて説明する。

イミュータブルとは、作成した後はその中身を変更できないデータやオブジェクトのもつ性質である。

イミュータブルなデータを実現する一番ストレートな方法は、全てのメンバをコピーして、新しいデータとすることである。

var before = {
    fuga: "fuga",
    hoge: "hogee"
}

var copied = JSON.parse(JSON.stringify(before)) // JSON変換によるディープコピー

var after = {
    ...copied, // スプレッド構文によるシャローコピー
    hoge: "hoge"
}

console.log(before) // { fuga: "fuga", hoge: "hogee" }
console.log(after)  // { fuga: "fuga", hoge: "hoge" }

ただし、そもそもJavaScriptにおいてプリミティブな値以外はミュータブルであるから、beforeはミュータブルである。

ただ、ここではイミュータブルに扱われているというだけである。

イミュータブルなデータは、関数型プログラミングにおいて利用されることが多い。

var data = {
    count: 0
}

function increment(data) {
    return {
        ...data,
        count: data.count + 1
    }
}

var incremented = increment(data)

console.log(data)      // { count: 0 }
console.log(incremented) // { count: 1 }

上記のincrement関数ではdataはイミュータブルに扱われている。

その為、data 変数の中身が変更されていない。

しかし、このやり方には問題がある。

というのも元のデータから新しいデータを作成するにあたり変数のディープコピーを行うことは、新たなメモリの確保が必要になるからである。

このメモリフットプリントの問題を解消するため、永続データ構造が用いられる。

永続データ構造では変化の差分のみが適用されるため、メモリの使用量を抑えつつ、高速にデータのコピーが実現できる。

JavaScriptだと、 Immutable.jsimmer.js が利用される。

下記は、Immutable.js の例である。

const { Map } = require('immutable');

const map1 = Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 50);

console.log(map1.get('b')) // 2
console.log(map2.get('b')) // 50

map1 はイミュータブルに取り扱われ、map2 がコピーされ新たにできたように見えるが、変更されたメンバ b の分以外はメモリの使用量は増えていない。

旧データ ⇒ 関数適用 ⇒ 新データ という一種の変化表現を学んだところで、次はミュータブルについて解説する。

ミュータブルはイミュータブルの逆で、変更可能である。

再度、increment の例を出そう。

var data = {
    count: 0
}

function increment(data) {
    return {
        ...data,
        count: data.count + 1
    }
}

var incremented = increment(data)

console.log(data)      // { count: 0 }
console.log(incremented) // { count: 1 }

この変化をミュータブルに表現するためにデータをオブジェクトとして扱う。

オブジェクトには、自身のメンバ(プロパティ)を変更させるための関数(メソッド)を同梱する。

var object = {
    count: 0,
    increment() {
        this.count++
    }
}

console.log(object.count) // 0

object.increment()

console.log(object.count) // 1

同じことが実現できていることが分かる。

より手続き的で順序性を重んじる代わりに、特殊なデータ構造を用意せずともメモリの消費を抑えることができている。

また、別の記事で説明する予定だが、オブジェクト指向における this は自身の持つデータを指していることも良く分かる。

このようにイミュータブルとミュータブルにはそれぞれの利点があり、どちらを採用するかは作るプログラムの性質次第となる。

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

【JavaScript】祝日取得クラス

先日作成したPHP版祝日取得クラス のJavaScript版です。
PHP版をJavaScriptに置き換えただけですので、使い方もほぼ同じです。
メソッドやプロパティについてはPHP版のほうを参照してください。

holiday_class.js
class Holiday {

    constructor(year = 0) {
        // 祝日定義
        // ('国民の祝日に関する法律'が公布・施行された1948年7月20日以降のもののみ)
        this.holidayDefinitions = {
            1 : {
                '1949:1'    : '元日',
                '1949:15, 2000:2_1'
                            : '成人の日',
            },
            2 : {
                '1967:11'   : '建国記念の日',
                '2020:23'   : '天皇誕生日',
                '1989:24, 1990:0'
                            : '大喪の礼',
            },
            3 : {
                '1949:s'    : '春分の日',
            },
            4 : {
                '1959:10, 1960:0'
                            : '結婚の儀',
                '1949:29'   : '天皇誕生日, 1989:みどりの日, 2007:昭和の日',
            },
            5 : {
                '2019:1, 2020:0'
                            : '皇太子殿下即位・改元',
                '1949:3'    : '憲法記念日',
                '2007:4'    : 'みどりの日',
                '1949:5'    : 'こどもの日',
            },
            6 : {
                '1993:9, 1994:0'
                            : '結婚の儀',
            },
            7 : {
                '1996:20, 2003:3_1, 2020:23, 2021:3_1'
                            : '海の日',
                '2020:24, 2021:0'
                            : 'スポーツの日',
            },
            8 : {
                '2016:11, 2020:10, 2021:11'
                            : '山の日',
            },
            9 : {
                '1966:15, 2003:3_1'
                            : '敬老の日',
                '1948:a'    : '秋分の日',
            },
            10 : {
                '1966:10, 2000:2_1, 2020:0, 2021:2_1'
                            : '体育の日, 2020:スポーツの日',
                '2019:22, 2020:0'
                            : '即位礼正殿の儀',
            },
            11 : {
                '1948:3'    : '文化の日',
                '1990:12, 1991:0'
                            : '即位礼正殿の儀',
                '1948:23'   : '勤労感謝の日',
            },
            12 : {
                '1989:23, 2019:0'
                            : '天皇誕生日',
            },
        };

        this.dateTime = new Date();
        this.dateTime.setHours(0, 0, 0);

        if(year < 1) year = this.dateTime.getFullYear();
        this.year = year;
        this.month = this.dateTime.getMonth() + 1;

        this.result = {};
        this.useIndefiniteHoliday = 1;
        this.resultType = 0;
    }

    /**
     *  1年分のリストを返す
     */
    getHolidayOfYear(year = 0) {
        if(year < 1) year = this.year;

        if(this.result[year] === undefined) {
            // 該当年の祝日オブジェクトに変換
            let holiday = {};
            const equinox = .242194 * (year - 1980) - Math.floor((year - 1980) / 4);
            for(const month in this.holidayDefinitions) {
                holiday[month] = {};
                const currentMonthData = this.holidayDefinitions[month];
                for(const days in currentMonthData) {
                    const names = currentMonthData[days];
                    // 対象年・日取得
                    const _days = days.split(',');
                    const arrTmp = {};

                    for(const i in _days) {
                        const tmp = _days[i].split(':');
                        arrTmp[Number(tmp[0])] = tmp[1].trim();
                    }
                    let yearTmp = 0;
                    for(const tmp in arrTmp)
                        if(year >= tmp && tmp >= yearTmp) yearTmp = tmp;

                    if(yearTmp === 0) continue;

                    // 日を記述形式ごとに取得
                    let day;
                    if(arrTmp[yearTmp] === 's')
                        day = Math.floor(20.8431 + equinox);
                    else if(arrTmp[yearTmp] === 'a')
                        day = Math.floor(23.2488 + equinox);
                    else if(arrTmp[yearTmp].indexOf('_') >= 0) {
                        const [num, w] = arrTmp[yearTmp].split('_');
                        day = this.getDayOfNumWeek(year, month, num, w);
                    }
                    else
                        day = Number(arrTmp[yearTmp]);

                    if(day < 1) continue;

                    // 名称取得
                    const _names = names.split(',');
                    const arrTmpName = {};
                    for(const i in _names) {
                        const tmp = _names[i].split(':');
                        if(tmp.length === 1)
                            arrTmpName[0] = tmp[0].trim();
                        else
                            arrTmpName[Number(tmp[0])] = tmp[1].trim();
                    }

                    yearTmp = 0;
                    for(const tmp in arrTmpName)
                        if(year >= tmp && tmp >= yearTmp) yearTmp = tmp;

                    holiday[month][day] = (holiday[month][day] === undefined) ?
                        arrTmpName[yearTmp] : holiday[month][day] + ', ' + arrTmpName[yearTmp];
                }
            }

            // 国民の休日・振替休日
            if(this.useIndefiniteHoliday)
                holiday = this.indefiniteHoliday(holiday, year);

            this.result[year] = holiday;
        }

        return this.resultType ?
            this.convertLinear(year, this.result[year]):
            this.result[year];
    }

    /**
     *  国民の休日・振替休日
     */
    indefiniteHoliday(holiday, year) {
        for(let month = 1; month <= 12; month++) {
            // 月末日
            this.dateTime.setFullYear(year, month, 0);
            const lastDay = this.dateTime.getDate();

            for(let day = 1; day <= lastDay; day++) {
                // 前日の月日
                this.dateTime.setFullYear(year, month - 1, day - 1);
                const prevMonth = this.dateTime.getMonth() + 1;
                const prevDay = this.dateTime.getDate();

                // 翌日の月日
                this.dateTime.setFullYear(year, month - 1, day + 1);
                const nextMonth = this.dateTime.getMonth() + 1;
                const nextDay = this.dateTime.getDate();

                // 祝日に挟まれた平日を国民の休日に変更(1986年以降)
                this.dateTime.setFullYear(year, month - 1, day);
                if( year >= 1986 && // 1986年以降
                    holiday[prevMonth][prevDay] !== undefined && // 前日が祝日
                    holiday[nextMonth][nextDay] !== undefined && // 翌日が祝日
                    holiday[month][day] === undefined && // 当日が祝日ではない
                    this.dateTime.getDay() !== 0 // 当日が日曜ではない
                ) {
                    holiday[month][day] = '国民の休日';
                }

                // 振替休日(1973年4月以降)
                if( (year > 1973 || (year == 1973 && month >= 4)) && // 1973年4月以降
                    holiday[month][day] !== undefined && // 祝日
                    this.dateTime.getDay() === 0 // 日曜
                ) {
                    // その日以降の直近の平日を振替休日に
                    for(let i = 1; i < 7; i++) {
                        this.dateTime.setFullYear(year, month - 1, day + i);
                        const m = this.dateTime.getMonth() + 1;
                        const d = this.dateTime.getDate();
                        if(holiday[m][d] === undefined) {
                            holiday[m][d] = '振替休日';
                            break;
                        }
                    }
                }
            }
        }
        return holiday;
    }

    /**
     *  year年 month月 第num w曜日に該当する日を返す
     */
    getDayOfNumWeek(year, month, num, w) {
        this.dateTime.setFullYear(year, month - 1, 1);
        const firstDayWeek = this.dateTime.getDay();
        return 1 + (num - 1) * 7 + (7 + w - firstDayWeek) % 7;
    }

    /**
     *  YYYY-MM-DDをキーとしたオブジェクトに変換
     */
    convertLinear(year, array) {
        const arrTmp = {};
        for(const month in array) {
            for(const day in array[month]) {
                const names = array[month][day];
                arrTmp[
                    String(year).padStart(4, '0') + '-' +
                    String(month).padStart(2, '0') + '-' +
                    String(day).padStart(2, '0')
                ] = names;
            }
        }
        return arrTmp;
    }

    /**
     *  1か月分のリストを返す
     */
    getHolidayOfMonth(month = 0) {
        if(month < 1 || month > 12) month = this.month;

        const year = this.year;

        // 該当年の結果が未取得であれば取得
        if(this.result[year] === undefined)
            this.getHolidayOfYear();

        if(this.resultType) {
            const arrTmp = {};
            for(const day in this.result[year][month]) {
                arrTmp[
                    String(year).padStart(4, '0') + '-' +
                    String(month).padStart(2, '0') + '-' +
                    String(day).padStart(2, '0')
                ] = this.result[year][month][day];
            }
            return arrTmp;
        } else {
            return this.result[year][month];
        }
    }

    /**
     *  国民の休日・振替休日 使用フラグ変更
     */
    setUseIndefiniteHoliday(flg = 1) {
        this.useIndefiniteHoliday = flg == 1 ? 1 : 0;

        // 取得済みの結果をリセット
        this.result = {};
    }

    /**
     *  戻り値形式変更
     */
    setResultType(flg = 0) {
        this.resultType = flg == 0 ? 0 : 1;
    }

    /**
     *  年変更
     */
    setYear(year = 0) {
        if(year < 1) year = new Date().getFullYear();
        this.year = year;
    }
}

使用例

const holiday = new Holiday;
// 年も同時に指定
//const holiday = new Holiday(2019);

// 国民の休日及び振替休日の使用指定(0:非使用 1:使用(デフォルト))
holiday.setUseIndefiniteHoliday(1);

// 戻り値形式変更メソッド
// 0: 月、日をキーとしたオブジェクト(デフォルト)
// 1: YYYY-MM-DDをキーとしたオブジェクト
holiday.setResultType(0);

// 1年分のリスト取得
// 引数は年、引数省略時は年プロパティの値を参照します
console.log(holiday.getHolidayOfYear());

// 1か月分のリスト取得
// 引数は月、引数省略時は実行時の月で取得します
// 年は年プロパティの値を参照します
console.log(holiday.getHolidayOfMonth());

// 戻り値をYYYY-MM-DD形式に変更
holiday.setResultType(1);

// 年プロパティ変更(引数省略時は現在の年で設定)
holiday.setYear(2019);

console.log(holiday.getHolidayOfYear());
console.log(holiday.getHolidayOfMonth(5));

結果

result.jpg

年、月をキーにして取得したものは、こんな使い方をする場合に楽かと思います。

const year  = 2020;
const month = 7;
const day   = 24;
const holidayName = new Holiday(year).getHolidayOfMonth(month)[day];
console.log(
    `${year}${month}${day}日は` +
    (holidayName !== undefined ? `${holidayName}です` : '祝日ではありません')
);

結果

2020年7月24日はスポーツの日です
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DELETEメソッド実行後のリダイレクトでNo route matches [DELETE]となってしまう

非同期処理で削除処理を行った後、リダイレクトを行うとメソッドがGETではなくDELETEとなってしまい正常にリダイレクトされない現象が発生したためメモ。

発生した状況

delete.js
// 一部抜粋
$.ajax({
  type: 'DELETE',
  url: '/posts/destroy_post',
  dataType: 'json',
  data: {
    post_id: $(this).data('post-id')
  }
}).done(function (data, status, xhr) {
  // 処理
});
posts_controller.rb
  # 一部抜粋
  def destroy_post
     # 処理

    redirect_to post_index_path
  end
// 発生エラー
ActionController::RoutingError (No route matches [DELETE] "/post"):

改善策

posts_controller.rb
  # 一部抜粋
  def destroy_post
     # 処理

    redirect_to post_index_path, status: 303
  end

上記のように「status: 303」と記載をすることで改善。

http://api.rubyonrails.org/classes/ActionController/Redirecting.html

If you are using XHR requests other than GET or POST and redirecting after the request then some browsers will follow the redirect using the original request method. This may lead to undesirable behavior such as a double DELETE. To work around this you can return a 303 See Other status code which will be followed using a GET request.

GETまたはPOST以外のXHRリクエストを使用していて、リクエストの後にリダイレクトしている場合、一部のブラウザは元のリクエストメソッドを使用してリダイレクトに従います。これにより、二重DELETEなどの望ましくない動作が発生する可能性があります。これを回避するには、GETリクエストを使用して追跡される303 See Otherステータスコードを返すことができます。

上記のように二重DELETEが発生していたと思われる。
引数にStatusコード303を指定することにより、回避することができるようです。

参考

Rails Redirect After Delete Using DELETE Instead of GET

以上です。
いいねやQiitaやTwitterのフォローいただけると励みになります!
他にも方法がありましたら、コメントお待ちしております。
宜しくお願いします〜

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

会計/人事労務freee APIとGoogleとチャットツールと在庫システムを使った業務効率化/自動化の実践例

はじめに

「Qiita夏祭り2020 オンライン」のfreeeテーマとして投稿しました。今までfreee APIを使って実現してきたことをまとめて書いていきます。
コーポレートエンジニアとして各部署の要望を聞きながら設計しているので、いち部署の中のちょっとした業務範囲を便利にするだけでなく、他部署との連携部分もありますので、少しでも参考になったらいいなと思います。
今回はコードは載せていません。APIを使ってどんなことを実際にしているかの共有です。コードについては、近いうちにまとめてGithubにでもまとめようと思います。

会社としての環境

  • 当社の顧客や取引先は製造業や物流業が主で、紙取引が当たり前です。
  • 日本の会社ですがエンジニアリンググループの大半は外国人ということもあり、日本ならではの経費精算を自分で行うことが困難/非常に時間が掛かる場合が多いです。
  • ロボットなど実物を在庫として持つ業態のため、入荷→在庫→出荷というステータス管理が常に発生します。

どのfreee APIを使っているか

会計freee

  • 振替伝票API
  • 取引登録API
  • 仕訳帳API
  • 請求書API
  • ファイルボックスAPI
  • 勘定科目API
  • 品目API
  • 取引先API
  • 部門API
  • セグメントタグAPI
  • メモタグAPI

人事労務freee

  • 勤怠API
  • 従業員API
  • 給与明細API

どんなことをしているか

用意されたUIでしか操作が出来ない、専任者しか分からない、聞かないと情報が分からない、調べないと情報が分からないなどをなくすために自動化/効率化をしています。
他には、freeeと連携していない勤怠システムをどうに人事労務freeeとうまく使うかを実践しています。
freee自動化イラスト.jpg

具体的にどんなことが出来て、どう便利なのか

フローチャート図で表したうち、freee APIに関する部分の【人が作業すること】【その結果】について書いていきます。
①~⑨が会計freee API、⑩~⑫が人事労務freee APIです。
【SS】は【Google スプレッドシート】を指しています。【GAS】【API】【作業】【JS】とそれぞれ色を付けています。

①経費精算情報をもとに振替伝票登録

前提
従業員の時間を経費精算で浪費しないように、手慣れた人間が一括して経費精算を行っています。
経費入力は経理ではないパートタイムの方が担当しており、経費精算SSの各人シートに入力しています。
経費項目(タクシー、会議代など)を予め設定しておき、選択した経費項目に応じて勘定科目や各種タグがまとめて設定されるようになっています。

フロー
1. 【API】経費精算SSを開いたことをトリガーに、勘定科目や各種タグをfreee APIで取得しSSに書き込む。
2. 【作業】経費精算SSには人別に経費精算表シートがあり、1で取得した情報をもとにプルダウンで会計タグを選択する。
3. 【GAS】2の作業時に経費項目から入力候補をサジェストする。
4. 【API】すべての情報が選択できたら<一括取込>を実行して、人ごとにfreee APIで振替伝票登録を行う。

結果
- 最新の勘定科目と各種タグをプルダウンで選択できるので、手入力/入力ミス削減。
- SSで作業することでコピペ等、繰り返し作業が簡単にできる。
- 設定した勘定科目と各種タグ以外にも、一括取込時に社員番号で自動的に社員名の取引先タグが入るようにする等し固定動作を自動化。

100人以上いるので、手作業インポートでは回数的にインポートする作業だけですら大変ですし、最新の勘定科目や各種タグを毎回手で取得するのも面倒です。無理にシステム内で作業するよりもSSやExcelの方が良かったりします。

フローチャート図
freee API flowchart_1.jpg

②発注納品情報をもとに支払請求書仕訳

フロー
1. 【API】<タグ取得ボタン>を押して、勘定科目や各種タグをfreee APIで取得しkintoneに書き込む。
2. 【作業】発注一覧アプリから、取引登録したいものにマークを付ける。
3. 【作業】1で取得した情報をもとにプルダウンで会計タグを選択する。
4. 【API】<一括取込>を実行して請求書ごとにfreee APIで取引登録を行う。

結果
- 購買部署が発注した情報をもとに作成するため、商品情報の手入力や金額の手入力がなく、簡単&ミス削減。
- 請求書が届く前に仕訳を作成することが出来て、前倒し作業が可能。(運用ルール決めが重要ですが…)
- freee取引登録なので、支払時はfreee支払一覧からFBデータを出力して総振処理ができる。対応している銀行であれば、freeeから直接振り込みの手続きも可能。

フローチャート図
freee API flowchart_2.jpg

③在庫システムの情報と発注情報を使って製品出荷仕訳を作成

前提
発注/入荷情報を購買担当が在庫システムに登録し、出荷担当が出荷情報を在庫システムに登録するプロセスになっています。
管理物品にはQRコードを貼付しており、そのQRコードを使って検索できるようになっているため、検索時のミス/検索の手間を削減しています。

フロー
1. 【API】<タグ取得ボタン>を押して、勘定科目や各種タグをfreee APIで取得しkintoneに書き込む。
2. 【作業】出荷アプリに登録された出荷情報から、振替伝票登録したいものにマークを付ける。
3. 【作業】1で取得した情報をもとにプルダウンで会計タグを選択する。
4. 【API】<一括取込>を実行して製品ごとにfreee APIで振替伝票登録を行う。

結果
- 在庫システムによる出荷情報と発注時の情報を連携させ処理をするので、発注時の情報をそのまま使えるため入力作業削減。
- 出荷情報と連携しているので、経理が出荷実態を把握するために現場から情報収集する工数を削減し、かつリアルタイムでの処理が可能になる。

フローチャート図
freee API flowchart_3.jpg

④クレジットカード明細仕訳のサジェスト&振替伝票登録

フロー
1. 【API】クレカSSを開いたことをトリガーに、勘定科目や各種タグをfreee APIで取得しSSに書き込む。
2. 【API】期間を指定して仕訳帳をAPIで取得しSSに書き込む。
3. 【作業】クレカ明細をクレカSSに貼り付け、1で取得した情報をもとにプルダウンで会計タグを選択する。
4. 【GAS】3の作業時に仕訳帳から入力候補をサジェストする。(クレカ明細摘要から勘定科目や各種タグ)
5. 【API】<取込>を実行して製品ごとにfreee APIで振替伝票登録を行う。

結果
- オリジナルの勘定科目/各種タグサジェスト機能を用意することで自社に合わせた入力補助を行える。
- ETCやタクシーなど同じ処理をする場合にコピペで簡単にできる。
- 指定した明細摘要で直近10件選択した勘定科目/各種タグを表示させるなど、仕訳帳を検索するより使い勝手が良い。

フローチャート図
freee API flowchart_4.jpg

⑤仕訳帳APIを使った月次推移表BS/PL/CR作成

フロー
1. 【API】仕訳帳APIで月次推移表SSに仕訳帳を書き込む。
2. 【API】勘定科目APIで月次推移表SSに勘定科目とカテゴリーを書き込む。
3. 【GAS】BS/PL/CRそれぞれのシートに勘定科目カテゴリーと勘定科目を書き込み、仕訳帳からデータを取得し推移表形式にする。

結果
- 推移表APIはないが、勘定科目APIと仕訳帳APIを使うことで推移表が簡単に作成できる。
- 勘定科目を取得する際にカテゴリーを取得することで各カテゴリーごとの集計ができる。

フローチャート図
freee API flowchart_5.jpg

⑥商談情報を使って請求書作成&営業から経理への請求依頼自動化

フロー
1. 【作業】営業が商談アプリで商談ステータスを請求依頼に変更する。
2. 【API】商談情報をベースにfreee APIで請求書を作成する。
3. 【JS】freee請求書が作成されたことを経理に通知する。
4. 【作業】請求書をfreeeで編集。

結果
- 営業が日常的に使用している商談アプリの情報を使うため、請求処理のために営業が追加で情報を入力する手間がない。
- 営業情報をそのまま使うため経理での手入力/ミスが削減でき、リアルタイムでの処理が可能になる。
- 請求依頼漏れを営業側でステータスとして確認することが出来る。

フローチャート図
freee API flowchart_6.jpg

⑦毎日10時台に発行済請求書の一覧作成&入金/未入金一覧を営業にチャット通知&商談アプリに書き込む

フロー
1. 【作業】freeeで入金消込を行う。
2. 【API】10時台タイムトリガーが発動して、請求書情報をfreee APIで取得する。
3. 【GAS】2で取得した情報のうち、発行済請求書情報をを請求ステータスSSに書き込む。
4. 【GAS】2で取得した情報から当日入金された情報と未入金情報を一覧化し、請求ステータスSSのURLを添えてチャットで営業に通知する。
5. 【JS】2で取得した情報から入金/未入金状況を抽出し、商談アプリに書き込む。

結果
- 毎日の入金状況が送付されること、過去分もSSで見れることから、営業から経理への入金状況確認依頼が0になった。
- 発行された請求書の内容を営業が確認できるため、過去分の請求情報共有依頼が0になった。
- 商談アプリのステータスが自動で更新されるため手作業がなくなり、ステータス更新忘れやミスがなくなった。

フローチャート図
freee API flowchart_7.jpg

⑧未承認仕訳数の通知

フロー
1. 【API】タイムトリガーが発動して、仕訳帳APIから期間指定で仕訳帳を取得、未承認仕訳数を取得する。
2. 【GAS】1で取得した未承認仕訳数などをチャットで通知する。

結果
- 仕訳承認機能を利用しており、未承認仕訳数を通知することで承認者に都度知らせることで進捗等が確認できる。
- 直近3ヶ月の仕訳数と比較して、現在の月次決算進捗を簡易的に計算して通知することで、おおよその進捗状況がリアルタイムで分かる。

フローチャート図
freee API flowchart_8.jpg

⑨未承認請求書数の通知

フロー
1. 【API】タイムトリガーが発動して、請求書APIから請求書を取得、未承認請求書数を取得する。
2. 【GAS】1で取得した未承認請求書数をチャットで通知する。

結果
- 請求書承認機能を使用しており、決められた時間に未承認請求書数を通知することで承認忘れを防ぎ、申請者から承認者への無駄なコミュニケーションが削減できる。

フローチャート図
freee API flowchart_9.jpg

⑩未処理振込申請数の通知

フロー
1. 【API】タイムトリガーが発動して、ファイルボックスAPIからファイルボックス内情報を取得、未解決コメント数を取得する。
2. 【GAS】1で取得した未解決仕訳数をチャットで通知する。

結果
- 月末振込以外の振込処理にファイルボックスを使用しており、振込依頼を行ったことをファイルへの解決/未解決コメントで管理することで、承認忘れ、申請者から承認者への無駄なコミュニケーションが削減できる。

フローチャート図
freee API flowchart_10.jpg

⑪出退勤記録を人事労務freeeに書き込む

前提
出退勤記録をつけるという作業を従業員に行わせるのは時間の無駄になるので、フラッパーゲートを各階オフィス出入口に設置し、フラッパーゲートを通過することで出退勤時間を取得しています。
フラッパーゲートとは、駅の改札のような機械です。

フロー
1. 【GAS】勤怠システム(フラッパーゲート)から勤怠集計表SSにデータを書き込む。
2. 【API】人事労務freeeから最新の従業員情報を取得し、勤怠集計表SSに書き込む。
3. 【API】勤怠集計表SSのデータを人別日別で人事労務freeeに書き込む。
4. 【GAS】3と並行して会社独自の勤怠チェックを行い、結果を勤怠集計表SSに書き込む。

結果
- 人事労務freeeに取り込むときに、会社の就業規則に合わせた独自のチェック機能を持たせることで、労務での確認に役立つ。
- 裁量労働制の勤怠はインポートでは対応出来ないが、APIを使用することで対応が出来ている。
- インポートでは月次サマリーでしか勤怠情報を登録できないが、APIを使用することで日次単位で登録が可能。
- 登録された勤怠のうち、休暇と出勤が同居していたり休憩時間が少ないor多いものがSSに一覧化されることで見やすい。条件付き書式などでより見やすくなる。

フローチャート図
freee API flowchart_11.jpg

⑫人事労務freeeに登録された勤怠情報を勤怠集計表SSに書き出す

フロー
1. 【API】人事労務freeeから勤怠集計表に人別日別で勤怠情報を書き出す。
2. 【GAS】事前に設定した内容でチェックを行い、問題となる箇所があれば表示させる。
3. 【作業】2で問題があれば勤怠集計表SS上で修正する。
4. 【API】3で修正した部分を人事労務freeeに書き込む。

結果
- 人事労務freee上で全員の勤怠状況を確認する場合、1人ずつ1日ずつ詳細を開かなければいけないが、勤怠集計表SSに書き出すことでまとめて確認することが可能。
- SSでの作業であるため独自のチェック機能を実装しやすく、労務でのチェック作業が楽になる。
- 出勤時間、退勤時間、休憩開始時間、休憩終了時間、休暇とそれぞれ個別に修正/反映できるため、修正作業が簡単にできる。

フローチャート図
freee API flowchart_12.jpg

⑬部門別人件費表SSの作成

フロー
1. 【API】人事労務freeeから部門別人件費表SSに指定した時点での、部門別所属者一覧と給与情報を書き込む。

結果
- 経理で行う人件費振替などに利用できる。

フローチャート図
freee API flowchart_13.jpg

さいごに

実際に現場で動いているものをまとめて記載しました。少しでも参考になっていれば嬉しいです。
freee APIを使っていろいろ作ってきましたが、1人で勝手に思い浮かんだものは少ないです。
こんなことができるよ~こんなものがあったらどう?とか他部署の人と世間話レベルで話すことからアイデアが生まれてきました。

最近はSSからkintoneに切り替え、全社に範囲を広げて開発しています。オリジナルERPを作っているので仕様決めからなにから大変ですが、パッケージソフトにはない快適さがユーザー部署にとって最高ですので価値はあります。
kintoneも落ち着いたらまた投稿します。

フローチャートの全体図

各項目に載せたフローチャート図をまとめたものです。
freee API flowchart.jpg

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

会計/人事労務freee APIとGoogleとチャットツールとkintoneを使った業務効率化/自動化の実践例

はじめに

「Qiita夏祭り2020 オンライン」のfreeeテーマとして投稿しました。今までfreee APIを使って実現してきたことをまとめて書いていきます。
コーポレートエンジニアとして各部署の要望を聞きながら設計しているので、いち部署の中のちょっとした業務範囲を便利にするだけでなく、他部署との連携部分もありますので、少しでも参考になったらいいなと思います。
今回はコードは載せていません。APIを使ってどんなことを実際にしているかの共有です。コードについては、近いうちにまとめてGithubにでもまとめようと思います。

会社としての環境

  • 当社の顧客や取引先は製造業や物流業が主で、紙取引が当たり前です。
  • 日本の会社ですがエンジニアリンググループの大半は外国人ということもあり、日本ならではの経費精算を自分で行うことが困難/非常に時間が掛かる場合が多いです。
  • ロボットなど実物を在庫として持つ業態のため、入荷→在庫→出荷というステータス管理が常に発生します。

どのfreee APIを使っているか

会計freee

  • 振替伝票API
  • 取引登録API
  • 仕訳帳API
  • 請求書API
  • ファイルボックスAPI
  • 勘定科目API
  • 品目API
  • 取引先API
  • 部門API
  • セグメントタグAPI
  • メモタグAPI

人事労務freee

  • 勤怠API
  • 従業員API
  • 給与明細API

どんなことをしているか

用意されたUIでしか操作が出来ない、専任者しか分からない、聞かないと情報が分からない、調べないと情報が分からないなどをなくすために自動化/効率化をしています。
他には、freeeと連携していない勤怠システムをどうに人事労務freeeとうまく使うかを実践しています。
freee自動化イラスト.jpg

具体的にどんなことが出来て、どう便利なのか

フローチャート図で表したうち、freee APIに関する部分の【人が作業すること】【その結果】について書いていきます。
①~⑨が会計freee API、⑩~⑫が人事労務freee APIです。
【SS】は【Google スプレッドシート】を指しています。【GAS】【API】【作業】【JS】とそれぞれ色を付けています。

①経費精算情報をもとに振替伝票登録

前提
従業員の時間を経費精算で浪費しないように、手慣れた人間が一括して経費精算を行っています。
経費入力は経理ではないパートタイムの方が担当しており、経費精算SSの各人シートに入力しています。
経費項目(タクシー、会議代など)を予め設定しておき、選択した経費項目に応じて勘定科目や各種タグがまとめて設定されるようになっています。

フロー
1. 【API】経費精算SSを開いたことをトリガーに、勘定科目や各種タグをfreee APIで取得しSSに書き込む。
2. 【作業】経費精算SSには人別に経費精算表シートがあり、1で取得した情報をもとにプルダウンで会計タグを選択する。
3. 【GAS】2の作業時に経費項目から入力候補をサジェストする。
4. 【API】すべての情報が選択できたら<一括取込>を実行して、人ごとにfreee APIで振替伝票登録を行う。

結果
- 最新の勘定科目と各種タグをプルダウンで選択できるので、手入力/入力ミス削減。
- SSで作業することでコピペ等、繰り返し作業が簡単にできる。
- 設定した勘定科目と各種タグ以外にも、一括取込時に社員番号で自動的に社員名の取引先タグが入るようにする等し固定動作を自動化。

100人以上いるので、手作業インポートでは回数的にインポートする作業だけですら大変ですし、最新の勘定科目や各種タグを毎回手で取得するのも面倒です。無理にシステム内で作業するよりもSSやExcelの方が良かったりします。

フローチャート図
freee API flowchart_1.jpg

②発注納品情報をもとに支払請求書仕訳

フロー
1. 【API】<タグ取得ボタン>を押して、勘定科目や各種タグをfreee APIで取得しkintoneに書き込む。
2. 【作業】発注一覧アプリから、取引登録したいものにマークを付ける。
3. 【作業】1で取得した情報をもとにプルダウンで会計タグを選択する。
4. 【API】<一括取込>を実行して請求書ごとにfreee APIで取引登録を行う。

結果
- 購買部署が発注した情報をもとに作成するため、商品情報の手入力や金額の手入力がなく、簡単&ミス削減。
- 請求書が届く前に仕訳を作成することが出来て、前倒し作業が可能。(運用ルール決めが重要ですが…)
- freee取引登録なので、支払時はfreee支払一覧からFBデータを出力して総振処理ができる。対応している銀行であれば、freeeから直接振り込みの手続きも可能。

フローチャート図
freee API flowchart_2.jpg

③在庫システムの情報と発注情報を使って製品出荷仕訳を作成

前提
発注/入荷情報を購買担当が在庫システムに登録し、出荷担当が出荷情報を在庫システムに登録するプロセスになっています。
管理物品にはQRコードを貼付しており、そのQRコードを使って検索できるようになっているため、検索時のミス/検索の手間を削減しています。

フロー
1. 【API】<タグ取得ボタン>を押して、勘定科目や各種タグをfreee APIで取得しkintoneに書き込む。
2. 【作業】出荷アプリに登録された出荷情報から、振替伝票登録したいものにマークを付ける。
3. 【作業】1で取得した情報をもとにプルダウンで会計タグを選択する。
4. 【API】<一括取込>を実行して製品ごとにfreee APIで振替伝票登録を行う。

結果
- 在庫システムによる出荷情報と発注時の情報を連携させ処理をするので、発注時の情報をそのまま使えるため入力作業削減。
- 出荷情報と連携しているので、経理が出荷実態を把握するために現場から情報収集する工数を削減し、かつリアルタイムでの処理が可能になる。

フローチャート図
freee API flowchart_3.jpg

④クレジットカード明細仕訳のサジェスト&振替伝票登録

フロー
1. 【API】クレカSSを開いたことをトリガーに、勘定科目や各種タグをfreee APIで取得しSSに書き込む。
2. 【API】期間を指定して仕訳帳をAPIで取得しSSに書き込む。
3. 【作業】クレカ明細をクレカSSに貼り付け、1で取得した情報をもとにプルダウンで会計タグを選択する。
4. 【GAS】3の作業時に仕訳帳から入力候補をサジェストする。(クレカ明細摘要から勘定科目や各種タグ)
5. 【API】<取込>を実行して製品ごとにfreee APIで振替伝票登録を行う。

結果
- オリジナルの勘定科目/各種タグサジェスト機能を用意することで自社に合わせた入力補助を行える。
- ETCやタクシーなど同じ処理をする場合にコピペで簡単にできる。
- 指定した明細摘要で直近10件選択した勘定科目/各種タグを表示させるなど、仕訳帳を検索するより使い勝手が良い。

フローチャート図
freee API flowchart_4.jpg

⑤仕訳帳APIを使った月次推移表BS/PL/CR作成

フロー
1. 【API】仕訳帳APIで月次推移表SSに仕訳帳を書き込む。
2. 【API】勘定科目APIで月次推移表SSに勘定科目とカテゴリーを書き込む。
3. 【GAS】BS/PL/CRそれぞれのシートに勘定科目カテゴリーと勘定科目を書き込み、仕訳帳からデータを取得し推移表形式にする。

結果
- 推移表APIはないが、勘定科目APIと仕訳帳APIを使うことで推移表が簡単に作成できる。
- 勘定科目を取得する際にカテゴリーを取得することで各カテゴリーごとの集計ができる。

フローチャート図
freee API flowchart_5.jpg

⑥商談情報を使って請求書作成&営業から経理への請求依頼自動化

フロー
1. 【作業】営業が商談アプリで商談ステータスを請求依頼に変更する。
2. 【API】商談情報をベースにfreee APIで請求書を作成する。
3. 【JS】freee請求書が作成されたことを経理に通知する。
4. 【作業】請求書をfreeeで編集。

結果
- 営業が日常的に使用している商談アプリの情報を使うため、請求処理のために営業が追加で情報を入力する手間がない。
- 営業情報をそのまま使うため経理での手入力/ミスが削減でき、リアルタイムでの処理が可能になる。
- 請求依頼漏れを営業側でステータスとして確認することが出来る。

フローチャート図
freee API flowchart_6.jpg

⑦毎日10時台に発行済請求書の一覧作成&入金/未入金一覧を営業にチャット通知&商談アプリに書き込む

フロー
1. 【作業】freeeで入金消込を行う。
2. 【API】10時台タイムトリガーが発動して、請求書情報をfreee APIで取得する。
3. 【GAS】2で取得した情報のうち、発行済請求書情報をを請求ステータスSSに書き込む。
4. 【GAS】2で取得した情報から当日入金された情報と未入金情報を一覧化し、請求ステータスSSのURLを添えてチャットで営業に通知する。
5. 【JS】2で取得した情報から入金/未入金状況を抽出し、商談アプリに書き込む。

結果
- 毎日の入金状況が送付されること、過去分もSSで見れることから、営業から経理への入金状況確認依頼が0になった。
- 発行された請求書の内容を営業が確認できるため、過去分の請求情報共有依頼が0になった。
- 商談アプリのステータスが自動で更新されるため手作業がなくなり、ステータス更新忘れやミスがなくなった。

フローチャート図
freee API flowchart_7.jpg

⑧未承認仕訳数の通知

フロー
1. 【API】タイムトリガーが発動して、仕訳帳APIから期間指定で仕訳帳を取得、未承認仕訳数を取得する。
2. 【GAS】1で取得した未承認仕訳数などをチャットで通知する。

結果
- 仕訳承認機能を利用しており、未承認仕訳数を通知することで承認者に都度知らせることで進捗等が確認できる。
- 直近3ヶ月の仕訳数と比較して、現在の月次決算進捗を簡易的に計算して通知することで、おおよその進捗状況がリアルタイムで分かる。

フローチャート図
freee API flowchart_8.jpg

⑨未承認請求書数の通知

フロー
1. 【API】タイムトリガーが発動して、請求書APIから請求書を取得、未承認請求書数を取得する。
2. 【GAS】1で取得した未承認請求書数をチャットで通知する。

結果
- 請求書承認機能を使用しており、決められた時間に未承認請求書数を通知することで承認忘れを防ぎ、申請者から承認者への無駄なコミュニケーションが削減できる。

フローチャート図
freee API flowchart_9.jpg

⑩未処理振込申請数の通知

フロー
1. 【API】タイムトリガーが発動して、ファイルボックスAPIからファイルボックス内情報を取得、未解決コメント数を取得する。
2. 【GAS】1で取得した未解決仕訳数をチャットで通知する。

結果
- 月末振込以外の振込処理にファイルボックスを使用しており、振込依頼を行ったことをファイルへの解決/未解決コメントで管理することで、承認忘れ、申請者から承認者への無駄なコミュニケーションが削減できる。

フローチャート図
freee API flowchart_10.jpg

⑪出退勤記録を人事労務freeeに書き込む

前提
出退勤記録をつけるという作業を従業員に行わせるのは時間の無駄になるので、フラッパーゲートを各階オフィス出入口に設置し、フラッパーゲートを通過することで出退勤時間を取得しています。
フラッパーゲートとは、駅の改札のような機械です。

フロー
1. 【GAS】勤怠システム(フラッパーゲート)から勤怠集計表SSにデータを書き込む。
2. 【API】人事労務freeeから最新の従業員情報を取得し、勤怠集計表SSに書き込む。
3. 【API】勤怠集計表SSのデータを人別日別で人事労務freeeに書き込む。
4. 【GAS】3と並行して会社独自の勤怠チェックを行い、結果を勤怠集計表SSに書き込む。

結果
- 人事労務freeeに取り込むときに、会社の就業規則に合わせた独自のチェック機能を持たせることで、労務での確認に役立つ。
- 裁量労働制の勤怠はインポートでは対応出来ないが、APIを使用することで対応が出来ている。
- インポートでは月次サマリーでしか勤怠情報を登録できないが、APIを使用することで日次単位で登録が可能。
- 登録された勤怠のうち、休暇と出勤が同居していたり休憩時間が少ないor多いものがSSに一覧化されることで見やすい。条件付き書式などでより見やすくなる。

フローチャート図
freee API flowchart_11.jpg

⑫人事労務freeeに登録された勤怠情報を勤怠集計表SSに書き出す

フロー
1. 【API】人事労務freeeから勤怠集計表に人別日別で勤怠情報を書き出す。
2. 【GAS】事前に設定した内容でチェックを行い、問題となる箇所があれば表示させる。
3. 【作業】2で問題があれば勤怠集計表SS上で修正する。
4. 【API】3で修正した部分を人事労務freeeに書き込む。

結果
- 人事労務freee上で全員の勤怠状況を確認する場合、1人ずつ1日ずつ詳細を開かなければいけないが、勤怠集計表SSに書き出すことでまとめて確認することが可能。
- SSでの作業であるため独自のチェック機能を実装しやすく、労務でのチェック作業が楽になる。
- 出勤時間、退勤時間、休憩開始時間、休憩終了時間、休暇とそれぞれ個別に修正/反映できるため、修正作業が簡単にできる。

フローチャート図
freee API flowchart_12.jpg

⑬部門別人件費表SSの作成

フロー
1. 【API】人事労務freeeから部門別人件費表SSに指定した時点での、部門別所属者一覧と給与情報を書き込む。

結果
- 経理で行う人件費振替などに利用できる。

フローチャート図
freee API flowchart_13.jpg

さいごに

実際に現場で動いているものをまとめて記載しました。少しでも参考になっていれば嬉しいです。
freee APIを使っていろいろ作ってきましたが、1人で勝手に思い浮かんだものは少ないです。
こんなことができるよ~こんなものがあったらどう?とか他部署の人と世間話レベルで話すことからアイデアが生まれてきました。

最近はSSからkintoneに切り替え、全社に範囲を広げて開発しています。オリジナルERPを作っているので仕様決めからなにから大変ですが、パッケージソフトにはない快適さがユーザー部署にとって最高ですので価値はあります。
kintoneも落ち着いたらまた投稿します。

フローチャートの全体図

各項目に載せたフローチャート図をまとめたものです。
freee API flowchart.jpg

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

JavaScript 配列操作

配列操作

// ! 配列の各要素を順番に取り出すには、以下のように記述します。
const fruits = ["apple", "orange", "banana"];

// * for文
for(let i = 0; i<fruits.length; i++){
    console.log(fruits[i])
}

// * forEach
fruits.forEach(function(fruit, index){
    console.log(index, fruit)
})

// ! 配列の要素を追加する
var array = ['b', 'c', 'd'];

// * 配列の最初にひとつ以上の要素を追加
array.unshift('a');
console.log(array)

// * 配列の末尾にひとつ以上の要素を追加
array.push('e')
console.log(array)

// *既存の要素を取り除いたり、置き換えたり、新しい要素を追加したりすることで、配列の内容を変更
array.splice(1, 0, 'A');
console.log(array)

// ! 配列の要素を削除する
var array = ['a', 'b', 'c'];

// * 最初の要素を取り除く
array.shift();
console.log(array)

// *配列から最後の要素を取り除きその要素を返す
array.pop();
console.log(array)

// * splice
array.splice(1, 1);
console.log(array)

// *配列の全要素を削除する
array.length = 0;
console.log(array)

// ! 配列の要素を置換する
array.splice(1, 1, 'Z');
console.log(array)

// ! 配列の要素を結合
var a = [1, 2, 3, 4, 5];
a.join(); // '1,2,3'
a.join(', '); // '1, 2, 3'
a.join(' + '); // '1 + 2 + 3'
a.join('') // '123'

// ! 配列の検索
var array = [2, 9, 9];
array.indexOf(2); //0
array.indexOf(7); //-1 見つからない
array.indexOf(9);     // 1 見つかった時点で検索を終了してしまう

// ! 配列を連結する
const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];

// * concat 返り血に新しい配列を
console.log(array1.concat(array2)); // ["a", "b", "c", "d", "e", "f"]

// * 複数の配列を連結
const num1 = [1, 2, 3];
const num2 = [4, 5, 6];
const num3 = [7, 8, 9];
const numbers = num1.concat(num2, num3);
console.log(numbers); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

// ! 配列を並び替える
const months = ['March', 'Jan', 'Feb', 'Dec'];
// * sort 配列は各要素の文字列比較に基づき辞書順にソート
months.sort();
console.log(months); // ["Dec", "Feb", "Jan", "March"]

const array1 = [1, 30, 4, 21, 100000];
array1.sort();
console.log(array1); // [1, 100000, 21, 30, 4] 頭文字の数字の若いものから並んでしまう

// *数値を大小比較
const array1 = [1, 30, 4, 21, 100000];
array1.sort((a, b) => (a - b));
console.log(array1)// [1, 4, 21, 30, 100000]

// *元の配列を破壊したくない場合は、配列の複製を行ってから並び替え
const arr1 = ['a', 'b', 1, 3, 'c', 4, 2];
const arr2 = arr1.slice().sort();

console.log(arr1); // ["a", "b", 1, 3, "c", 4, 2];
console.log(arr2); // [1, 2, 3, 4, "a", "b", "c"]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nuxt plugins では process.client は undefined なので注意(axios のクライアント側エラー処理を書く時のハマりどころ)

これは何

axios のクライアント側エラー処理を書く時にはまったところについて共有のための記事です。
process.clientは使えないので、そもそもクライアント用とサーバ用でファイルを分けようという話です。

バージョン情報

{
    "nuxt": "^2.11.0",
    "@nuxt/typescript-build": "^0.5.2",
    "@nuxt/typescript-runtime": "^0.3.3",
    "@nuxtjs/axios": "^5.9.5"
}

トラブルについて

クライアントサイドでその処理が実行されるか、というのを制御できるprocess.clientプロパティがnuxtアプリケーションではよく使われます。

plugins読み込みの時にはこのプロパティが設定されていないのでundefinedになってしまいます。

plugins/plugin.ts
if (process.client) {
  // 何か処理
}

と書くと、一見動くように見えるのですが、実際にはprocess.clientundefinedになるのでifの中の処理は常に実行されませんでした。

具体的に起こった問題は以下のようなもの

  • サーバ側処理もクライアント側処理も同じプラグインファイルに書いていた
  • クライアント側の処理をして問題なければreturnして処理おわり、その後にサーバサイドの処理をして問題あればnuxt errorを吐くみたいな処理を書いていた
  • そもそもifに入っていないのでreturnで処理が止まらず、クライアント側のクライアントエラー(4xx)でもnuxt errorを吐いて死んでしまう

解決策

clientとserverでそれぞれ実行する処理をファイル名を分けて、それぞれnuxt設定ファイルで読み込みます。

nuxt.config.ts
  plugins: [
    '@/plugins/axios.client.ts',
    '@/plugins/axios.server.ts',
  ],

他にもmodeを書く方法もあるようですが、ファイル名だけをみた時にどちらで実行されるのか?というのがはっきり分かるので、ファイル名で分けたほうが個人的にはいいかなと思います。

補足

process.clientundefined になってしまうのですが、process オブジェクト自体は定義されているようでした。
これはnuxtがnodejsのインターフェースの拡張しているもののようです。

image.png

plugin のなかで console.log(process);すると以下のようになりました。

image.png

.browser を使えばフロントかサーバかの判定はできるようですが、すでにnuxtでは非推奨になっていました。

参考

https://ja.nuxtjs.org/api/configuration-plugins
https://ja.nuxtjs.org/guide/plugins/
https://stackoverflow.com/questions/58146662/process-server-is-undefined-in-nuxt-js-modules

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

Server-Side Renderingって何なのさ ~あるいは雑な用語定義に対するお気持ち表明~

最初はプンスコしてたんですが、週末を挟んで寝かせていたら、割とどうでも良くなってきたので、もうそこまで怒ってないです。
ただ、どうしてこういう用語が誕生するのかには興味があるので、調査結果とお気持ちをしたためておきます。

引っかかるポイント

先に疑問に思うポイントをまとめておきます。

  • あるレイヤーの最終出力を"レンダリング"と呼ぶのは良しとしても、すぐ隣のレイヤーでブラウザが HTML のレンダリングを頑張ってるのに、何で被せるようなネーミングにしたのか
  • 従来の CGI や PHP、JSP、ASP.NET でやってきたことと何が違うのか
    • 違うとしたら JavaScript フレームワークから出力することを特別視する理由は何か

クラウドゲーミングなどにおけるレンダリングとも混同しがちなんですが、それは私が CG・ゲーム寄りの立ち位置にいるせいだと思うので、とりあえず気にしないことにします。

きっかけ

きっかけはこのツイートでした。

なるほどなーと流し読みしてたんですが、脳内で「サーバー上でラスター画像にしてそれをストリーミングする技術」という解釈が生まれた瞬間にギャップが生じてしまい、

このツイートが生まれました。直後はそうでもなかったんですが、一晩寝かせたらバズってしまったようで、様々な方からご意見・知見を頂戴しました。ありがとうございました。
RT よりは Fav の方が多かったので、何となく共感してくださる方も多かったのかなぁ、という印象です。

render の意味の多様性

私は CG・ゲーム 業界寄りの人間なので、レンダリングと言われると「グラフィックの描画」がどうしても最初に出てきます。それが固定観念に囚われていると指摘されたら、否定できません。
しかし「サウンドレンダリング」や「テキストレンダリング」など、様々なレイヤーにおけるレンダリングが存在していることは理解しているつもりです。

https://talking-english.net/render/

  • 〜の状態にする
  • 製造する
  • レンダリング
  • 与える、提供する
  • 壁などを塗る

今回調べて知りましたが、他にも「脂抜きをする」「肉骨粉にする」なんて意味もあるそうです。

なので、多くのフレームワークで HTML 出力の関数が render になっていること自体は、ごく自然で適切なネーミングなのだろうと理解しました。

HTML レンダリング is 何

とはいえ、ちょっと前まで Web の世界で「レンダリング」と言ったら、ブラウザが HTML を画面に描画することを指していたはずです。

https://qiita.com/umashiba/items/8cb47825624c5cb043d6

この記事にまとめられているように、

  • KHTML
  • WebKit
  • Blink
  • Gecko
  • Servo
  • Trident
  • EdgeHTML
  • Presto

こういったものがレンダリングエンジンである、と認識していました。

しかし、Server-Side Rendering に関する記事を見ていると、HTML を出力する JavaScript フレームワークを「レンダリングエンジン」と呼称している方が見られました。

https://www.publickey1.jp/blog/17/server_side_renderingserver_side_rendering_ng-japan_2017.html

だいたいブラウザでのレンダリングエンジンはAngularやReactなどJavaScriptで動いていることが多いので、それをサーバで動かそうとするとNode.jsを使うことになります。

ブラウザが持つレンダリングエンジンが JavaScript で実装されている事例は聞いたことがないので、恐らく「HTML を出力(rendering)するフレームワークをレンダリングエンジンと呼んでいる」と解釈しました。

https://www.publickey1.jp/blog/20/jamjavascriptapimarkupcms_pr.html

JAMスタックでは、サーバサイドでのHTMLレンダリングはデプロイ時などにあらかじめ済ませることで、可能な限り静的HTMLを生成しておくことが大事な要素となっています。

ここでもしれっと「HTML レンダリング」と述べられていますが、文脈からして HTML の生成のことであろうと解釈できます。

文脈依存で意味が変わる用語は多数ありますし、他の業界で使われている言葉との被りまで配慮して欲しいとは言いません。でも、同じ分野の近しいレイヤーとの用語被りは避けようよというのが率直なお気持ちです。

そもそも JavaScript フレームワークのお仕事は HTML を render するのに留まらない、ViewModel 的な立ち位置のものであると認識しています。テンプレートエンジンじゃダメなんです?
(追記)何か別物っぽそうだったのでこの部分は取り下げます。

CGI とかと何が違うのさ問題

これは色々調べているうちに分かってきました。

広義の SSR には、恐らく CGI や PHP などによる HTML 生成も含まれると思われます。
しかし、JavaScript フレームワーク界隈における狭義の SSR では アイソモーフィック JavaScript という概念が重要そうです。

https://medium.com/@sundaycrafts/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E4%BD%93%E9%A8%93%E3%82%92%E5%90%91%E4%B8%8A%E3%81%95%E3%81%9B%E3%82%8B%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%B5%E3%82%A4%E3%83%89%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0javascript-%E6%AD%B4%E5%8F%B2%E3%81%A8%E5%88%A9%E7%82%B9-df68cd7cd991

アイソモーフィック(Isomorphic) とは、「同型の」という意味で、同じJavaScriptのコードがサーバー(Node.js)とクライアント(ブラウザ)両方で実行されることを意味します。

これはとても分かりやすいですね。サーバーとクライアントで同じコードが動作するのであれば、HTML 生成を状況に応じて分担するのも容易にできそうな気がします。

自分なりにまとめてみます。

  • クライアント側に偏りがちな HTML 生成をサーバー側にも分担するという目的のもとサーバーサイドレンダリングという言葉が掲げられた
  • それを効率的に行うために JavaScript フレームワークでサポート、ないし実践されるようになった手段がアイソモーフィック JavaScript
  • JavaScript 利用者の間ではサーバーサイドレンダリングを目的とした場合、手段としてアイソモーフィック JavaScript を用いることが暗黙的な前提として存在する

これなら JavaScript からの出力に話が限定される理由も納得いきます。

でもそれなら、もっと前提が分かりやすい名前を掲げて欲しいというのも同時に湧き上がってくるお気持ちです。他のレイヤーを担う人たちと話す際にも混乱しそうなものですが。

恐らくポイントは、サーバーとクライアントで同一言語が使えることにあるのだと思います。クライアント側が JavaScript で動作する関係上、サーバ側でも JavaScript でアイソモーフィックに書かれることで、レンダリングの分担を行いやすい、みたいな説明ならストンと腑に落ちます。

それを表すには、サーバーサイドレンダリングという言葉はスコープが広すぎるのでしょうね。

まとめ

  • HTML 出力を render と呼ぶのはおかしくない
    • でも「レンダリングエンジン」とか言い出すとブラウザのお仕事と被るからやめてほしい
  • 狭義の SSR とは、同一言語でサーバー・クライアント間で処理を分担できるのがポイントっぽい
    • でも広義の意味だとスコープが広すぎるので、ちゃんと前提を含んだ言葉をバズらせてほしい

以上です。単なるお気持ち表明のつもりで書き始めた文章ですが、アイソモーフィックという概念に触れられたのは良かったです。

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

LIFFで繰り返しログイン画面が表示されるバグに出会った時にやったこと

沼って数日間吹っ飛ばしたのでメモ。未来の誰かの時間を削減できると嬉しい。

事象

個人情報が含まれるので動画は投稿できないが、次のようなLINEのログイン画面が半無限に表示されるというバグ。("ログイン"を押してもすぐに再度 access.line.me にリダイレクトされる)("半"無限というのは何回もリトライするとタイミング次第でうまく行く時があるから)

再現条件

なんらか liff.init の時のURLとエンドポイントURL(もしくはエンドポイントURL + アクセス時に後ろについていたパス?)が違う場合に発現する模様です。(正直LIFFのSDK読み込んだわけではないので断言はできない)(難読化されたものを読みたくない & 読めない)

私の場合はランディング後のページ切り替えが速過ぎたせいで再現したみたいでした。(ちなみになぜかAndroidでしか再現しませんでした)
私が今開発しているアプリでは / にアクセスした後、ログイン状態によって /signin のようなパスに切り替えていました。

解決策

解決策のヒントはここに書いてありました。ありがたや
https://www.line-community.me/question/5e956551851f7402cd9699a2/line-app%E3%81%8B%E3%82%89liff-line-me%E3%82%92%E9%96%8B%E3%81%84%E3%81%9F%E5%BE%8C%E3%81%AB%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88%E3%81%95%E3%82%8C%E3%82%8B

LIFFとして登録しているエンドポイントのURLと、
クエリパラメータが付いたURLを比較している行を発見しました。
相違している場合、locationを書き換えている模様です。
(クエリパラメータを付与している時点で100%相違します)

今回の私の事象に対しては、パスを書き換えずにコンポーネントを出し分けるようにしまして、そうすると無事再現しなくなりました。
上記が原因である場合、Androidだけでしか再現しないのが腑に落ちないのでもしかしたら別の原因かもしれないんですが、似たようなバグが出ている場合にお役に立てれば幸いです。

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

JavaScript + CSS: ヘッダを上部に固定してカラムの中身はスクロールさせる

お題は、つぎのサンプル001のようなレイアウトです。ヘッダを上部に固定し、左カラムのリストはスクロールします。

サンプル001■JavaScript + CSS: Fixed top header and scrolling column

See the Pen JavaScript + CSS: Fixed top header and scrolling column by Fumio Nonaka (@FumioNonaka) on CodePen.

たとえば、Bootstrapの「Documentation」もこのスタイルを採り入れています。

図001■BootstrapのDocumentationページ

2007001_001.png

基本となるCSSの設定はごく簡単です。けれど、ユーザーがウィンドウサイズを変えたときなど、設定は動的に更新しなければなりません。この部分は、JavaScriptコードで処理します。

ページの基本的な構成

サンプルの<body>要素の中身はつぎのように組み立てました。ページを構成する要素は大きく3つ、ヘッダと左カラム、そしてメインコンテンツです(図002)。CSSのclass属性の定めは省いています。確かめたい方は、前掲サンプル001をご覧ください(Bootstrap 4.5使用)。

<body>要素
<header id="header">
    <h1>Header</h1>
</header>
<div>
    <div id="left-column">
        <h3>Left column</h3>
        <ul id="list">
            <li>...[中略]...</li>
        </ul>
    </div>
    <main>
        <h2>Main contents</h2>
        <p>...[中略]...</p>
    </main>
</div>

図002■ページはヘッダと左カラムにメインコンテンツで組み立てられる

2007001_002.png

ヘッダをページ上部に固定する

まずはサイトでよく見かける、ヘッダあるいはトップナビゲーションをページ上部に固定するCSSの設定です。

位置はpositionプロパティにfixedを与えれば固定できます。具体的な置き場所は、別にプロパティで定めなければなりません。上部ならtop: 0です。ただし、<body>要素の領域に含まれなくなることにお気をつけください。そのままでは、ページの上部がかぶって隠れてしまうのです(図003)。

<style>要素
#header {
    position: fixed;
    top: 0;
}

図003■ページ上部にヘッダがかぶって見えない

2007001_003.png

<body>要素のpadding-topは、ヘッダの高さ分下げなければなりません。サンプルでは、ヘッダの高さを調べると72pxでした。

body {
    padding-top: 72px;
}

これで、ヘッダはページ上部に固定されます。

カラムの中身をスクロールさせる

要素の中のテキストをスクロールさせるには、その高さ(height)が具体的に決められていなければなりません。そのうえで、overflowプロパティはscrollにします。高さに収まらないテキストは、スクロールして表示する設定です(図004)。

#left-column {
    height: 500px;
    overflow: scroll;
}

図004■左カラムの高さに収まらないテキストはスクロールで表示される

2007001_004.png

もっとも、高さを決め打ちしたのでは、役に立ちません。ユーザーが開いたウィンドウサイズに合わせて、カラムの高さをJavaScriptで変えましょう。

<styel>要素
#left-column {
    /* height: 500px; */

}

カラムの高さを動的に決める

ブラウザウィンドウの内側(ビューポート)の高さを返すのはwindow.innerHeightプロパティです。また、要素の高さはelement.clientHeightで得られます。

したがって、ビューポートの高さからヘッダの高さを差し引いて、左カラムの高さとすればよさそうです。ただし、element.clientHeightは、読み取り専用プロパティであることにご注意ください。そのため、高さの設定には、HTMLElement.styleを用います。プロパティ(height)の値は、要素のインラインstyle属性と同じ文字列です。単位(px)も忘れないようにしてください。

<script>要素
document.addEventListener('DOMContentLoaded', () => {
    const header = document.getElementById('header');
    const leftColumn = document.getElementById('left-column');
    const setLayout = () =>
    leftColumn.style.height = (window.innerHeight - header.clientHeight) + 'px';
    setLayout();
    window.addEventListener('resize', setLayout);
}

スクリプトを改善する

ヘッダの高さはJavaScriptコードでわかったのですから、<body>要素のpadding-topもこの処理の中で定めましょう。ヘッダやナビゲーションバーは、その中身によってはウィンドウ幅に応じて変わることもあるからです。

<style>要素
body {
    /* padding-top: 72px; */

}

HTMLElement.styleに定めるのはJavaScriptのプロパティです。つまり、ケバブケース(padding-top)は使えないことにお気をつけください。キャメルケース(paddingTop)に改めるのが決まりです。

<script>要素
document.addEventListener('DOMContentLoaded', () => {
    const body = document.querySelector('body');
    const setLayout = () => {
        const headerHeight = header.clientHeight;
        body.style.paddingTop = headerHeight + 'px';
        leftColumn.style.height = (window.innerHeight - headerHeight) + 'px';
    }

}

スクロール操作を試していると、ちょっとした不具合が見つかります。メインの領域のテキストが多い場合、下までスクロールすると左カラムも上に動いてしまうのです(図005)。

図005■メイン領域とともに左カラムが上に動いてしまう

2007001_005.png

動かなくするには、<body>要素と同じく左カラムも固定しなければなりません。すると、カラムが<body>の領域から外れて、メイン(<main>要素)のテキストが左に潜ってしまいます。左余白の調整も必要です(marginを使ったのはCSSですでにpaddingを定めていたからです)。

<script>要素
document.addEventListener('DOMContentLoaded', () => {

    const main = document.querySelector('main');

    const setLayout = () => {

        leftColumn.style.position = 'fixed';
        leftColumn.style.top = headerHeight;
        main.style.marginLeft = leftColumn.clientWidth + 'px';
    }

}

CSSの定めと書き上げたJavaScriptの記述は、つぎのコード001のとおりです。細かい内容や動きは、冒頭のサンプル001でお確かめください。

コード001■<header>を上部に固定して<div>の中身はスクロールさせる

<style>要素
#header {
    position: fixed;
    top: 0;
}
#left-column {
    overflow: scroll;
}
<script>要素
document.addEventListener('DOMContentLoaded', () => {
    const body = document.querySelector('body');
    const main = document.querySelector('main');
    const header = document.getElementById('header');
    const leftColumn = document.getElementById('left-column');
    const setLayout = () => {
        const headerHeight = header.clientHeight;
        body.style.paddingTop = headerHeight + 'px';
        leftColumn.style.height = (window.innerHeight - headerHeight) + 'px';
        leftColumn.style.position = 'fixed';
        leftColumn.style.top = headerHeight;
        main.style.marginLeft = leftColumn.clientWidth + 'px';
    }
    setLayout();
    window.addEventListener('resize', setLayout);
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsとMongoDBを連携

今回はnode.jsとMongoDBの連携方法について簡単に解説していきます。

MongoDBの特徴

まず初めにMongoDBの特徴について解説します。

MongoDBの特徴として,以下のようなものが挙げられます。

(1)スキーマレスであるドキュメント指向データベースであること
・データはスキーマレスなドキュメントで格納され,任意のフィールドを好きなときに追加できる
・KVSでは苦手なValueを検索の条件としたり,ソート・集計を実現できる

(2)スケーラビリティに関する機能を標準機能として備えていること
・レプリケーション機能,オートフェールオーバー機能を備えている
・レンジパーティション機能,オートバランシング機能を備えている

(3)RDBと比較してRead/Writeの性能が高い
・トランザクション・リレーションを制限した結果,KVSに近いパフォーマンスを出せる

(4)開発のしやすさ
・どのような環境でもパフォーマンスが出るように,ネイティブソケットプロトコルを使用したドライバを主要なプログラミング言語で開発元が提供している
・RESTインターフェイスを標準で備えており,Webで広く普及しているJSONを使用してデータ送受信ができる

以上のような特徴があります。

MongoDBのインストール

それでは実際にMongoDBをインストールしていきましょう。

今回はHomebrewを使ってインストールしていきます。
Homebrewがインストールされていない場合はインストールしておきましょう。

以下のコマンドでインストールしていきます。
brew tap mongodb/brew

続いて、以下のコマンドでソフトウェアのパッケージをインストールします。
brew install mongodb-community

これでインストールは完了です。

実際に起動できるか確認してみましょう。

起動
brew services start mongodb-community

停止
brew services stop mongodb-community

これで問題なければOKです。

詳しくはこちらの公式を確認しましょう。

Node.jsとの接続

それでは実際にNode.jsからMongoDBに接続してみましょう。
まず、Sample/index.jsを作成してください。

次に、以下のコマンドでpackage.jsonを作成します。
npm init

次に、以下のコマンドでMongoDBのパッケージをインストールします。
npm install mongodb --save

これで準備はOKです。
それでは、コードを書いていきます。

index.js
var MongoClinet = require('mongodb').MongoClient;//MongoClient取得

var url = 'mongodb://localhost:27017/'; //今回はlocalhostなのでこの表記

MongoClinet.connect(url, (error, client) => {
  var db = client.db('sample');//どのDBを使用するか
  client.close();//操作を行った後に切断
});

異常が最低限必要なコードになります。
これでMongoDBの中のsampleというDBを操作する準備ができました。

しかしこのままでは接続できているのかよくわかりません。
なので実際に何か指示を書いてみましょう。

index.js
var MongoClinet = require('mongodb').MongoClient;

var url = 'mongodb://localhost:27017/';

MongoClinet.connect(url, (error, client) => {
  var db = client.db('sample');
  db.createCollection('test', (error, collection) => {
    client.close();
  });
});

上記のコードはsampleというDBに対してtestというcollectionを作成するという指示です。

それでは実行してみましょう。
node index.js

※実行前にローカル上でMongoDBが起動していることを確認しましょう
(起動していないと"TypeError: Cannot read property 'db' of undefined"というエラーが出ます)

次に実行結果の確認をします。

ターミナル上でMongoDBに接続し、確認します。
[手順]
1.mongo でMongoに接続
2.show dbsでDBのリストを確認
3.use sample でsampleというDBにアクセス
4.show collectionで中身を確認

"test"というcollectionが作成されていれば成功です。
これでNode.jsをMongoDBの接続し、collectionの作成に成功しました。

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

three.js x React x redux で3Dオセロゲームを作った

はじめに

WebGLの3Dブラウザゲームを作りたいと考えた時、どのようなフレームワークを選択すればよいのでしょうか?three.jsは一番メジャーなWebGLの3DCGライブラリですが、基本的に3DCGを描画する機能しかないので、これ単体でゲームを作るには少し苦しい局面もあります。そこで、Reactとreduxに、UIやゲームの進行管理を任せることでそこそこの規模のゲーム開発ができるのでは?と思ったのでthree.js x React x redux で3Dオセロゲームを作ってみました。

作った物

こちらでプレイできます。
https://arihide.github.io/reversi3d/
スクリーンショット 2020-07-01 0.03.51.png

ソースコードはこちら
UI・3D描画周り: https://github.com/Arihide/reversi3d
オセロ のルール・AI周り: https://github.com/Arihide/reversi.js

実装時に行ったこと

three.jsのオブジェクトもコンポーネントとして扱う

Reactでは React.Component を継承することでコンポーネントを作成しますが、Canvas内に描画される3Dオブジェクトも THREE.Mesh を継承してコンポーネントっぽくしてみました。
以下はオセロ盤のオブジェクトの実装を簡素にした例です。

import store from 'js/store'
import { Mesh } from 'three'

export default class BoardMesh extends Mesh {
    constructor(geometry, material) {
        store.subscribe(this.onChange.bind(this))
    }

    onChange() {
         // 石がひっくり返ったりしたときの処理などゲームが進んだときの処理を書く…
    }
}

流石にReactのコンポーネントほど便利なライフサイクルはないのですが、
reduxのstoreも購読することであたかもView Treeの一部に組み込まれているかのように扱えているのでとても便利になりました。
three.jsは一つのファイルにオブジェクト追加処理を書いていくとどんどん複雑になってしまうので、こういった形で分離できるのはとても良いですね。

AI・ゲームロジック部分の別モジュール化

今回実装するにあたって、UI表示とゲームロジックの部分を別リポジトリに分けました。
UI・3D描画周り: https://github.com/Arihide/reversi3d
オセロ のロジック・AI周り: https://github.com/Arihide/reversi.js
そして、以下のようにUI側のpackage.jsonにパッケージとして追加しました。

package.json
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-redux": "^7.2.0",
    "react-transition-group": "^4.4.1",
    "redux": "^4.0.5",
    "reversi.js": "git+https://github.com/Arihide/reversi.js.git",
    "three": "^0.118.3"
  }

このようにした1番の理由は、ゲームロジックの部分はUIと比較しても複雑なので、パッケージとして分離した上で単体テストなども用意して動作確認をしたかったからです。

とにかくコアな部分は分離して粒度を小さくして信頼性をあげるのが良いと思います。

Redux DevToolsの活用

ゲーム開発においてデバッグは必要不可欠です!
デバッグはゲーム中で起こりうる全ての状態に対して行われるべきですが、ゲームによっては再現が難しい状態があったりします。
例えば今回のオセロの例だと、
- パスの局面
- 全ての石が同じ色になる局面
- 引き分け
などが挙げられます。(物凄い再現が難しいというわけではないのですが)

こういった特定の状況再現のためにデバッグツールを使用することはよくありますが、特に個人開発だとツールの用意までには手が回らないことも多いです。
なので、今回はRedux DevToolsを利用をしてみました。このツールにはStoreの履歴をJson形式でインポート・エクスポートできるので容易に状態の再現ができるようになります。
こういったツールが利用できるのもまたフレームワークを利用するメリットですね!
スクリーンショット 2020-07-04 15.27.27.png

Web Workerの活用

オセロの最善手計算は物凄い数ゲーム木のノードを探索するので、とても重い処理です。これが行われる最中は3Dの描画がカクついてしまってよろしくないので、Web Workerを使用して処理を別スレッドに移すことで、カクツキを防ぎました。
Workerの呼び出し側と受けてのコードは以下です。

actions.js
export async function computeMove(state) {
    return new Promise(resolve => {
        worker.addEventListener("message", e => {
            resolve(placeDisc(state, e.data))
        }, { once: true })
        worker.postMessage(state.playedMoves)
    })
}
Worker側の実装
self.addEventListener('message', e => {
    board.initialize()

    for (let move of e.data) {
        board.pushMove(move.place)
    }

    alphaBetaPlayer.computeMove(board, bestMove => {
        self.postMessage(bestMove)
    })
})

どうしてもゲームなどは負荷対策が必要になってくるので、マルチスレッドなどの知識も必要になってきます。

まとめ

今回はthree.js x React x redux で3Dオセロゲームを作ってみました。
そのままthree.jsでオブジェクトを量産していくとゴチャっとしてしまう場合もありますが、コンポーネント化しreduxと結び付けることですっきりして結構大規模なゲームも作れるのでは?という感じもしました。

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

Vue.js 削除ボタンを作ってスロットを理解する

スロットとは

スロット(slot)は、Vue.jsにおける親コンポーネントから子コンポーネントにデータを渡す手段の一つです。
スロットという名前はハーデスリゼロなど様々な機種がある遊技マシン...のことではなく「差し込み口」という意味で使われています。

つまり、コンポーネントに外からコンテンツの差し込みを受け付けるという目的で使用されます。

スロットはその性質上、再利用の高いコンポーネントによく使用されます。再利用の高いコンポーネントは、Atomic DesignにおけるAtomのようなものが代表されます。

まずはさっそくスロットの基礎的な使い方を見ていきましょう。

スロットコンテンツ

スロットを使用コンポーネントとして、汎用的に利用する削除ボタンを実装します。
コンポーネントの内容は、Vuetifyのv-buttonをラップしただけ簡単なものです。

DeleteBtn.vue
<template>
  <v-btn
    color="error"
    dark
    min-width="300"
    rounded
  >
    <slot></slot>
  </v-btn>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  name: 'DeleteBtn',
})
</script>

見た目はこんな感じです。
小さなコンポーネントですが、このボタンはいろんなページで使用するためコンポーネント化することでデザインを共通化することができます。

スクリーンショット 20200712 15.22.48.png

<slot></slot>を置換する

子コンポーネント側のテンプレートに<slot>タグを記述すると、その場所ではスロットコンテンツが埋め込まれます。
今はボタンのテキストが表示されていない状態ですが、親からスロットコンテンツを渡して表示させてみましょう。

親からスロットコンテンツを渡すには、以下のようにスロットコンテンツをテンプレート上でタグで囲います。

App.vue
<template>
  <v-row>
    <v-col class="text-center">
      <delete-btn>削除ボタン</delete-btn>
    </v-col>
  </v-row>
</template>

<script lang="ts">
import Vue from 'vue'
import DeleteBtn from '~/components/atom/DeleteBtn.vue'
export default Vue.extend({
  components: {
    DeleteBtn
  },
})
</script>

これは次のように出力されます。

スクリーンショット 20200712 15.57.50.png

スロットには、HTML要素やコンポーネントを入れることもできます。

App.vue
<template>
  <v-row>
    <v-col class="text-center">
      <delete-btn><h1>削除ボタン</h1></delete-btn>
    </v-col>
    <v-col class="text-center">
      <delete-btn><v-icon color="black">fab fa-github</v-icon></delete-btn>
    </v-col>
  </v-row>
</template>

<script lang="ts">
import Vue from 'vue'
import DeleteBtn from '~/components/atom/DeleteBtn.vue'
export default Vue.extend({
  components: {
    DeleteBtn
  },
  auth: false,
  layout: 'unauthorized'
})
</script>

スクリーンショット 20200712 16.02.48.png

フォールバックコンテンツ

スロットに対して、コンテンツがない場合に描画されるデフォルトのコンテンツを指定することができます。
フォールバックコンテンツを使用するには、<slot>タグの中に記述します。

DeleteBtn.vue
<template>
  <v-btn
    color="error"
    dark
    min-width="300"
    rounded
  >
    <slot>DELETE</slot>
  </v-btn>
</template>

親コンポーネントでスロットを指定しなかった場合にはDELETEというテキストが出力され、指定された場合にはその文字が出力されます。

App.vue
<template>
  <v-row>
    <v-col class="text-center">
      <!-- スロットコンテンツを指定せず -->
      <delete-btn></delete-btn>
    </v-col>
    <v-col class="text-center">
      <!-- スロットコンテンツを指定 -->
      <delete-btn>削除</delete-btn>
    </v-col>
  </v-row>
</template>

<script lang="ts">
import Vue from 'vue'
import DeleteBtn from '~/components/atom/DeleteBtn.vue'
export default Vue.extend({
  components: {
    DeleteBtn
  }
})
</script>

スクリーンショット 20200712 16.09.20.png

名前付きスロット

この削除ボタンにさらに機能を追加しましょう、
ボタンをクリックしたときに、「本当に削除しますか?」というダイアログが出現するようにします。

とりあえず簡単にVuetifyのダイアログを追加します。

DeleteBtn.vue
<template>
  <span>
    <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog">
      <slot>DELETE</slot>
    </v-btn>
    <v-dialog v-model="dialog" max-width="290">
      <v-card>
        <v-card-title class="subtitle-1">
          削除します。よろしいですか?
        </v-card-title>
        <v-card-text>
          この操作は取り消せません。
        </v-card-text>
        <v-divider></v-divider>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn color="primary" text @click="clickCancel">
            キャンセル
          </v-btn>
          <v-btn color="error" text @click="clickOK">
            削除する
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </span>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  name: 'DeleteButton',
  data() {
    return {
      dialog: false
    }
  },
  methods: {
    openDialog() {
      this.dialog = true
    },
    closeDialog() {
      this.dialog = false
    },
    clickCancel() {
      this.closeDialog()
      this.$emit('clickCancel')
    },
    clickOK() {
      this.closeDialog()
      this.$emit('clickOK')
    }
  }
})
</script>

このコンポーネントを汎用的に利用するために、ダイアログのメッセージもスロット化したいはずです。
しかし、次のようにそのまま<slot>タグで囲むとうまく動作しません。

DeleteBtn.vue
<!-- 誤ったスロットの使いかた -->
<template>
  <span>
    <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog">
      <slot>DELETE</slot>
    </v-btn>
    <v-dialog v-model="dialog" max-width="290">
      <v-card>
        <v-card-title class="subtitle-1">
          <slot>削除します。よろしいですか?</slot>
        </v-card-title>
        <v-card-text>
          <slot>この操作は取り消せません。</slot>
        </v-card-text>
        <v-divider></v-divider>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn color="primary" text @click="clickCancel">
            <slot>キャンセル</slot>
          </v-btn>
          <v-btn color="error" text @click="clickOK">
            <slot>削除する</slot>
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </span>
</template>

なぜなら、スロットが複数あるため親コンポーネントからどのスロットに対してコンテンツを差し込めばよいか判断することができないからです。

このように、スロットが複数ある場合にはスロットに名前を付けて利用します。<slot>要素はnameという属性を持っているのでこれを利用して名前を定義します。

DeleteBtn.vue
<template>
  <span>
    <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog">
      <slot>DELETE</slot>
    </v-btn>
    <v-dialog v-model="dialog" max-width="290">
      <v-card>
        <v-card-title>
          <slot name="dialogTitle">削除します。よろしいですか?</slot>
        </v-card-title>
        <v-card-text>
          <slot name="dialogText">この操作は取り消せません。</slot>
        </v-card-text>
        <v-divider></v-divider>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn color="primary" text @click="clickCancel">
            <slot name="cancelBtn">キャンセル</slot>
          </v-btn>
          <v-btn color="error" text @click="clickOK">
            <slot name="okBtn">削除する</slot>
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </span>
</template>

name属性を持たない<slot>は暗黙的にdefaultという名前を持ちます。

親コンポーネントからスロットコンテンツを指定するには、<template>に対してv-slotディレクティブでスロット名を与えます。

App.vue
<template>
  <v-row>
    <v-col class="text-center">
      <delete-btn>
        削除
        <template v-slot:dialogTitle>
          {{ item.id }}のデータを本当に削除しますか?
        </template>

        <template v-slot:okBtn>
          はい、削除します。
        </template>
      </delete-btn>
    </v-col>
  </v-row>
</template>

<script lang="ts">
import Vue from 'vue'
import DeleteBtn from '~/components/atom/DeleteBtn.vue'
export default Vue.extend({
  components: {
    DeleteBtn
  },
  data() {
    return {
      item: {
        id: '12345'
      }
    }
  }
})
</script>

<template>で囲まれていな要素は、デフォルトスロットに対するものとして扱われます。
これは、次のように描画されます。

スクリーンショット 20200712 17.10.14.png

名前付きスロットの省略記法

v-slotディレクティブにも省略記法が使用できます。
省略記法では、v-slot:の代わりに#を使用します。

App.vue
<template>
  <v-row>
    <v-col class="text-center">
      <delete-btn>
        削除
        <template #dialogTitle>
          {{ item.id }}のデータを本当に削除しますか?
        </template>

        <template #okBtn>
          はい、削除します。
        </template>
      </delete-btn>
    </v-col>
  </v-row>
</template>

Propsとスロットの違い

ここまでは基礎的なスロットの使い方をみてきました。しかし、ある程度Vue.jsを触ったことがある人なら、次のように思ったんじゃないでしょうか。

「結局これってPropsとやってることは変わらないんじゃ・・・わざわざスロットを使わなくてもいいのでは?」

例えば、先程の例では次のようにPropsを使ったコンポーネントに書き換えることもできそうです。

DeleteBtn.vue
<template>
  <span>
    <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog">
      {{ btnText }}
    </v-btn>
    <v-dialog v-model="dialog" max-width="290">
      <v-card>
        <v-card-title>
          {{ dialogTitle }}
        </v-card-title>
        <v-card-text>
          {{ dialogText }}
        </v-card-text>
        <v-divider name="dialogText"></v-divider>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn color="primary" text @click="clickCancel">
            {{ cancelBtn }}
          </v-btn>
          <v-btn color="error" text @click="clickOK">
            {{ okBtn }}
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </span>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  name: 'DeleteBtn',
  props: {
    btnText: {
      type: String,
      required: false,
      default: 'DELETE'
    },
    dialogTitle: {
      type: String,
      required: false,
      default: '削除します。よろしいですか?'
    },
    dialogText: {
      type: String,
      required: false,
      default: 'この操作は取り消せません。'
    },
    cancalBtn: {
      type: String,
      required: false,
      default: 'キャンセル'
    },
    okBtn: {
      type: String,
      required: false,
      default: '削除'
    }
  },
  data() {
    return {
      dialog: false
    }
  },
  methods: {
    openDialog() {
      this.dialog = true
    },
    closeDialog() {
      this.dialog = false
    },
    clickCancel() {
      this.closeDialog()
      this.$emit('clickCancel')
    },
    clickOK() {
      this.closeDialog()
      this.$emit('clickOK')
    }
  }
})
</script>
<template>
  <v-row>
    <v-col class="text-center">
      <delete-btn
        btnText="削除"
        :dialogTitle="`${item.id}のデータを本当に削除しますか?`"
        okBtn="はい、削除します。"
      />
    </v-col>
  </v-row>
</template>

このような仕事をさせるのなら、Propsとスロットに違いはないようにも思えます。
実際にはどのようにPropsとスロットを使い分けていくのでしょうか?

Propsは値を渡し、スロットは描画内容を渡す

Propsと比較したときに挙げられるスロットの役割とし、親コンポーネントに描画内容を任せるという点があります。

例えば、作成中の削除ボタンに次のような機能を持たせたいとします。

  • 重要なデータAを削除する際には、ダイアログの文字を大きく太文字で表示させる
  • そこそこ大切なデータBを削除するときはダイアログの文字の前にアイコンを表示させる
  • それ以外のデータを削除するときにはデフォルトのダイアログを表示

この機能をPropsを使って実装しようする場合、データの状態に関するロジックを子コンポーネントに記述しなくてはいけなくなってしまいます。

DeleteBtn.vue
<v-card-title v-if="dataLevel === 3" class="display-1">
  {{ dialogTitle }}
</v-card-title>
<v-card-title v-else-if="dataLevel === 2">
  <v-icon>fas fa-exclamation-triangle</v-icon>{{ dialogTitle }}
</v-card-title>
<v-card-title v-else>
  {{ dialogTitle }}
</v-card-title>

このように、データの状態が増えるたびに分岐が増えてしまい、コンポーネントが肥大化してしまします。さらには親の状態の増加によって本来手を加えるべきではない子コンポーネントに記述しなければいけない状態となり、汎用的に使用できるコンポーネントとは言えなくなっていしまいます。

この時、スロットを利用すればコンポーネントを使う側が描画内容を決定すること可能です。

ただこれなら、それぞれのデータの状態に合わせた削除ボタンコンポーネントを作成すれば、子コンポーネント側の肥大化は解消することができます。(DataLevel3DeleteBtn.vueとDataLevel2DeleteBtn.vueを作成して、それぞれに描画内容をもたせる)

ただし、その場合には動作は同じだが描画内容だけが違うコンポーネントを作成することになり、DRY原則からしてあまりイケてない実装になってしまいます。

スロットは、再利用の高いコンポーネントを作成するときに効果を発揮するともいえるでしょう。

スコープ付きスロット

通常のスロットのスコープを確認する

ここからは、基礎的な内容から更に一歩進んだものとなります。
通常、v-slotディレクティブからアクセスできるデータはそのコンポーネント自身のデータになります。

次の例で確認してみましょう。親コンポーネントと子コンポーネントは、同じuserというプロパティをもっています。

Hello.vue
<template>
  <div>Hello, <slot></slot></div>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  data() {
    return {
      user: {
        name: 'aaa'
      }
    }
  }
})
</script>
App.vue
<template>
  <div>
    <Hello>{{ user.name }}</Hello>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import Hello from '~/components/atom/Hello.vue'
export default Vue.extend({
  components: {
    Hello
  },
  data() {
    return {
      user: {
        name: 'bbb'
      }
    }
  },
})
</script>

この時、描画される内容は子のもつuserプロパティなのか、親の持つuserプロパティなのかどちらでしょうか?
スロットは子コンポーネントに描画されるので、一見子コンポーネントプロパティが使われるようにも思えますが、実際に使用されるのは親コンポーネントの持つプロパティです。

スクリーンショット 20200712 20.32.59.png

これは、デフォルトの動作では例えスロットを使用したとしても、子コンポーネントのプロパティにはアクセスできないことを意味します。
試しに、親コンポーネントのuserプロパティを削除してみると、エラーが発生します。

親から子のプロパティを参照する

一切子のプロパティを親から参照できないとなると不便になることも多いので、スロットには子のプロパティを参照するための機能が提供されています。

子コンポーネントから親コンポーネントのスロットコンテンツとしてプロパティを渡す場合、<slot>要素の属性としバインドします。

Hello.vue
<template>
  <div>Hello, <slot :user="user"></slot></div>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  data() {
    return {
      user: {
        name: 'aaa'
      }
    }
  }
})
</script>

<slot>要素にバインドされた属性は、スロットプロパティ と呼ばれます。これは、親コンポーネント内でスロットの名前を指定することでスロットプロパティを受け取ることができます。

App.vue
<template>
  <div>
    <Hello>
      <!-- slotPropという名前はコンポーネント内で一意である名前であれば好きな名前を使用することができます。 -->
      <template #default="slotProp">
        {{ slotProp.user.name }}
      </template>
    </Hello>
  </div>
</template>

子コンポーネントのプロパティが描画されています。

スクリーンショット 20200712 20.50.09.png

デフォルトスロットの省略記法

スロットがデフォルトスロットだけの場合には、<template>タグで名前を指定せずとも、次のように記述することができます。

App.vue
<template>
  <div>
    <Hello v-slot="slotProp">
      {{ slotProp.user.firstName }}
    </Hello>
  </div>
</template>

スロットプロパティの分割代入

v-slotでは、JavaScriptの式を記述することができます。ですので、分割代入を利用すれば、よりきれいにプロパティを取得することができます。

App.vue
<template>
  <div>
    <Hello v-slot="{ user }">
      {{ user.firstName }}
    </Hello>
  </div>
</template>

実践的な例

スコープ付きスロットを利用した、より実践的な例を見ていきましょう。
もう一度、先程の削除ボタンコンポーネントに登場していただきます。

今度は、ダイアログ自体をスロットとして提供できるようにしましょう。
新たに、<v-dialog>全体をスロットで囲んでいます。

DeleteBtn.vue
<template>
  <span>
    <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog">
      <slot>DELETE</slot>
    </v-btn>
    <slot name="dialog">
      <v-dialog v-model="dialog" max-width="290">
        <v-card>
          <v-card-title>
            <slot name="dialogTitle">削除します。よろしいですか?</slot>
          </v-card-title>
          <v-card-text>
            <slot name="dialogText">この操作は取り消せません。</slot>
          </v-card-text>
          <v-divider name="dialogText"></v-divider>
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn color="primary" text @click="clickCancel">
              <slot name="cancelBtn">キャンセル</slot>
            </v-btn>
            <v-btn color="error" text @click="clickOK">
              <slot name="okBtn">削除する</slot>
            </v-btn>
          </v-card-actions>
        </v-card>
      </v-dialog>
    </slot>
  </span>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  name: 'DeleteBtn',
  data() {
    return {
      dialog: false
    }
  },
  methods: {
    openDialog() {
      this.dialog = true
    },
    closeDialog() {
      this.dialog = false
    },
    clickCancel() {
      this.closeDialog()
      this.$emit('clickCancel')
    },
    clickOK() {
      this.closeDialog()
      this.$emit('clickOK')
    }
  }
})
</script>

親コンポーネントから異なるタイプのダイアログを描画するために、次のように記述したいはずでしょう。

App.vue
<template>
  <v-row>
    <v-col class="text-center">
      <delete-btn>
        <template #dialog>
          <v-dialog v-model="dialog" persistent min-width="500">
            <v-card>
              <v-card-title class="headline">
                本当に削除しますか?
              </v-card-title>
              <v-card-actions>
                <v-spacer></v-spacer>
                <v-btn color="primary" @click="clickCancel">
                  キャンセル
                </v-btn>
                <v-btn color="error" @click="clickOK">
                  削除する
                </v-btn>
              </v-card-actions>
            </v-card>
          </v-dialog>
        </template>
      </delete-btn>
    </v-col>
  </v-row>
</template>

お察しの通り、これはうまく動作しませんなぜならダイアログの表示を制御するdialogプロパティは子コンポーネントが持っているからです。(v-modelで渡しているところです。)
親にスロットプロパティとして渡してみます。

DeleteBtn.vue
<template>
  <span>
    <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog">
      <slot>DELETE</slot>
    </v-btn>
    <slot name="dialog" :dialog="dialog">
      <v-dialog v-model="dialog" max-width="290">
        <v-card>
          <v-card-title>
            <slot name="dialogTitle">削除します。よろしいですか?</slot>
          </v-card-title>
          <v-card-text>
            <slot name="dialogText">この操作は取り消せません。</slot>
          </v-card-text>
          <v-divider name="dialogText"></v-divider>
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn color="primary" text @click="clickCancel">
              <slot name="cancelBtn">キャンセル</slot>
            </v-btn>
            <v-btn color="error" text @click="clickOK">
              <slot name="okBtn">削除する</slot>
            </v-btn>
          </v-card-actions>
        </v-card>
      </v-dialog>
    </slot>
  </span>
</template>
App.vue
<template>
  <v-row>
    <v-col class="text-center">
      <delete-btn>
        <template #dialog="{ dialog }">
          <v-dialog v-model="dialog" persistent min-width="500">
            <v-card>
              <v-card-title class="headline">
                本当に削除しますか?
              </v-card-title>
              <v-card-actions>
                <v-spacer></v-spacer>
                <v-btn color="primary" @click="clickCancel">
                  キャンセル
                </v-btn>
                <v-btn color="error" @click="clickOK">
                  削除する
                </v-btn>
              </v-card-actions>
            </v-card>
          </v-dialog>
        </template>
      </delete-btn>
    </v-col>
  </v-row>
</template>

しかし、残念なことにこれは好ましい実装ではありません。なぜなら、スロットプロパティの値をv-modelで直接更新してしまっているからです。スロットプロパティで渡された値はあくまで参照だけに留めるべきで、直接値を更新してしまうのは禁じ手です。

親のスロット内での変更を、どのように子に伝えればよいのでしょうか?

スロットプロパティでメソッドを渡す

実は、スロットプロパティには子のメソッドを渡すことも可能です。
スロットプロパティの中では渡したメソッドは子コンポーネントのものなので、子の値を変更することができるというわけです。

メソッドの渡し方は通常の記法変わりません。

DeleteBtn.vue
<template>
  <span>
    <v-btn color="error" dark min-width="300" rounded @click.stop="openDialog">
      <slot>DELETE</slot>
    </v-btn>
    <slot
      name="dialog"
      :dialog="dialog"
      :closeDialog="closeDialog"
      :clickCancel="clickCancel"
      :clickOK="clickOK"
    >
      <v-dialog v-model="dialog" max-width="290">
        <v-card>
          <v-card-title>
            <slot name="dialogTitle">削除します。よろしいですか?</slot>
          </v-card-title>
          <v-card-text>
            <slot name="dialogText">この操作は取り消せません。</slot>
          </v-card-text>
          <v-divider name="dialogText"></v-divider>
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn color="primary" text @click="clickCancel">
              <slot name="cancelBtn">キャンセル</slot>
            </v-btn>
            <v-btn color="error" text @click="clickOK">
              <slot name="okBtn">削除する</slot>
            </v-btn>
          </v-card-actions>
        </v-card>
      </v-dialog>
    </slot>
  </span>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  name: 'DeleteButton',
  data() {
    return {
      dialog: false
    }
  },
  methods: {
    openDialog() {
      this.dialog = true
    },
    closeDialog() {
      this.dialog = false
    },
    clickCancel() {
      this.closeDialog()
      this.$emit('clickCancel')
    },
    clickOK() {
      this.closeDialog()
      this.$emit('clickOK')
    }
  }
})
</script>

ついでにキャンセルボタンをOKボタンをクリックした際のイベントも渡しておきましょう。
親側の記述は次のようになります。
v-modelを分解して:value@inputを使用します。

App.vue
<template>
  <v-row>
    <v-col class="text-center">
      <delete-btn>
        <template #dialog="{ dialog, closeDialog, clickCancel,clickOK }">
          <v-dialog :value="dialog" @input="closeDialog" dark min-width="500">
            <v-card>
              <v-card-title class="headline">
                本当に削除しますか?
              </v-card-title>
              <v-card-actions>
                <v-spacer></v-spacer>
                <v-btn color="primary" @click="clickCancel">
                  キャンセル
                </v-btn>
                <v-btn color="error" @click="clickOK">
                  削除する
                </v-btn>
              </v-card-actions>
            </v-card>
          </v-dialog>
        </template>
      </delete-btn>
    </v-col>
  </v-row>
</template>

これで、親からスロットを利用してダイアログを差し替えることができました。

タイトルなし.gif

終わりに

私自身も、Propsとスロットの違いがよくわかっておらず、スロットからは避けていました。
スロットを利用すると、再利用性の高いコンポーネントが作成できたりと、作成の幅が広がります。
さらに、ライブラリのコンポーネントを使用する機会も多いと思いますがそういったものはAPIとしてスロットを公開していることが多いです。
スロットを理解することで、ライブラリをより効果的に使用することも期待できます。

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

HTML上でJavaScriptを使ってAPIコールし、レスポンスをCSVダウンロード(っぽく)取得する

目的

HTML上でJavaScriptを使ってAPIコールし、レスポンスをCSVダウンロード(っぽく)取得する。

方法

  1. APIからのレスポンス値をCSV形式へ変換する。
  2. 変換したCSVでdataURLを使ってリンク(aタグ)を生成する。

実装

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

<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<script>
    //実際にはAPIコールを行う
    // ※配列の配列(CSVの1行に該当)で返す
    const getApiResFromAPI = async () => {
        return [
            ['row 1 / cell 1', 'row 1 / cell 2', 'row 1 / cell 3'],
            ['row 2 / cell 1', 'row 2 / cell 2'],
            ['row 3 / cell 1', 'row 3 / cell 2', 'row 3 / cell 3', 'row 3 / cell 4']
        ];
    }

    // CSVダウンロードボタンを押下した際のclickイベント
    const csvdownload = async () => {

        // APIからCSVの中身を取得
        var apiResData = await getApiResFromAPI();

        // 取得した結果をCSV形式でつなげていく
        var csv = [];
        apiResData.forEach((row, index) => {
            csv.push(row.join(','));
        });

        // APIからダウンロードしたCSVファイルのデータURIを持ったaタグを生成
        var element = document.createElement('a');
        element.text = "テスト"
        element.href = 'data:text/csv;charset=utf-8,%EF%BB%BF' + encodeURIComponent(csv.join('\n'));
        element.setAttribute('download', 'basic.csv');

        // aタグを裏で追加して、クリック(csvファイルダウンロード用のダイアログが表示される)、裏で削除する
        document.body.appendChild(element);
        element.click();
        document.body.removeChild(element);
    }

</script>

<body>
    <input type="button" onClick="csvdownload(); return false;" value="CSVダウンロード">
</body>

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

そろそろlockファイルを理解するための最初のページ【composer.lock/package-lock.json】

.lockファイルって邪魔だよね。って思っている人いたら校舎裏来てください。優しく解説します。

こんにちは! @ykhirao です。最近ジョインさせていただいた案件で composer.lock のバージョンが本番が一番新しくて、ローカルが一番古い素敵な状況を発見しました :relaxed:

本番環境でSSHして composer update かけたんだなあーー。そっかーーー、バグ起きたらどないすんねん、。。。。。。

というのが私の感想で、実際にnpmパッケージを使ってバグを再現しながら
ゆっくり解説していこうと思います。どうぞ最後までよろしくお願いします。!!!

このQiitaを呼んで得ること

  • あーー、ほんとだ。バグが入る可能性があるんだ!
  • lockファイルはちゃんとコミットするね!

というお気持ちになれると思います。

このQiitaで解説しないこと

  • 詳しい依存関係の解消法
  • packageの公開の仕方
  • むずかしいはなし

です!

さて時系列を見てみよう!

こんな事故が起こる可能性がありますよって話をします。

時系列 OSSのmypackage.js ローカル 本番サーバー
1日目 v1.0.0公開 動いている 動いている
2日目 ローカルでmypackage.jsをinstall
動作確認OK、明後日本番に適応させる
3日目 v1.0.1公開 バグあり
4日目 本番サーバーで新しいコードを反映、install。本番が動かなくなってしまった。ぴえん
もちろんソースコードはローカルと同じだし、ローカルで動いていたのになんで…?:flushed:
5日目 v1.0.2公開 バグ修正された
6日目 v1.0.2を適応ローカル確認OK
今度はlockファイルを本番にアップロードする予定!
7日目 package-lock.jsonを使ってinstallする!今度は動いた!

npmでパッケージを作りながら説明する

OSSっぽい動作をするpackageフォルダと、ローカル環境と本番環境っぽいフォルダを作る。

yk@yk ~ % cd qiita 
yk@yk qiita % mkdir package
yk@yk qiita % mkdir local
yk@yk qiita % mkdir production
yk@yk qiita % ll
total 0
drwxr-xr-x   5 yk  staff   160  7 11 00:34 .
drwxr-xr-x+ 83 yk  staff  2656  7 11 00:33 ..
drwxr-xr-x   2 yk  staff    64  7 11 00:34 local
drwxr-xr-x   2 yk  staff    64  7 11 00:34 package
drwxr-xr-x   2 yk  staff    64  7 11 00:34 production
yk@yk qiita % cd package 
yk@yk package % npm init -y
Wrote to /Users/yk/qiita/package/package.json:

{
  "name": "package",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}


yk@yk package % vim package.json
yk@yk package % vim index.js
yk@yk package % npm publish --access=public
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN package@1.0.0 No description
npm WARN package@1.0.0 No repository field.

up to date in 0.429s
found 0 vulnerabilities

/Users/yk/.nodebrew/node/v13.8.0/lib/node_modules/package -> /Users/yk/qiita/package

yk@yk package % cat index.js 
const text = "sample"

module.exports = text;

index.jsにただただ "sample" という文字列を返す動作を記述して、パッケージとして認識させる。
@y_hirao/mypackage というパッケージをnpmに公開した。

localフォルダでいろいろゴリゴリしてみる。

yk@yk local % npm init -y
Wrote to /Users/yk/qiita/local/package.json:

{
  "name": "local",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@y_hirao/mypackage": "^1.0.0"
  },
  "devDependencies": {},
  "description": ""
}


yk@yk local % cat index.js 
const text = require("@y_hirao/mypackage");
console.log(text)

yk@yk local % node index.js 
sample

sampleという文字列が返却されました。!!!!!!!!!

このときpackage.jsonを見ると ^ キャレット指定なのでバージョンアップは 1.系 は守られるけど残りはすべてアップデートされます。

  "dependencies": {
    "@y_hirao/mypackage": "^1.0.0"
  },

さてパッケージの方を更新します。 v1.0.2 にしましょう!

yk@yk package % cat package.json 
{
  "name": "@y_hirao/mypackage",
  "version": "1.0.2",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

yk@yk package % cat index.js    
const text = "sample2"
throw 'エラーを出したい気分';

module.exports = text;
yk@yk package % npm publish   

このパッケージをそのまま使うとthrowされます。

さてローカルでinstallしてみましょう!

yk@yk local % npm install
npm WARN local@1.0.0 No description
npm WARN local@1.0.0 No repository field.

audited 1 package in 0.411s
found 0 vulnerabilities

yk@yk

あれ、エラーが起きませんね。
これは 一度ダウンロードしたパッケージはlockファイルというものでバージョン指定されているので、新しく勝手にバージョンがあがったりしないのです。

さてローカルから本番環境に package.json と index.js をコピーしてnpm installして動かしてみましょう!

yk@yk production % cp ../local/package.json package.json
yk@yk production % cp ../local/index.js index.js        
yk@yk production % npm i
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN local@1.0.0 No description
npm WARN local@1.0.0 No repository field.

added 1 package and audited 1 package in 2.811s
found 0 vulnerabilities

yk@yk production % node index.js 

/Users/yk/qiita/production/node_modules/@y_hirao/mypackage/index.js:2
throw 'エラーを出したい気分';
^
エラーを出したい気分
(Use `node --trace-uncaught ...` to show where the exception was thrown)

さて、、、本番環境でinstallを走らせるとエラーがおきました。。。。

npm install したときに package-lock.json がないと package.json を使って ^1.0.0 が指定されているので 1.系で一番新しいもの つまり今回だとバグが混入している v1.0.2 がインストールされたことになります。

lockファイルを見てみましょう。

yk@yk production % cat package-lock.json 
{
  "name": "local",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "@y_hirao/mypackage": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/@y_hirao/mypackage/-/mypackage-1.0.2.tgz",
      "integrity": "sha512-W2RoQsC1FVnHxximVJMovEvfl/3WNI95EjGxPbMlmyuDZ+0SImS76dOII/0AiP4cVVjXZUbFzaLs9h6l8TrrFQ=="
    }
  }
}

まさしく v1.0.2 が指定されていますね!localだともちろん最初に npm install したときに最新だった @y_hirao/mypackagev1.0.0 が使われています。

yk@yk local % cat package-lock.json 
{
  "name": "local",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "@y_hirao/mypackage": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/@y_hirao/mypackage/-/mypackage-1.0.0.tgz",
      "integrity": "sha512-auQ5grIpWd0IRkMR3rn6xRjIhGZqBu/Q/g1lDkS5AWakAysrZ3iQAIRngbgBGBAFlPZxQ/LCVYdAW+3VLxeSEw=="
    }
  }
}

更にインストールされているフォルダを確認しても

yk@yk production % cat node_modules/@y_hirao/mypackage/index.js
const text = "sample2"
throw 'エラーを出したい気分';

module.exports = text;
yk@yk production % 

どう見てもエラーが起きそうです。

package-lock.jsonをlocalからコピーしてきましょう。

yk@yk production % cp ../local/package-lock.json package-lock.json
yk@yk production % npm i
npm WARN local@1.0.0 No description
npm WARN local@1.0.0 No repository field.

updated 1 package and audited 1 package in 0.45s
found 0 vulnerabilities

yk@yk production % cat node_modules/@y_hirao/mypackage/index.js
const text = "sample"

module.exports = text;
yk@yk production % node index.js 
sample
yk@yk production % 

lockファイルを持ってきてインストールするだけで、エラーが起きなくなりました! :clap::clap::clap::clap::clap:

まとめ

本番環境で lockファイルごと更新する composer update とかlockファイルの存在しない状態での npm install とか、そんなことはやめましょう。ってことを主張したいです!!!!!

ローカルでしっかり確認してlockファイルを作り、それを本番にアップロードして、lockファイルの状態で依存パッケージをインストールしちゃってください。。 :pray:

インストールする時期によって最新のバージョンが使われるので、もしそのバージョンにバグがあるのなら本番死んじゃいますよ。。

終わりに

新しい会社に入って色々根本的に変えたいところがあるので、最近はdevopsの領域をごりごり触らせていただいてます。環境構築、開発フローの策定とかもろもろやらせてもらっているのでかなり楽しいです :muscle:

読んでいただきありがとうございました。lockファイル入門したい方、事故りそうな不安があるかたは同僚と一緒に「弊社のパッケージ依存関係の運用どうする…?」と一度話し合ってみてください。

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

.

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

Array.prototype.reduce()を使いこなしたい

reduce()とは

Array.prototype.reduce()

配列からひとつの値を求めるときに使われる。
いろんなこと出来そう、便利そう、面白そう、、、と思ったので、理解を深めるためにメモに残す。

構文

  1. accumulator(前回の値)
  2. currentValue(現在の値)
  3. currentIndex(現在currentとして処理されている要素のindex)
  4. array(現在操作されている配列)

initialValueは第2引数で初期値がはいる。

const result = array.reduce((accumulator, currentValue, currentIndex, array) => {
  return accumulator
}, initialValue)

arrayの数字を足し上げて、合計値を得る。
初期値(initialValue)は0とする。

const array = [0, 1, 2, 3]
const initialValue = 0

const result = array.reduce(
  (accumulator, currentValue, currentIndex, array) => {
    console.log(
      "accumulator:",
      accumulator,
      "currentValue:",
      currentValue,
      "currentIndex:",
      currentIndex,
      "array:",
      array
    )
    return accumulator + currentValue
  },
  initialValue
)

console.log("result:", result) //6

スクリーンショット 2020-07-11 19.53.35.png

reduce() の返値は、最後の返値である (6) となった。

これをforで書くと。。。
const array = [0, 1, 2, 3]
let value = 0

for (let i = 0; i < array.length; i++) {
  value += array[i]
}

console.log(value) //6
これをforEach()で書くと。。。
const array = [0, 1, 2, 3]
const forEachResult = array => {
  let sum = 0
  array.forEach(number => {
    return (sum += number)
  })
  return sum
}

console.log("forEachResult:", forEachResult(array)) //6

forやforEach()は、「配列の要素を1つずつ処理する」というのが本来の目的であり、reduce() は「単一の値を返す」のが目的なので、reduce()を使うほうが、今回の目的「arrayの数字を足し上げて、合計値を得る」手段としては、適していると言える。

理解を深めるために色々なサンプルコードを書いてみる

sample01: オブジェクトの配列の値の合計値を出す

const initialValue = 0
const array = [{ x: 0 }, { x: 1 }, { x: 2 }, { x: 3 }]

const sample01 = array.reduce((accumulator, currentValue) => {
  return accumulator + currentValue.x
}, initialValue)

console.log(sample01) //6

sample02: 配列から要素を抜き出して、好きな形のオブジェクトにする

const initialValue = {}
const array = [
  { name: "keko", age: 25 },
  { name: "taro", age: 19 },
  { name: "mai", age: 17 }
]

const sample02 = array.reduce((accumulator, currentValue) => {
  accumulator[currentValue.name] = currentValue.age
  return accumulator
}, initialValue)

console.log(sample02) //{keko: 25, taro: 19, mai: 17}

sample03: 配列をindex付きのオブジェクトにする

const initialValue = {}
const array = [
  { name: "keko", age: 25 },
  { name: "taro", age: 19 },
  { name: "mai", age: 17 }
]

const sample03 = array.reduce((accumulator, currentValue, index) => {
  accumulator[index + 1] = currentValue
  return accumulator
}, initialValue)

console.log(sample03)
// {
//   1: {name: "keko", age: 25}
//   2: {name: "taro", age: 19}
//   3: {name: "mai", age: 17}
// }

sample04: 配列を欲しい要素だけにしてオブジェクトにする

const initialValue = []
const array = [
  { name: "keko", age: 25, pref: "Ehime" },
  { name: "taro", age: 19, pref: "Ehime" },
  { name: "mai", age: 17, pref: "Osaka" }
]

const sample04 = {
  ...array.reduce((accumulator, currentValue) => {
    const { name, age } = currentValue
    accumulator.push({ name, age })
    return accumulator
  }, initialValue)
}

console.log(sample04)
// {
//   { name: "keko", age: 25 },
//   { name: "taro", age: 19 },
//   { name: "mai", age: 17 }
// }

sample05: プロパティによってオブジェクトをグループ化する

const initialValue = {};
const array = [
  { name: "keko", age: 25, pref: "Ehime" },
  { name: "taro", age: 19, pref: "Ehime" },
  { name: "mai", age: 17, pref: "Osaka" }
];

const sample05 = array.reduce((accumulator, currentValue) => {
  let key = currentValue.pref;
  if (!accumulator[key]) {
    accumulator[key] = [];
  }
  const { name, age } = currentValue;
  accumulator[key].push({ name, age });
  return accumulator;
}, initialValue);

console.log(sample05);

// {
//   Ehime: [
//     {name: "keko", age: 25 },
//     {name: "taro", age: 19 }],
//   Osaka: [
//     {name: "mai", age: 17 }]
// }

sample06_1: 二次元配列を一次元配列にする(concat編)

const initialValue = []
const array = [[0, 1], [2, 3], [4, 5]]

const sample06_1 = array.reduce((accumulator, currentValue) => {
  return accumulator.concat(currentValue)
}, initialValue)

console.log(sample06_1) // [0, 1, 2, 3, 4, 5]

sample06_2: 二次元配列を一次元配列にする(spred演算子編)

const initialValue = []
const array = [[0, 1], [2, 3], [4, 5]]

const sample06_2 = array.reduce((accumulator, currentValue) => {
  return [...accumulator, ...currentValue]
}, initialValue)

console.log(sample06_2) // [0, 1, 2, 3, 4, 5]

sample07_01: 配列内の重複している値を除外する(someで除外編)

const initialValue = []
const array = [1, 2, 2, 3, 1, "a", "b", "b"]

const sample07_01 = array.reduce((accumulator, currentValue) => {
  if (!accumulator.some(item => item === currentValue)) {
    accumulator.push(currentValue)
  }
  return accumulator
}, initialValue)

console.log(sample07_01)

sample07_02: 配列内の重複している値を除外する(indexOfで除外編)

const initialValue = []
const array = [1, 2, 2, 3, 1, "a", "b", "b"]

const sample07_02 = array.reduce((accumulator, currentValue) => {
  if (accumulator.indexOf(currentValue) === -1) {
    return accumulator.push(currentValue)
  }
  return accumulator
}, initialValue)

console.log(sample07_02)

sample08: filter() + map() の代わりに使う

map() は入力&出力は要素数は変わらないので、配列の一部要素を除外する時、filter() を適用させる必要があり、filter() と map() でそれぞれループするが、reduce() を使えばループは一度で済む。

const initialValue = []
const array = [
  { id: 1, name: "keko", age: 25 },
  { id: 2, name: "taro", age: 19 },
  { id: 3, name: "mai", age: 17 },
  { id: 4, name: "hiro", age: 30 }
]

//未成年を除外して、idとnameを取得して配列にする
const sample08 = array.reduce((accumulator, currentValue) => {
  if (currentValue.age > 20) {
    const { id, name } = currentValue
    accumulator.push({ id, name })
  }
  return accumulator
}, initialValue)

console.log(sample08)
// [
//   {id: 1, name: "keko"},
//   {id: 4, name: "hiro"}
// ]


//filter().map()の場合
const sample08_test = array
.filter(item => item.age > 20)
.map(item => {
  const { id, name } = item;
  return { id, name }
})

console.log(sample08_test)
// [
//   {id: 1, name: "keko"},
//   {id: 4, name: "hiro"}
// ]

sample09: 多次元配列を一次元配列にする

const array = [0, [1, 2, 3, [4, 5], [6, 7, [8, 9]]]];

const mergeArry = arr => {
  const result = arr.reduce((acc, current) => {
    return acc.concat(current);
  }, []);
  return result;
};

const hasArry = arr => arr.some(item => Array.isArray(item));

const mergeAll = arr => {
  if (!hasArry(arr)) {
    return arr;
  }
  const result = mergeArry(arr);
  return mergeAll(result);
};

const sample09 = mergeAll(array);

console.log(sample09); //[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

今後もsample追加していく

参考

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

[JavaScript] express() で "express is not a function" ついでに TS の "export =" と "import = require()"

Expressを読み込んだ際に express is not a function と出たので原因と解決策を共有します。
あとついでに export = と import = require() についてです。

環境

import側はJavaScript、トランスパイルとかは一切なし。
node: v14.1.0

package.json
{
  ...
  "type": "module",
  "dependencies": {
    "express": "^4.17.1",
    ...
  },
  ...
}

再現VTR

index.js
import * as express from "express";
const app = express();
node ./index.js

実行すると

const app = express();
            ^
TypeError: express is not a function

エラー発生。
因みに、USAGEに従っていた。:thinking: 1

@types/express/index.d.ts
/* =================== USAGE ===================

    import * as express from "express";
    var app = express();

 =============================================== */

原因

CommonJSとESModulesdでの返却される値に違いがあった。

import * as express from "express";
console.log(typeof express); // => object
const express = require("express");
console.log(typeof express); // => function
import
default: [Function: createApplication] { ... }
require
[Function: createApplication] { ... }

つまり、返却値がdefault exportとなっているため参照エラーを起こしていた。

対応策

こうする。

import express from "express";
const app = express();

または、

import * as express from "express";
const app = express.default();

そもそもトランスパイルもせずに使うなら、CommonJSモジュールならCommonJSのインポート (const express = require("express");) で読み込むのがベターっていうツッコミは覚悟してます!

以降おまけ

export =

名前は「エクスポート代入」。
Expressの型定義ファイルを覗くとこうなっている。

@types/express/index.d.ts
...
/**
 * Creates an Express application. The express() function is a top-level function exported by the express module.
 */
declare function e(): core.Express;
...
export = e;

CommonJSとAMDを考慮せずにエクスポートできる便利機能。
言い換えればこれは、CommonJSまたはAMDのモジュールだよ。っていう印。

import = require()

名前は「インポート代入」。
エクスポート代入(export =)を使用してエクスポートされたモジュールをインポートする際に使われていたらしい。
今ではあえて使う理由はなさそう。

インポート代入、インポート代入の使用例

// export側
function hoge() {
  return 'hoge';
}
const fuga = 'fuga';
export = {
  hoge,
  fuga
};

// import側
import hogefuga = require('./export');
console.log(hogefuga); // => { hoge: [Function: hoge], fuga: 'fuga' }

感想

地味に時間を溶かしたため記事にしてやったぜ。。
何かご指摘等あればコメントいただけると幸いです。

参考

export = and import = require()

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

kintoneのグループフィールド開閉がArray.map() で上手く行かないので forEach() を使ったら上手く行った話し

kintoneのグループフィールドをPC版では開いておきたいけど、モバイルの時は初期状態で閉じておきたいとの要望があり、フォーム設定情報から取得した一覧に対してグループフィールドの開閉をしたいと思い試してみました。

試してみた

1. kintone.events.on の イベントハンドラ内で直接書く

一般的な書き方。もちろん問題無く動きます。

    kintone.events.on('mobile.app.record.detail.show', async function(event) {
        try {
            kintone.mobile.app.record.setGroupFieldOpen('group1', false);

2. Array.map() のコールバック内に書く

動きませんでした。

    kintone.events.on('mobile.app.record.detail.show', async function(event) {
        try {
            const params = {'app': await kintone.mobile.app.getId()};
            const response = await kintone.api(kintone.api.url('/k/v1/app/form/fields', true), 'GET', params);
            // 指定したフィールドタイプを配列として取得する
            const fields = getFields(response, 'GROUP');
            // mapを利用
            fields.map((field) => {
                kintone.mobile.app.record.setGroupFieldOpen(field.code, 'false');
            });

3. forEach のコールバック内に書く

動きました。

    kintone.events.on('mobile.app.record.detail.show', async function(event) {
        try {
            const params = {'app': await kintone.mobile.app.getId()};
            const response = await kintone.api(kintone.api.url('/k/v1/app/form/fields', true), 'GET', params);
            // 指定したフィールドタイプを配列として取得する
            const fields = getFields(response, 'GROUP');
            // forEachを利用
            fields.forEach(field => {kintone.mobile.app.record.setGroupFieldOpen(field.code, false)});

4. forEach() 部分を関数に書く

動きませんでした。

(function() {
    'use strict';
    var setGroupFieldsOpen = (fields, flug) => {
        fields.forEach(field => {kintone.mobile.app.record.setGroupFieldOpen(field.code, flug)});
    };
    kintone.events.on('mobile.app.record.detail.show', async function(event) {
        try {
            const params = {'app': await kintone.mobile.app.getId()};
            const response = await kintone.api(kintone.api.url('/k/v1/app/form/fields', true), 'GET', params);
            // 指定したフィールドタイプを配列として取得する
            const fields = getFields(response, 'GROUP');
            setGroupFieldsOpen(fields, 'false');
        } catch (error) {
            console.log(error);
        };
        return event;
    });

結論

  • Array.map() で上手くいかない時は forEach() を使ってみる。
  • forEach()部分を関数にすると動かない。

動かない理由は良く分からないのですが、まあ良しとします。
変数のスコープ?が影響するのか。。。
kintone JS API は関数によりますが、素直に kintone.events.on 内で書いた方が良いかもしれませんね。

参考

参考までにコードを載せておきます。

(function() {
    'use strict';
    kintone.events.on('mobile.app.record.detail.show', async function(event) {
        try {
            const params = {'app': await kintone.mobile.app.getId()};
            // フォーム情報の取得
            const response = await kintone.api(kintone.api.url('/k/v1/app/form/fields', true), 'GET', params);
            // 指定したフィールドタイプを配列として取得する
            const fields = getFields(response, 'GROUP');
            fields.forEach(field => {kintone.mobile.app.record.setGroupFieldOpen(field.code, false)});
        } catch (error) {
            console.log(error);
        };
        return event;
    });

    function getFields (fields, fieldType) {
        let result = [];
        try {
            const values = Object.values(fields.properties);
            if (fieldType) {
                values.map((field) => {
                    if (field.type === fieldType) {
                        result.push(field);
                    }
                });
            } else {
                result = values;
            }
            return result;
        } catch (error) {
            return error;
        }
    }
})();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Marked.jsはハッシュのあとにスペースがないと見出しにならない

簡単なMarkdownエディタを作る」を参考にMarkdownエディタを作成したのですが、現行バージョンのMarkdown.jsでは、#のあとにスペースがないと見出しとして認識されませんでした。気になって経緯と対策を調べたのでシェアします。

現象

QiitaのMarkdownエディタは、#のあとにスペースがあろうとなかろうと、見出しとして認識してくれます(##, ###も同様)

#見出しになる
# 見出しになる

ですが、現行バージョン(1.1.0)のMarked.js#のあとにスペースがないと、見出しとして認識してくれません。

<div id="content"></div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
  document.getElementById('content').innerHTML =
    marked('#見出しにならない');
</script>

#のあとにスペースが必要になった経緯

Marked.jsバージョン0.3.2以前は、#のあとにスペースがあってもなくてもよかったみたいですが、

  • GithubのMarkdownエディタと挙動が異なる
  • GFM(GitHub Flavored Markdown)のマニュアルにはあくまでも#(スペース)見出しと書かれている

という主張が入り、議論はあったものの、修正されたようです。

https://github.com/markedjs/marked/issues/201

#のあとにスペースがないことを許可するには

Marked.jsでは、正規表現を使ってMarkdown記法を自分で定義することができます。Marked.jsを読み込んだあと、marked()を使用するより前に以下のように記述します。

https://marked.js.org/#/USING_PRO.md#tokenizer

const tokenizer = {
  heading(src) {
    const match = src.match(/^ {0,3}(#{1,6}) *([^\n]*?)(?: +#+)? *(?:\n+|$)/);
    if (match) {
      return {
        type: 'heading',
        raw: match[0],
        depth: match[1].length,
        text: match[2]
      };
    }

    // return false to use original codespan tokenizer
    return false;
  }
};
marked.use({ tokenizer });

以上です。

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

Puppeteerで検索フォームにテキストを入力してページ遷移する

はじめに

  • 「とあるサイトに文字を入力してボタンを押して結果一覧を表示する」というのを自動化したいケースがあったのでQiitaの検索ページで試してみた。

ソースコード

  • waitForNavigation は「呼び出してから次の繊維を待つ」という意味らしく click と同期させないと想定通りに動かなかった。
  • 後は直感的でとても簡単に書ける。
const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({headless: false});
    const page = await browser.newPage();
    await page.goto('https://qiita.com/search');
    await page.type('#q', 'puppeteer');
    await Promise.all([
        page.waitForNavigation(),
        page.click('.searchResultContainer_searchButton')
    ]);
    await page.screenshot({path: 'sample.png'});
    await browser.close();
})();

実行結果

  • ちゃんと検索フォームに文字が入力されて結果が表示されている。 result.png

参考文献

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

スクロールに連動して現在地を示す目次を作りたい

ブログみたいにWEB上で長い文章を読ませるときは、いまどこを読んでいるのか教えてくれるような目次があるほうが便利です。
目次をクリックするとそのセクションに飛んでくれるとさらに便利です。

イメージとしてはangular.jpみたいなこれ↓↓とか、Qiitaの横にもあるアレとか。
なんかのライブラリが探せばありそうだけど普通にJavaScript書けば十分そう、ということで作ってみましょう。

20200712.gif

HTML構造

サンプル。
目次(Table of contents)になるul要素と、記事の表示領域になるwrapperoverflow: autoでスクロールさせる)を作ります。
中身はdivで区切ってありますが特に意味はないです。heightで疑似的に記事の長さを出したり背景色をつけて見やすくするためのものです。

<ul>
  <li class="toc" scrollTo="content1">AAAA</li>
  <li class="toc" scrollTo="content2">BBBB</li>
  <li class="toc" scrollTo="content3">CCCC</li>
  <li class="toc" scrollTo="content4">DDDD</li>
</ul>
<div class="wrapper">
  <div class="content content1">
    <h1>AAAA</h1>
  </div>
  <div class="content content2">
    <h1>BBBB</h1>
  </div>
  <div class="content content3">
    <h1>CCCC</h1>
  </div>
  <div class="content content4">
    <h1>DDDD</h1>
  </div>
</div>

CSS

目次上の現在地は.activeクラスで丸印を付けて判別しやすくします。
他はオマケ。

ul {
  list-style: none;
}
li {
  border-left: solid 1px silver;
  padding-left: 1rem;
  position: relative;
  box-sizing: border-box;
}
.toc {
  cursor: pointer;
  width: 100px;
}
.toc:hover {
  background: whitesmoke;
  color: royalblue;
}
.toc:hover:not(.active)::before {
  content: '';
  position: absolute;
  top: 0.5rem;
  left: -3px;
  border-radius: 50%;
  width: 5px;
  height: 5px;
  background: silver;
}
.toc.active:before {
  content: '';
  position: absolute;
  top: 0.5rem;
  left: -3px;
  border-radius: 50%;
  width: 5px;
  height: 5px;
  background: skyblue;
}
.wrapper {
  width: 400px;
  height: 300px;
  overflow: auto;
}
.content {
  overflow: hidden;
}
.content1 {
  width: 100%;
  height: 500px;
  background: skyblue;
}
.content2 {
  width: 100%;
  height: 500px;
  background: royalblue;
}
.content3 {
  width: 100%;
  height: 500px;
  background: lightblue;
}
.content4 {
  width: 100%;
  height: 500px;
  background: aliceblue;
}

JavaScript

本題。
アプローチとしては以下の通りです。

  1. 記事中の各セクションの開始位置、終了位置を覚えておく。
  2. スクロールイベントリスナで、現在のスクロール位置と各セクションの範囲を比較して.activeクラスを付ける。
  3. 最後までスクロールしたら最後のセクションに.activeクラスを付ける。
  4. 目次をクリックしたら、対応するセクションの開始位置を取得してスクロール位置を変更する。

3は、セクションの高さがスクロール領域よりも小さい場合にスクロール位置が最後のセクションの範囲に入らないことがあるので、その対策です。
たとえば、スクロール可能なwrapper領域が500pxあったとして、コンテンツの合計heightが2000px、最後のセクションのheightが300pxであった場合、wrapperを最後までスクロールさせても最大1500までしか行けない(1500~2000が表示された状態)ので、単純な比較では最後のセクション(1700~2000)には永遠に入らないことになります。

// これをbody.onLoadとかで動かす
function onLoad() {
  const wrapper = document.querySelector('.wrapper'); // ラッパー(スクロール領域)
  const contents = document.querySelectorAll('.content'); // 各セクションのコンテンツ
  const toc = document.querySelectorAll('.toc'); // 目次(クリックしたらそのセクションにスクロール)
  const contentsPosition = [];
  contents.forEach((content, i) => {
    const startPosition =
      content.getBoundingClientRect().top -
      wrapper.getBoundingClientRect().top +
      wrapper.scrollTop;
    const endPosition = contents.item(i + 1)
      ? contents.item(i + 1).getBoundingClientRect().top -
        wrapper.getBoundingClientRect().top +
        wrapper.scrollTop
      : wrapper.scrollHeight;
    contentsPosition.push({ startPosition, endPosition });
  });

  // スクロール位置に応じてTOCの現在位置を変更する
  const calcCurrentPosition = () => {
    toc.forEach((item, i) => {
      const { startPosition, endPosition } = contentsPosition[i];
      item.classList.remove('active');
      if (
        wrapper.scrollTop + wrapper.getBoundingClientRect().height ===
        wrapper.scrollHeight
      ) {
        toc.item(toc.length - 1).classList.add('active');
      } else if (
        wrapper.scrollTop >= startPosition &&
        wrapper.scrollTop < endPosition
      ) {
        item.classList.add('active');
      }
    });
  };

  // スクロールイベントリスナを登録
  wrapper.addEventListener('scroll', calcCurrentPosition);

  // 目次にクリックイベントリスナを登録
  toc.forEach((item) => {
    item.addEventListener('click', () => {
      const destination = event.target.getAttribute('scrollTo');
      wrapper.scrollTop =
        document.querySelector(`.${destination}`).getBoundingClientRect().top -
        wrapper.getBoundingClientRect().top +
        wrapper.scrollTop;
    });
  });

  calcCurrentPosition();
}

セクションの位置を求めるのにはElement.getBoundingClientRect()を利用してます。
Element.getBoundingClientRect()で指定した要素のビューポート上の位置を取得できるので、wrapperのtop位置やscrollTopを足し引きして「wrapper内での絶対位置」を計算しています。
各セクションの位置がわかれば、あとはwrapper.scrollTopとの比較で現在地を特定することができます。

Element.getBoundingClientRect() - Web API | MDN
Element.scrollTop - Web API | MDN

目次に対応するセクションを特定するのはscrollToみたいなワケワカラン属性で対象クラスを指定させていますが、ちゃんとやるならたぶんidとかを使う方がいい気がします。

結果

20200712-1.gif

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