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

数値の小数点以下以外をカンマ区切りにする

この記事を書く数時間前、数値をカンマ区切りにしたいと思って手っ取り早くQiitaで手法を調べて使用したところ、「小数点以下を3桁までしか表示しない(toLocaleString)」や「小数点以下の数列もカンマで区切ってしまう(正規表現)」など、小数点以下について考慮していないものばかりでちょっと面喰いました。

なんてこった.js
var n = 123456789.01234;

function separate(num){
    return String(num).replace( /(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
}

separate(n);
// "123,456,789.01,234"

n.toLocaleString();
// "123,456,789.012"

カンマ区切りをするようなものは大きな数値であることが殆どなので、小数点以下を考慮する必要があまりないのでしょうね。ですがいずれにせよ、私の場合は考慮したかったので、以下のように正規表現を書き換えてみました。

これでどうだ.js
const comma = num => isNaN(num) || String(Number(num)).replace(/(?<!\.\d*?)(\d)(?=(\d{3})+(?!\d))/g, '$1,');

comma(n);
// "123,456,789.01234"

否定後読みによってピリオドが前方に無いことを確認させています。また数値に見えて数値でない文字列(123.456.789など)をはじくため、isNaNで数値かどうかのチェックもしています。

「これで完璧!」と思いきや、一つ問題が。

SafariとIEでは否定後読みが(というよりは後読み系が)できません

IEは別にどうでもいいとして、Safari未サポートは問題ですね。

ですので、否定後読みを無理矢理できるようにする裏ワザ(Javascriptでの正規表現の後読みの代替 - @yumarule さん)か、あるいは愚直にピリオド以下を取り外して実数部分をカンマ区切りした後に結合させるかのどちらかをしなければいけません。

これにて一件落着.js
/*
  文字をsplitで配列に変換し、reverseで反転させてjoinで結合する。
  すると、Safari・IEでも使える否定先読みで問題を解決できる。
  再度反転させればできあがり。
*/
const comma_2 = num => isNaN(num) || String(Number(num)).split('').reverse().join('').replace(/(\d{3})(?!\d*?\.|$)/g, '$1,').split('').reverse().join('');

comma_2(n);
// "123,456,789.01234"

/*
  ピリオド以下を一度取り外す
*/
const comma_3 = num => {
  var a = String(Number(num)).split('.');
  return isNaN(num) || a[0].replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,') + (a[1] ? '.' + a[1] : '');
};

comma_3(n);
// "123,456,789.01234"

こんな具合ですね。

Safariでも後読み実装されないかな…。


追記:
@il9437 さんが正規表現ではなくtoLocaleStringで書いたものをコメント欄の方にご提示くださいました。comma_3のような形式の関数ですが、こちらは非常に直感的で分かりやすいものとなっています。ぜひご確認ください。

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

【JavaScript】Vue-chartjsを使い始める

現場では、Charjsを使用して、グラフを描いています。
勉強ノードをメモします。

Chart.jsの公式サイト:https://www.chartjs.org/

Chartjsを導入する方法:

  1. 外部スクリプト
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
  1. インストール
     開発ディレクトリ内、npmで下記のコマンドでインストールする。
     (スタンドアロン版、バンドル版両方インストールされる)
$ npm install chart.js --save

下記のnode_modulesモジュールとpacage-lock.jsonパッケージが生成される
スクリーンショット 2021-02-13 21.10.27.png

スタンドアロン版とバンドル版

スタンドアロン版
ファイル:
 dist/Chart.js
 dist/Chart.min.js
 (時間軸を使用したい場合は、Moment.jsをインストールする必要)

バンドル版(時間軸を使用可。Moment.jsインストール不要。)
ファイル:
 dist/Chart.bundle.js
 dist/Chart.bundle.min.js

必要に応じて、pathを確認、index.htmlファイルに導入する。例えば:Chart.js

<script src="node_modules/chart.js/dist/Chart.js"></script>

以上の設定は準備でしたら、例のコードを練習します。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 外部スクリプト方法 -->
    <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script> -->
    <!-- インストール方法 -->
    <script src="node_modules/chart.js/dist/Chart.js"></script>  
</head>
<body>
    <p>chart_</p>
    <!-- canvas要素を設置 -->
    <canvas id="myChart" width="400" height="400"></canvas>
    <script>
        // 要素またはコンテキストを取得
        var ctx = document.getElementById("myChart");
        // チャートタイプをインスタンスかする
        var myChart = new Chart(ctx, {
            // 棒グラフ
            type: 'bar',  
            // データを設置
            data: {
                // データのラベル
                labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
                datasets: [{
                    // 凡例
                    label: '# of Votes',
                    // データの量
                    data: [12, 19, 3, 5, 2, 3],
                    // 各データの色
                    backgroundColor: [
                        'rgba(255, 99, 132, 0.2)',
                        'rgba(54, 162, 235, 0.2)',
                        'rgba(255, 206, 86, 0.2)',
                        'rgba(75, 192, 192, 0.2)',
                        'rgba(153, 102, 255, 0.2)',
                        'rgba(255, 159, 64, 0.2)'
                    ],
                    // 棒枠色
                    borderColor: [
                        'rgba(255,99,132,1)',
                        'rgba(54, 162, 235, 1)',
                        'rgba(255, 206, 86, 1)',
                        'rgba(75, 192, 192, 1)',
                        'rgba(153, 102, 255, 1)',
                        'rgba(255, 159, 64, 1)'
                    ],
                    // 棒枠線の厚さ
                    borderWidth: 1
                }]
            },
            // オプション
            options: {
                // スケール設定
                scales: {
                    // 縦スケールを設定
                    yAxes: [{
                        ticks: {
                            // ゼロから表示
                            beginAtZero:true
                        }
                    }]
                }
            }
        });
        </script>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】Vue-chartjsを使い始める ーー実装編

現場では、Charjsを使用して、グラフを描いています。
勉強ノードをメモします。

Chart.jsの公式サイト:https://www.chartjs.org/

Chartjsを導入する方法:

  1. 外部スクリプト
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
  1. インストール
     開発ディレクトリ内、npmで下記のコマンドでインストールする。
     (スタンドアロン版、バンドル版両方インストールされる)
$ npm install chart.js --save

下記のnode_modulesモジュールとpacage-lock.jsonパッケージが生成される
スクリーンショット 2021-02-13 21.10.27.png

スタンドアロン版とバンドル版

スタンドアロン版
ファイル:
 dist/Chart.js
 dist/Chart.min.js
 (時間軸を使用したい場合は、Moment.jsをインストールする必要)

バンドル版(時間軸を使用可。Moment.jsインストール不要。)
ファイル:
 dist/Chart.bundle.js
 dist/Chart.bundle.min.js

必要に応じて、pathを確認、index.htmlファイルに導入する。例えば:Chart.js

<script src="node_modules/chart.js/dist/Chart.js"></script>

以上の設定は準備でしたら、例のコードを練習します。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 外部スクリプト方法 -->
    <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script> -->
    <!-- インストール方法 -->
    <script src="node_modules/chart.js/dist/Chart.js"></script>  
</head>
<body>
    <p>chart_</p>
    <!-- canvas要素を設置 -->
    <canvas id="myChart" width="400" height="400"></canvas>
    <script>
        // 要素またはコンテキストを取得
        var ctx = document.getElementById("myChart");
        // チャートタイプをインスタンスかする
        var myChart = new Chart(ctx, {
            // 棒グラフ
            type: 'bar',  
            // データを設置
            data: {
                // データのラベル
                labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
                datasets: [{
                    // 凡例
                    label: '# of Votes',
                    // データの量
                    data: [12, 19, 3, 5, 2, 3],
                    // 各データの色
                    backgroundColor: [
                        'rgba(255, 99, 132, 0.2)',
                        'rgba(54, 162, 235, 0.2)',
                        'rgba(255, 206, 86, 0.2)',
                        'rgba(75, 192, 192, 0.2)',
                        'rgba(153, 102, 255, 0.2)',
                        'rgba(255, 159, 64, 0.2)'
                    ],
                    // 棒枠色
                    borderColor: [
                        'rgba(255,99,132,1)',
                        'rgba(54, 162, 235, 1)',
                        'rgba(255, 206, 86, 1)',
                        'rgba(75, 192, 192, 1)',
                        'rgba(153, 102, 255, 1)',
                        'rgba(255, 159, 64, 1)'
                    ],
                    // 棒枠線の厚さ
                    borderWidth: 1
                }]
            },
            // オプション
            options: {
                // スケール設定
                scales: {
                    // 縦スケールを設定
                    yAxes: [{
                        ticks: {
                            // ゼロから表示
                            beginAtZero:true
                        }
                    }]
                }
            }
        });
        </script>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsでAdobe Fonts(TypeKit)を使う

Nuxt.jsでもAdobe Fontsを使いたい

静的なHTMLページをNuxt.jsで作り直そうと思ったのですが、Adobe Fontsの埋め込みコードを発行しても、以下のようにピュアなJS用のコードしか発行されない。

AdobeFontsが生成するJS
(function (d) {
  var config = {
      kitId: '各自異なるID',
      scriptTimeout: 3000,
      async: true,
    },
    h = d.documentElement,
    t = setTimeout(function () {
      h.className = h.className.replace(/\bwf-loading\b/g, '') + ' wf-inactive'
    }, config.scriptTimeout),
    tk = d.createElement('script'),
    f = false,
    s = d.getElementsByTagName('script')[0],
    a
  h.className += ' wf-loading'
  tk.src = 'https://use.typekit.net/' + config.kitId + '.js'
  tk.async = true
  tk.onload = tk.onreadystatechange = function () {
    a = this.readyState
    if (f || (a && a != 'complete' && a != 'loaded')) return
    f = true
    clearTimeout(t)
    try {
      Typekit.load(config)
    } catch (e) {}
  }
  s.parentNode.insertBefore(tk, s)
})(document)

これだとプロジェクトにそのまま読み込めない。

Googleで調べてみる

Cannot find name 'Typekit'

Nuxt.jsでいい感じにAdobe fontsのwebフォントを読み込む

こちらの記事の方法でもできるはできるのですが、Cannot find name 'Typekit'.エラーが出てしまい、buildができない。

Vue.jsでのやり方があった

Vue.jsとBootstrapVue上でAdobeの日本語ウェブフォントを使う

pluginとして、Adobe Fontsで生成されたJSを単体ファイルとして作成し、それを読み込むという方法。これならいけそう!

解決

まずはAdobe Fontsで生成されたJSをpluginsで作成

plugins/typekit.js
/* eslint-disable */
// めちゃめちゃ怒られるのでESLintは切っておく
;(function (d) {
  var config = {
      kitId: '各自異なるID',
      scriptTimeout: 3000,
      async: true,
    },
    h = d.documentElement,
    t = setTimeout(function () {
      h.className = h.className.replace(/\bwf-loading\b/g, '') + ' wf-inactive'
    }, config.scriptTimeout),
    tk = d.createElement('script'),
    f = false,
    s = d.getElementsByTagName('script')[0],
    a
  h.className += ' wf-loading'
  tk.src = 'https://use.typekit.net/' + config.kitId + '.js'
  tk.async = true
  tk.onload = tk.onreadystatechange = function () {
    a = this.readyState
    if (f || (a && a != 'complete' && a != 'loaded')) return
    f = true
    clearTimeout(t)
    try {
      Typekit.load(config)
    } catch (e) {}
  }
  s.parentNode.insertBefore(tk, s)
})(document)

作成したtypekit.jsをグローバルに読み込む

nuxt.config.js
export default {
  ~ 省略 ~
  plugins: [
    { src: '~/plugins/typekit.js', mode: 'client' }
  ],
  ~ 省略 ~
}

ここでmode: 'client'オプションをつけておかないと、SSR時に「documentが見つからないよ!」となるので注意。

おしまい

有料だし、あんまりAdobe Fontsを使うことはないかもしれないけどAdobe CC契約している人はぜひ。

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

VueとLaravelでStripeElementsを使ってクレジットカード情報を更新する方法

毎日投稿59日目

毎日記事を更新しているのですが、意外とネタは尽きないものです。

さて今回は、タイトルの通りStripeでクレジットカードの変更を行っていきます。

StripeのAPIについてはそこまで詳しく説明は致しません!

恐らくクレジットカードの情報を変更したいなと思った方は、Stripeの基礎的な部分は理解していると思うので。

イメージとしてはこんな感じです。

無題.png

また、HTMLやCSSの記述が多いのでここらへんはコピペしてください。

話はこの辺にして早速説明を見ていきましょう!

StripeElementsの設置

Stripeでクレジットカードの情報を変更するには、カード番号、有効期限、セキュリティコードを入力してもらいトークンを作成します。

そのトークンを使ってクレジットカードの情報を変更します。

とりあえずStripeElementsの設置

Stripe.vue
<template>
   <div class="group">
      <label for="card-element"> クレジットカード情報 </label>
      <div id="card-element"></div>
   </div>
</template>

<script>
export default {
  data() {
    return {
      stripe: null,
      card: null,
      token: ''
    }
  },
  async mounted() {
    this.stripe = window.Stripe(process.env.VUE_APP_STRIPE_PUBLIC_KEY)
    const elements = this.stripe.elements()
    this.card = await elements.create('card', {
      hidePostalCode: true,
      style: {
        base: {
          iconColor: '#666EE8',
          color: '#31325F',
          lineHeight: '40px',
          fontWeight: 300,
          fontFamily: 'Helvetica Neue',
          fontSize: '15px',

          '::placeholder': {
            color: '#CFD7E0'
          }
        }
      }
    })
    this.card.mount('#card-element')
  },
}
</script>

これでクレジットカードを入力する要素が表示されると思います。

必要項目の入力

次に、住所や名前を入力してもらうフォームを作成します。

ここに関しては先ほどの記述を一旦消してコピペしてください。

CSSの記述も多いので。

Stripe.vue
<template>
  <div class="creditcard-change">
    <div class="cell">
          <form>
            <div class="group">
              <label for="card-element"> クレジットカード情報 </label>
              <div id="card-element"></div>
            </div>
            <div class="group">
              <label>
                <span>氏名</span>
                <input
                  id="name"
                  name="name"
                  type="text"
                  class="field"
                  placeholder="YOSHIHIRO FUJIWARA"
                  autocomplete="name"
                />
              </label>
              <label>
                <span>メールアドレス</span>
                <input
                  id="email"
                  v-model="email"
                  type="email"
                  name="email"
                  class="field"
                  placeholder="zaemonia@example.com"
                  autocomplete="email"
                />
              </label>
            </div>
            <div class="group">
              <label>
                <span>郵便番号</span>
                <input
                  id="postal-code"
                  name="address_line1"
                  type="text"
                  class="field"
                  placeholder="1234567"
                  maxlength="7"
                  autocomplete="postal-code"
                />
              </label>
              <label>
                <span>都道府県</span>
                <input
                  id="address-state"
                  name="address_state"
                  type="text"
                  class="field"
                  placeholder="神奈川県"
                  autocomplete="address-level1"
                />
              </label>
              <label>
                <span>市区町村</span>
                <input
                  id="address-city"
                  name="address_city"
                  type="text"
                  class="field"
                  placeholder="相模原市緑区二本松"
                  autocomplete="address-level2"
                />
              </label>
              <label>
                <span>番地</span>
                <input
                  id="address-line1"
                  name="address-line1"
                  type="text"
                  class="field"
                  placeholder="1-34-10"
                  autocomplete="address-line1"
                />
              </label>
              <label>
                <span>マンション・アパート名</span>
                <input
                  id="address-line2"
                  name="address-line2"
                  type="text"
                  class="field"
                  placeholder="マンションズタワー502"
                  autocomplete="address-line2"
                />
              </label>
            </div>
            <b-button
              class="recaptcha-button-v3"
              type="is-success"
              disabled="true"
              style="width: 100%; padding: 25px 0"
              @click="submit"
            >
              クレジットカードを変更する
            </b-button>
          </form>
        </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      stripe: null,
      card: null,
      token: ''
    }
  },
  async mounted() {
    this.stripe = window.Stripe(process.env.VUE_APP_STRIPE_PUBLIC_KEY)
    const elements = this.stripe.elements()
    this.card = await elements.create('card', {
      hidePostalCode: true,
      style: {
        base: {
          iconColor: '#666EE8',
          color: '#31325F',
          lineHeight: '40px',
          fontWeight: 300,
          fontFamily: 'Helvetica Neue',
          fontSize: '15px',

          '::placeholder': {
            color: '#CFD7E0'
          }
        }
      }
    })
    this.card.mount('#card-element')
  },
  methods: {
    submit() {
      const options = {
        name: document.getElementById('name').value,
        email: document.getElementById('email').value,
        address_line1: document.getElementById('address-line1').value,
        address_line2: document.getElementById('address-line2').value,
        address_city: document.getElementById('address-city').value,
        address_state: document.getElementById('address-state').value,
        address_zip: document.getElementById('postal-code').value,
        address_country: 'JP'
      }
      Object.keys(options).forEach((value) => {
        if (value === '') {
          alert('未入力の項目があります')
        }
      })
      this.stripe.createToken(this.card, options).then((result) => {
        // エラーの場合
        if (result.error) {
          alert(result.error.message)
          // 成功の場合
        } else {
          this.token = result.token.id
        }
      })
    },
  }
}
</script>

<style scoped>
.creditcard-change {
  padding: 120px 0;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  min-height: 100%;
  width: 550px;
  margin: 0 auto;
}
.creditcard-change > div {
  flex: 0 0 100%;
}
/* クレジットカード入力欄 */
.cell {
  margin-top: 30px;
  background: #e6ebf1;
  padding: 30px;
}
.group {
  background: white;
  box-shadow: 0 7px 14px 0 rgba(49, 49, 93, 0.1), 0 3px 6px 0 rgba(0, 0, 0, 0.08);
  border-radius: 4px;
  margin-bottom: 20px;
}

.cell label {
  position: relative;
  color: #8898aa;
  font-weight: 300;
  height: 100%;
  line-height: 40px;
  margin-left: 20px;
  display: flex;
  flex-direction: row;
}

.group label:not(:last-child) {
  border-bottom: 1px solid #f0f5fa;
}

label > span {
  width: 120px;
  text-align: right;
  margin-right: 30px;
}

.field {
  background: transparent;
  font-weight: 300;
  border: 0;
  color: #31325f;
  outline: none;
  flex: 1;
  padding-right: 10px;
  padding-left: 10px;
  cursor: text;
}

.field::-webkit-input-placeholder {
  color: #cfd7e0;
}

.field::-moz-placeholder {
  color: #cfd7e0;
}

.outcome {
  float: left;
  width: 100%;
  padding-top: 8px;
  min-height: 24px;
  text-align: center;
}
</style>

b-button初めて気になった方はこちらの記事をご覧ください。

初心者必見!サイト制作は楽してなんぼ。CSSフレームワークBuefyの紹介!!

Buefyはほんとにおススメなのでぜひ使ってください!

クレジットの更新

submitで取得したresult.token.idをバックエンドに送ってください。

適当にaxiosとかで。

そしたら、Laravelのコントローラーでクレジットカードを更新します。

StripeController.php
        public function creditUpdate(Request $request) {
        // ユーザーのクレジットカード情報の更新
        require_once(__DIR__.'/../../../vendor/autoload.php');

        $secret_key = config('app.STRIPE_SECRET_KEY');

        $stripe = new \Stripe\StripeClient($secret_key);

        // カスタマーID、トークンの取得
        $customer_id = $request->id;
        $token = $request->token;

        $customer = $stripe->customers->retrieve(
            $customer_id,
            []
        );

        // クレジットを登録していない場合、処理を終了
        if(!$customer) {
            return response()->json([
                'message' => 'The customer is undefiend',
            ], 400);
        }

        // ソースの作成
        $new_card = $stripe->customers->createSource(
            $customer_id,
            ['source' => $token]
        );
        $customer->default_source = $new_card->id;
        $customer->save();

        return response()->json([
            'data' => $new_card,
            'message' => 'Updating customer`s paymentmethods is success',
        ], 200);
    }

$stripe->customers->createSourceに先ほど取得したトークンをソースとして登録することでクレジットカードの情報を更新することができます。

save()忘れないように!!

実際に変更をされているかはStripeの画面から確認して見て下さい!

いかがだったでしょうか??

記述が多いので嫌かもしれないですが、本当にコピペしてもらって結構です。

むしろコピペしてください!

また、クレジットカードの情報を取得したい場合はStripeの公式ドキュメントを参照してください!

https://stripe.com/docs/api

以上、「VueとLaravelでStripeElementsを使ってクレジットカード情報を更新する方法」でした!

良ければ、LGTM、コメントお願いします。

また、何か間違っていることがあればご指摘頂けると幸いです。

他にも初心者さん向けに記事を投稿しているので、時間があれば他の記事も見て下さい!!

Thank you for reading

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

【Vue3】オブジェクトのプロパティを追加してもリアクティブに動作するようになりました

【Vue.js】オブジェクトのプロパティを追加してもリアクティブに動作しなかった時の備忘録にて、Vue2.xにおいて、オブジェクトのプロパティを追加してもリアクティブに動作しないこと、またその解決策について書きました。

Vue3で同じことをしようとすると、どのような挙動になるのか確認してみました。結論を先に言ってしまうと、Vue3からオブジェクトにプロパティを追加した場合もリアクティブに動作するようになりました。

Vue3環境の準備

vue-cliコマンドを利用してVue3をインストールしていきます。vue createコマンドでVue 3 Previewを選択します。(vue-cliがv4.5以上でないとVue3環境は作成できません。)

vue create study-vue3
? Please pick a preset: Default (Vue 3 Preview) ([Vue 3] babel, eslint)

(ディレクトリ名はstudy-vue3としました。)

npm run serveを実行して以下の画面が確認できれば、インストールは無事に完了しています。

image.png

HelloWorldを書き換える

オブジェクトのプロパティがv-ifに指定されるようHelloWorld.vueを以下に書き換えます。

<template>
  <div>
    <button @click="show">表示する</button>
    <p v-if="hoge.isShow">表示</p>
  </div>
</template>

<script>
import { reactive } from 'vue'
export default {
  name: 'HelloWorld',
  setup() {
    const hoge = reactive({})
    const show = () => {
      hoge.isShow = true
    }
    return { hoge, show }
  }
}
</script>

Vue2.xの場合、ボタンを押しても表示の文字が出ないことは、【Vue.js】オブジェクトのプロパティを追加してもリアクティブに動作しなかった時の備忘録の記事で説明しましたが...

Vue3から導入されたreactiveメソッドによってこの課題が解決されました。

reactiveメソッドから返されるオブジェクトは、プロパティが追加された場合でもリアクティブに動作させる事が可能になります。??

Reactivity API
The reactive conversion is "deep"—it affects all nested properties.

【動作イメージ】
Kapture 2021-02-13 at 01.30.39.gif

Vue3環境でVue2.xコードを書いて動作確認してみる

Vue2.xでは、リアクティブに動作しなかったコードを、Vue3環境で動かすとどうなるのか確認してみました。

<template>
  <div>
    <button @click="show">表示する</button>
    <p v-if="hoge.isShow">表示</p>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      hoge: {
        // isShow: false, コメントアウトで未定義にしています
      }
    }
  },
  methods: {
    show () {
      this.hoge.isShow = true
    }
  }
}
</script>

Vue3ではリアクティブなプロパティとして認識されるようになっていました。
Kapture 2021-02-13 at 01.30.39.gif

この挙動になる理由について記載されたリファレンス等は見つかり次第追記していきます。?

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

【Vue】Vuetify.jsにコントリビュートするための準備

スクリーンショット 2021-02-14 1.54.29.png

Vuetify.jsにコントリビュートしてみたい!という方に開発環境の構築を本記事でまとめました。

コードをcloneする

git clone https://github.com/vuetifyjs/vuetify.git

以降のコマンドはすべてcloneで取得したvuetifyディレクト配下で実行しています。

yarnコマンドを使用できるよう準備

  • npmからyarnをインストールする
npm install -g yarn

パッケージをインストールする

yarn

ビルドの実行

yarn build

開発サーバの起動

  • 起動する前に、packages/vuetify/dev/Playgroud.vue を以下に書き換える
packages/vuetify/dev/Playgroud.vue
<template>
  <v-container>
    <p>ただいま開発環境の動作確認中</p>
  </v-container>
</template>

<script>
  export default {
    data: () => ({
    //
    }),
  }
</script>

  • 開発サーバを起動する
yarn dev

image.png

Playgroud.vueの内容がブラウザから確認できれば、vuetify.jsの開発環境構築は完了です。

issueで上がっているバグを再現する際は、Playground.vueにコピペすると大抵は再現確認できるので、改修作業を行う際はPlaygroud.vueを利用しましょう。?

PRを出す際は、既にMergeされているPRを参考に作成すると、レビューを見てもらいやすくなります。報告内容をFixes:xxxだけにしないで、修正前後の挙動の違いについても記載すると良さそうです。

参考記事:https://vuetifyjs.com/en/getting-started/contributing/#working-with-github

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

ES2015における{ClassName}構文についての解説

ES2015における{ClassName}構文についての解説

1.前提知識となりますが、クラスとは、function型のインスタンスである。例として、任意のクラスClass01を定義し、typeof Class01を実行して戻り値が'function'であることをそれを証明します。補足の例として、下記構文を参考に。

    // 下にあるClass01定義と同じクラス(constructor)を定義している
    let Class01 =
      class Class01 {
        p1: string = '';
     }

    class Class01 {
      p1: string = '';
    }

2.{ClassName}についての説明です、同じ効果を持つ二つの構文を参考に

    // 下にある構文と同じ効果を持つ
    let v1 = { Class01 };

    let v1: { Class01: typeof Class01 } = {
      Class01: Class01  //プロパティClass01: function型の値Class01
    }

3.補足:Typeofオペレーターは実行時のみ働く仕様のようです。(同じfunctionなのにtypeof Class01 <> typeof Class02)

    class Class02 {
      p2: string = '';
    }
    let v2: { Class01: typeof Class01 } = {
      Class01: Class02  //エラー:typeof Class01 <> typeof Class02
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Express】静的ファイルをホスティングする方法

プログラミング勉強日記

2021年2月13日
静的ファイルのホスティングをする方法を間違えていたため、以下のようなエラーが出た。

net::ERR_ABORTED 404 (Not Found)

Expressで静的ファイルをホスティングする方法

 Expressで、 画像、CSSファイル、JSファイルなどの静的ファイルをホスティングするためには標準で組み込まれているexpress.staticを使用する。

関数のシグネチャ
express.static(root, [options])

 express.staticの基本的な使い方は、Applicationオブジェクトのuse()を使ってミドルウェアの設定を行う。

ファイルの構造
app.js
public/
    +-- images/
    +-- js/
    +-- css/
app.js
app.use(express.static(__dirname + '/public'));
app.jsの全体のコード
var express = require('express');
var app = express();

app.use(express.static('public', { hidden: true }));
app.listen(process.env.PORT || 7000);

参考文献

Serving static files in Express
javascript - JSファイルはnet::ERR_ABORTED 404(Not Found)を取得します

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

Node.js からC++関数への引数、返り値まとめ

はじめに

  • node.jsからC++関数を利用する際に、引数・返り値の渡し方について困ったことはありませんか?
    • この記事では、node-addon-api を使う場合の、引数・返り値の受け渡し方をまとめています。

環境構築がまだの方は?

目次

1. 関数の基本形

  • node-addon-apiを利用する場合、ラップされるC++の関数は以下のように定義します。
    • Napi::Value 関数名(const Napi::CallbackInfo& info);
      • 引数の個数、型に関係なく、引数は const Napi::CallbackInfo& infoと記述
      • 返り値は、型に関係なくNapi::Valueと記述
  • 以下は、関数宣言と関数定義の例です。
    • 引数、返り値ともにvoidです。
void.h
#include <napi.h>
Napi::Value func(const Napi::CallbackInfo& info);
void.cpp
#include <napi.h>

Napi::Value func(const Napi::CallbackInfo& info) {
  // Do nothing.
  return env.Null();
}

2. 値の受け渡し

  • js ↔︎ C++でやり取りするにあたって必要な引数、返り値の処理についてまとめました。

引数の型対応表

C++型 napi型
int .AsNapi::Number().Int32Value()
double .AsNapi::Number().DoubleValue()
std::string .AsNapi::String().ToString()

返り値の型対応表

C++型 napi型
int, double return Napi::Number::New(env, C++変数名)
std::string return Napi::String::New(env, C++変数名)
  • jsは数値型が1種類しかないため、数値であればNapi::Number型で良い

関数例

  • 例として、jsから2つの引数(a,b)をC++で受け取り、その和をjsに返却する関数add()を考えてみましょう。
example.cpp
#include <napi.h>

Napi::Value add(const Napi::CallbackInfo& info) {
  // お約束
  Napi::Env env = info.Env();

  // 引数は、配列infoから取り出す。
  double a = info[0].As<Napi::Number>().DoubleValue();
  double b = info[1].As<Napi::Number>().DoubleValue();

  // C++で行いたい処理を行う
  double ans = a + b;

  // 返り値は、Napi::○○ 型にキャストして返却する
  return Napi::Number::New(env, ans);
}
  • jsファイルからは次のように見えます
example.js
// (前処理は省略)
let ans = add(1,2)
console.log(ans);
// >> expected: 3

3. 配列の受け渡し

 引数に配列を渡す( js → C++ )

array.cpp
Napi::Value setArr(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  Napi::Array arr = info[0].As<Array>();

  // C++の配列
  std::vector<double> vec(arr.Length(), 0.0);

  // for文で要素を順に代入
  for(size_t i = 0; i < arr.Length(); i++){
    Napi::Value val = arr[i];
    vec[i] = val.As<Napi::Number>().DoubleValue();
  }

  return env.Null();
}

 返り値に配列を渡す( C++ → js )

array.cpp
Napi::Value getArr(const CallbackInfo& info){
  Napi::Env env = info.Env();

  // C++の配列
  std::vector<double> vec = {1.0, 0.5, 0.25};

  // for文で要素を順に代入
  Napi::Array outArr = Napi::Array::New(env, vec.size());
  for (size_t i = 0; i < vec.size(); i++) {
      outArr[i] = Napi::Number::New(env, vec[i]);
  }
  return outArr;
}
  • jsファイルからは次のように利用します。
array.js
// (前処理は省略)
setArr([1,2,3,4,5]);

var arr = getArr();
console.log(arr);
// >> expected: [1.0, 0.5, 0.25]

4. (応用)異なるプリミティブ型を配列に入れて返却する

  • C++で複数の返り値を返したい場合に有効です。
    • リターンコードと計算結果の組み合わせなどを返却できます。
array2.cpp
Napi::Value getReturns(const CallbackInfo& info){
  Napi::Env env = info.Env();

  // do Something C++

  // 返り値として、 1.0 と "aabbcc" を返却する例
  const int returnArgNums = 2;
  int zero = 0;
  int one = 1;
  Napi::Array retArr = Napi::Array::New(env, returnArgNums);
  retArr[zero] = Napi::Number::New(env, 1.0);
  retArr[one] = Napi::String::New(env, "aabbcc");
  return retArr;
}
array2.js
// (前処理は省略)
var arr = getReturns();
console.log("ret1 =", arr[0], "ret2 =", arr[1]);
// >> expected: ret1 = 1 ret2 = aabbcc

5. 試してみよう

  • 前回の記事 で作成したプロジェクトのwrapper.h, wrapper.cpp, index.js を下記のコードで上書きすると、本記事の内容を試すことができます!

 ├── node_modules
 ├── package-lock.json
 ├── package.json
 ├── index.js <-- 上書き
 ├── addon.cc
 ├── wrapper.cc <-- 上書き
 ├── wrapper.h <-- 上書き
 └── binding.gyp

サンプルコードはこちら

wrapper.hを開く
wrapper.h
#ifndef WRAPPER
#define WRAPPER

#include <napi.h> // 必要なヘッダ


class Wrapper : public Napi::ObjectWrap<Wrapper> {
public:
    static Napi::Object Init(Napi::Env env, Napi::Object exports);
    static Napi::Object NewInstance(Napi::Env env, const Napi::CallbackInfo& info);

    Wrapper(const Napi::CallbackInfo& info);
    ~Wrapper();

    Napi::Value getNum(const Napi::CallbackInfo& info);
    Napi::Value add(const Napi::CallbackInfo& info); // <-- 追加
    Napi::Value setArr(const Napi::CallbackInfo& info); // <-- 追加
    Napi::Value getArr(const Napi::CallbackInfo& info); // <-- 追加
    Napi::Value getReturns(const Napi::CallbackInfo& info); // <-- 追加
private:
    double m_value;
};

#endif

wrapper.ccを開く
wrapper.cc
#include "wrapper.h"
#include <napi.h>

using namespace Napi;

// ---------------------------------------------------------- //
// ---------------------のり付け部分--------------------------- //
// ---------------------------------------------------------- //
// new() の定義
Napi::Object Wrapper::NewInstance(Napi::Env env, const Napi::CallbackInfo &info)
{
    Napi::EscapableHandleScope scope(env);
    // jsからコンストラクタに渡されるArgsは infoに配列として入っている
    const std::initializer_list<napi_value> initArgList = {info[0]};
    // ここでWrapper:::Wrapper()が呼ばれる
    Napi::Object obj = env.GetInstanceData<Napi::FunctionReference>()->New(initArgList);
    // gcにメモリ解放されないようにスコープを除外する
    return scope.Escape(napi_value(obj)).ToObject();
}

// メンバ関数のバインド
Napi::Object Wrapper::Init(Napi::Env env, Napi::Object exports)
{
    Napi::Function func = DefineClass(
        env, "Wrapper", {
            // ここにメソッドを登録する
            InstanceMethod("getNum", &Wrapper::getNum),
            InstanceMethod("add", &Wrapper::add), // <-- 追加
            InstanceMethod("setArr", &Wrapper::setArr), // <-- 追加
            InstanceMethod("getArr", &Wrapper::getArr), // <-- 追加
            InstanceMethod("getReturns", &Wrapper::getReturns), // <-- 追加
        });

    Napi::FunctionReference *constructor = new Napi::FunctionReference();
    *constructor = Napi::Persistent(func);
    env.SetInstanceData(constructor);

    exports.Set("Wrapper", func);
    return exports;
}

// ---------------------------------------------------------- //
// --------------- Wrapperクラスの定義はこれより下 --------------- //
// ---------------------------------------------------------- //

// コンストラクタ
Wrapper::Wrapper(const Napi::CallbackInfo &info)
    : Napi::ObjectWrap<Wrapper>(info)
{
    m_value = 0.0;
};

Wrapper::~Wrapper(){};

// メンバ関数
Napi::Value Wrapper::getNum(const Napi::CallbackInfo &info)
{
    Napi::Env env = info.Env();
    return Napi::Number::New(env, this->m_value);
}

// 引数・返り値の受け渡し
Napi::Value Wrapper::add(const Napi::CallbackInfo& info) {
  // お約束
  Napi::Env env = info.Env();

  // 引数は、配列infoから取り出す。
  double a = info[0].As<Napi::Number>().DoubleValue();
  double b = info[1].As<Napi::Number>().DoubleValue();

  // C++で行いたい処理を行う
  double ans = a + b;

  // 返り値は、Napi::○○ 型にキャストして返却する
  return Napi::Number::New(env, ans);
}

// 配列の受け取り
Napi::Value Wrapper::setArr(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  Napi::Array arr = info[0].As<Array>();

  // C++の配列
  std::vector<double> vec(arr.Length(), 0.0);

  // for文で要素を順に代入
  for(size_t i = 0; i < arr.Length(); i++){
    Napi::Value val = arr[i];
    vec[i] = val.As<Napi::Number>().DoubleValue();
  }

  return env.Null();
}

// 配列の返却
Napi::Value Wrapper::getArr(const CallbackInfo& info){
  Napi::Env env = info.Env();

  // C++の配列
  std::vector<double> vec = {1.0, 0.5, 0.25};

  // for文で要素を順に代入
  Napi::Array outArr = Napi::Array::New(env, vec.size());
  for (size_t i = 0; i < vec.size(); i++) {
      outArr[i] = Napi::Number::New(env, vec[i]);
  }
  return outArr;
}

// プリミティブ型が混在した配列の返却
Napi::Value Wrapper::getReturns(const CallbackInfo& info){
  Napi::Env env = info.Env();

  // do Something C++

  // 返り値として、 1.0 と "aabbcc" を返却する例
  const int returnArgNums = 2;
  int zero = 0;
  int one = 1;
  Napi::Array retArr = Napi::Array::New(env, returnArgNums);
  retArr[zero] = Napi::Number::New(env, 1.0);
  retArr[one] = Napi::String::New(env, "aabbcc");
  return retArr;
}

index.jsを開く
index.js
// addon.cc内の NODE_API_MODULE(addon, InitAll) が呼ばれる
var Wrapper = require('bindings')('addon');

// addon.cc内の CreateObject() が呼ばれる
var obj = new Wrapper()

// wrapper.cc内で登録した getNum()が呼ばれる
console.log(obj.getNum());


// ---- 本記事で追加した関数 ---- //
let ans = obj.add(1,2)
console.log(ans);
// >> expected: 3
obj.setArr([1,2,3,4,5]);

var arr = obj.getArr();
console.log(arr);
// >> expected: [1.0, 0.5, 0.25]

var arr = obj.getReturns();
console.log("ret1 =", arr[0], "ret2 =", arr[1]);
// >> expected: ret1 = 1 ret2 = aabbcc

// ---- 本記事で追加した関数 ---- //

その他

  • 返り値にユーザー定義型やオブジェクトを返す場合は このあたり が参考になるかもしれません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

音楽理論の学習・音楽制作支援ウェブアプリを作りました。

こんにちは!クフルダモノーツYoshito Kimura(k1mu)です!

音楽理論の学習・音楽制作支援ウェブアプリ「O-TO」を作りました。

音楽を20年以上やっている僕が「あったら良いな」と思う機能を具現化しました。

プログラミングは得意ではないので、技術的に優れているかは分かりません。
ただ、機能的にはめちゃくちゃ便利だと思います。(๑˃̵ᴗ˂̵)و

O-TO」について

基本的には音楽に関する「音楽の本質と関係無い思考を肩代わりしてくれるアプリ」です。

たとえば、音符の長さの計算を頑張っても、言って音楽が良くなるわけではありません。むしろ疲れます。
音楽理論の学習も、難しい文章から読み解くより、視覚的に分かりやすく勉強できた方が楽です。

そういった“創造の本質”とは関係の無い部分に、余計な頭脳と時間を使わないために活用してもらえればな と思います。

作った経緯

個人的に音楽の作業には「コレは無駄じゃ無いか?」と感じるものがたくさんありました。

たとえば、ディレイタイムを計算したり、五度圏で音の流れを考える作業などです。

これらの作業は、音楽の良し悪しには関係ありません。
むしろ、瞬時に結果が分かった方が試行錯誤の時間が多く確保できます。

また、僕は音楽理論の学習に苦戦した経験から
「もっと音楽の専門知識を活用する敷居を下げたい」気持ちがずっとありました。
(音楽理論関係のブログを書いているのもそのため)
https://khufrudamonotes.com/category/music-theory-and-dtm

そういうわけで、「音楽制作や音楽理論の学習を補助するウェブアプリ」の目指して出来上がったのが「O-TO(Ongaku Tools)」です。

現在提供している機能と、その説明

コードネーム

主な和音の構成音を視覚的に確認できます。
コードを学ぶ方は、解説図を見ながら表示を切り替えてみると、コードネームの仕組みが分かってくると思います。

最終的には(特にジャズをやる方は)、頭の中でコードの構成音がパッと把握出来た方が良いかもしれません。
ただ、僕自身は出来るまで10年近くかかった気がするので、補助輪としても使えると思います。

スケール

主要な音階の構成音を視覚的に確認できます。
こちらも、初学者の方がスケールを学ぶのに役立つはずです。

少し変わったスケールも登録しておいたので
色々なスケールを試してみたいときに、いちいちキーの変更を頭の中で考えなくて良いので楽な筈です。

モーダル・インターチェンジ

モーダルインターチェンジの候補を絞り込む機能です。

コード知識がある人は、特定のコード上で使用できるスケールを探すのにも使えます。
たとえば、ドミナント7thコード上でアドリブできるスケールと
テンションノートの関係などは視覚的に理解しやすいと思います。

コード進行

良く使用されるコード進行を50種類以上まとめました。

オリジナル曲を作る方なんかは、ネタに困った時に便利だと思います。

しかも、表示を全てのキーに切り替えられます。
(ピアノの黒鍵に当たる異名同音をちゃんと処理しているところが、個人的な頑張りポイントです。)

ダイアトニック・コード

主なダイアトニックコードの一覧表です。
こちらも全てのキーに表示を切り替えられます。

ちなみに、僕は大体この表の環境を概ね「1つの環境」として頭の中で捉えていて、
この環境から離れる場合に「転調している」と考える場合が多いです。

転調の間隔

転調の間隔を簡単に調べられます。

転調と言っても、キーが半音上がるくらいは簡単に分かると思います。
しかし、モードチェンジを絡めた複雑な転調とそのキー(調号)の判定を頭の中でやるのはなかなか大変だと思います。

この機能を使えば、どんな複雑な関係にあるキーの転調も一瞬で分かります。

音価の計算

シンプルに音価の計算ができるだけです。

ここまで紹介した機能は、音楽を長くやっていればある程度脳内で対応できる人も多いと思います。
しかし、各BPMごとの音価を暗記している人は少ないんじゃないでしょうか。
ある意味一番便利な機能かもしれません。

メトリック・モジュレーション

メトリックモジュレーションに関係する情報を調べられます。
メトリックモジュレーションは、整合性を持ったテンポチェンジなどに使われる手法です。

おまけで音符を「分音符のみで表記した場合」や「符点音符のみで表記した場合」も計算される機能をつけました。
「現在の記譜法」の上手くできている部分と、そうでない部分を感じれる気がします。笑

プログラミングの技術的な内容など

こちらの記事にまとめました!(๑˃̵ᴗ˂̵)و

・プログラミング何も分からない初心者が、独学でウェブアプリを作るまでの軌跡 まとめ
https://qiita.com/k0419/items/292d10f729992e7e22e3

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

Vueでローディング画面の実装

概要

Vueでローディング画面を作成します。

こういうの↓
ダウンロード (2).gif

実装

ローディングのモジュールはいくつかありますが、今回はvue-loadersというモジュールを利用します。
アニメーションの種類が多かったのでこれにしました。

導入方法はこちら
github

今回はmounted()のタイミングでローディングを開始し、mounted()が終わったタイミングでローディングを終了したいと思います。

store

store/loading.js
const state ={
    loading: true, //true:ローディングを表示, false:ローディング非表示
};

const getters ={
    loading: state => state.loading ? state.loading: '',
};

const mutations ={
    setLoading(state,loading){
        state.loading = loading;
    },
};
const actions = {
    async startLoad(context){
        context.commit('setLoading', true);
    },
    async endLoad(context){
        context.commit('setLoading', false);
    } 
};

export default{
    namespaced: true,
    state,
    getters,
    mutations,
    actions
}

ローディングが開始したらstartLoadを発火、終わったらendLoadを発火して非表示にします。

template

<template>
    <div class="new-article">
            <div class="loader-space" v-show="loading">
                <vue-loaders-ball-beat color="#FFF" class="loader"></vue-loaders-ball-beat>
            </div>
            <div class="contents" v-show="!loading">
            <!--ローディング後に表示したいコンテンツ-->
            </div>
        </div>

</template>
<script>
export default {
    computed: {
        loading(){
            return this.$store.getters["loading/loading"];
        },
    },
    methods: {
        async getPost() {
            const post = this.$store.getters["newtimeline/post"];
            if(!post){
                await this.$store.dispatch('newtimeline/getPost');
            }
        },
    },
    mounted() {
        console.log('mounted start');
        this.$store.dispatch('loading/startLoad')               //ローディング開始をする loading = true;
            .then(()=>this.getPost())                           //getPost()メソッドを開始
            .then(()=>this.$store.dispatch('loading/endLoad')); //ローディング終了 loading = false;
    },
}
</script>

非同期処理で動かさないとendLoad()の方が先に走ってしまうので、非同期で実装しましょう。

最後に

割と簡単に実装することができました。
面白いアニメーションもあり、使ってて楽しかったです。

参考文献

Vue.jsで28種類のローディングアニメーションを実装する「vue-loaders」

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

【Vue.js】はじめてのVue.js〜概要+インスタンス+ディレクティブ〜

今回はJavaScriptのフレームワークである「Vue.js」についての記事です。学習途中で至らない点等あると思うのですが、備忘録兼自分と同じ初学者の方向けにまとめてみました。

現在はVue.jsを用いてポートフォリオを書き換えつつ、学習を進めています。記述したコードも用いているので、是非参考にしてみてください。ポートフォリオはこちら、GitHubはこちらのページをご覧ください。ポートフォリオ内の学習スキルを記述した以下の部分と
代替テキスト
ヘッダーのアイコン部分を
代替テキスト
の2つを例にインスタンス、ディレクティブの書き方をまとめています。

目次

  1. 概要

    1. Vue.jsとは
    2. メリット
    3. 環境準備
  2. インスタンス

    1. インスタンスとは
    2. 書き方
    3. elオプションオブジェクト
    4. dataオプションオブジェクト
    5. methodsオプションオブジェクト
  3. ディレクティブ

    1. ディレクティブとは
    2. v-bind
    3. v-on
    4. v-html
    5. v-for
  4. 最後に

1. 概要

1.1 Vue.jsとは

JavaScriptのフレームワーク1の一種。DOM操作を自動化して効率良く、そしてコードを複雑にしすぎることなくシンプルにフロントエンドの開発が行うことができる。
Vue.jsという土台の上で、ライブラリを用いて必要な機能を追加していくことで、効率良く開発を進めることができる。

1.2 メリット

扱うデータやイベントが多くなっても、コードが複雑になりにくいことが挙げられる。理由は大きく分けて2つある。

①DOM操作を自動で行ってくれる
今回の記事で紹介するインスタンスやディレクティブを用いることで実現することができる。詳細は以下の「2.インスタンス」「3.ディレクティブ」を参照。

②DOM要素を部品化して使い回すことができる
こちらは次の記事で紹介するコンポーネントを用いることで実現することができる。詳細は次回更新する記事に記載。

1.3 環境準備

HTMLファイルにCDN2を読み込む。至ってシンプル。HTMLのファイル名をindex.htmlとする。

index.html
<!-- 開発バージョン、便利なコンソールの警告が含まれています -->
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

または

index.html
<!-- 本番バージョン、サイズと速度のために最適化されています -->
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>

2. インスタンス

2.1 インスタンスとは

一言で表すと、複製したオブジェクト3のこと。

2.2 書き方

new Vue({...}) によって、Vueオブジェクトのインスタンスを作る。今回はskillsというインスタンスを作成している。

main.js
const skills = new Vue({
    el:'#skills',
    data:{
        skills:[
            {
                id:'frontend_skills',
            },
            //他のバックエンド、DataBase等も同様なので省略   
        ],
    },
})

const header = new Vue({
    el:'header',
    methods:{
        scroll:function(event){
            switch(event.currentTarget){
                case $('.about-icon')[0]:
                    const about = $('#about').offset().top;
                    $("html, body").animate({ scrollTop: about - 20 }, 200);
                    break;
                //他のcaseも同様なので省略
            }
        }
    }
})

newは演算子の一つ、Vue({...})は、オブジェクトを複製するための特別な関数(コンストラクタ)である。コンストラクタの引数には、オブジェクトを渡す。Vue.jsではこのオブジェクトを、オプションオブジェクトという。オプションオブジェクトに決められたプロパティを設定することで、Vue.jsの機能を利用することができる。

skillsインスタンスではelオプションオブジェクトdataオプションオブジェクトを、headerインスタンスではelオプションオブジェクトmethodsオプションオブジェクトを使用している。

2.3 elオプションオブジェクト

element(要素)の略称。DOM要素を設定する。
#skillsでは、id属性がskillsの要素を設定している。この要素内でのみインスタンスの中身を使用することができる。

2.4 dataオプションオブジェクト

データを設定する。このデータにはVueインスタンスのプロパティとしてアクセスできる。

index.html
<div id="skills">
  <p>{{ skills[0].id }}</p>
</div>

このように記述することで、インスタンスのdataオプションオブジェクト内のskills配列のインデックス番号0番目要素のidプロパティの値を表示することができる。
{{...}}をMustache構文といい、インスタンスのdataオプションオブジェクト内のデータとHTMLを結びつけることができる。これをデータバインディングという。
ただし、Mustache構文では値をテキストデータとして扱うため、プロパティの値に<br><span>要素を記述しても、文字列として表示されてしまう。HTMLへの変換はv-htmlディレクティブを用いる必要がある。

2.5 methodsオプションオブジェクト

elで設定したDOM要素内で関数を使用したい場合に用いる。v-onディレクティブで指定することで関数を使用することができる。

3. ディレクティブ

3.1 ディレクティブとは

「2.4 dataオプションオブジェクト」で紹介したように、Mustache構文を用いることで、DOM要素とインスタンスのデータをバインディングすることができた。
しかし、DOM要素の属性とインスタンスのデータをバインディングする際にMustache構文は使うことができない。
そこで登場するのがディレクティブ。一言で表すと、Vue.jsで使用する特別な属性のこと。

3.2 v-bind

上記の例で、インスタンスのdataオプションオブジェクト内のskills配列のインデックス番号0番目要素のidプロパティの値を表示するのではなく、<p>のid属性に設定したい場合は、以下のように記述する。

index.html
<div id="skills">
  <p v-bind:id="skills[0].id">v-bindディレクティブ</p>
</div>

v-bind:の後に属性を指定し、属性値にインスタンスのdataオプションオブジェクトのプロパティを記述する。

動的なclass属性のバインディングは少し特殊なのでメモしておく。
v-bind:classにオブジェクトを渡すことでクラスを動的に切り替えることができる。その際、クラスの付与をデータプロパティの真偽性によって決めている。参考までに、ドキュメントの例を載せておく。
以下のような要素があったとする。isActivetrueの場合はactiveクラスが付与され、hasErrortrueの場合はtext-dengerクラスが付与されることを表している。

<div
  class="static"
  v-bind:class="{ active: isActive, 'text-danger': hasError }"
></div>

インスタンス内のdataオプションオブジェクトは以下のものだったとする。

data: {
  isActive: true,
  hasError: false
}

すると、以下のように特定のクラスのみを付与することができる。

<div class="static active"></div>

3.3 v-on

「2.5 methodsオプションオブジェクト」で紹介したmethodsオプションオブジェクトをDOM要素内で使用する際に用いる。

index.html
<header>
  <span v-on:click="scroll"><i class="fas fa-user-alt"></i></span>
  <!-- 他のアイコンも同様なので省略 -->
</header>
main.js
const header = new Vue({
    el:'header',
    methods:{
        scroll:function(event){
            switch(event.currentTarget){
                case $('.about-icon')[0]:
                    const about = $('#about').offset().top;
                    $("html, body").animate({ scrollTop: about - 20 }, 200);
                    break;
                //他のcaseも同様なので省略
            }
        }
    }
})

methodsオプションオブジェクトを使用したいDOM要素にv-onディレクティブを記述する。
v-on:click="scroll"のように、v-on:イベントの発生条件:"イベントハンドラ4"の形で記述する。

3.4 v-html

「2.4 dataオプションオブジェクト」で紹介したように、Mustache構文を用いることでDOM要素とインスタンスのデータをバインディングすることができるが、値をテキストデータとして扱ってしまう。値に<br><span>を使用してDOM要素内で反映させたい場合、v-htmlディレクティブを用いる。

main.js
const skills = new Vue({
    el:'#skills',
    data:{
        skills:[
            {
                show:'<strong>フロントエンド</strong>',
            },
            //他のバックエンド、DataBase等も同様なので省略   
        ],
    },
})
index.html
<div id="skills">
  <p v-html="skills[0].show">v-htmlディレクティブ</p>
</div>

このように記述することで、インスタンスのdataオプションオブジェクト内のskills配列のインデックス番号0番目要素のshowプロパティの値を生のHTMLとして出力することができるため、<strong>が反映される。

3.5 v-for

繰り返し処理を行うために用いる。
例えば、以下のskills配列のshowプロパティの値を全てDOM要素にバインディングして表示したいとする。

main.js
const skills = new Vue({
    el:'#skills',
    data:{
        skills:[
            {
                id:'frontend_skills',
                show:'フロントエンド',
            },
            {
                id:'backend_skills',
                show:'バックエンド',
            },
            {
                id:'database_skills',
                show:'DataBase',
            },
            {
                id:'infrastructure_skills',
                show:'インフラ',
            },
            {
                id:'others',
                show:'その他',
            },
            {
                id:'stydying',
                show:'学習中',
            },

        ],
    },
})

「2.4 dataオプションオブジェクト」で紹介したように、Mustache構文を用いて以下のように記述することができる。

index.html
<div id="skills">
  <p v-bind:id="skills[0].id">{{ skills[0].show }}</p>
  <p v-bind:id="skills[1].id">{{ skills[1].show }}</p>
  <p v-bind:id="skills[2].id">{{ skills[2].show }}</p>
  <p v-bind:id="skills[3].id">{{ skills[3].show }}</p>
  <p v-bind:id="skills[4].id">{{ skills[4].show }}</p>
  <p v-bind:id="skills[5].id">{{ skills[5].show }}</p>
</div>

しかし、変更が生じた場合、上記の書き方だと全ての要素を変えないといけないため非効率である。そこでv-forディレクティブを用いる。以下のように書き換えることができる。

index.html
<div id="skills">
  <p v-for="skill in skills" v-bind:id="skill.id">{{ skill.show }}</p>
</div>

v-for="skill in skills"で、skills配列の中身を取り出し、skillという変数に格納し、配列の要素の数だけ繰り返すことを表している。以降skill.プロパティ名でデータにアクセスしている。

4. 最後に

ポートフォリオを修正する中で使用したディレクティブのみ紹介しています。今後他のディレクティブを使用した際は、再度記事を更新します。

調べて概要を理解してコードを書く。これを繰り返すことが理解の1番の近道だと思うので、今後も継続していきます。

来週はDOM要素を部品化をするためのコンポーネントを紹介します。


  1. フレームワークとは、Webアプリケーションやシステムを開発するのに必要な機能が予め用意された骨組みのこと。便利ではあるが、フレームワーク毎にルールが存在する。 

  2. CDNとは、「コンテンツデリバリーネットワーク」(Content Delivery Network) の略で、世界中に張り巡らされた配信ネットワークを利用して、Webサイトにアクセスしようとするユーザーに効率的かつ高速にWebコンテンツを配信する仕組み。
    配信元のサーバー(オリジンサーバー)にエンドユーザーのリクエストが集中するとサーバーへの負担が大きくなってしまう。そこで、オリジンサーバーのウェブコンテンツのコピーを取得した複数のキャッシュサーバーでウェブコンテンツを配信することで、オリジンサーバーへの負担を軽減している。 

  3. データや処理のまとまり。配列との違いは、データや処理に名前(キー)を指定することができる点。特徴は「継承:変数を持たせてオブジェクトを複製することができる」、「多態性:インスタンス内でオブジェクトの内容を上書きすることができる」、「カプセル化:変数のアクセス権の指定」がある。 

  4. 指定したイベント(click,hover等)が発生した際にプログラムが実行する処理。 

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

ES2015における分割代入の使い方まとめ

ES2015における分割代入の使用例

使う場面:

1.複数の変数の宣言・代入を一つの文の中で行うことができる
2.データの代入元がオブジェクトのプロパティ群、或いは配列となります。

使い方:

1.オブジェクトの分割代入

1.宣言なしで代入のみを行う

  • データ元となるオブジェクトのプロパティ名と一致する変数にのみ対して代入を行う。
  • {}がブロック構文と解釈されないために、 ()で囲む必要がある。
    const obj1 = {
      a: 1,
      b: 2,
      c: 3
    };

    let b, c;
    ({ b, c } = obj1);
    console.log(`b:${b}`);  // 出力結果:b:2
    console.log(`c:${c}`);  // 出力結果:c:3

2.宣言と代入を同時に行う

  • 変数宣言に使うキーワードconst、letを使うこと
  • ()をつける必要がない
  • オブジェクトのプロパティ名と一致する変数名が必要
    const obj1 = {
      a: 1,
      b: 2,
      c: 3
    };

    const { b, c } = obj1;
    console.log(`b:${b}`);  // 出力結果:b:2
    console.log(`c:${c}`);  // 出力結果:c:3

2.配列の分割代入

1.宣言なしで代入のみを行う

  • 左辺に並べた変数を[]で囲む
  • 右側の配列要素に対応して順番に代入していきます。
  • 残りの要素を捕捉して配列型変数に渡せる
    const obj1 = [1, 2, 3, 4];

    let a, b, rest;
    [a, b, ...rest] = obj1;
    console.log(`a:${a}`);  // 出力結果:a:1
    console.log(`b:${b}`);  // 出力結果:b:2
    console.log(`rest:${rest}`);  // 出力結果:rest:3,4

2.宣言と代入を同時に行う

  • 変数宣言に使うキーワードconst、letを使うこと
    const obj1 = [1, 2, 3, 4];

    const [a, b, ...rest] = obj1;
    console.log(`a:${a}`);  // 出力結果:a:1
    console.log(`b:${b}`);  // 出力結果:b:2
    console.log(`rest:${rest}`);  // 出力結果:rest:3,4

応用

配列分割代入を使うと、変数の値の入れ替えは簡単にできにできます。

let a = 5, b = 10;
[a, b] = [b, a];  // 一時的な変数を使わずに済む
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue.jsでaxiosを使用してログイン時にレスポンスからユーザ情報を取得する

初めに

RailsとVue.jsを使用してアプリを作成していて、ログイン時にvue側でユーザ情報を取得したいと思ったのでやってみた。

謎にハマってかなり時間がかかったので、備忘録として残しておく。

やり方

取得するときのvue.jsの処理。簡潔にするために色々と省略している。

methods: {
  loginUser: function () {
   axios.post('api/auth/sign_in', this.user).then((response) => {
  }, (error) => {
   console.log(error)
  })
}

この処理を行うと以下の結果が返ってくる。今回はテストで「Advanced REST client」を使ってる。

スクリーンショット 2021-02-13 15.32.35.png
スクリーンショット 2021-02-13 15.32.49.png

そうするとユーザ情報を持っているdataが返ってくる。まずdataは以下のコードで取得することができる。

response.data['data']

これをさっきの処理に入れてコンソールに出力する。

methods: {
  loginUser: function () {
   axios.post('api/auth/sign_in', this.user).then((response) => {
    // 追加
    console.log(response.data['data'])
  }, (error) => {
   console.log(error)
  })
}

スクリーンショット 2021-02-13 15.46.06.png

画像のようにデータが取得できるので、この後は自分が欲しい値を選択するだけ。例えばidが欲しいならこんな感じ。末尾のキーを変更すればOK。

response.data['data'].id

後はこれをlocalStrageとか実装で使ってるやつにうまく当てはめて使用すればいい。

response.dataの中にさらにdataあるとか思わなくてめちゃくちゃ沼って時間かかった。最初にresponse.data.idを試してデータが取得できなかった時点で丁寧にvalueとkeyを確認してやってくべきだった。

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

Nuxt.jsとTypescriptとSCSSでかんたんなページ作成入門

公式ドキュメントに沿ってNuxt.jsでページを作成していきます。
日本語版があるのはありがたいですね。

テキストエディタはおすすめされているVS Codeを利用します。OSはMacOS Big Surです。

プロジェクトの作成

対話式で各種設定が行われます。
UI frameworkは少し悩みましたが無しにしてみました。

# yarn create nuxt-app nuxt-practice
yarn create v1.22.10
[1/4] ?  Resolving packages...
[2/4] ?  Fetching packages...
[3/4] ?  Linking dependencies...
[4/4] ?  Building fresh packages...

success Installed "create-nuxt-app@3.5.2" with binaries:
      - create-nuxt-app
[#######################################################################################################################################] 342/342
create-nuxt-app v3.5.2
✨  Generating Nuxt.js project in nuxt-practice
? Project name: nuxt-practice
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: None

開発サーバーの起動

# cd nuxt-practice
# yarn dev
? Are you interested in participating? No

これで、 http://localhost:3000/ にサーバーが立ち上がって、Nuxt.jsの空ページが表示されます。

image.png

ページの作成

codeコマンドで、カレントディレクトリをVS Codeで開けるようにしておくと便利です。

# code .

NuxtLinkでルーティングも出来ます。

pages/about.vue
<template>
  <NuxtLink to="/">Home page</NuxtLink>
</template>
pages/index.vue
<template>
  <main>
    <h1>Home page</h1>
    <NuxtLink to="/about">
      About Page
    </NuxtLink>
  </main>
</template>

scssの追加

sassパッケージのインストール

yarn add -D sass sass-loader@10 fibers

グローバルに読み込ませたいcssを設定しておきます。

  // Global CSS: https://go.nuxtjs.dev/config-css
  css: [
    { src: '~/assets/style.scss', lang: 'scss' },
  ],

SCSSぽい入れ子のCSSを作成します。

assets/style.scss
main {
    margin: 20px;
    h1 {
        color: red;
    }
}

無事CSSが反映されました。

image.png

機能追加

少しVueぽい書き方で動きをつけてみます。

pages/index.vue
<template>
  <main>
    <h1>Home page</h1>
    <NuxtLink to="/about">
      About Page
    </NuxtLink>
    <br><br>
    <button v-on:click="counter += 1">Click!</button>
    <p>The button above has been clicked {{ counter }} times.</p>
  </main>
</template>

<script>
export default {
  data() {
    return{
      counter: 0
    }
  },
  mounted() {
    alert('mounted!');
  }
}
</script>

ボタンをクリックするとカウントアップするようになりました。

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

GASとJSを使ってサイドバーで進行状況や途中結果を表示させる

TL;DR

やたら時間のかかるGASの処理に対して、サイドバーを使って進行状況や途中結果を表示させるようにします。

大まかな方針

私が考える限り、実現方法はだいたい3通りありそうです。

  1. 計算がある程度進むたびにサイドバーを起動しなおす
  2. サイドバーの更新と計算を非同期に実行する
  3. 計算のキックをサイドバーに任せる

で試したところ、3.が一番良さげな結果でした。
ひとまず1から順に説明していきます。ひとまず、スプレッドシート上で何かやる、という想定です。

1. 計算がある程度進むたびにサイドバーを起動しなおす

この方法は以下の記事でほぼ解説されています。

ただこれ、やってみるとわかるんですがサイドバーが再起動するたびチラつくんです。
単純に見づらいのもありますが、サイドバーを表示に使うだけじゃなくて、そこからコピペしたりサイドバー上で何か操作するような用途まで考えるとちょっと厳しいです。

2. サイドバーの更新と計算を非同期に実行する

なので再起動はせず、サイドバーはサイドバーで動かし、サイドバー内で表示を更新することを考えます。
つまり、以下の2つを同時にやります。

  1. GASで計算をする
  2. サイドバー上のJavascriptで計算を読み取り、表示する。

この場合当たり前ですが、表示部分はGASではなくJSでやることになります。なのでそっち側のデバッグにはブラウザのデベロッパーツールなんかを使います。

先に全部のコードを書いてしまいます。まずはGAS側。

gas1.gs
const sheet = SpreadsheetApp.getActive().getSheetByName('作業用シート');
const cell = sheet.getRange(1, 1);

// サイドバーからキックされて進捗を返す
function getProgress() {
  return cell.getValue();
}

// 実際にする計算
function calc() {
  for (let i = 0 ; i < 10 ; i++) {
    Utilities.sleep(2000);  // 何か計算してる代わり
    cell.setValue(cell.getValue() + 1);
  }
}

// 状態の初期化
function init() {
  cell.setValue(0);
}

// スタート
function start() {
  // 初期化
  init();

  // サイドバー表示
  const html = HtmlService.createHtmlOutputFromFile('サイドバー');
  SpreadsheetApp.getUi().showSidebar(html);

  // 計算  
  calc();
}

こっちはサイドバーのHTMLです。

サイドバー.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script>
      function run() {
        id = setInterval(function() {
          google.script.run.withSuccessHandler(function(response) {
            if (response >= 10) {
              clearInterval(id);
              document.getElementById("progress").innerHTML = '終了';
            } else {
              document.getElementById("progress").innerHTML = response.toString();
            }
          }).getProgress();
        }, 1000);
      }
    </script>
  </head>
  <body onload="run()">
    現在: <span id="progress"></span>
  </body>
</html>

実行途中のスナップショット。
スクリーンショット 2021-02-13 13.30.25.png

非同期なのでたまにシート上の内容とサイドバーの表示がずれますが、それはそういうものということで。
以下はコードの説明です。

GAS側

計算本体はこのcalc()です。ここにやりたいことを書きます。

// 実際にする計算
function calc() {
  for (let i = 0 ; i < 10 ; i++) {
    Utilities.sleep(2000);  // 何か計算してる代わり
    cell.setValue(cell.getValue() + 1);
  }
}

一方で、サイドバーから叩かれたら途中結果を返すのがこのgetProgress()です。

// サイドバーからキックされて進捗を返す
function getProgress() {
  return cell.getValue();
}

そして、すべての計算の開始はここです。サイドバーを起動しつつ、計算を開始します。

// スタート
function start() {
  // 初期化
  init();

  // サイドバー表示
  const html = HtmlService.createHtmlOutputFromFile('サイドバー');
  SpreadsheetApp.getUi().showSidebar(html);

  // 計算  
  calc();
}

サイドバー側

サイドバー側はちょっとややこしいので、多めに説明します。

サイドバーからGASを叩く仕組み

サイドバー内のJavascriptからGASのfoo()を叩きたい場合、google.script.run.foo()と書けば可能です。
ただし、この関数はfoo()の返り値を返しません。返り値を受け取る場合、withSuccessHandler()にハンドラとなるコールバック関数を登録して、そのコールバックの引数として返り値を受け取ります。
つまり、コールバック関数function success(response) {...}みたいなものを用意したとすると、google.script.run.withSuccessHandler(success).foo()と呼び出すことでsuccess()の引数にfoo()の結果が返ります。

この例ではGAS側のgetProgress()を使って状況を取得したいので、google.script.run.withSuccessHandler(success).getProgress() を呼び出すことになります。

setInterval()で定期的に状況をチェックする

setInterval(bar, time) とやればtime(ミリ秒単位)ごとにbarがキックされます。
これと、さっきのgoogle.script.runを組み合わせて、定期的にGAS側のgetProgress()を叩き、状況をチェックします。
なお処理終了が確認できたらclearInterval()を叩きます。使用方法は上のコードを参照。

問題点とかハマりポイントとか

この方法で済んでしまうことも多々あると思うのですが、1つだけ注意点があります。
google.script.runで叩かれる関数は別プロセスで走ります。つまり、GAS側と同じコードを叩いてもGAS側の計算途中の状態をgoogle.script.runで叩いても直接読み取ることはできません
ただしスプレッドシートの状態は共有できます。なので、GAS側で途中の状態を返す関数(この例ではgetProgress())はスプレッドシートから現在の計算状態を読み取る必要があります。

計算の状態がスプレッドシートにすぐ出ないか、出てもぱっと読み取れない場合、途中経過の値をどこか専用の別シートに書き出す方法もありますが、それできるんなら別にサイドバー要らないって場合も多いです。

3. 計算のキックをサイドバーに任せる

2の方法だとうまくハマらない場合に使えるのがこちらの方法です。私はこれでやることが多いです。
簡単に言うと、計算の主導権をサイドバー側にしてしまう方法です。

こちらも先にコードを貼ります。まずはGAS側。

gas2.gs
// 実際にする計算
function calc(value) {
  Utilities.sleep(2000);  // 何か計算してる代わり
  value += 1;
  return value;
}

// 状態の初期化
function init() {
}

// スタート
function start() {
  // 初期化
  init();

  // サイドバー表示
  const html = HtmlService.createHtmlOutputFromFile('サイドバー2');
  SpreadsheetApp.getUi().showSidebar(html);
}

随分スッキリしました。ここではスプレッドシートを直接読み書きしていません。
(実際にはcalc()内で色々操作するんでしょうが)

次はサイドバーのHTMLです。

サイドバー2.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script>
      function iterate(value) {
        google.script.run.withSuccessHandler(function(response) {
          if (response >= 10) {
            document.getElementById("progress").innerHTML = '終了';
          } else {
            document.getElementById("progress").innerHTML = response.toString();
            iterate(response);
          }
        }).calc(value);
      }
      function run() {
        iterate(0);
      }
    </script>
  </head>
  <body onload="run()">
    現在: <span id="progress"></span>
  </body>
</html>

考え方

この方法の基本的な考え方は、以下のようになります。

  1. サイドバーからcalc()をキック
  2. calc()では1区切りごとの計算だけやって結果を返す
  3. サイドバーはcalc()の結果を受け取って表示、続きがあるならcalc()をまたキック
  4. 2〜3を繰り返す

で、これを何も考えずにwhile文で書くとこんな感じでしょう。

function run() {
  while (現在の状態が終了条件を満たさない) {
    var r = calc();
    表示(r);
    var 現在の状態 = なんか計算(r);
  }
}

これを、敢えて再帰で書き直します。

function iterate(前の状態) {
  if (終了条件を満たさない) {
    var r = calc(前の状態);
    var 現在の状態 = なんか計算(r);
    iterate(現在の計態);
  }
}
function run() {
  iterate(初期状態;
}

で、これをサイドバー内のスクリプトで実現します。
しかしcalc()はGAS側なので、google.script.runを使って呼び出します。

function iterate(前の状態) {
  if (終了条件を満たさない) {
    var r = google.script.run.calc(前の状態);
    var 現在の状態 = なんか計算(r);
    iterate(現在の計態);
  }
}
function run() {
  iterate(初期状態;
}

しかしこれは動きません。
上の2の方法のところで説明したとおり、google.script.runでGAS側関数を呼び出したときの返り値はwithSuccessHandler()を使ってコールバックで受け取らないといけません。
つまり、

    var r = google.script.run.calc(前の状態);
    var 現在の状態 = なんか計算(r);
    iterate(現在の計態);

この返り値rを受け取って以降の処理はコールバック関数として別にする必要があります。
すると、こうなります。

function iterate(前の状態) {
  // コールバック関数
  var success = function(r) {
    var 現在の状態 = なんか計算(r);
    iterate(現在の計態);
  }
  if (終了条件を満たさない) {
    google.script.run.withSuccessHandler(success).calc(前の状態);
  }
}
function run() {
  iterate(初期状態;
}

最初に示したコードと多少形が違いますが、理屈は同じです。

さらなる工夫

途中でユーザーの確認を入れてもいい場合

で、3の方法も穴がないかと言うとそんなことはないです。
自分が色々やってみると、全体の計算がやたら重くて時間がかかる場合に意味不明のエラーで止まることがあります。
これは原因を突き止められていなくてあくまで想像なのですが…G Suiteではなく無料でGoogleスプレッドシートを使っている場合、GASで一度に6分以上計算をするとタイムアウトします。それと同様の原因で、計算リソースを食いすぎると止められてしまうのかもしれません。

で、そういう場合の対処として、計算途中でユーザとのインタラクションを入れていい場合はこんな工夫ができます。
コールバック関数でiterate()を呼ぶ代わりに、計算を継続するか確認するボタンを表示するshowButton()を呼び出します。

  var success = function(r) {
    var 現在の状態 = なんか計算(r);
    // iterate(現在の計態);     <- 以前のもの
    showButton(現在の計算);  // <- こうする
  }

showButton()の中では、

<button data-status=現在の状態 onClick="iterate(this.dataset.status)">続きを計算する</button>

みたいなボタンを生成します。

これで、calc()を呼び出す前にユーザに続きを計算するか確認できるようになります。
経験上、これを入れると意味不明なエラーで落ちることはほぼないです。
また、元々全部の計算をする必要がなくて、必要な結果が得られたら止めたい(検索とか)場合にもこの方法は有効です。

withFailureHandler()は積極的に使ったほうがいい

上のコードでは一切使っていませんが、google.script.runでGAS側の関数がエラーで落ちた場合のエラーハンドラを登録する withFailureHandler() があります。
単純な異常処理のためだけでなく、これをちゃんと使ったほうがデバッグの生産性も格段に上がるので、実際にここにあるようなコードを書くときは積極的に使った方がいいです。

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

innerHTML()で作成した要素へのクリックイベントを追加するには(jQuery)

main.js
 $("#update_profile").html("<input type='button' id='update' value='更新'>")
index.html
    <div id="update_profile"></div>
main.js
$("#update").click(function() { 
      console.log("click");
 })

#updateに対して直接Clickイベントを発生させても上手くいきません
これはjsファイル読み込み時には#updateが存在していないからですね
この場合はon()を使ってclickイベントを追加していきます。

main.js
$("#update_profile").on("click","#update", function() { 
      console.log("click");
});

on()には第一引数にはイベントを第二引数には実際にclickイベントを追加したい要素を間接的に指定しております。
もちろんですが親要素の#update_profileはjsファイルを読み込む前に存在していないといけないです。

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

【Step-By-Step】Node.jsからC++クラスを利用する

はじめに

この記事は、javascriptからC++を呼び出す処理が必要になった時の備忘録です。
node-addon-apiというラッピングライブラリを活用します。
この記事の内容を「マネすれば動く」ように意識して書いています。
Electronなどのデスクトップアプリに応用できます。

事前準備

  • 下記のパッケージは事前にインストールしておいてください
    • npm
    • node.js

応用記事はこちら!

1. Node.js からC++関数への引数、返り値まとめ

2. Coming soon...

目次

1. プロジェクトの新規作成

  • 空のフォルダを新規作成します。今回は例として、napi_sampleというフォルダ名にしました。
  • 作成したフォルダに移動し、下記のコマンドを実行し、必要モジュールをインストールします。
terminal
$ npm init -y
$ npm install node bindings node-addon-api
  • コマンド実行後、下記のようなフォルダ構成となります。

 カレントディレクトリ
  ├── node_modules
  ├── package-lock.json
  └── package.json

  • さらに、下記のようなpackage.jsonが自動的に作成されます。
package.json
{
  "name": "doit_myself",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",  //<-- 開始時にこの.jsファイルが読み込まれる
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bindings": "^1.5.0",
    "node": "^15.8.0",
    "node-addon-api": "^3.1.0"
  }
}

index.jsを作成する

カレントディレクトリ
 ├── node_modules
 ├── package-lock.json
 ├── package.json
 └── index.js <-- 新規作成

  • 起動時に読み込まれるindex.jsを追加しましょう。
    • 下記のようなサンプルとします。
index.js
console.log("Hello! node.js");

ターミナルで動作確認する

  • ここまでの環境構築がうまくいっているか確認します。
    • index.jsがあるディレクトリで下記のコマンドを入力してください。
terminal
$ node .
 >> Hello! node.js

2. Cppファイルを追加する

  • ここからは、jsで利用するためのCppラッパークラスを作成していきます。
    • wrapper.h, wrapper.cc, addon.ccの順に説明します。

カレントディレクトリ
 ├── node_modules
 ├── package-lock.json
 ├── package.json
 ├── index.js
 ├── addon.cc <-- 新規作成
 ├── wrapper.cc <-- 新規作成
 └── wrapper.h <-- 新規作成
※ 拡張子.cc はC++ファイルのことです。本質は .cppと変わりません。

ラッパークラスの作成

  • ネイティブC++をラッピングするクラスを作成します。
    • このクラスの目的は、jsから渡された引数をC++で解釈できる形にし、C++の返り値をjsが利用できる形式に変換して渡すことです。
  • 下記のwrapper.h, wrapper.ccをテンプレとしてお使いください。
wrapper.h
#ifndef WRAPPER
#define WRAPPER

#include <napi.h> // 必要なヘッダ


class Wrapper : public Napi::ObjectWrap<Wrapper> {
public:
    static Napi::Object Init(Napi::Env env, Napi::Object exports);
    static Napi::Object NewInstance(Napi::Env env, const Napi::CallbackInfo& info);

    Wrapper(const Napi::CallbackInfo& info);
    ~Wrapper();

    Napi::Value getNum(const Napi::CallbackInfo& info);

private:
    double m_value;
};

#endif
wrapper.cc
#include "wrapper.h"
#include <napi.h>

using namespace Napi;

// ---------------------------------------------------------- //
// ---------------------のり付け部分--------------------------- //
// ---------------------------------------------------------- //
// new() の定義
Napi::Object Wrapper::NewInstance(Napi::Env env, const Napi::CallbackInfo &info)
{
    Napi::EscapableHandleScope scope(env);
    // jsからコンストラクタに渡されるArgsは infoに配列として入っている
    const std::initializer_list<napi_value> initArgList = {info[0]};
    // ここでWrapper:::Wrapper()が呼ばれる
    Napi::Object obj = env.GetInstanceData<Napi::FunctionReference>()->New(initArgList);
    // gcにメモリ解放されないようにスコープを除外する
    return scope.Escape(napi_value(obj)).ToObject();
}

// メンバ関数のバインド
Napi::Object Wrapper::Init(Napi::Env env, Napi::Object exports)
{
    Napi::Function func = DefineClass(
        env, "Wrapper", {
            // ここにメンバ関数を登録する
            InstanceMethod("getNum", &Wrapper::getNum),
            // InstanceMethod("jsから呼び出す際の関数名", "呼び出したいC++メンバ関数名"),
        });

    Napi::FunctionReference *constructor = new Napi::FunctionReference();
    *constructor = Napi::Persistent(func);
    env.SetInstanceData(constructor);

    exports.Set("Wrapper", func);
    return exports;
}

// ---------------------------------------------------------- //
// --------------- Wrapperクラスの定義はこれより下 --------------- //
// ---------------------------------------------------------- //

// コンストラクタ
Wrapper::Wrapper(const Napi::CallbackInfo &info)
    : Napi::ObjectWrap<Wrapper>(info)
{
    m_value = 0.0;
};

Wrapper::~Wrapper(){};

// メンバ関数
Napi::Value Wrapper::getNum(const Napi::CallbackInfo &info)
{
    Napi::Env env = info.Env();
    return Napi::Number::New(env, this->m_value);
}

  • 補足
    • wrapper.cpp内の関数Napi::Function func = DefineClass()において、自作のC++メンバ関数を登録する必要があります。サンプルコードInstanceMethod("getNum", &Wrapper::getNum),のように、InstanceMethod("jsから呼び出す際の関数名", "呼び出したいC++メンバ関数名")として登録しなければなりません。

jsとの結合用cppファイルを作る

  • 次に、Wrapperクラスをjsモジュールとしてエクスポートするための処理をaddon.ccに記述します。
    • こちらも詳細説明は省略します。テンプレとしてお使いください。
addon.cc
#include <napi.h>
#include "wrapper.h"
#include <iostream>

// jsオブジェクトが初期化された時 new()の呼び出し
Napi::Object CreateObject(const Napi::CallbackInfo& info) {
    return Wrapper::NewInstance(info.Env(), info);
}

// js内でexport()が呼び出されたとき
Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
    // 関数定義
    Napi::Object new_exports = Napi::Function::New(env, CreateObject);
    return Wrapper::Init(env, new_exports);
}

// jsへバインドするためのマクロ
// jsで、 export('bindings')('addon')と記述したとき、上記のInitAll()が呼び出される
NODE_API_MODULE(addon, InitAll)

3. Cppファイルをビルドする

  • 作成してたcppをビルドするための設定ファイルを作ります。
    • binding.gyp というファイルです。
    • VisualStudioのprojectのプロパティ設定、CMakeLists.txtと似たような設定をします。

binding.gypの追加

カレントディレクトリ
 ├── node_modules
 ├── package-lock.json
 ├── package.json
 ├── index.js
 ├── addon.cc
 ├── wrapper.cc
 ├── wrapper.h
 └── binding.gyp <-- 新規作成

binding.gyp
{
  "targets": [
    {
      # ↓addon.cc内の NODE_API_MODULE(addon, InitAll) と同名にする
      "target_name": "addon",
      "cflags!": [ "-fno-exceptions" ],
      "cflags_cc!": [ "-fno-exceptions" ],
      # ↓必要な.ccファイルを全て記述する
      "sources": [ "addon.cc", "wrapper.cc"],
      "include_dirs": [
        "<!@(node -p \"require('node-addon-api').include\")"
      ],
      "defines": [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
    }
  ]
}
  • 上記ファイルをコピペいただければ問題ないです。
    • 注意点として sources セクションには、使用する .cc (or .cpp) 拡張子のファイルを全て登録してください。

ビルド実行

  • 下記コマンドを実行し、ビルドしてください。
terminal
$ npm install .

  >> gyp info ok と表示されればビルド完了

4. JavaScriptからビルドしたCppクラスを使う

  • お待たせしました。最後に index.jsから wrapper.ccのクラスを使ってみましょう。

index.jsの書き換え

カレントディレクトリ
 ├── node_modules
 ├── package-lock.json
 ├── package.json
 ├── index.js <-- 書き換え
 ├── addon.cc
 ├── wrapper.cc
 ├── wrapper.h
 └── binding.gyp

  • index.jsを以下のように書き換えてください。
index.js
// addon.cc内の NODE_API_MODULE(addon, InitAll) が呼ばれる
var Wrapper = require('bindings')('addon');

// addon.cc内の CreateObject() が呼ばれる
var obj = new Wrapper()

// wrapper.cc内で登録した getNum()が呼ばれる
console.log(obj.getNum());

index.jsの実行

ターミナルで次のように実行します。

terminal
$ node .
>> 0 と表示されれば成功
>> Wrapper.m_valueの値が表示されています。

お疲れ様でした。
node-addon-api は"お約束ごと"が多いので、
私はこの記事のようなテンプレを作り、使いまわしています。
参考になれば幸いです。

参考リンク

Others

  • コンストラクタに複数の引数を渡す
  • メンバ関数の引数、返り値について
  • 別のC++クラスを利用する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

50行でちゃんと動くリマインダーアプリ「Notification-CLI」をリリースしました

50行でちゃんと動くリマインダーアプリ「Notification-CLI」をリリースしました

以下かんたんな日本語版クイックスタートになります。

Minimalistic Command Line Notification Application under 50 Lines

50行で実装のシンプルなコマンドラインベースのリマインダーアプリケーション。

クイックスタート Quick Start

This will nofity you when 2021 February 20, PM 6:00.

以下で2021年2月20日18時に通知をします。

./noc.js -d 2021022018 --desktop

Very much minimalistic.

とてもミニマリスティック。

インストール Installation

git clone https://github.com/yuis-ice/notification-cli.git
cd notification-cli
chmod 755 ./noc.js
npm i

必要環境 Requirements

  • node.js v13.10.1 or higher
# node.js [nvm-sh/nvm](https://github.com/nvm-sh/nvm)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash
bash
nvm install v13.10.1
node -v

使用例 Examples

This will only notify you by command line console output:

コマンドライン出力で通知します。

./noc.js -d 2021022018

The output be like:

出力はこんな感じです。

$ ./noc.js -d 202102120234
2021-02-12T02:33:16+09:00 Jobs started...
2021-02-12T02:33:16+09:00 You will be notified at: 2021-02-12T02:34:00+09:00
2021-02-12T02:34:00+09:00 Notified.
$

You can abbreviate your seconds, minutes, hours and so on:

秒、分などを省略しても動きます。

./noc.js -d 2022

This will notify you when PM 11:00 in the day:

その日の23時になったら通知します。

./noc.js -d 23 --format HH

An alias makes your code much more minimalistic:

エイリアスを定義すると、よりシンプルになります。

alias notify="./noc.js --format MM,DD,HH" # Specifying absolute path recommended
notify -d 2,20,18

This will run your command:

シェルコマンド、外部コマンドも実行できます。

./noc.js -d 2021022018 -c "firefox.exe sound.mp3"

No logs on background be like:

バックグラウンドでマルチインスタンスに使用するのもいいですね。

./noc.js -d 2021022018 --desktop --hide-log --log "" &

My favorite format <3

僕のお気に入り。

./noc.js -d 2021022018 -c "firefox.exe sound.mp3" --desktop -t "your appointment soon"

コマンドラインオプション Command line Options

$ ./noc.js
Usage: notification-cli [options]

Options:
  -d, --date <date>             specify date to fire (e.g. "2022010100" for 2020/1/1 00:00) (default: null)     
  -f, --format <format>         specify date format (default: "YYYYMMDDHHmmss")
  -D, --desktop                 enables desktop notification
  -t, --title <text>            specify title (default: "Notification-CLI")
  -m, --message <text>          specify message (default: ":)")
  -c, --exec-command <command>  specify command to run (e.g. firefox.exe ringtone.mp3) (default: null)
  -l, --log <text>              specify console log message (default: "Notified.")
  -H, --hide-log                hide infomation logs
  -h, --help                    display help for command

一言

コードを書くよりreadmeを書く時間のほうが長くなってしまった例。

momentjsとnode-scheduleのおかげでimportやコマンドラインオプション部分を除くメインのコードはなんと10行程度で収まっています。

ソースコードを見られたら少し恥ずかしいかもしれない。対してソースコードのシンプリシティは初学者にとってそのコードを研究する敷居を低くしてくれると思うので、node.jsあるいはプログラミング初学者の人に積極的に参考にして頂ければ嬉しいと思う。

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

LaravelでFullcalendarに登録した内容を更新する方法

はじめに

前回に続き、今回はFullcalendarで登録した情報を更新する方法についてご説明します。
LaravelでFullcalendarを実装する方法

また、今回はカレンダー上の登録したイベントをクリックするとモーダルが出てきて、そのモーダル上で更新ができるようにします。モーダルでは更新前の情報も確認できるようにしたいと思います。

-各バージョン
-Laravel 6.x
-PHP 7.4.9
-MySQL 5.7.30
-Fullcalendar v5

Fullcalendarはバージョンによって記述方法が異なるので注意してください。
v4の記事を参考にしてもうまくいかないことが多かったです。

なお、フォルダ名とうは登録時に作成したものをそのまま使用しておりますのでご了承ください。

更新用のモーダルを用意する

モーダルはJavaScriptのプアグインであるMicromodal.jsを使用しました。
モーダルに関しては自作のものではなく、プラグインを利用した方が利点が多いというツイートを見かけたので、今回は自作をせずにプラグインを使用しています。

Micoromodal.jsのダウンロードは以下から行えます。
(https://micromodal.now.sh/)

今回はCDNで読み込ませました。

event.blade.php
<!-- Micromodal.js -->
<script src="https://unpkg.com/micromodal/dist/micromodal.min.js"></script>

続いて、モーダル用のbladeを作成し、更新フォームを作っていきます。
こちらはこの後@includeでevent.blade.php内で読み込ませるので、直接event.blade.php内に書いても問題ないです。

また、headerに関してはMicromodal.jsの公式ドキュメントを参考にしています。
フォームはmainタグ内に書いていきます。

moda.blade.php
<div class="modal micromodal-slide" id="modal-1" aria-hidden="true">
    <div class="modal__overlay" tabindex="-1" data-micromodal-close>
        <div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="modal-1-title">
            <header class="modal__header">
                <h2>Editing my task list</h2>
                <button class="modal__close" aria-label="Close modal" data-micromodal-close></button>
            </header>
            <main>
         <form method="POST" action="{{ route('editEvent') }}">
                 @csrf
                 <input type="hidden" id="id" value="" name="id">
           <input type="text" id="edit_title" name="title" value="">
           <input type="date" id="edit_start" name="start" value="">
           <input type="color" id="edit_color" name="textColor" value="">
          </form> 
            </main>
        </div>
    </div>
</div>

通常だとvalueに更新前の情報を入れることで、フォームで確認できるのですが、今回はJSで指定するので空にしています。

続いて、カレンダー上のイベントをクリックした際に、モーダルが出てくるように設定していきます。
カレンダー上のイベントをクリックした時の挙動は、Fullcalendarの公式ドキュメントを確認すると、eventClickというプロパティが用意されているようなので、こちらを前回作成したJSに追加していきます。
eventClick

event.blade.php
<script>
  $(document).ready(function () {
    $('#calendar').fullCalendar({
      // はじめりの曜日を月曜日に変更 デフォルトは日曜日になっており、日=0,月=1になる
      firstDay: 1,
      headerToolbar: {
                     right: 'prev,next'
                     },
      events: '/home',

     // ここから追加
    eventClick: function(info){
        document.getElementById("id").value = info.id;
        document.getElementById("edit_title").value = info.title
        document.getElementById("edit_start").value = info.start._i
        document.getElementById("edit_color").value = info.textColor
        MicroModal.show('modal-1');
     }
    });
  });
</script>

infoの中には更新前の情報が入っているので、その情報をそれぞれ先ほど指定したフォーム内のid属性をgetElementByIdで取得し、value値に指定しています。
また、startに関しては、console.log(info.start)で見てみると、_isAMomentObject: true, _i: "2021-02-06", _f: "YYYY-MM-DD", _isUTC: true, _pf:となっており、_iの箇所に日付が格納されているのがわかると思います。
そのため、info.start._iとすることで日付を取得するようにしています。

これで、更新前の情報が表示されるようになりました。

また、event.blade.php内でmodal.blade.phpを忘れずに読み込んでください。
私は前回作成した新規登録用のフォームの真下に書きました。

event.blade.php
@include('modal')

コントローラー、ビューを追加する

先ほどaction="{{ route('editEvent') }}で指定したルートを設定します。

web.php
Route::post('/editEvent', 'EventController@editEvent');

続いてコントローラーです。

EventController.php
use App\Models\Event; //冒頭で宣言する

 public function editEvent(Request $request)
    {  
     // 送信されてきたidをEventテーブルに登録されているデータと紐付ける
        $event = Event::find($request->input('id'));
        $event->title = $request->input('title');
        $event->start = $request->input('start');
        $event->textColor = $request->input('textColor');

        $event->save();

        return redirect('/home');
    }

以上で更新用モーダルの完成です!

さいごに

今回は、前回作成したFullcalendarの更新処理を、モーダルを利用して作ってみました。
また次回以降は登録したデータの削除方法についても書いていきたいと思います!

最後まで読んでいただきありがとうございました!

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

Javascriptのカリー化と部分適用とパイプライン演算子の話。

皆さんアロー関数大好きですよね。
アロー関数かっこいいですよね。

const plus = (x, y) => x + y

引数を2つ受け取って和を返す関数を作りました。さてカリー化しましょう。
は?カリー化ってなにって感じですよね...

考えるな、感じろ。

というわけで理論的な説明は置いといてカリー化を感じてください。

const curriedPlus = x => y => x + y

さて問題です。関数curriedPlusの説明として正しいのはどちらでしょう?
1. 引数を2つ受け取って和を返す関数。
2. 引数を1つ受け取って(引数一号くんと名付けましょう)、「引数を1つ受け取って引数一号くんとの和を返す関数」を返す関数。

勘のいいあなたならもうお気づきでしょう。答えは2です。

const curriedPlus = x => (y => x + y)

ほら、関数が返ってるように見えるでしょ?

例えばxに10を代入してみましょう。

const curriedPlus10 = curriedPlus(10)

//これは下の文と同義です。

const curriedPlus10 = y => 10 + y

じゃあそろそろ和を求めましょう。関数curriedPlus10の引数に3をわたす、つまり、yに3を代入しましょう。

curriedPlus10(3)  /* 13 */

え???カリー化って毎回curriedPlus10みたいな中間関数を作るの???
いいえ、説明のために作成しただけです。

curriedPlus(10)(3)  /* 13 */

もしいきなり上の記述を見たら何が起きているのか理解に時間がかかったかもしれません。
しかし、今ならわかりますね?
curriedPlus(10)が返却する関数の引数に3という引数を与えているのです。

さて、カリー化を感じていただけたでしょうか?
ここまでくれば、カリー化の定義を聞いても「???」とはならないでしょう。

カリー化とは複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。
by Wikipedia

で、カリー化の何が美味しいの?ってなると「部分適用」の話になるんですけど、ここまで読んでくれたみなさんはすでに部分適用を経験しています

そう、「curriedPlus10」です。こいつです。

「部分適用とは、複数の引数をとる関数の一部の引数に実引数を適用する操作のこと」なんですね。
by Wikipedia

あれ?最初のクイズの答えで1は間違いって言ってたくせにWikipediaは「複数の引数をとる関数」って言ってるぞ?

説明しましょう。

const plus = (x, y) => x + y
const curriedPlus10 = y => plus(10, y)

これが部分適用で、これをカリー化と組み合わせてるわけですね。

で、なぜこの話をしたかというとES Nextで定義されているパイプライン演算子を使用するときにこの部分適用が効いてくるからなんですね。

パイプライン演算子は「|>」こういうやつです。

実験的なパイプライン演算子 |> (現在はステージ 1 です) は、式の値を関数に接続します。これによって、読みやすい方法で一連の関数呼び出しを作成することができます。結果的に、単一の引数を用いた関数呼び出しの糖衣構文となり、次のように書くことができます。
by MDN Web Docs

f(g(h(i(10))))
// と記述するところを

10 |> i |> h |> g |> f
// のように記述できるんです。こういうときに可読性を上げてくれそうですよね!

おいおい、カリー化と部分適用の話はどこいった?今からしますよ。

const plus = (x, y) => x + y
const double = num => num * 2

このような関数たちが定義されているとして、

double(plus(10, 3)) 

をパイプライン演算子を使って表現してみましょう。

3 |> plus(10, ...) |> double

あれ。こまりました。「...」の部分はどうしたらいいのだろう?
パイプライン演算子は「単一の引数を用いた関数呼び出しの糖衣構文」でしたね。
引数が2つの関数だから1つしか引数を渡さなかったら当然エラーになりますね...

ここでカリー化、部分適用が効くんですね。

const curriedPlus = x => y => x + y
const double = num => num * 2

3 |> curriedPlus(10) |> double /* 26 */

なんでこれはうまく行くのか?
「curiiedPlus(10)」は引数を1つ受け取って10足して返す関数だからですね。
そう、「単一の引数を用いた関数呼び出し」になっているんですね。

以上、ありがとうございました!!!

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

駆け出しエンジニアよ、これがカリー化だ。

駆け出しエンジニアよ、これがカリー化だ。

などと偉そうに言っている私もWebエンジニアになって11ヶ月でまだまだ駆け出しエンジニアの域を出ていないのですが、少しだけ得意なJSについて書くので大目に見ていただければと思います。

本記事では以下について言及しています。

  • アロー関数
  • カリー化
  • 部分適用
  • パイプライン演算子

皆さんアロー関数大好きですよね。
アロー関数かっこいいですよね。

const plus = (x, y) => x + y

引数を2つ受け取って和を返す関数を作りました。さてカリー化しましょう。
は?カリー化ってなにって感じですよね...

考えるな、感じろ。

というわけで理論的な説明は置いといてカリー化を感じてください。

const curriedPlus = x => y => x + y

さて問題です。関数curriedPlusの説明として正しいのはどちらでしょう?
1. 引数を2つ受け取って和を返す関数。
2. 引数を1つ受け取って(引数一号くんと名付けましょう)、「引数を1つ受け取って引数一号くんとの和を返す関数」を返す関数。

勘のいいあなたならもうお気づきでしょう。答えは2です。

const curriedPlus = x => (y => x + y)

ほら、関数が返ってるように見えるでしょ?

例えばxに10を代入してみましょう。

const curriedPlus10 = curriedPlus(10)

//これは下の文と同義です。

const curriedPlus10 = y => 10 + y

じゃあそろそろ和を求めましょう。関数curriedPlus10の引数に3をわたす、つまり、yに3を代入しましょう。

curriedPlus10(3)  /* 13 */

え???カリー化って毎回curriedPlus10みたいな中間関数を作るの???
いいえ、説明のために作成しただけです。

curriedPlus(10)(3)  /* 13 */

もしいきなり上の記述を見たら何が起きているのか理解に時間がかかったかもしれません。
しかし、今ならわかりますね?
curriedPlus(10)が返却する関数の引数に3という引数を与えているのです。

さて、カリー化を感じていただけたでしょうか?
ここまでくれば、カリー化の定義を聞いても「???」とはならないでしょう。

カリー化とは複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。
by Wikipedia

で、カリー化の何が美味しいの?ってなると「部分適用」の話になるんですけど、ここまで読んでくれたみなさんはすでに部分適用を経験しています

そう、「curriedPlus10」です。こいつです。

「部分適用とは、複数の引数をとる関数の一部の引数に実引数を適用する操作のこと」なんですね。
by Wikipedia

あれ?最初のクイズの答えで1は間違いって言ってたくせにWikipediaは「複数の引数をとる関数」って言ってるぞ?

説明しましょう。

const plus = (x, y) => x + y
const curriedPlus10 = y => plus(10, y)

これが部分適用で、これをカリー化と組み合わせてるわけですね。

で、なぜこの話をしたかというとES Nextで定義されているパイプライン演算子を使用するときにこの部分適用が効いてくるからなんですね。

パイプライン演算子は「|>」こういうやつです。

実験的なパイプライン演算子 |> (現在はステージ 1 です) は、式の値を関数に接続します。これによって、読みやすい方法で一連の関数呼び出しを作成することができます。結果的に、単一の引数を用いた関数呼び出しの糖衣構文となり、次のように書くことができます。
by MDN Web Docs

f(g(h(i(10))))
// と記述するところを

10 |> i |> h |> g |> f
// のように記述できるんです。こういうときに可読性を上げてくれそうですよね!

おいおい、カリー化と部分適用の話はどこいった?今からしますよ。

const plus = (x, y) => x + y
const double = num => num * 2

このような関数たちが定義されているとして、

double(plus(10, 3)) 

をパイプライン演算子を使って表現してみましょう。

3 |> plus(10, ...) |> double

あれ。こまりました。「...」の部分はどうしたらいいのだろう?
パイプライン演算子は「単一の引数を用いた関数呼び出しの糖衣構文」でしたね。
引数が2つの関数だから1つしか引数を渡さなかったら当然エラーになりますね...

ここでカリー化、部分適用が効くんですね。

const curriedPlus = x => y => x + y
const double = num => num * 2

3 |> curriedPlus(10) |> double /* 26 */

なんでこれはうまく行くのか?
「curiiedPlus(10)」は引数を1つ受け取って10足して返す関数だからですね。
そう、「単一の引数を用いた関数呼び出し」になっているんですね。

以上、ありがとうございました!!!

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

カリー化と部分適用とパイプライン演算子の話。

皆さんアロー関数大好きですよね。
アロー関数かっこいいですよね。

const plus = (x, y) => x + y

引数を2つ受け取って和を返す関数を作りました。さてカリー化しましょう。
は?カリー化ってなにって感じですよね...

考えるな、感じろ。

というわけで理論的な説明は置いといてカリー化を感じてください。

const curriedPlus = x => y => x + y

さて問題です。関数curriedPlusの説明として正しいのはどちらでしょう?
1. 引数を2つ受け取って和を返す関数。
2. 引数を1つ受け取って(引数一号くんと名付けましょう)、「引数を1つ受け取って引数一号くんとの和を返す関数」を返す関数。

勘のいいあなたならもうお気づきでしょう。答えは2です。

const curriedPlus = x => (y => x + y)

ほら、関数が返ってるように見えるでしょ?

例えばxに10を代入してみましょう。

const curriedPlus10 = curriedPlus(10)

//これは下の文と同義です。

const curriedPlus10 = y => 10 + y

じゃあそろそろ和を求めましょう。関数curriedPlus10の引数に3をわたす、つまり、yに3を代入しましょう。

curriedPlus10(3)  /* 13 */

え???カリー化って毎回curriedPlus10みたいな中間関数を作るの???
いいえ、説明のために作成しただけです。

curriedPlus(10)(3)  /* 13 */

もしいきなり上の記述を見たら何が起きているのか理解に時間がかかったかもしれません。
しかし、今ならわかりますね?
curriedPlus(10)が返却する関数の引数に3という引数を与えているのです。

さて、カリー化を感じていただけたでしょうか?
ここまでくれば、カリー化の定義を聞いても「???」とはならないでしょう。

カリー化とは複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。
by Wikipedia

で、カリー化の何が美味しいの?ってなると「部分適用」の話になるんですけど、ここまで読んでくれたみなさんはすでに部分適用を経験しています

そう、「curriedPlus10」です。こいつです。

「部分適用とは、複数の引数をとる関数の一部の引数に実引数を適用する操作のこと」なんですね。
by Wikipedia

あれ?最初のクイズの答えで1は間違いって言ってたくせにWikipediaは「複数の引数をとる関数」って言ってるぞ?

説明しましょう。

const plus = (x, y) => x + y
const curriedPlus10 = y => plus(10, y)

これが部分適用で、これをカリー化と組み合わせてるわけですね。

で、なぜこの話をしたかというとES Nextで定義されているパイプライン演算子を使用するときにこの部分適用が効いてくるからなんですね。

パイプライン演算子は「|>」こういうやつです。

実験的なパイプライン演算子 |> (現在はステージ 1 です) は、式の値を関数に接続します。これによって、読みやすい方法で一連の関数呼び出しを作成することができます。結果的に、単一の引数を用いた関数呼び出しの糖衣構文となり、次のように書くことができます。
by MDN Web Docs

f(g(h(i(10))))
// と記述するところを

10 |> i |> h |> g |> f
// のように記述できるんです。こういうときに可読性を上げてくれそうですよね!

おいおい、カリー化と部分適用の話はどこいった?今からしますよ。

const plus = (x, y) => x + y
const double = num => num * 2

このような関数たちが定義されているとして、

double(plus(10, 3)) 

をパイプライン演算子を使って表現してみましょう。

3 |> plus(10, ...) |> double

あれ。こまりました。「...」の部分はどうしたらいいのだろう?
パイプライン演算子は「単一の引数を用いた関数呼び出しの糖衣構文」でしたね。
引数が2つの関数だから1つしか引数を渡さなかったら当然エラーになりますね...

ここでカリー化、部分適用が効くんですね。

const curriedPlus = x => y => x + y
const double = num => num * 2

3 |> curriedPlus(10) |> double /* 26 */

なんでこれはうまく行くのか?
「curiiedPlus(10)」は引数を1つ受け取って10足して返す関数だからですね。
そう、「単一の引数を用いた関数呼び出し」になっているんですね。

以上、ありがとうございました!!!

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

Firebaseでメールアドレス、パスワード入力なしで「emailVerified」をtrueにする方法

皆さんこんにちは!

今日は、FirebaseのemailVerifiedについて書いていきます。

このemailVerifiedはメール認証を行っているかを示すものです。

これでユーザーの動作やページの制御を行えます。

これを行うにはsendEmailVerificationと言う関数を用いるのですが、通常メールアドレスとパスワードの入力を要求されます。

ユーザーからしたらめんどくさいですよね。

また、emailVerifiedはフロントからは変更を行うことができません。

それをメールに送ったリンクをクリックしたらメール認証完了という感じにしたいと思います。

それではやっていきましょう!

FirebaseFunctionsの設定

最初にFirebaseFunctionsで独自の関数を作成していきます。

functins/index.js
const functions = require('firebase-functions')
const admin = require('firebase-admin')

admin.initializeApp()

// メールアドレス認証(emailVerified)の更新
exports.updateEmailVerified = functions.region('asia-northeast1').https.onCall((data, context) => {
  const uid = context.auth.uid
  if (!uid) {
    return
  }
  admin
    .auth()
    .updateUser(uid, {
      emailVerified: true
    })
    .then()
})

このupdateUseremailVerifiedtrueにすることができます。

メール認証を行うメールの送信

      const actionCodeSettings = {
        url: window.location.origin + '/actionemailverified',
        handleCodeInApp: true
      }
      const user = this.$auth.currentUser
      // メール送信
      await user
        .sendEmailVerification(actionCodeSettings)
        .then(() => {
          //
        })
        .catch((error) => {
          //
        })

アクションコードの設定などは、下記の記事に詳しく書いているので是非ご覧ください。

Firebase初心者向け!Firebaseでパスワードを使わずにメールリンク認証を行う方法

関数の呼び出し

sendEmailVerificationで送信したURL先でfunctions\index.jsで定義した関数を呼び出します。

action.js
      const func = this.$function.httpsCallable('updateEmailVerified')
      func()
        .then(() => {
          alert('メール認証が完了しました')
          user.reload()
        })
        .catch((error) => {
          //
        })

いかがだったでしょうか?

ちなみにメールアドレスを変更した際は、自動的にemailVerifiedfalseになりますのでユーザーには再度認証を行ってもらう必要があります。

このようにしてemailVerifiedを更新することができます。

あまり情報がなかったので書いておきました。

皆さんのお力になれれば幸いです。

以上、「Firebaseでメールアドレス、パスワード入力なしで「emailVerified」をtrueにする方法」でした!

良ければ、LGTM、コメントお願いします。

また、何か間違っていることがあればご指摘頂けると幸いです。

他にも毎日初心者さん向けに記事を投稿しているので、時間があれば他の記事も見て下さい!!

Thank you for reading

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

【Deno】相対パスでファイル操作をするときに気を付けること

Denoでファイル操作

Denoには標準でファイル操作を行うことのできる関数がいくつか備わっています。
例えば、これらのものです。

Deno.readFileSync("filepath") // filepathの読み込み
Deno.writeFileSync("filepath") //filepathに書き込み

これらによって、Denoで様々なファイル操作(読み書き・コピー・削除など)が簡単に行えます。
そして、これらの関数の第1引数には対象のファイルパスを入れるのですが、ここには絶対パスだけでなく相対パスも使うことができます。

test/
  ̩̩├data/
  │  └test.txt
  └main.ts

上記のようなディレクトリ構成になっている場合、main.tsを以下のようにして実行します。

main.ts
const text = Deno.readTextFileSync("./data/test.txt");
console.log(text);

export {}

するとdataフォルダ内のtest.txtファイルの中身が出力されます。

相対パスの落とし穴

Denoのファイル操作関数の引数に与える相対パスのルートは、カレントディレクトリとなります。
下記の各コマンドはどれも同じmain.tsを実行しているが、test.txtが読み込めるのは一番上のカレントディレクトリをtest直下にした時のみです。

/user/test$ deno run -A main.ts // OK

/user$ deno run -A ./test/main.ts // NG
/user/test/data$ deno run -A ../main.ts // NG

ただこのようなコマンドは誰でも実行してしまうのではないでしょうか?
別ディレクトリで作業してて、カレントディレクトリを移動するほどでもないけどmain.tsを実行したいなって時とか。

解決案:pathResolverの作成

pathResolverとは、その名の通りパスを解決する(相対パス→絶対パス)ものです。
相対パスで問題が起こるなら、絶対パスに変換しちゃおうぜって考えです。

main.tsを次のように変えてみましょう。

main.ts
import * as path from "https://deno.land/std@0.79.0/path/mod.ts";

function pathResolver(meta: ImportMeta): (p: string) => string {
  return (p) => path.fromFileUrl(new URL(p, meta.url));
}

const resolve = pathResolver(import.meta);

const text = Deno.readTextFileSync(resolve("./data/test.txt"));
console.log(text);

このようにすることで、resolve関数が相対パスを絶対パスに変換してくれるので、どのカレントディレクトリから実行しても同じ結果になります。

pathResolverの解説

pathResolverimport.metaを引数にとります。import.metaは、そのファイル自体のファイルパスを格納しており、import.meta.urlで取得できます。これで取得できるのはファイルのURLなので、file:///~で始まるものになります。
pathResolverはそのmeta.urlを格納した新しい関数を返します。main.tsではこの関数の名前をresolveとしています。

このresolve関数に相対パスを渡してあげればURLクラスがいい感じにパスの関係を計算してくれます。ファイルにアクセスするには最初のfile://の部分は邪魔なので、Denoの標準モジュールstdpathにあるfromFileUrl関数で消して素のファイルパスを返します。

注意点

solverはファイルごとに生成する必要があります。importしてはいけません。なぜならimport.metaはファイルによって違うからです。

まとめ

ファイル操作系...というか相対パスを扱うときにはpathResolverを使用しましょう。意図しないバグの発生を抑えることができます。

ちなみにimport文の相対パスは勝手に解決してくれるのでpathResolverは使わなくて大丈夫です。

参考にしたコード

このpathResolverは、僕がDenoでサーバを立てるときにお世話になっているモジュールservestのコードをヒントに作りました。

servestではpathResolverを次のようにしています。
実際のコード(Github)

export function pathResolver(meta: ImportMeta): (p: string) => string {
  return (p) => new URL(p, meta.url).pathname;
}

この記事内で示したものと外形は同じですが、pathモジュールを使っている所に違いがあります。
じつは、上記のコード1つ欠点があってWindowsではうまく動作しないということです。

Linux系(たぶんmacも)はパスの最初がスラッシュ/で始まっているときに絶対パスとみなします。
一方、Windows系は絶対パスの最初はドライブ名で始まるということになっています。

new URL().pathnameでは、import.meta.urlから取得したfile:///~付きパスのfile://しかとってくれません。Linux系だとスラッシュから始まることで絶対パスを表すので問題ないですが、Windowsではおかしくなります。
この問題を解決するのにpathモジュールを使用しています。
(このpathモジュールを探すまでに時間かかったんだよなぁ…)

/*** Linux系では ***/
const urlLinux = "file:///home/test/test.txt";
const pathLinux = new URL("",urlLinux).pathname;
console.log(pathLinux); 
// 出力:/home/test/test.txt

/*** Windowsでは ***/
const urlWin = "file:///c:/users/test/test.txt";
const pathWin = new URL("",urlWin).pathname;
console.log(pathWin);
// 出力:/c:/users/test/test.txt
// ↑パスの形式としておかしい

余談

ちなみにDenoのファイル操作系関数はURLクラスの引数にも対応してるっぽいのでpathResolverはいらないっぽい?(未確認)

ただ、URLクラス未対応だけどファイルパスを引数にとる関数とかには使えるよ。

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

Optional Chaining のエラーをもう一度整理

前回の反省点として、Optional Chainingに問題をすり替えてしまって、結果、混沌としてしまったです。
よくよく考えれば、ts-loaderもbabelも Optional Chaining はサポートしているのであって、こいつらはローダー君は指定されたバージョンのjavascriptへ変換するのがお仕事です。
つまり、ここが原因でエラーがどうとか話しすと話が混乱するのですな。

ということで、もう一度出力されているエラーを見てみる。

storybook をビルドして発生するエラー

ERROR in ./src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts& (./node_modules/vue-docgen-loader/lib??ref--12!./node_modules/ts-loader??ref--4-0!./node_modules/vue-loader/lib??vue-loader-options!./src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&) 8:36
Module parse failed: Unexpected token (8:36)
File was processed with these loaders:
 * ./node_modules/vue-docgen-loader/lib/index.js
 * ./node_modules/vue-docgen-loader/lib/index.js
 * ./node_modules/ts-loader/index.js
 * ./node_modules/vue-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
|         const canvas = ref();
|         watch(canvas, () => {
>             const gl = canvas.value?.getContext("webgl");
|             if (gl) {
|                 gl.clearColor(0.0, 0.0, 0.0, 1.0);
 @ ./src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts& 1:0-236 1:252-255 1:257-490 1:257-490
 @ ./src/components/_sandbox/WebGL.vue
 @ ./src/components/_sandbox/WebGL.stories.ts
 @ ./src sync ^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(js|jsx|ts|tsx))$
 @ ./.storybook/generated-stories-entry.js
 @ multi ./node_modules/@storybook/core/dist/server/common/polyfills.js ./node_modules/@storybook/core/dist/server/preview/globals.js ./.storybook/storybook-init-framework-entry.js ./node_modules/@storybook/addon-docs/dist/frameworks/common/config.js-generated-other-entry.js ./node_modules/@storybook/addon-docs/dist/frameworks/vue/config.js-generated-other-entry.js ./node_modules/@storybook/addon-links/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-actions/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-actions/dist/preset/addArgs.js-generated-other-entry.js ./node_modules/@storybook/addon-backgrounds/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-backgrounds/dist/preset/addParameter.js-generated-other-entry.js ./node_modules/@storybook/addon-knobs/dist/preset/addDecorator.js-generated-other-entry.js ./.storybook/preview.js-generated-config-entry.js ./.storybook/generated-stories-entry.js (webpack)-hot-middleware/client.js?reload=true&quiet=false&noInfo=undefined

Module parse failed: Unexpected token (8:36) ということは、どうやら期待されていないトークンに遭遇したらしい。
次に、const gl = canvas.value?.getContext("webgl");という、ソースコードの場所を明示してくれている。
で、webpackというヤツはルールに従ってloaderがバケツリレーをしながら javascript に置き換えるので、「誰が音を上げた」というのが問題になる。
スタックトレース的なものが表示されているが、コイツが昇順なのか降順なのか、想像はできるが確定できないので、少し手を変えてみる。

エラーを表示している場所を探す

スクリプト系の言語なら、エラーを表示しているコードを探せるかも、って事で You may need an additional loader to handle the result of these loaders. を検索してみると webpack/lib/ModuleParseError.js で検索されていることがわかる。
こいう時のコツとしては、runtimeのエラー文字列っぽくないものを探すのがコツ。

で、このエラー文字列は ModuleParseError の constructor() で使っている(生成している)ことが分かる。
じゃぁ、どこから呼び出されているのかを調べたくなるので、console.trace()を追加してみて、再度ビルドしてみる。

Trace
    at new ModuleParseError (/Users/teru/git/uilib/node_modules/webpack/lib/ModuleParseError.js:27:12)
    at handleParseError (/Users/teru/git/uilib/node_modules/webpack/lib/NormalModule.js:469:19)
    at /Users/teru/git/uilib/node_modules/webpack/lib/NormalModule.js:503:5
    at /Users/teru/git/uilib/node_modules/webpack/lib/NormalModule.js:358:12
    at /Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:373:3
    at iterateNormalLoaders (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:214:10)
    at iterateNormalLoaders (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:221:10)
    at /Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:236:3
    at context.callback (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:111:13)
    at Object.module.exports (/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js:28:5)
    at LOADER_EXECUTION (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:119:14)
    at runSyncOrAsync (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:120:4)
    at iterateNormalLoaders (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:232:2)
    at iterateNormalLoaders (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:221:10)
    at /Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:236:3
    at context.callback (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:111:13)

ふむ。。。 あ〜 vue-docgen-loader が居るな。。。
こいつ何なんだろうか。。。
まぁ、それは後で調べるにして、もうちょい情報が欲しいから、この constructor() に渡されるパラメータをダンプしてみよう。
定義はこんな感じModuleParseError(module, source, err, loaders)だったんで。。。

module

/* NormalModule */ {
  dependencies: [],
  blocks: [],
  variables: [],
  type: 'javascript/auto',
  context: '/Users/teru/git/uilib/src/components/_sandbox',
  debugId: 1422,
  hash: undefined,
  renderedHash: undefined,
  resolveOptions: {},
  factoryMeta: {},
  warnings: [],
  errors: [],
  buildMeta: { tsLoaderFileVersion: 1 },
  buildInfo: {
    cacheable: true,
    fileDependencies: Set { '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue' },
    contextDependencies: Set {},
    assets: undefined,
    assetsInfo: undefined
  },
  reasons: [
    ModuleReason {
      module: [NormalModule],
      dependency: [HarmonyImportSideEffectDependency],
      explanation: undefined,
      _chunks: null
    },
    ModuleReason {
      module: [NormalModule],
      dependency: [HarmonyImportSpecifierDependency],
      explanation: undefined,
      _chunks: null
    },
    ModuleReason {
      module: [NormalModule],
      dependency: [HarmonyImportSideEffectDependency],
      explanation: undefined,
      _chunks: null
    },
    ModuleReason {
      module: [NormalModule],
      dependency: [HarmonyExportImportedSpecifierDependency],
      explanation: undefined,
      _chunks: null
    }
  ],
  _chunks: SortableSet [Set] {
    _sortFn: [Function: sortById],
    _lastActiveSortFn: null,
    _cache: undefined,
    _cacheOrderIndependent: undefined
  },
  id: null,
  index: null,
  index2: null,
  depth: null,
  issuer: NormalModule {
    dependencies: [
      [HarmonyCompatibilityDependency],
      [HarmonyInitDependency],
      [ConstDependency],
      [HarmonyImportSideEffectDependency],
      [HarmonyExportHeaderDependency],
      [HarmonyExportExpressionDependency],
      [HarmonyImportSpecifierDependency],
      [ConstDependency],
      [HarmonyImportSideEffectDependency],
      [HarmonyExportImportedSpecifierDependency]
    ],
    blocks: [],
    variables: [],
    type: 'javascript/auto',
    context: '/Users/teru/git/uilib/src/components/_sandbox',
    debugId: 1421,
    hash: undefined,
    renderedHash: undefined,
    resolveOptions: {},
    factoryMeta: {},
    warnings: [],
    errors: [],
    buildMeta: { exportsType: 'namespace' },
    buildInfo: {
      cacheable: true,
      fileDependencies: Set {},
      contextDependencies: Set {},
      assets: undefined,
      assetsInfo: undefined,
      strict: true,
      exportsArgument: '__webpack_exports__'
    },
    reasons: [ [ModuleReason], [ModuleReason], [ModuleReason], [ModuleReason] ],
    _chunks: SortableSet [Set] {
      _sortFn: [Function: sortById],
      _lastActiveSortFn: null,
      _cache: undefined,
      _cacheOrderIndependent: undefined
    },
    id: null,
    index: null,
    index2: null,
    depth: null,
    issuer: NormalModule {
      dependencies: [Array],
      blocks: [],
      variables: [],
      type: 'javascript/auto',
      context: '/Users/teru/git/uilib/src/components/_sandbox',
      debugId: 1217,
      hash: undefined,
      renderedHash: undefined,
      resolveOptions: {},
      factoryMeta: {},
      warnings: [],
      errors: [],
      buildMeta: [Object],
      buildInfo: [Object],
      reasons: [Array],
      _chunks: [SortableSet [Set]],
      id: null,
      index: null,
      index2: null,
      depth: null,
      issuer: [NormalModule],
      profile: undefined,
      prefetched: false,
      built: true,
      used: null,
      usedExports: null,
      optimizationBailout: [],
      _rewriteChunkInReasons: undefined,
      useSourceMap: true,
      _source: [OriginalSource],
      request: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js??vue-loader-options!/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue',
      userRequest: '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue',
      rawRequest: './WebGL.vue',
      binary: false,
      parser: [Parser],
      generator: JavascriptGenerator {},
      resource: '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue',
      matchResource: undefined,
      loaders: [Array],
      error: null,
      _sourceSize: null,
      _buildHash: 'd57de076f7ac96f23ab1c0ce9011f9e8',
      buildTimestamp: 1613107043574,
      _cachedSources: Map {},
      lineToLine: false,
      _lastSuccessfulBuildMeta: [Object],
      _ast: null
    },
    profile: undefined,
    prefetched: false,
    built: true,
    used: null,
    usedExports: null,
    optimizationBailout: [],
    _rewriteChunkInReasons: undefined,
    useSourceMap: true,
    _source: OriginalSource {
      _value: 'import mod from "-!../../../node_modules/vue-docgen-loader/lib/index.js??ref--12!../../../node_modules/ts-loader/index.js??ref--4-0!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./WebGL.vue?vue&type=script&lang=ts&"; export default mod; export * from "-!../../../node_modules/vue-docgen-loader/lib/index.js??ref--12!../../../node_modules/ts-loader/index.js??ref--4-0!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./WebGL.vue?vue&type=script&lang=ts&"',
      _name: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/vue-loader/lib/loaders/pitcher.js??ref--4!/Users/teru/git/uilib/node_modules/ts-loader/index.js??ref--4-0!/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js??vue-loader-options!/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&'
    },
    request: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/vue-loader/lib/loaders/pitcher.js??ref--4!/Users/teru/git/uilib/node_modules/ts-loader/index.js??ref--4-0!/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js??vue-loader-options!/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
    userRequest: '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
    rawRequest: './WebGL.vue?vue&type=script&lang=ts&',
    binary: false,
    parser: Parser {
      _pluginCompat: [SyncBailHook],
      hooks: [Object],
      options: {},
      sourceType: 'auto',
      scope: undefined,
      state: undefined,
      comments: undefined
    },
    generator: JavascriptGenerator {},
    resource: '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
    matchResource: undefined,
    loaders: [ [Object], [Object], [Object], [Object] ],
    error: null,
    _sourceSize: null,
    _buildHash: '76f6da385d20205e0eefa56e89ec04d1',
    buildTimestamp: 1613107043951,
    _cachedSources: Map {},
    lineToLine: false,
    _lastSuccessfulBuildMeta: { exportsType: 'namespace' },
    _ast: null
  },
  profile: undefined,
  prefetched: false,
  built: true,
  used: null,
  usedExports: null,
  optimizationBailout: [],
  _rewriteChunkInReasons: undefined,
  useSourceMap: true,
  _source: SourceMapSource {
    _value: 'import { defineComponent, ref, watch } from "@vue/composition-api";\n' +
      'export default defineComponent({\n' +
      '    name: "WebGL",\n' +
      '    props: {},\n' +
      '    setup: (props, ctx) => {\n' +
      '        const canvas = ref();\n' +
      '        watch(canvas, () => {\n' +
      '            const gl = canvas.value?.getContext("webgl");\n' +
      '            if (gl) {\n' +
      '                gl.clearColor(0.0, 0.0, 0.0, 1.0);\n' +
      '                gl.clear(gl.COLOR_BUFFER_BIT);\n' +
      '            }\n' +
      '        });\n' +
      '        return { canvas };\n' +
      '    }\n' +
      '});\n',
    _name: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/ts-loader/index.js??ref--4-0!/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js??vue-loader-options!/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
    _sourceMap: {
      version: 3,
      file: '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue.ts',
      sourceRoot: '',
      sources: [Array],
      names: [],
      mappings: 'AAKA,OAAO,EAAE,eAAe,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAEnE,eAAe,eAAe,CAAC;IAC7B,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,EAAE;IACT,KAAK,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACpB,MAAM,MAAM,GAAG,GAAG,EAAqB,CAAC;QACxC,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE;YACjB,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;YAC7C,IAAI,EAAE,EAAE;gBACN,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;gBAClC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC;aAC/B;QACH,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,MAAM,EAAE,CAAC;IACpB,CAAC;CACF,CAAC,CAAC',
      sourcesContent: [Array]
    },
    _originalSource: undefined,
    _innerSourceMap: undefined,
    _removeOriginalSource: undefined
  },
  request: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/ts-loader/index.js??ref--4-0!/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js??vue-loader-options!/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
  userRequest: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/ts-loader/index.js??ref--4-0!/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js??vue-loader-options!/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
  rawRequest: '-!../../../node_modules/vue-docgen-loader/lib/index.js??ref--12!../../../node_modules/ts-loader/index.js??ref--4-0!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./WebGL.vue?vue&type=script&lang=ts&',
  binary: false,
  parser: Parser {
    _pluginCompat: SyncBailHook {
      _args: [Array],
      taps: [Array],
      interceptors: [],
      call: [Function: lazyCompileHook],
      promise: [Function: lazyCompileHook],
      callAsync: [Function: lazyCompileHook],
      _x: undefined
    },
    hooks: {
      evaluateTypeof: [HookMap],
      evaluate: [HookMap],
      evaluateIdentifier: [HookMap],
      evaluateDefinedIdentifier: [HookMap],
      evaluateCallExpressionMember: [HookMap],
      statement: [SyncBailHook],
      statementIf: [SyncBailHook],
      label: [HookMap],
      import: [SyncBailHook],
      importSpecifier: [SyncBailHook],
      export: [SyncBailHook],
      exportImport: [SyncBailHook],
      exportDeclaration: [SyncBailHook],
      exportExpression: [SyncBailHook],
      exportSpecifier: [SyncBailHook],
      exportImportSpecifier: [SyncBailHook],
      varDeclaration: [HookMap],
      varDeclarationLet: [HookMap],
      varDeclarationConst: [HookMap],
      varDeclarationVar: [HookMap],
      canRename: [HookMap],
      rename: [HookMap],
      assigned: [HookMap],
      assign: [HookMap],
      typeof: [HookMap],
      importCall: [SyncBailHook],
      call: [HookMap],
      callAnyMember: [HookMap],
      new: [HookMap],
      expression: [HookMap],
      expressionAnyMember: [HookMap],
      expressionConditionalOperator: [SyncBailHook],
      expressionLogicalOperator: [SyncBailHook],
      program: [SyncBailHook],
      hotAcceptCallback: [SyncBailHook],
      hotAcceptWithoutCallback: [SyncBailHook]
    },
    options: {},
    sourceType: 'auto',
    scope: undefined,
    state: undefined,
    comments: undefined
  },
  generator: JavascriptGenerator {},
  resource: '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
  matchResource: undefined,
  loaders: [
    {
      options: [Object],
      ident: 'ref--12',
      loader: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js'
    },
    {
      loader: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js',
      options: [Object],
      ident: 'ref--12'
    },
    {
      loader: '/Users/teru/git/uilib/node_modules/ts-loader/index.js',
      options: [Object],
      ident: 'ref--4-0'
    },
    {
      loader: '/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js',
      options: {},
      ident: 'vue-loader-options'
    }
  ],
  error: null,
  _sourceSize: null,
  _buildHash: '',
  buildTimestamp: 1613107043952,
  _cachedSources: Map {},
  lineToLine: false,
  _lastSuccessfulBuildMeta: {},
  _ast: null
}

source

import { defineComponent, ref, watch } from "@vue/composition-api";
export default defineComponent({
    name: "WebGL",
    props: {},
    setup: (props, ctx) => {
        const canvas = ref();
        watch(canvas, () => {
            const gl = canvas.value?.getContext("webgl");
            if (gl) {
                gl.clearColor(0.0, 0.0, 0.0, 1.0);
                gl.clear(gl.COLOR_BUFFER_BIT);
            }
        });
        return { canvas };
    }
});

err

SyntaxError: Unexpected token (8:36)
    at Parser.pp$4.raise (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2825:15)
    at Parser.pp.unexpected (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:689:10)
    at Parser.pp$3.parseExprAtom (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2270:12)
    at Parser.pp$3.parseExprSubscripts (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2089:21)
    at Parser.pp$3.parseMaybeUnary (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2066:19)
    at Parser.pp$3.parseExprOps (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2010:21)
    at Parser.pp$3.parseMaybeConditional (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1993:21)
    at Parser.pp$3.parseMaybeAssign (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1968:21)
    at Parser.pp$3.parseMaybeConditional (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1998:30)
    at Parser.pp$3.parseMaybeAssign (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1968:21)
    at Parser.pp$1.parseVar (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1228:26)
    at Parser.pp$1.parseVarStatement (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1092:10)
    at Parser.pp$1.parseStatement (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:842:19)
    at Parser.pp$1.parseBlock (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1161:23)
    at Parser.pp$3.parseFunctionBody (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2671:24)
    at Parser.pp$3.parseArrowExpression (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2634:10) {
  pos: 260,
  loc: Position { line: 8, column: 36 },
  raisedAt: 261
}

loaders

[
  './node_modules/vue-docgen-loader/lib/index.js',
  './node_modules/vue-docgen-loader/lib/index.js',
  './node_modules/ts-loader/index.js',
  './node_modules/vue-loader/lib/index.js'
]

ここにきて、ようやくエラーの根源を発見!(実は、当初のビルドで表示されているんだけどね)
acorn が例外投げてるっぽい。
で、acornって、何者だ?

あと、errのスタックトレースが acorn 内で終わっているので、そもそも、これ呼び出しているのは誰?って事で調べてみる。

エラーの根源

もう一度、整理すると。

  • ModuleParseError でエラーが構成されているが、これは発生したエラーを受け、表示用にエラーを構成しているもので、エラーそのものではない。
  • ModuleParseError を誰かが呼び出している。
  • エラーの発生は acorn で確定。

ということで、ModuleParseError って誰が呼び出しているのかを調べてみる。

ModuleParseError の生成箇所を追う

とりあえず、検索してみると NormalModule.js で生成していることが分かる。
そして、NormalModule.build() 内で、 doBuild() を呼び出し、ここのコールバックで生成している。
で、条件としては、 this.parser.parse() でエラーが発生した時、または、this.parser.parse()が例外を発生させた時のみ。
念の為、この2つのケースに console.log() を入れてみて確認してみると、catch節で ModuleParseError が生成されることを確認した。

acorn って何者?

で、javascriptのパーサーでした。
npm で週4千万ダウンロードされてる、神様的なライブラリですね。
いつもお世話になっているのに、知らなくてゴメンね。
で、この子は、文字列とパラメータを与えると、構文解析して、ESTree Specにくれるものみたいです。

https://github.com/acornjs/acorn/tree/master/acorn

つまり、この子が理解できないと例外を発生させたのですが、最新の acorn 8.0.5 は ECMA12までサポートしてます。
storybook が使っている acorn はというと。。。 storybook -> webpack -> acorn で 7.1.0 か。。。
どうなんだろう? これかな?

そういえば NormalModule って?

そうそう、エラー出しているのは NormalModule の this.parser.parse() だったっけ。
ところで、こいつの this.parser って何者なんだ?
acron にも parse() って関数あるんだけど、引数違うから、まだ途中に誰か居るな。

ということで、 NormalModule.parser は誰なのか追ってみる。

どうやら、this.parserに代入している箇所は、constructor() と updateCacheModule() だが、取り敢えず、何者かを知りたいので、constructor に絞って探してみる。
(あ〜、動的型付け言語は追いづれ〜〜〜)
試しに、 "new NormalModule" で検索してみると、NormalModuleFactory.js がヒットするので、何となくコレっぽい。

  let createdModule = this.hooks.createModule.call(result);
  if (!createdModule) {
    if (!result.request) {
      return callback(new Error("Empty dependency (no request)"));
    }

    createdModule = new NormalModule(result);
  }

が、こいつ、条件付きで new されるみたいで、hook とかいうヤツが生成する場合がある(どっちが多いんだか分からん)。

ちと、 NormalModule って、資料ないのかと調べてみる。

https://webpack.js.org/api/loaders/

あれ? webpack 本家サイトで "NormalObject" が登場するのはここしかない。
NormalObject を生成するファクトリについては解説しているが。。。
https://webpack.js.org/api/normalmodulefactory-hooks/

何だ? 知ってて当然で、知らない人はお断り的なヤツなんか?
まぁ、実際はwebpackの内部クラスで、webpack作っている方々以外は触るもんじゃないんでしょう。
で、NormalModuleを置き換えるプラグイン的な切り口については解説してますな。
https://webpack.js.org/plugins/normal-module-replacement-plugin/

ここまでくると、デバッガで動かさないと分からん。。。
ひとます、置いといて。。。

え〜っと、何やってるんだっけ?(苦笑)

残りは vue-docgen-loader

こいつは、vue を解析して、propertyやらeventやらを抽出する便利なヤツです。
こいつが動くってことは、誰かが呼び出しているはずなので探してみると。。。。
@storybook/addon-docs の中で、webpackFilnal() 関数の中におりました。
まともに動かないのを覚悟で、 vue-docgen-loader を module.rules に追加している箇所をコメントアウトしてみると。

ERROR in ./src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts& (./node_modules/ts-loader??ref--4-0!./node_modules/vue-loader/lib??vue-loader-options!./src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&) 8:36
Module parse failed: Unexpected token (8:36)
File was processed with these loaders:
 * ./node_modules/ts-loader/index.js
 * ./node_modules/vue-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
|         const canvas = ref();
|         watch(canvas, () => {
>             const gl = canvas.value?.getContext("webgl");
|             if (gl) {
|                 gl.clearColor(0.0, 0.0, 0.0, 1.0);
 @ ./src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts& 1:0-174 1:190-193 1:195-366 1:195-366
 @ ./src/components/_sandbox/WebGL.vue
 @ ./src/components/_sandbox/WebGL.stories.ts
 @ ./src sync ^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(js|jsx|ts|tsx))$
 @ ./.storybook/generated-stories-entry.js
 @ multi ./node_modules/@storybook/core/dist/server/common/polyfills.js ./node_modules/@storybook/core/dist/server/preview/globals.js ./.storybook/storybook-init-framework-entry.js ./node_modules/@storybook/addon-docs/dist/frameworks/common/config.js-generated-other-entry.js ./node_modules/@storybook/addon-docs/dist/frameworks/vue/config.js-generated-other-entry.js ./node_modules/@storybook/addon-links/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-actions/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-actions/dist/preset/addArgs.js-generated-other-entry.js ./node_modules/@storybook/addon-backgrounds/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-backgrounds/dist/preset/addParameter.js-generated-other-entry.js ./node_modules/@storybook/addon-knobs/dist/preset/addDecorator.js-generated-other-entry.js ./.storybook/preview.js-generated-config-entry.js ./.storybook/generated-stories-entry.js (webpack)-hot-middleware/client.js?reload=true&quiet=false&noInfo=undefined

まだエラー出てるけど、vue-docgen-loader がエラー出てるんじゃないのね。
で、更に、webpackの設定周りを追っていると、 iframe-webpack.config.js にぶちあたり、"generated-stories-entry.js" という、エラーで見かけた文字列を発見。

こりゃ、まだ続くな。。。

取り敢えず、これまでの解析結果は。。。

  • storybook で Optional Chaining を使うとエラーになる
  • tsconfig.json で target を ES6 にするとエラーはなくなる
  • エラーの発生に関連するものは。。。
    • acron(例外発生場所)
    • webpack の NormalModule.build() (エラーハンドリング場所)
    • vue-docgen-loader
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む